UNPKG

ecash-lib

Version:

Library for eCash transaction building

257 lines (230 loc) 8.15 kB
// 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. import { OP_RETURN_MAX_BYTES } from '../consts.js'; import { toHex } from '../io/hex.js'; import { OP_RETURN, OP_0, OP_1NEGATE, OP_RESERVED, OP_1, OP_2, OP_3, OP_4, OP_5, OP_6, OP_7, OP_8, OP_9, OP_10, OP_11, OP_12, OP_13, OP_14, OP_15, OP_16, OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, } from '../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 */ export function getStackArray(outputScript: string): string[] { const opReturnHex = toHex(new Uint8Array([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 * OP_RETURN_MAX_BYTES) { throw new Error( `Invalid eCash OP_RETURN size: ${ outputScript.length / 2 } bytes. eCash OP_RETURN outputs cannot exceed ${OP_RETURN_MAX_BYTES} bytes.`, ); } // Create stack, the input object required by consumeNextPush const stack = { remainingHex: outputScript.slice(opReturnHex.length), }; // Initialize stackArray const stackArray: string[] = []; while (stack.remainingHex.length > 0) { stackArray.push(consumeNextPush(stack).data); } return stackArray; } /** * One-byte stack additions that can be pushed to OP_RETURN in isolation */ const ONE_BYTE_STACK_ADDS = [ OP_0, OP_1NEGATE, OP_RESERVED, OP_1, OP_2, OP_3, OP_4, OP_5, OP_6, OP_7, OP_8, OP_9, OP_10, OP_11, OP_12, OP_13, OP_14, OP_15, OP_16, ]; /** * One-byte pushdata opcodes (0x01-0x4b) */ const ONE_BYTE_PUSHDATAS: number[] = []; 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 */ export function swapEndianness(hexString: string): string { 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; } /** * 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 */ export function consume( stack: { remainingHex: string }, byteCount: number, ): string { // 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; } /** * 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 */ export function consumeNextPush(stack: { remainingHex: string }): { data: string; pushedWith: string; } { // 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: string; // 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 === OP_PUSHDATA1) { // The next byte contains the number of bytes to be pushed onto the stack. pushBytecountHex = consume(clonedStack, 1); } else if (pushOpCode === 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 === 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 : '' }`, }; }