import { Bytes } from './bytes.js'; import { type WalletTemplateVariable, assembleBytecodeBCH, disassembleBytecodeBCH, createCompilerBCH, stringify, } from '@bitauth/libauth'; /** * Script Entity. * * @example * * ```ts * // Import our primitives * import { Script } from '@xocash/primitives'; * * // Create a script from ASM. * const script = Script.fromASM('OP_RETURN <0xbeef>'); * * // Output as in hexadecimal representation. * console.log(script.toHex()); * * // Output as bytecode. * console.log(script); * ``` */ export class Script { protected readonly bytes: Bytes; /** * Construct a new Script * * @param bytes {Uint8Array} The raw bytes of the Script. */ protected constructor(bytes: Bytes | Uint8Array) { this.bytes = Bytes.from(bytes); } /** * Instantiates a Script from either Bytes or Hex. * * @param script The script as either Bytes or Hex. * * @throws {Error} If the script cannot be decoded. * * @returns The created Script Primitive. */ public static from(script: Bytes | Uint8Array | string) { return script instanceof Uint8Array ? this.fromBytes(script) : this.fromHex(script); } /** * Create a Script Entity from bytes. * * @param bytes {string} Script Bytecode * * @throws {Error} If script cannot be decoded. * * @returns {Script} The created Script Entity. */ public static fromBytes(bytes: Bytes | Uint8Array): Script { // Disassemble the bytecode. const disassembledBytecode = disassembleBytecodeBCH(bytes); // And then try to instantiate it from ASM to catch any errors. return this.fromASM(disassembledBytecode); } /** * Create a Script Entity from the given Assembly String. * * @param asm {string} String of assembly code. * * @throws {Error} If script cannot be decoded. * * @returns {Script} The created Script Entity. */ public static fromASM(asm: string, disallowUnknown = true): Script { // If we should disallow unknown OP codes... if (disallowUnknown) { // Check if there are any unknown OP Codes. const containsUnknown = asm .split(/\s+/) .some((opCode) => opCode.startsWith('OP_UNKNOWN')); // Throw an error if there are unknown OP codes. if (containsUnknown) { throw new Error( `Failed to decode script '${asm}': Unknown OP codes detected (set disallowUnknown to false if this is expected)`, ); } } // Attempt to convert the given ASM to bytecode. const assembleResult = assembleBytecodeBCH(asm); // If a string is returned, this indicates an error... if (typeof assembleResult === 'string') { throw new Error(assembleResult); } // If the success flag is not marked, this indicates an error... if (!assembleResult.success) { throw new Error( `Failed to decode '${asm}': Could not assemble bytecode from assembly.`, ); } // Return new Script Entity from the assembled bytecode. return new this(assembleResult.bytecode); } /** * Create a Script Entity from the given CashASM String. * * @param cashASM String of CashASM to compile. * @param data AddressData to pass to the CashASM compiler * * @throws {Error} If script cannot be decoded. * * @returns The created Script Entity. */ public static fromCashASM( cashASM: string, data?: Record, ) { // Create a BCH Compilation context. const compiler = createCompilerBCH({ scripts: { // NOTE: We need to name the script, so we give it the name "_main". _main: cashASM, }, variables: Object.keys(data || {}).reduce<{ [name: string]: WalletTemplateVariable; }>( (acc, variableName: string) => { acc[variableName] = { type: 'AddressData' }; return acc; }, {}, // Initial value ), }); // Attempt to compile the script called "main" const compilationResult = compiler.generateBytecode({ data: { bytecode: data, }, scriptId: '_main', debug: true, }); // If compilation failed... if (!compilationResult.success) { throw new Error( `Failed to resolve CashASM: ${stringify(compilationResult)}`, ); } // Return a new script instance containing the compiled bytecode. return new Script(compilationResult.bytecode); } /** * Create a Script Entity from the given bytes (as hexadecimal string). * * @param hex {string} Raw script bytes encoded as hexadecimal. * * @throws {Error} If script cannot be decoded. * * @returns {Script} The created Script Entity. */ public static fromHex(hex: string): Script { // Return new Script entity constructed from the binary. return this.fromBytes(Bytes.fromHex(hex)); } /** * Creates an instance from serialized data. * * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. * * @param bytes - Serialized Primitive Data * * @returns A new instance of this primitive. */ public static fromRaw(bytes: Uint8Array) { return new this(bytes); } /** * Convert this script to a human-readable string of assembly. * * @returns {string} Human-readable assembly. */ public toASM(joinWith = '\n'): string { const disassembledBytecode = disassembleBytecodeBCH(this.bytes); if (joinWith) { return disassembledBytecode.split(' ').join(joinWith); } return disassembledBytecode; } /** * Get the raw bytecode for this script. * * @returns {Uint8Array} The compiled bytes. */ public toBytes(): Bytes { return this.bytes; } /** * Get the raw bytecode for this script as a hex string. * * @returns {string} The compiled bytes as a hex string. */ public toHex(): string { return Bytes.fromBytes(this.bytes).toHex(); } /** * Converts this primitive to a serializable raw object representation. * * @returns Serialzied primitive data. */ toRaw() { return this.bytes; } }