@bsv/sdk
Version:
BSV Blockchain Software Development Kit
565 lines • 18.5 kB
JavaScript
import OP from './OP.js';
import { encode, toHex, toArray } from '../primitives/utils.js';
import BigNumber from '../primitives/BigNumber.js';
/**
* The Script class represents a script in a Bitcoin SV transaction,
* encapsulating the functionality to construct, parse, and serialize
* scripts used in both locking (output) and unlocking (input) scripts.
*
* @property {ScriptChunk[]} chunks - An array of script chunks that make up the script.
*/
const BufferCtor = typeof globalThis !== 'undefined' ? globalThis.Buffer : undefined;
export default class Script {
_chunks;
parsed;
rawBytesCache;
hexCache;
/**
* @method fromASM
* Static method to construct a Script instance from an ASM (Assembly) formatted string.
* @param asm - The script in ASM string format.
* @returns A new Script instance.
* @example
* const script = Script.fromASM("OP_DUP OP_HASH160 abcd... OP_EQUALVERIFY OP_CHECKSIG")
*/
static fromASM(asm) {
const chunks = [];
const tokens = asm.split(' ');
let i = 0;
while (i < tokens.length) {
const token = tokens[i];
let opCode;
let opCodeNum = 0;
if (token.startsWith('OP_') && typeof OP[token] !== 'undefined') {
opCode = token;
opCodeNum = OP[token];
}
// we start with two special cases, 0 and -1, which are handled specially in
// toASM. see _chunkToString.
if (token === '0') {
opCodeNum = 0;
chunks.push({
op: opCodeNum
});
i = i + 1;
}
else if (token === '-1') {
opCodeNum = OP.OP_1NEGATE;
chunks.push({
op: opCodeNum
});
i = i + 1;
}
else if (opCode === undefined) {
let hex = tokens[i];
if (hex.length % 2 !== 0) {
hex = '0' + hex;
}
const arr = toArray(hex, 'hex');
if (encode(arr, 'hex') !== hex) {
throw new Error('invalid hex string in script');
}
const len = arr.length;
if (len >= 0 && len < OP.OP_PUSHDATA1) {
opCodeNum = len;
}
else if (len < Math.pow(2, 8)) {
opCodeNum = OP.OP_PUSHDATA1;
}
else if (len < Math.pow(2, 16)) {
opCodeNum = OP.OP_PUSHDATA2;
}
else if (len < Math.pow(2, 32)) {
opCodeNum = OP.OP_PUSHDATA4;
}
chunks.push({
data: arr,
op: opCodeNum
});
i = i + 1;
}
else if (opCodeNum === OP.OP_PUSHDATA1 ||
opCodeNum === OP.OP_PUSHDATA2 ||
opCodeNum === OP.OP_PUSHDATA4) {
chunks.push({
data: toArray(tokens[i + 2], 'hex'),
op: opCodeNum
});
i = i + 3;
}
else {
chunks.push({
op: opCodeNum
});
i = i + 1;
}
}
return new Script(chunks);
}
/**
* @method fromHex
* Static method to construct a Script instance from a hexadecimal string.
* @param hex - The script in hexadecimal format.
* @returns A new Script instance.
* @example
* const script = Script.fromHex("76a9...");
*/
static fromHex(hex) {
if (hex.length === 0)
return Script.fromBinary([]);
if (hex.length % 2 !== 0) {
throw new Error('There is an uneven number of characters in the string which suggests it is not hex encoded.');
}
if (!/^[0-9a-fA-F]+$/.test(hex)) {
throw new Error('Some elements in this string are not hex encoded.');
}
const bin = toArray(hex, 'hex');
const rawBytes = Uint8Array.from(bin);
return new Script([], rawBytes, hex.toLowerCase(), false);
}
/**
* @method fromBinary
* Static method to construct a Script instance from a binary array.
* @param bin - The script in binary array format.
* @returns A new Script instance.
* @example
* const script = Script.fromBinary([0x76, 0xa9, ...])
*/
static fromBinary(bin) {
const rawBytes = Uint8Array.from(bin);
return new Script([], rawBytes, undefined, false);
}
/**
* @constructor
* Constructs a new Script object.
* @param chunks=[] - An array of script chunks to directly initialize the script.
* @param rawBytesCache - Optional serialized bytes that can be reused instead of reserializing `chunks`.
* @param hexCache - Optional lowercase hex string that matches the serialized bytes, used to satisfy `toHex` quickly.
* @param parsed - When false the script defers parsing `rawBytesCache` until `chunks` is accessed; defaults to true.
*/
constructor(chunks = [], rawBytesCache, hexCache, parsed = true) {
this._chunks = chunks;
this.parsed = parsed;
this.rawBytesCache = rawBytesCache;
this.hexCache = hexCache;
}
get chunks() {
this.ensureParsed();
return this._chunks;
}
set chunks(value) {
this._chunks = value;
this.parsed = true;
this.invalidateSerializationCaches();
}
ensureParsed() {
if (this.parsed)
return;
if (this.rawBytesCache != null) {
this._chunks = Script.parseChunks(this.rawBytesCache);
}
else {
this._chunks = [];
}
this.parsed = true;
}
/**
* @method toASM
* Serializes the script to an ASM formatted string.
* @returns The script in ASM string format.
*/
toASM() {
let str = '';
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
str += this._chunkToString(chunk);
}
return str.slice(1);
}
/**
* @method toHex
* Serializes the script to a hexadecimal string.
* @returns The script in hexadecimal format.
*/
toHex() {
if (this.hexCache != null) {
return this.hexCache;
}
if (this.rawBytesCache == null) {
this.rawBytesCache = this.serializeChunksToBytes();
}
const hex = BufferCtor != null
? BufferCtor.from(this.rawBytesCache).toString('hex')
: encode(Array.from(this.rawBytesCache), 'hex');
this.hexCache = hex;
return hex;
}
/**
* @method toBinary
* Serializes the script to a binary array.
* @returns The script in binary array format.
*/
toBinary() {
return Array.from(this.toUint8Array());
}
toUint8Array() {
if (this.rawBytesCache == null) {
this.rawBytesCache = this.serializeChunksToBytes();
}
return this.rawBytesCache;
}
/**
* @method writeScript
* Appends another script to this script.
* @param script - The script to append.
* @returns This script instance for chaining.
*/
writeScript(script) {
this.invalidateSerializationCaches();
this.chunks = this.chunks.concat(script.chunks);
return this;
}
/**
* @method writeOpCode
* Appends an opcode to the script.
* @param op - The opcode to append.
* @returns This script instance for chaining.
*/
writeOpCode(op) {
this.invalidateSerializationCaches();
this.chunks.push({ op });
return this;
}
/**
* @method setChunkOpCode
* Sets the opcode of a specific chunk in the script.
* @param i - The index of the chunk.
* @param op - The opcode to set.
* @returns This script instance for chaining.
*/
setChunkOpCode(i, op) {
this.invalidateSerializationCaches();
this.chunks[i] = { op };
return this;
}
/**
* @method writeBn
* Appends a BigNumber to the script as an opcode.
* @param bn - The BigNumber to append.
* @returns This script instance for chaining.
*/
writeBn(bn) {
this.invalidateSerializationCaches();
if (bn.cmpn(0) === OP.OP_0) {
this.chunks.push({
op: OP.OP_0
});
}
else if (bn.cmpn(-1) === 0) {
this.chunks.push({
op: OP.OP_1NEGATE
});
}
else if (bn.cmpn(1) >= 0 && bn.cmpn(16) <= 0) {
// see OP_1 - OP_16
this.chunks.push({
op: bn.toNumber() + OP.OP_1 - 1
});
}
else {
const buf = bn.toSm('little');
this.writeBin(buf);
}
return this;
}
/**
* @method writeBin
* Appends binary data to the script, determining the appropriate opcode based on length.
* @param bin - The binary data to append.
* @returns This script instance for chaining.
* @throws {Error} Throws an error if the data is too large to be pushed.
*/
writeBin(bin) {
this.invalidateSerializationCaches();
let op;
const data = bin.length > 0 ? bin : undefined;
if (bin.length > 0 && bin.length < OP.OP_PUSHDATA1) {
op = bin.length;
}
else if (bin.length === 0) {
op = OP.OP_0;
}
else if (bin.length < Math.pow(2, 8)) {
op = OP.OP_PUSHDATA1;
}
else if (bin.length < Math.pow(2, 16)) {
op = OP.OP_PUSHDATA2;
}
else if (bin.length < Math.pow(2, 32)) {
op = OP.OP_PUSHDATA4;
}
else {
throw new Error("You can't push that much data");
}
this.chunks.push({
data,
op
});
return this;
}
/**
* @method writeNumber
* Appends a number to the script.
* @param num - The number to append.
* @returns This script instance for chaining.
*/
writeNumber(num) {
this.invalidateSerializationCaches();
this.writeBn(new BigNumber(num));
return this;
}
/**
* @method removeCodeseparators
* Removes all OP_CODESEPARATOR opcodes from the script.
* @returns This script instance for chaining.
*/
removeCodeseparators() {
this.invalidateSerializationCaches();
const chunks = [];
for (let i = 0; i < this.chunks.length; i++) {
if (this.chunks[i].op !== OP.OP_CODESEPARATOR) {
chunks.push(this.chunks[i]);
}
}
this.chunks = chunks;
return this;
}
/**
* Deletes the given item wherever it appears in the current script.
*
* @param script - The script containing the item to delete from the current script.
*
* @returns This script instance for chaining.
*/
findAndDelete(script) {
this.invalidateSerializationCaches();
const buf = script.toHex();
for (let i = 0; i < this.chunks.length; i++) {
const script2 = new Script([this.chunks[i]]);
const buf2 = script2.toHex();
if (buf === buf2) {
this.chunks.splice(i, 1);
}
}
return this;
}
/**
* @method isPushOnly
* Checks if the script contains only push data operations.
* @returns True if the script is push-only, otherwise false.
*/
isPushOnly() {
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
const opCodeNum = chunk.op;
if (opCodeNum > OP.OP_16) {
return false;
}
}
return true;
}
/**
* @method isLockingScript
* Determines if the script is a locking script.
* @returns True if the script is a locking script, otherwise false.
*/
isLockingScript() {
throw new Error('Not implemented');
}
/**
* @method isUnlockingScript
* Determines if the script is an unlocking script.
* @returns True if the script is an unlocking script, otherwise false.
*/
isUnlockingScript() {
throw new Error('Not implemented');
}
/**
* @private
* @method _chunkToString
* Converts a script chunk to its string representation.
* @param chunk - The script chunk.
* @returns The string representation of the chunk.
*/
static computeSerializedLength(chunks) {
let total = 0;
for (const chunk of chunks) {
total += 1;
if (chunk.data == null)
continue;
const len = chunk.data.length;
if (chunk.op === OP.OP_RETURN) {
total += len;
break;
}
if (chunk.op < OP.OP_PUSHDATA1) {
total += len;
}
else if (chunk.op === OP.OP_PUSHDATA1) {
total += 1 + len;
}
else if (chunk.op === OP.OP_PUSHDATA2) {
total += 2 + len;
}
else if (chunk.op === OP.OP_PUSHDATA4) {
total += 4 + len;
}
}
return total;
}
serializeChunksToBytes() {
const chunks = this.chunks;
const totalLength = Script.computeSerializedLength(chunks);
const bytes = new Uint8Array(totalLength);
let offset = 0;
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
bytes[offset++] = chunk.op;
if (chunk.data == null)
continue;
if (chunk.op === OP.OP_RETURN) {
bytes.set(chunk.data, offset);
offset += chunk.data.length;
break;
}
offset = Script.writeChunkData(bytes, offset, chunk.op, chunk.data);
}
return bytes;
}
invalidateSerializationCaches() {
this.rawBytesCache = undefined;
this.hexCache = undefined;
}
static writeChunkData(target, offset, op, data) {
const len = data.length;
if (op < OP.OP_PUSHDATA1) {
target.set(data, offset);
return offset + len;
}
else if (op === OP.OP_PUSHDATA1) {
target[offset++] = len & 0xff;
target.set(data, offset);
return offset + len;
}
else if (op === OP.OP_PUSHDATA2) {
target[offset++] = len & 0xff;
target[offset++] = (len >> 8) & 0xff;
target.set(data, offset);
return offset + len;
}
else if (op === OP.OP_PUSHDATA4) {
const size = len >>> 0;
target[offset++] = size & 0xff;
target[offset++] = (size >> 8) & 0xff;
target[offset++] = (size >> 16) & 0xff;
target[offset++] = (size >> 24) & 0xff;
target.set(data, offset);
return offset + len;
}
return offset;
}
static parseChunks(bytes) {
const chunks = [];
const length = bytes.length;
let pos = 0;
let inConditionalBlock = 0;
while (pos < length) {
const op = bytes[pos++] ?? 0;
if (op === OP.OP_RETURN && inConditionalBlock === 0) {
chunks.push({
op,
data: Script.copyRange(bytes, pos, length)
});
break;
}
if (op === OP.OP_IF ||
op === OP.OP_NOTIF ||
op === OP.OP_VERIF ||
op === OP.OP_VERNOTIF) {
inConditionalBlock++;
}
else if (op === OP.OP_ENDIF) {
inConditionalBlock--;
}
if (op > 0 && op < OP.OP_PUSHDATA1) {
const len = op;
const end = Math.min(pos + len, length);
chunks.push({
data: Script.copyRange(bytes, pos, end),
op
});
pos = end;
}
else if (op === OP.OP_PUSHDATA1) {
const len = pos < length ? bytes[pos++] ?? 0 : 0;
const end = Math.min(pos + len, length);
chunks.push({
data: Script.copyRange(bytes, pos, end),
op
});
pos = end;
}
else if (op === OP.OP_PUSHDATA2) {
const b0 = bytes[pos] ?? 0;
const b1 = bytes[pos + 1] ?? 0;
const len = b0 | (b1 << 8);
pos = Math.min(pos + 2, length);
const end = Math.min(pos + len, length);
chunks.push({
data: Script.copyRange(bytes, pos, end),
op
});
pos = end;
}
else if (op === OP.OP_PUSHDATA4) {
const len = ((bytes[pos] ?? 0) |
((bytes[pos + 1] ?? 0) << 8) |
((bytes[pos + 2] ?? 0) << 16) |
((bytes[pos + 3] ?? 0) << 24)) >>>
0;
pos = Math.min(pos + 4, length);
const end = Math.min(pos + len, length);
chunks.push({
data: Script.copyRange(bytes, pos, end),
op
});
pos = end;
}
else {
chunks.push({ op });
}
}
return chunks;
}
static copyRange(bytes, start, end) {
const size = Math.max(end - start, 0);
const data = new Array(size);
for (let i = 0; i < size; i++) {
data[i] = bytes[start + i] ?? 0;
}
return data;
}
_chunkToString(chunk) {
const op = chunk.op;
let str = '';
if (typeof chunk.data === 'undefined') {
const val = OP[op];
str = `${str} ${val}`;
}
else {
str = `${str} ${toHex(chunk.data)}`;
}
return str;
}
}
//# sourceMappingURL=Script.js.map