import { Bytes } from './bytes.js'; import { assembleBytecodeBCH, disassembleBytecodeBCH, compileCashAssembly, } 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.toBytes()); * ``` */ export class Script { protected readonly bytes: Uint8Array; /** * Construct a new Script * * @param bytes {Uint8Array} The raw bytes of the Script. */ protected constructor(bytes: Uint8Array) { this.bytes = 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: 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: 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); } // NOTE: In its current form, this is pretty useless because it does not allow OP_CODES. // Disabling because it will probably just cause confusion. /* public static fromCashASM(cashASM: string) { const result = compileCashAssembly(cashASM); if (typeof result === 'string') { // TODO: Make error more detailed. throw new Error(`Failed to parse CashASM: ${result}`); } return new this(result); } */ /** * 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).toBytes()); } /** * 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(): Uint8Array { 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; } }