238 lines
6.0 KiB
TypeScript
238 lines
6.0 KiB
TypeScript
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: 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);
|
|
}
|
|
|
|
/**
|
|
* 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<string, Uint8Array>,
|
|
) {
|
|
// 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<true>({
|
|
data: {
|
|
...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(): 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;
|
|
}
|
|
}
|