Skip to main content
Glama
transactionBuilder.ts4.42 kB
import { P2PKH, type PrivateKey, Script, Transaction, Utils } from "@bsv/sdk"; import { isBroadcastFailure, isBroadcastResponse } from "@bsv/sdk"; import { V5Broadcaster } from "../../utils/broadcaster"; import type { Utxo } from "../wallet/utxo"; const DEFAULT_SAT_PER_BYTE = 0.05; const DUST_LIMIT = 546; interface TransactionBuilderConfig { outputs: Array<{ script?: Script; address?: string; satoshis: number; }>; utxos: Utxo[]; changeAddress: string; paymentKey: PrivateKey; feePerByte?: number; } interface TransactionResult { success: boolean; txid?: string; rawTx?: string; error?: string; fee?: number; } /** * Build and optionally broadcast a transaction with automatic UTXO selection and fee calculation */ export async function buildAndSendTransaction( config: TransactionBuilderConfig, broadcast = true, ): Promise<TransactionResult> { const { outputs, utxos, changeAddress, paymentKey, feePerByte = DEFAULT_SAT_PER_BYTE, } = config; if (!utxos || utxos.length === 0) { return { success: false, error: "No UTXOs available to fund transaction", }; } const tx = new Transaction(); const p2pkh = new P2PKH(); // Add all outputs for (const output of outputs) { if (output.script) { tx.addOutput({ lockingScript: output.script, satoshis: output.satoshis, }); } else if (output.address) { tx.addOutput({ lockingScript: p2pkh.lock(output.address), satoshis: output.satoshis, }); } } // Calculate total output amount const totalOutputSatoshis = outputs.reduce( (sum, out) => sum + out.satoshis, 0, ); // Select UTXOs and add inputs let totalInputSatoshis = 0; const selectedUtxos: Utxo[] = []; // Sort UTXOs by value (ascending) to minimize the number of inputs const sortedUtxos = [...utxos].sort((a, b) => a.satoshis - b.satoshis); for (const utxo of sortedUtxos) { selectedUtxos.push(utxo); totalInputSatoshis += utxo.satoshis; const input = { sourceTransaction: utxo.tx, sourceOutputIndex: utxo.vout, unlockingScriptTemplate: p2pkh.unlock(paymentKey), }; tx.addInput(input); // Estimate fee with current inputs const estimatedSize = tx.toHex().length / 2 + 150; // rough estimate for unsigned tx const estimatedFee = Math.ceil(estimatedSize * feePerByte); // Check if we have enough to cover outputs + fee + potential change if (totalInputSatoshis >= totalOutputSatoshis + estimatedFee + DUST_LIMIT) { break; } } // Calculate final fee const estimatedSize = tx.toHex().length / 2 + 35; // more accurate with all inputs const fee = Math.ceil(estimatedSize * feePerByte); // Check if we have enough funds if (totalInputSatoshis < totalOutputSatoshis + fee) { return { success: false, error: `Insufficient funds. Have ${totalInputSatoshis} sats, need ${totalOutputSatoshis + fee} sats`, }; } // Add change output if needed const change = totalInputSatoshis - totalOutputSatoshis - fee; if (change >= DUST_LIMIT) { tx.addOutput({ lockingScript: p2pkh.lock(changeAddress), satoshis: change, }); } // Sign the transaction await tx.sign(); const rawTx = tx.toHex(); const txid = tx.id("hex") as string; // Check if broadcasting is disabled if (process.env.DISABLE_BROADCASTING === "true" || !broadcast) { return { success: true, txid, rawTx, fee, }; } // Broadcast the transaction try { const broadcaster = new V5Broadcaster(); const broadcastResult = await tx.broadcast(broadcaster); if (isBroadcastResponse(broadcastResult)) { return { success: true, txid: broadcastResult.txid || txid, rawTx, fee, }; } if (isBroadcastFailure(broadcastResult)) { return { success: true, txid, rawTx, fee, error: `Transaction created but broadcast failed: ${broadcastResult.description}`, }; } return { success: true, txid, rawTx, fee, error: "Transaction created but broadcast status uncertain", }; } catch (error) { return { success: true, txid, rawTx, fee, error: `Transaction created but broadcast failed: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Build OP_RETURN script from data arrays */ export function buildOpReturnScript(dataArrays: Uint8Array[]): Script { return new Script([ { op: 0 }, // OP_0 { op: 106 }, // OP_RETURN ...dataArrays.map((data) => ({ data })), ]); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/b-open-io/bsv-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server