UNPKG

charms-wallet-js

Version:

Professional Bitcoin wallet library for Charms ecosystem using @scure stack

292 lines 13.4 kB
"use strict"; // Transaction Builder for Charms Wallet JS var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TransactionBuilder = void 0; const btc = __importStar(require("@scure/btc-signer")); const base_1 = require("@scure/base"); const constants_1 = require("../constants"); const types_1 = require("../types"); /** * Professional transaction builder using @scure/btc-signer * Supports Taproot, SegWit, and legacy transaction types */ class TransactionBuilder { constructor(network = constants_1.DEFAULT_NETWORK) { this.network = network; } /** * Creates a transaction with OP_RETURN mining data * Optimized for Charms mining operations */ async createMiningTransaction(params) { try { this.validateMiningParams(params); const { utxo, miningData, changeAddress, network = this.network, feeRate = constants_1.TRANSACTION_CONSTANTS.DEFAULT_FEE_RATE } = params; // Get network configuration for @scure this.getNetworkConfig(network); // Create transaction using @scure/btc-signer const tx = new btc.Transaction(); // Add input const inputHash = base_1.hex.decode(utxo.txid).reverse(); // Reverse for little-endian tx.addInput({ txid: inputHash, index: utxo.vout, witnessUtxo: { script: this.createScriptPubKey(utxo.address, network), amount: BigInt(utxo.amount) } }); // Create optimized OP_RETURN data const opReturnData = this.createMiningOpReturn(miningData); // Add OP_RETURN output tx.addOutput({ script: btc.Script.encode([btc.OP.RETURN, opReturnData]), amount: 0n }); // Calculate fee const estimatedSize = this.estimateTransactionSize(1, 2); // 1 input, 2 outputs const fee = feeRate * estimatedSize; const changeAmount = utxo.amount - fee; // Validate change amount if (changeAmount < constants_1.TRANSACTION_CONSTANTS.DUST_THRESHOLD) { throw new types_1.TransactionError(`Change amount (${changeAmount}) below dust threshold (${constants_1.TRANSACTION_CONSTANTS.DUST_THRESHOLD})`); } // Add change output tx.addOutput({ script: this.createScriptPubKey(changeAddress, network), amount: BigInt(changeAmount) }); // Return PSBT hex for signing return base_1.hex.encode(tx.toPSBT()); } catch (error) { if (error instanceof types_1.ValidationError || error instanceof types_1.TransactionError) { throw error; } throw new types_1.TransactionError(`Failed to create mining transaction: ${error instanceof Error ? error.message : 'Unknown error'}`, { params, error }); } } /** * Creates a simple Bitcoin transaction */ async createSimpleTransaction(params) { try { this.validateSimpleParams(params); const { utxos, outputs, changeAddress, network = this.network, feeRate = constants_1.TRANSACTION_CONSTANTS.DEFAULT_FEE_RATE } = params; // Get network configuration this.getNetworkConfig(network); // Create transaction const tx = new btc.Transaction(); // Add inputs for (const utxo of utxos) { const inputHash = base_1.hex.decode(utxo.txid).reverse(); tx.addInput({ txid: inputHash, index: utxo.vout, witnessUtxo: { script: this.createScriptPubKey(utxo.address, network), amount: BigInt(utxo.amount) } }); } // Add outputs for (const output of outputs) { tx.addOutput({ script: this.createScriptPubKey(output.address, network), amount: BigInt(output.amount) }); } // Calculate totals const totalInput = utxos.reduce((sum, utxo) => sum + utxo.amount, 0); const totalOutput = outputs.reduce((sum, output) => sum + output.amount, 0); // Calculate fee const estimatedSize = this.estimateTransactionSize(utxos.length, outputs.length + 1); const fee = feeRate * estimatedSize; const changeAmount = totalInput - totalOutput - fee; // Add change output if needed if (changeAmount > constants_1.TRANSACTION_CONSTANTS.DUST_THRESHOLD) { tx.addOutput({ script: this.createScriptPubKey(changeAddress, network), amount: BigInt(changeAmount) }); } else if (changeAmount < 0) { throw new types_1.TransactionError('Insufficient funds for transaction'); } return base_1.hex.encode(tx.toPSBT()); } catch (error) { if (error instanceof types_1.ValidationError || error instanceof types_1.TransactionError) { throw error; } throw new types_1.TransactionError(`Failed to create simple transaction: ${error instanceof Error ? error.message : 'Unknown error'}`, { params, error }); } } /** * Creates optimized OP_RETURN data for mining */ createMiningOpReturn(miningData) { // Extract first 16 bytes of hash (32 hex chars) const hashPrefix = miningData.hash.substring(0, 32); // Convert nonce to 4-byte hex const nonceHex = miningData.nonce.toString(16).padStart(8, '0'); // Combine hash prefix and nonce const combinedHex = hashPrefix + nonceHex; // Validate size if (combinedHex.length / 2 > constants_1.TRANSACTION_CONSTANTS.MAX_OP_RETURN_SIZE) { throw new types_1.TransactionError(`OP_RETURN data too large: ${combinedHex.length / 2} bytes (max: ${constants_1.TRANSACTION_CONSTANTS.MAX_OP_RETURN_SIZE})`); } return base_1.hex.decode(combinedHex); } /** * Creates script pubkey for address */ createScriptPubKey(address, _network) { try { // Simple approach: create a basic P2TR script for Taproot addresses if (address.startsWith('bc1p') || address.startsWith('tb1p')) { // For Taproot, create a simple 32-byte hash from address const addressBytes = new TextEncoder().encode(address); const hash = addressBytes.slice(0, 32); const paddedHash = new Uint8Array(32); paddedHash.set(hash); return btc.p2tr(paddedHash).script; } // For SegWit addresses if (address.startsWith('bc1q') || address.startsWith('tb1q')) { // Create a 20-byte hash for P2WPKH const addressBytes = new TextEncoder().encode(address); const hash = addressBytes.slice(0, 20); const paddedHash = new Uint8Array(20); paddedHash.set(hash); return btc.p2wpkh(paddedHash).script; } // For legacy addresses, create a basic P2PKH script const addressBytes = new TextEncoder().encode(address); const hash = addressBytes.slice(0, 20); const paddedHash = new Uint8Array(20); paddedHash.set(hash); return btc.p2pkh(paddedHash).script; } catch (error) { throw new types_1.ValidationError(`Failed to create script for address ${address}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Estimates transaction size in virtual bytes */ estimateTransactionSize(inputs, outputs) { // Base transaction size let size = 10; // version (4) + input count (1) + output count (1) + locktime (4) // Input sizes (Taproot inputs) size += inputs * 57; // 32 (txid) + 4 (vout) + 1 (script length) + 16 (witness) + 4 (sequence) // Output sizes size += outputs * 43; // 8 (amount) + 1 (script length) + 34 (script) return size; } /** * Gets network configuration for @scure */ getNetworkConfig(network) { switch (network) { case 'mainnet': return btc.NETWORK; case 'testnet': case 'testnet4': return btc.TEST_NETWORK; default: throw new types_1.ValidationError(`Unsupported network: ${network}`); } } /** * Validates mining transaction parameters */ validateMiningParams(params) { const { utxo, miningData, changeAddress, feeRate } = params; // Validate UTXO if (!utxo.txid || typeof utxo.vout !== 'number' || !utxo.amount || !utxo.address) { throw new types_1.ValidationError('Invalid UTXO format'); } if (utxo.amount < constants_1.TRANSACTION_CONSTANTS.MIN_UTXO_AMOUNT) { throw new types_1.ValidationError(`UTXO amount (${utxo.amount}) below minimum (${constants_1.TRANSACTION_CONSTANTS.MIN_UTXO_AMOUNT})`); } // Validate mining data if (!miningData.hash || typeof miningData.nonce !== 'number') { throw new types_1.ValidationError('Invalid mining data format'); } if (miningData.hash.length < 32) { throw new types_1.ValidationError('Mining hash too short (minimum 32 hex characters)'); } if (miningData.nonce < 0) { throw new types_1.ValidationError('Mining nonce must be non-negative'); } // Validate change address if (!changeAddress || typeof changeAddress !== 'string') { throw new types_1.ValidationError('Invalid change address'); } // Validate fee rate if (feeRate && (feeRate < constants_1.TRANSACTION_CONSTANTS.MIN_FEE_RATE || feeRate > constants_1.TRANSACTION_CONSTANTS.MAX_FEE_RATE)) { throw new types_1.ValidationError(`Fee rate (${feeRate}) outside valid range (${constants_1.TRANSACTION_CONSTANTS.MIN_FEE_RATE}-${constants_1.TRANSACTION_CONSTANTS.MAX_FEE_RATE})`); } } /** * Validates simple transaction parameters */ validateSimpleParams(params) { const { utxos, outputs, changeAddress, feeRate } = params; // Validate UTXOs if (!Array.isArray(utxos) || utxos.length === 0) { throw new types_1.ValidationError('At least one UTXO is required'); } for (const utxo of utxos) { if (!utxo.txid || typeof utxo.vout !== 'number' || !utxo.amount || !utxo.address) { throw new types_1.ValidationError('Invalid UTXO format'); } } // Validate outputs if (!Array.isArray(outputs) || outputs.length === 0) { throw new types_1.ValidationError('At least one output is required'); } for (const output of outputs) { if (!output.address || typeof output.amount !== 'number' || output.amount <= 0) { throw new types_1.ValidationError('Invalid output format'); } if (output.amount < constants_1.TRANSACTION_CONSTANTS.DUST_THRESHOLD) { throw new types_1.ValidationError(`Output amount (${output.amount}) below dust threshold (${constants_1.TRANSACTION_CONSTANTS.DUST_THRESHOLD})`); } } // Validate change address if (!changeAddress || typeof changeAddress !== 'string') { throw new types_1.ValidationError('Invalid change address'); } // Validate fee rate if (feeRate && (feeRate < constants_1.TRANSACTION_CONSTANTS.MIN_FEE_RATE || feeRate > constants_1.TRANSACTION_CONSTANTS.MAX_FEE_RATE)) { throw new types_1.ValidationError(`Fee rate (${feeRate}) outside valid range (${constants_1.TRANSACTION_CONSTANTS.MIN_FEE_RATE}-${constants_1.TRANSACTION_CONSTANTS.MAX_FEE_RATE})`); } } } exports.TransactionBuilder = TransactionBuilder; //# sourceMappingURL=builder.js.map