UNPKG

charms-js

Version:

TypeScript SDK for decoding Bitcoin transactions containing Charms data

332 lines (331 loc) 12.8 kB
/** * WASM Integration for Charms.js * Provides a simplified API that uses WASM when available, falls back to current implementation */ import { extractAddress } from '../address.js'; // WASM module reference let wasmModule = null; /** * Initialize WASM module (to be called by consuming applications) * @param wasm - The loaded WASM module */ export function initializeWasm(wasm) { wasmModule = wasm; } /** * Check if WASM is available */ export 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 */ export 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, txHex); 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 using the extractAddress function */ function deriveAddressFromScriptPubKey(scriptPubKey, network = 'testnet4') { try { // Convert scriptPubKey hex to Buffer const scriptBuffer = Buffer.from(scriptPubKey, 'hex'); // Use the extractAddress function which supports P2TR (Taproot) with network parameter const address = extractAddress(scriptBuffer, network); return address; } catch (error) { // If extraction fails, return empty string return ''; } } /** * Convert WASM result to CharmObj array */ function convertWasmResultToCharms(wasmResult, txId, network = 'testnet4', txHex) { const charms = []; // Extract transaction hex for address derivation - use provided txHex or extract from app_public_inputs let transactionHex = txHex || null; // Build a map of appId -> app data (excluding transaction hex) const appDataMap = new Map(); if (wasmResult.app_public_inputs) { const entries = wasmResult.app_public_inputs instanceof Map ? Array.from(wasmResult.app_public_inputs.entries()) : Object.entries(wasmResult.app_public_inputs); for (const [appId, value] of entries) { const appData = {}; if (value && typeof value === 'object') { const valueEntries = value instanceof Map ? Array.from(value.entries()) : Object.entries(value); for (const [subKey, subValue] of valueEntries) { // Store transaction hex if found if (subKey === 'Bitcoin' && typeof subValue === 'string' && subValue.length > 100) { transactionHex = subValue; continue; // Don't include in app data } // Add to app data appData[subKey] = subValue; } } appDataMap.set(appId, appData); } } // If no app data found, return empty array if (appDataMap.size === 0) { return []; } // Process transaction outputs containing charm data if (wasmResult.tx && wasmResult.tx.outs && wasmResult.tx.outs.length > 0) { wasmResult.tx.outs.forEach((outputCharms, outputIndex) => { const charmEntries = outputCharms instanceof Map ? Array.from(outputCharms.entries()) : Object.entries(outputCharms); if (charmEntries.length === 0) { return; } // Extract the Bitcoin address for this output let address = ''; let actualOutputIndex = outputIndex; try { if (transactionHex) { let scriptPubKey = extractScriptPubKeyFromTxHex(transactionHex, outputIndex); if (scriptPubKey) { // OP_RETURN outputs (0x6a) cannot receive funds, so we find an alternative output if (scriptPubKey.startsWith('6a')) { for (let i = 0; i < 10; i++) { if (i === outputIndex) continue; const altScriptPubKey = extractScriptPubKeyFromTxHex(transactionHex, i); if (altScriptPubKey && !altScriptPubKey.startsWith('6a')) { address = deriveAddressFromScriptPubKey(altScriptPubKey, network); actualOutputIndex = i; if (address) break; } } } else { address = deriveAddressFromScriptPubKey(scriptPubKey, network); } } } } catch (error) { // Continue with empty address if extraction fails } // Create charm objects for each app in this output for (const [appIndexOrId, charmData] of charmEntries) { let appId; let appData = {}; // Resolve app ID from index or use directly const appIndex = typeof appIndexOrId === 'number' ? appIndexOrId : parseInt(appIndexOrId); if (!isNaN(appIndex)) { const appIds = Array.from(appDataMap.keys()); if (appIndex < appIds.length) { appId = appIds[appIndex]; appData = appDataMap.get(appId) || {}; } else { continue; } } else { appId = appIndexOrId; appData = appDataMap.get(appId) || {}; } // Extract charm amount from various possible formats let amount = 0; if (typeof charmData === 'number') { amount = charmData; } else if (charmData && typeof charmData === 'object') { if (typeof charmData.amount === 'number') { amount = charmData.amount; } else if (typeof charmData.remaining === 'number') { amount = charmData.remaining; } else if (typeof charmData.value === 'number') { amount = charmData.value; } // Merge charm-specific metadata into app data if (charmData instanceof Map) { for (const [k, v] of charmData.entries()) { if (k !== 'amount' && k !== 'remaining' && k !== 'value') { appData[k] = v; } } } else { for (const [k, v] of Object.entries(charmData)) { if (k !== 'amount' && k !== 'remaining' && k !== 'value') { appData[k] = v; } } } } // Create CharmObj 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: outputIndex, txid: txId, address: address }; charms.push(charm); } }); } charms.sort((a, b) => a.outputIndex - b.outputIndex); return charms; } /** * Returns diagnostic information about the WASM module state. * Useful for troubleshooting initialization issues. * * @returns Object containing availability status and module metadata */ export function getWasmInfo() { if (!wasmModule) { return { available: false, module: null }; } return { available: true, hasExtractAndVerifySpell: typeof wasmModule.extractAndVerifySpell === 'function', moduleKeys: Object.keys(wasmModule) }; }