ecash-lib
Version:
Library for eCash transaction building
188 lines (187 loc) • 8.06 kB
JavaScript
;
// 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