charms-wallet-js
Version:
Professional Bitcoin wallet library for Charms ecosystem using @scure stack
243 lines • 9.36 kB
JavaScript
;
// Transaction Signer 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.TransactionSigner = void 0;
const bip39_1 = require("@scure/bip39");
const bip32_1 = require("@scure/bip32");
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 signer using @scure/btc-signer
* Supports Taproot key-path spending with BIP86 derivation
*/
class TransactionSigner {
constructor(network = constants_1.DEFAULT_NETWORK) {
this.network = network;
}
/**
* Signs a PSBT using the provided mnemonic
* Automatically derives the correct keys for Taproot signing
*/
async signPSBT(psbtHex, utxo, mnemonic, derivationPath = constants_1.DEFAULT_DERIVATION_PATH) {
try {
// Validate inputs
this.validateSigningInputs(psbtHex, utxo, mnemonic);
// Parse PSBT
const psbtBytes = base_1.hex.decode(psbtHex);
const tx = btc.Transaction.fromPSBT(psbtBytes);
// Derive Taproot keys
const keys = await this.deriveTaprootKeys(mnemonic, derivationPath, 0);
// Update input with witness UTXO if needed
this.updateWitnessUtxo(tx, utxo, keys);
// Sign the transaction
tx.sign(keys.privateKey);
// Finalize the transaction
tx.finalize();
// Extract the final transaction
const finalTx = tx.extract();
const txHex = base_1.hex.encode(finalTx);
const txId = this.calculateTxId(finalTx);
return {
success: true,
txid: txId,
hex: txHex,
size: finalTx.length,
vsize: this.calculateVirtualSize(finalTx),
fee: this.calculateFee(utxo, tx)
};
}
catch (error) {
return {
success: false,
error: `Signing failed: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Signs a transaction and returns a SignedTransaction object
*/
async signTransaction(psbtHex, utxo, mnemonic, derivationPath) {
const result = await this.signPSBT(psbtHex, utxo, mnemonic, derivationPath);
if (!result.success || !result.txid || !result.hex) {
throw new types_1.TransactionError(result.error || 'Transaction signing failed');
}
return {
txid: result.txid,
hex: result.hex,
size: result.size || 0,
vsize: result.vsize || 0,
fee: result.fee || 0
};
}
/**
* Derives Taproot keys from mnemonic using BIP86 derivation
*/
async deriveTaprootKeys(mnemonic, derivationPath = constants_1.DEFAULT_DERIVATION_PATH, index = 0) {
try {
// Convert mnemonic to seed
const seed = await (0, bip39_1.mnemonicToSeed)(mnemonic);
// Create HD key from seed
const hdkey = bip32_1.HDKey.fromMasterSeed(seed);
// Derive account key using provided path
const accountKey = hdkey.derive(derivationPath);
// Derive receiving chain (0) and address index
const chainKey = accountKey.derive('0');
const addressKey = chainKey.derive(index.toString());
if (!addressKey.privateKey || !addressKey.publicKey) {
throw new types_1.CryptographyError('Failed to derive private or public key');
}
// Extract x-only public key for Taproot
const xOnlyPubkey = addressKey.publicKey.slice(1);
// Get network configuration
const networkConfig = this.getNetworkConfig();
// Create Taproot payment
const p2tr = btc.p2tr(xOnlyPubkey, undefined, networkConfig);
if (!p2tr.address) {
throw new types_1.CryptographyError('Failed to generate Taproot address');
}
return {
privateKey: addressKey.privateKey,
publicKey: addressKey.publicKey,
xOnlyPubkey,
address: p2tr.address
};
}
catch (error) {
throw new types_1.CryptographyError(`Failed to derive Taproot keys: ${error instanceof Error ? error.message : 'Unknown error'}`, { derivationPath, index, error });
}
}
/**
* Updates PSBT input with witness UTXO data
*/
updateWitnessUtxo(tx, utxo, keys) {
try {
// Check if input already has witnessUtxo
const input = tx.getInput(0);
if (input.witnessUtxo) {
return; // Already has witness UTXO
}
// Create script for the UTXO address
const networkConfig = this.getNetworkConfig();
const p2tr = btc.p2tr(keys.xOnlyPubkey, undefined, networkConfig);
// Update input with witness UTXO
tx.updateInput(0, {
witnessUtxo: {
script: p2tr.script,
amount: BigInt(utxo.amount)
}
});
}
catch (error) {
throw new types_1.TransactionError(`Failed to update witness UTXO: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Calculates transaction ID from raw transaction bytes
*/
calculateTxId(txBytes) {
try {
const tx = btc.Transaction.fromRaw(txBytes);
return tx.id;
}
catch (error) {
// Fallback: create a simple hash-based ID
return base_1.hex.encode(txBytes.slice(0, 32)); // Use first 32 bytes as ID
}
}
/**
* Calculates virtual size of transaction
*/
calculateVirtualSize(txBytes) {
// For Taproot transactions, virtual size is typically close to actual size
// This is a simplified calculation
return Math.ceil(txBytes.length * 0.75); // Approximate vsize calculation
}
/**
* Calculates transaction fee
*/
calculateFee(utxo, tx) {
try {
// Get total output amount
let totalOutput = 0;
const outputCount = tx.outputsLength || 0;
for (let i = 0; i < outputCount; i++) {
const output = tx.getOutput(i);
totalOutput += Number(output.amount);
}
// Fee = input amount - output amount
return utxo.amount - totalOutput;
}
catch (error) {
console.warn('Failed to calculate fee:', error);
return 0;
}
}
/**
* Gets network configuration for @scure
*/
getNetworkConfig() {
switch (this.network) {
case 'mainnet':
return btc.NETWORK;
case 'testnet':
case 'testnet4':
return btc.TEST_NETWORK;
default:
throw new types_1.ValidationError(`Unsupported network: ${this.network}`);
}
}
/**
* Validates signing inputs
*/
validateSigningInputs(psbtHex, utxo, mnemonic) {
// Validate PSBT hex
if (!psbtHex || typeof psbtHex !== 'string') {
throw new types_1.ValidationError('Invalid PSBT hex');
}
try {
base_1.hex.decode(psbtHex);
}
catch (error) {
throw new types_1.ValidationError('Invalid PSBT hex format');
}
// Validate UTXO
if (!utxo || !utxo.txid || typeof utxo.vout !== 'number' || !utxo.amount || !utxo.address) {
throw new types_1.ValidationError('Invalid UTXO format');
}
// Validate mnemonic
if (!mnemonic || typeof mnemonic !== 'string') {
throw new types_1.ValidationError('Invalid mnemonic');
}
const words = mnemonic.trim().split(/\s+/);
if (words.length < 12 || words.length > 24) {
throw new types_1.ValidationError('Invalid mnemonic word count');
}
}
}
exports.TransactionSigner = TransactionSigner;
//# sourceMappingURL=signer.js.map