UNPKG

charms-js

Version:

TypeScript SDK for decoding Bitcoin transactions containing Charms data

373 lines (372 loc) 14.4 kB
"use strict"; /** * WASM Integration for Charms.js * Provides a simplified API that uses WASM when available, falls back to current implementation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.initializeWasm = initializeWasm; exports.isWasmAvailable = isWasmAvailable; exports.extractCharmsWithWasm = extractCharmsWithWasm; exports.getWasmInfo = getWasmInfo; // WASM module reference let wasmModule = null; /** * Initialize WASM module (to be called by consuming applications) * @param wasm - The loaded WASM module */ function initializeWasm(wasm) { wasmModule = wasm; } /** * Check if WASM is available */ function isWasmAvailable() { return wasmModule !== null && typeof wasmModule.extractAndVerifySpell === 'function'; } /** * Extract charms using WASM (when available) * @param txHex - Transaction hex string * @param txId - Transaction ID * @returns Standardized CharmExtractionResult */ async function extractCharmsWithWasm(txHex, txId, network = 'testnet4') { if (!isWasmAvailable()) { return { success: false, charms: [], error: 'WASM module not initialized. Call initializeWasm() first.' }; } try { const param = { "Bitcoin": txHex }; // Try with mock mode enabled first (for test transactions) let wasmResult; try { wasmResult = await wasmModule.extractAndVerifySpell(param, true); // mock mode = true } catch (mockError) { try { wasmResult = await wasmModule.extractAndVerifySpell(param, false); // mock mode = false } catch (prodError) { // Both modes failed - this is expected for transactions without charms return { success: true, charms: [], message: 'No charms found in transaction' }; } } // Debug the structure of app_public_inputs if (!wasmResult || !wasmResult.app_public_inputs || !wasmResult.tx) { return { success: true, charms: [], message: 'No valid charm data found in transaction' }; } const charmObjects = convertWasmResultToCharms(wasmResult, txId, network); return { success: true, charms: charmObjects, message: charmObjects.length > 0 ? `Found ${charmObjects.length} charm(s)` : 'No charms found in transaction' }; } catch (error) { return { success: false, charms: [], error: `WASM extraction failed: ${error.message}` }; } } /** * Find transaction hex in app data */ function findTransactionHexInAppData(appData) { // Look for Bitcoin field that contains transaction hex if (appData.Bitcoin && typeof appData.Bitcoin === 'string' && appData.Bitcoin.length > 100) { return appData.Bitcoin; } // Look for other fields that might contain transaction hex for (const [key, value] of Object.entries(appData)) { if (typeof value === 'string' && value.length > 100 && /^[0-9a-fA-F]+$/.test(value)) { return value; } } return null; } /** * Extract scriptPubKey from transaction hex for a specific output index */ function extractScriptPubKeyFromTxHex(txHex, outputIndex) { try { // Parse Bitcoin transaction structure properly // Structure: version(4) + input_count(varint) + inputs + output_count(varint) + outputs + locktime(4) let pos = 0; // Skip version (4 bytes = 8 hex chars) pos += 8; // Skip witness flag if present (00 01) if (txHex.slice(pos, pos + 4) === '0001') { pos += 4; } // Read input count (varint) const inputCount = parseInt(txHex.slice(pos, pos + 2), 16); pos += 2; // Skip all inputs for (let i = 0; i < inputCount; i++) { // Skip txid (32 bytes = 64 hex chars) pos += 64; // Skip vout (4 bytes = 8 hex chars) pos += 8; // Read script length const scriptLen = parseInt(txHex.slice(pos, pos + 2), 16); pos += 2; // Skip script pos += scriptLen * 2; // Skip sequence (4 bytes = 8 hex chars) pos += 8; } // Read output count const outputCount = parseInt(txHex.slice(pos, pos + 2), 16); pos += 2; // Parse outputs to find the one we want for (let i = 0; i < outputCount; i++) { // Read amount (8 bytes = 16 hex chars) const amount = txHex.slice(pos, pos + 16); pos += 16; // Read script length const scriptLen = parseInt(txHex.slice(pos, pos + 2), 16); pos += 2; // Read script const script = txHex.slice(pos, pos + scriptLen * 2); pos += scriptLen * 2; if (i === outputIndex) { return script; } } return null; } catch (error) { return null; } } /** * Derive address from scriptPubKey hex */ function deriveAddressFromScriptPubKey(scriptPubKey, network = 'testnet4') { try { // Basic scriptPubKey parsing for common types if (scriptPubKey.length === 68 && scriptPubKey.startsWith('5120')) { // P2TR (Taproot) - OP_1 + 32 bytes const witnessProgram = scriptPubKey.slice(4); const prefix = network === 'mainnet' ? 'bc1p' : network === 'testnet4' ? 'tb1p' : 'bcrt1p'; // Direct witness program to address conversion return `${prefix}${witnessProgram}`; } else if (scriptPubKey.length === 44 && scriptPubKey.startsWith('0014')) { // P2WPKH - OP_0 + 20 bytes const witnessProgram = scriptPubKey.slice(4); const prefix = network === 'mainnet' ? 'bc1q' : network === 'testnet4' ? 'tb1q' : 'bcrt1q'; return `${prefix}${witnessProgram}`; } else { return ''; } } catch (error) { return ''; } } /** * Convert WASM result to CharmObj array */ function convertWasmResultToCharms(wasmResult, txId, network = 'testnet4') { const charms = []; // Check if app_public_inputs has any charm data let hasCharmData = false; let appData = {}; let appId = null; let transactionHex = null; // Store transaction hex for address extraction if (wasmResult.app_public_inputs instanceof Map) { // Iterate through the Map entries for (const [key, value] of wasmResult.app_public_inputs.entries()) { hasCharmData = true; appId = key; // Process the value to extract app data if (typeof value === 'object' && value !== null) { // Store transaction hex before filtering for address extraction for (const [subKey, subValue] of Object.entries(value)) { if (subKey === 'Bitcoin' && typeof subValue === 'string' && subValue.length > 100) { transactionHex = subValue; // Exclude transaction hex from app data continue; } if (subValue instanceof Map) { const filteredValue = { ...subValue }; if (filteredValue.Bitcoin && typeof filteredValue.Bitcoin === 'string' && filteredValue.Bitcoin.length > 100) { delete filteredValue.Bitcoin; } Object.assign(appData, filteredValue); } } } } } else if (typeof wasmResult.app_public_inputs === 'object') { const keys = Object.keys(wasmResult.app_public_inputs); if (keys.length > 0) { hasCharmData = true; // Extract from Object for (const [key, value] of Object.entries(wasmResult.app_public_inputs)) { if (!appId) { appId = key; } if (value && typeof value === 'object') { // Store transaction hex before filtering for address extraction for (const [subKey, subValue] of Object.entries(value)) { if (subKey === 'Bitcoin' && typeof subValue === 'string' && subValue.length > 100) { transactionHex = subValue; } } // Remove transaction hex from app data const filteredValue = { ...value }; if (filteredValue.Bitcoin && typeof filteredValue.Bitcoin === 'string' && filteredValue.Bitcoin.length > 100) { delete filteredValue.Bitcoin; } Object.assign(appData, filteredValue); } } } } // If no charm data found, return empty array if (!hasCharmData || !appId) { return []; } // Process transaction outputs to create CharmObj instances if (wasmResult.tx && wasmResult.tx.outs && wasmResult.tx.outs.length > 0) { wasmResult.tx.outs.forEach((output, index) => { // Process output // Extract amount from output - handle Map and Object structures let amount = 0; if (output instanceof Map) { // Extract amount from Map // For Map, look for numeric values for (const [key, val] of output.entries()) { if (typeof val === 'number' && val > 0) { // Found amount amount = val; break; } } } else if (typeof output === 'object' && output !== null) { // Try common field names for amount if (typeof output.amount === 'number') { amount = output.amount; } else if (typeof output.value === 'number') { amount = output.value; } else if (typeof output.satoshis === 'number') { amount = output.satoshis; } else if (typeof output.sats === 'number') { amount = output.sats; } else { // Search all numeric fields as fallback for (const [key, val] of Object.entries(output)) { if (typeof val === 'number' && val > 0) { amount = val; break; } } } } // Amount extracted // Extract address from transaction hex let address = ''; // Extract address from transaction try { // Use the stored transaction hex for address extraction if (transactionHex) { // Parse transaction for address const scriptPubKey = extractScriptPubKeyFromTxHex(transactionHex, index); // Got scriptPubKey if (scriptPubKey) { // Check if this is an OP_RETURN script (starts with 6a) if (scriptPubKey.startsWith('6a')) { // Search for the first non-OP_RETURN output with funds for (let i = 0; i < 10; i++) { // Check up to 10 outputs if (i === index) continue; // Skip the current OP_RETURN output const altScriptPubKey = extractScriptPubKeyFromTxHex(transactionHex, i); if (altScriptPubKey && !altScriptPubKey.startsWith('6a')) { address = deriveAddressFromScriptPubKey(altScriptPubKey, network); if (address) { break; } } } } else { // Normal address derivation for non-OP_RETURN scripts address = deriveAddressFromScriptPubKey(scriptPubKey, network); } // Address derived } else { // Could not extract scriptPubKey } } else { // No transaction hex found } } catch (error) { // Error extracting address } // Final address extracted // Only create charm if we have valid charm data (not just empty outputs) if (!hasCharmData && amount === 0) { // Skip empty output return; } // Create CharmObj with the extracted data const charm = { appId: appId, amount: amount, version: wasmResult.version, metadata: { ticker: appData.ticker, name: appData.name, description: appData.description, image: appData.image, image_hash: appData.image_hash, url: appData.url }, app: appData, outputIndex: index, txid: txId, address: address }; // Charm created charms.push(charm); }); } return charms; } /** * Get WASM module info for debugging */ function getWasmInfo() { if (!wasmModule) { return { available: false, module: null }; } return { available: true, hasExtractAndVerifySpell: typeof wasmModule.extractAndVerifySpell === 'function', moduleKeys: Object.keys(wasmModule) }; }