Files
xo-stack/packages/primitives/src/script.ts
T

200 lines
5.1 KiB
TypeScript

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;
}
}