UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

457 lines (427 loc) 12.3 kB
import ScriptChunk from './ScriptChunk.js' import OP from './OP.js' import { encode, toHex, Reader, Writer, 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. */ export default class Script { chunks: ScriptChunk[] /** * @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: string): Script { const chunks: ScriptChunk[] = [] const tokens = asm.split(' ') let i = 0 while (i < tokens.length) { const token = tokens[i] let opCode let opCodeNum: number = 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: string): Script { 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.') } return Script.fromBinary(toArray(hex, 'hex')) } /** * @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: number[]): Script { bin = [...bin] const chunks: ScriptChunk[] = [] let inConditionalBlock: number = 0 const br = new Reader(bin) while (!br.eof()) { const op = br.readUInt8() // if OP_RETURN and not in a conditional block, do not parse the rest of the data, // rather just return the last chunk as data without prefixing with data length. if (op === OP.OP_RETURN && inConditionalBlock === 0) { chunks.push({ op, data: br.read() }) 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-- } let len = 0 // eslint-disable-next-line @typescript-eslint/no-shadow let data: number[] = [] if (op > 0 && op < OP.OP_PUSHDATA1) { len = op chunks.push({ data: br.read(len), op }) } else if (op === OP.OP_PUSHDATA1) { try { len = br.readUInt8() data = br.read(len) } catch { br.read() } chunks.push({ data, op }) } else if (op === OP.OP_PUSHDATA2) { try { len = br.readUInt16LE() data = br.read(len) } catch { br.read() } chunks.push({ data, op }) } else if (op === OP.OP_PUSHDATA4) { try { len = br.readUInt32LE() data = br.read(len) } catch { br.read() } chunks.push({ data, op }) } else { chunks.push({ op }) } } return new Script(chunks) } /** * @constructor * Constructs a new Script object. * @param chunks=[] - An array of script chunks to directly initialize the script. */ constructor (chunks: ScriptChunk[] = []) { this.chunks = chunks } /** * @method toASM * Serializes the script to an ASM formatted string. * @returns The script in ASM string format. */ toASM (): string { 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 (): string { return encode(this.toBinary(), 'hex') as string } /** * @method toBinary * Serializes the script to a binary array. * @returns The script in binary array format. */ toBinary (): number[] { const writer = new Writer() for (let i = 0; i < this.chunks.length; i++) { const chunk = this.chunks[i] const op = chunk.op writer.writeUInt8(op) if (op === OP.OP_RETURN && chunk.data != null) { // special case for unformatted data writer.write(chunk.data) break } else if (chunk.data != null) { if (op < OP.OP_PUSHDATA1) { writer.write(chunk.data) } else if (op === OP.OP_PUSHDATA1) { writer.writeUInt8(chunk.data.length) writer.write(chunk.data) } else if (op === OP.OP_PUSHDATA2) { writer.writeUInt16LE(chunk.data.length) writer.write(chunk.data) } else if (op === OP.OP_PUSHDATA4) { writer.writeUInt32LE(chunk.data.length) writer.write(chunk.data) } } } return writer.toArray() } /** * @method writeScript * Appends another script to this script. * @param script - The script to append. * @returns This script instance for chaining. */ writeScript (script: Script): Script { 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: number): Script { 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: number, op: number): Script { 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: BigNumber): Script { 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: number[]): Script { let op 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: bin, 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: number): Script { this.writeBn(new BigNumber(num)) return this } /** * @method removeCodeseparators * Removes all OP_CODESEPARATOR opcodes from the script. * @returns This script instance for chaining. */ removeCodeseparators (): Script { const chunks: ScriptChunk[] = [] 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: Script): Script { 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 (): boolean { 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 (): boolean { 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 (): boolean { 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. */ private _chunkToString (chunk: ScriptChunk): string { const op = chunk.op let str = '' if (typeof chunk.data === 'undefined') { const val = OP[op] as string str = `${str} ${val}` } else { str = `${str} ${toHex(chunk.data)}` } return str } }