charms-js
Version:
TypeScript SDK for decoding Bitcoin transactions containing Charms data
373 lines (372 loc) • 14.4 kB
JavaScript
;
/**
* 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)
};
}