UNPKG

j-bitcoin

Version:

Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features

299 lines (258 loc) 9.06 kB
/** * @fileoverview Transaction Parser * @description Parse raw Bitcoin transaction hex back into structured objects * @version 1.0.0 * @author yfbsei * @license ISC */ /** * Custom error for transaction parsing * @class TransactionParseError * @extends Error */ class TransactionParseError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'TransactionParseError'; this.code = code; this.details = details; } } /** * Transaction Parser class * @class TransactionParser */ class TransactionParser { /** * Parse transaction from hex string * @param {string} hex - Raw transaction hex * @returns {Object} Parsed transaction object */ static fromHex(hex) { const buffer = Buffer.from(hex, 'hex'); return this.fromBuffer(buffer); } /** * Parse transaction from buffer * @param {Buffer} buffer - Raw transaction bytes * @returns {Object} Parsed transaction object */ static fromBuffer(buffer) { let offset = 0; // Version (4 bytes, LE) const version = buffer.readInt32LE(offset); offset += 4; // Check for witness marker let hasWitness = false; if (buffer[offset] === 0x00 && buffer[offset + 1] === 0x01) { hasWitness = true; offset += 2; } // Input count const inputCountResult = this._readVarInt(buffer, offset); const inputCount = inputCountResult.value; offset = inputCountResult.offset; // Parse inputs const inputs = []; for (let i = 0; i < inputCount; i++) { const inputResult = this._parseInput(buffer, offset); inputs.push(inputResult.input); offset = inputResult.offset; } // Output count const outputCountResult = this._readVarInt(buffer, offset); const outputCount = outputCountResult.value; offset = outputCountResult.offset; // Parse outputs const outputs = []; for (let i = 0; i < outputCount; i++) { const outputResult = this._parseOutput(buffer, offset); outputs.push(outputResult.output); offset = outputResult.offset; } // Parse witness data if present const witnesses = []; if (hasWitness) { for (let i = 0; i < inputCount; i++) { const witnessResult = this._parseWitness(buffer, offset); witnesses.push(witnessResult.items); offset = witnessResult.offset; } } // Locktime (4 bytes, LE) const locktime = buffer.readUInt32LE(offset); offset += 4; return { version, inputs, outputs, witnesses: hasWitness ? witnesses : [], locktime, hasWitness }; } /** * Parse a single input * @private */ static _parseInput(buffer, offset) { // txid (32 bytes, reversed) const txidBytes = buffer.slice(offset, offset + 32); const txid = Buffer.from(txidBytes).reverse().toString('hex'); offset += 32; // vout (4 bytes, LE) const vout = buffer.readUInt32LE(offset); offset += 4; // scriptSig length const scriptLenResult = this._readVarInt(buffer, offset); const scriptLen = scriptLenResult.value; offset = scriptLenResult.offset; // scriptSig const scriptSig = buffer.slice(offset, offset + scriptLen); offset += scriptLen; // sequence (4 bytes, LE) const sequence = buffer.readUInt32LE(offset); offset += 4; return { input: { txid, vout, scriptSig, sequence }, offset }; } /** * Parse a single output * @private */ static _parseOutput(buffer, offset) { // value (8 bytes, LE) const value = Number(buffer.readBigUInt64LE(offset)); offset += 8; // scriptPubKey length const scriptLenResult = this._readVarInt(buffer, offset); const scriptLen = scriptLenResult.value; offset = scriptLenResult.offset; // scriptPubKey const scriptPubKey = buffer.slice(offset, offset + scriptLen); offset += scriptLen; return { output: { value, scriptPubKey }, offset }; } /** * Parse witness data * @private */ static _parseWitness(buffer, offset) { const countResult = this._readVarInt(buffer, offset); const count = countResult.value; offset = countResult.offset; const items = []; for (let i = 0; i < count; i++) { const lenResult = this._readVarInt(buffer, offset); const len = lenResult.value; offset = lenResult.offset; items.push(buffer.slice(offset, offset + len)); offset += len; } return { items, offset }; } /** * Read variable-length integer * @private */ static _readVarInt(buffer, offset) { const first = buffer[offset]; if (first < 0xfd) { return { value: first, offset: offset + 1 }; } else if (first === 0xfd) { return { value: buffer.readUInt16LE(offset + 1), offset: offset + 3 }; } else if (first === 0xfe) { return { value: buffer.readUInt32LE(offset + 1), offset: offset + 5 }; } else { return { value: Number(buffer.readBigUInt64LE(offset + 1)), offset: offset + 9 }; } } /** * Get transaction ID from raw hex * @param {string} hex - Raw transaction hex * @returns {string} Transaction ID */ static getTxid(hex) { const { createHash } = require('node:crypto'); const buffer = Buffer.from(hex, 'hex'); // Remove witness data for txid calculation const tx = this.fromBuffer(buffer); const noWitnessHex = this.serializeWithoutWitness(tx); const hash = createHash('sha256') .update(createHash('sha256').update(noWitnessHex).digest()) .digest(); return hash.reverse().toString('hex'); } /** * Serialize transaction without witness (for txid) * @param {Object} tx - Parsed transaction * @returns {Buffer} Serialized bytes */ static serializeWithoutWitness(tx) { const parts = []; // Version const version = Buffer.alloc(4); version.writeInt32LE(tx.version, 0); parts.push(version); // Inputs parts.push(this._encodeVarInt(tx.inputs.length)); for (const input of tx.inputs) { parts.push(Buffer.from(input.txid, 'hex').reverse()); const vout = Buffer.alloc(4); vout.writeUInt32LE(input.vout, 0); parts.push(vout); const scriptSig = Buffer.isBuffer(input.scriptSig) ? input.scriptSig : Buffer.from(input.scriptSig, 'hex'); parts.push(this._encodeVarInt(scriptSig.length)); parts.push(scriptSig); const seq = Buffer.alloc(4); seq.writeUInt32LE(input.sequence, 0); parts.push(seq); } // Outputs parts.push(this._encodeVarInt(tx.outputs.length)); for (const output of tx.outputs) { const value = Buffer.alloc(8); value.writeBigUInt64LE(BigInt(output.value), 0); parts.push(value); const script = Buffer.isBuffer(output.scriptPubKey) ? output.scriptPubKey : Buffer.from(output.scriptPubKey, 'hex'); parts.push(this._encodeVarInt(script.length)); parts.push(script); } // Locktime const locktime = Buffer.alloc(4); locktime.writeUInt32LE(tx.locktime, 0); parts.push(locktime); return Buffer.concat(parts); } /** * Encode variable-length integer * @private */ static _encodeVarInt(n) { if (n < 0xfd) { return Buffer.from([n]); } else if (n <= 0xffff) { const buf = Buffer.alloc(3); buf[0] = 0xfd; buf.writeUInt16LE(n, 1); return buf; } else if (n <= 0xffffffff) { const buf = Buffer.alloc(5); buf[0] = 0xfe; buf.writeUInt32LE(n, 1); return buf; } else { const buf = Buffer.alloc(9); buf[0] = 0xff; buf.writeBigUInt64LE(BigInt(n), 1); return buf; } } } export { TransactionParser, TransactionParseError }; export default TransactionParser;