UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

565 lines 18.5 kB
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