charms-wallet-js
Version:
Professional Bitcoin wallet library for Charms ecosystem using @scure stack
292 lines • 13.4 kB
JavaScript
;
// 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