UNPKG

ecash-lib

Version:

Library for eCash transaction building

188 lines (187 loc) 8.06 kB
"use strict"; // Copyright (c) 2025 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. Object.defineProperty(exports, "__esModule", { value: true }); exports.consumeNextPush = exports.consume = exports.swapEndianness = exports.getStackArray = void 0; const consts_js_1 = require("../consts.js"); const hex_js_1 = require("../io/hex.js"); const opcode_js_1 = require("../opcode.js"); /** * Convert an OP_RETURN outputScript into an array of pushes * @param outputScript - An OP_RETURN output script, e.g. 6a042e7865630003333333150076458db0ed96fe9863fc1ccec9fa2cfab884b0f6 * @returns An array of hex pushes, e.g. ['2e786563', '00', '333333', '0076458db0ed96fe9863fc1ccec9fa2cfab884b0f6'] * @throws Error if outputScript is not a valid OP_RETURN outputScript */ function getStackArray(outputScript) { const opReturnHex = (0, hex_js_1.toHex)(new Uint8Array([opcode_js_1.OP_RETURN])); // Validate for OP_RETURN outputScript if (typeof outputScript !== 'string' || !outputScript.startsWith(opReturnHex)) { throw new Error(`outputScript must be a string that starts with ${opReturnHex}`); } if (outputScript.length > 2 * consts_js_1.OP_RETURN_MAX_BYTES) { throw new Error(`Invalid eCash OP_RETURN size: ${outputScript.length / 2} bytes. eCash OP_RETURN outputs cannot exceed ${consts_js_1.OP_RETURN_MAX_BYTES} bytes.`); } // Create stack, the input object required by consumeNextPush const stack = { remainingHex: outputScript.slice(opReturnHex.length), }; // Initialize stackArray const stackArray = []; while (stack.remainingHex.length > 0) { stackArray.push(consumeNextPush(stack).data); } return stackArray; } exports.getStackArray = getStackArray; /** * One-byte stack additions that can be pushed to OP_RETURN in isolation */ const ONE_BYTE_STACK_ADDS = [ opcode_js_1.OP_0, opcode_js_1.OP_1NEGATE, opcode_js_1.OP_RESERVED, opcode_js_1.OP_1, opcode_js_1.OP_2, opcode_js_1.OP_3, opcode_js_1.OP_4, opcode_js_1.OP_5, opcode_js_1.OP_6, opcode_js_1.OP_7, opcode_js_1.OP_8, opcode_js_1.OP_9, opcode_js_1.OP_10, opcode_js_1.OP_11, opcode_js_1.OP_12, opcode_js_1.OP_13, opcode_js_1.OP_14, opcode_js_1.OP_15, opcode_js_1.OP_16, ]; /** * One-byte pushdata opcodes (0x01-0x4b) */ const ONE_BYTE_PUSHDATAS = []; for (let i = 1; i <= 0x4b; i++) { ONE_BYTE_PUSHDATAS.push(i); } /** * Swap endianness of a hex string * @param hexString a string of hex bytes, e.g. 04000000 * @returns a string of hex bytes with swapped endianness, e.g. for 04000000, returns 00000004 */ function swapEndianness(hexString) { const byteLength = 2; if (hexString.length % byteLength === 1) { throw new Error(`Invalid input length ${hexString.length}: hexString must be divisible by bytes, i.e. have an even length.`); } // Check if input contains only hex characters if (!/^[\da-f]+$/i.test(hexString)) { throw new Error(`Invalid input. ${hexString} contains non-hexadecimal characters.`); } let swappedEndianHexString = ''; let remainingHex = hexString; while (remainingHex.length > 0) { // Get the last byte on the string const thisByte = remainingHex.slice(-byteLength); // Add thisByte to swappedEndianHexString in swapped-endian order swappedEndianHexString += thisByte; // Remove thisByte from remainingHex remainingHex = remainingHex.slice(0, -byteLength); } return swappedEndianHexString; } exports.swapEndianness = swapEndianness; /** * Consume a specified number of bytes from a stack object * @param stack an object containing a hex string outputScript of an eCash tx, e.g. {remainingHex: '6a...'} * @param byteCount integer * @returns consumed, a hex string of byteCount bytes * The stack object is modified in place so that consumed bytes are removed */ function consume(stack, byteCount) { // Validation for stack if (typeof stack !== 'object' || typeof stack.remainingHex !== 'string') { throw new Error('Invalid input. Stack must be an object with string stored at key remainingHex.'); } if (stack.remainingHex.length % 2 === 1) { throw new Error('Invalid input: stack.remainingHex must be divisible by bytes, i.e. have an even length.'); } // Throw an error if byteCount input is not an integer if (!Number.isInteger(byteCount)) { throw new Error(`byteCount must be an integer, received ${byteCount}`); } // One byte is 2 characters of a hex string const byteLength = 2; // Get byte slice size const byteSliceSize = byteCount * byteLength; // Throw an error if byteCount is greater than consumable hex bytes in outputScript if (byteSliceSize > stack.remainingHex.length) { throw new Error(`consume called with byteCount (${byteCount}) greater than remaining bytes in outputScript (${stack.remainingHex.length / byteLength})`); } // Get consumed bytes const consumed = stack.remainingHex.slice(0, byteSliceSize); // Remove consumed from the stack stack.remainingHex = stack.remainingHex.slice(byteSliceSize); return consumed; } exports.consume = consume; /** * Parse, decode and consume the data push from the top of the stack. * If the stack does not start with a valid push, it raises an error and the stack is left untouched. * @param stack an object containing a hex string outputScript of an eCash tx, e.g. {remainingHex: '4d...'} * @returns {data, pushedWith} * stack is modified in place so that the push is removed */ function consumeNextPush(stack) { // Clone stack in case you have an error and wish to leave it unmodified const clonedStack = structuredClone(stack); // Get the first byte on the stack const pushOpCodeHex = consume(clonedStack, 1); const pushOpCode = parseInt(pushOpCodeHex, 16); if (ONE_BYTE_STACK_ADDS.includes(pushOpCode)) { // If this is a one-byte push, consume stack and return the byte stack.remainingHex = clonedStack.remainingHex; return { data: pushOpCodeHex, pushedWith: pushOpCodeHex }; } // Initialize variables let pushBytecountHex; // Apply conditional checks to determine the size of this push if (ONE_BYTE_PUSHDATAS.includes(pushOpCode)) { // If the first byte on the stack is 0x01-0x4b, then this is pushedBytesHex pushBytecountHex = pushOpCodeHex; } else if (pushOpCode === opcode_js_1.OP_PUSHDATA1) { // The next byte contains the number of bytes to be pushed onto the stack. pushBytecountHex = consume(clonedStack, 1); } else if (pushOpCode === opcode_js_1.OP_PUSHDATA2) { // The next two bytes contain the number of bytes to be pushed onto the stack in little endian order. pushBytecountHex = consume(clonedStack, 2); } else if (pushOpCode === opcode_js_1.OP_PUSHDATA4) { // The next four bytes contain the number of bytes to be pushed onto the stack in little endian order. pushBytecountHex = consume(clonedStack, 4); } else { throw new Error(`${pushOpCodeHex} is invalid pushdata`); } // Now that you know how many bytes are in the push, get the pushed data const data = consume(clonedStack, parseInt(swapEndianness(pushBytecountHex), 16)); // If no error, consume stack stack.remainingHex = clonedStack.remainingHex; /* Return {data, pushedWith} Note that if the first byte on the stack is 0x01-0x4b, this is both pushOpCode and pushBytecountHex You don't want to return '0404' for e.g. '042e786563' Conditionally remove pushBytecountHex for this case */ return { data, pushedWith: `${pushOpCodeHex}${pushOpCodeHex !== pushBytecountHex ? pushBytecountHex : ''}`, }; } exports.consumeNextPush = consumeNextPush; //# sourceMappingURL=opreturn.js.map