UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

188 lines (187 loc) 8.23 kB
import { P2PKH, PrivateKey, Script, Transaction, Utils } from "@bsv/sdk"; // useSendBSV Hook - Send BSV transactions import { useMutation } from "@tanstack/react-query"; import { useBitcoinAuth } from "../../../hooks/useBitcoinAuth.js"; import { useBlockchainService } from "../../../hooks/useBlockchainService.js"; export function useSendBSV(options = {}) { const { feeRate = 0.5, app: _app = "bigblocks", walletData, onSuccess, onError, } = options; const { user } = useBitcoinAuth(); const blockchainService = useBlockchainService(); // Use provided wallet data or fall back to auth user (which may not have wallet props) const currentWalletData = walletData || user; const sendMutation = useMutation({ mutationFn: async ({ toAddress, satoshis, message }) => { if (!user || !currentWalletData?.paymentKey) { throw { code: "UNAUTHORIZED", message: "User must be authenticated with wallet data to send BSV", }; } // Validate inputs if (!toAddress || toAddress.length < 26) { throw { code: "INVALID_INPUT", message: "Invalid recipient address", }; } if (satoshis <= 0) { throw { code: "INVALID_INPUT", message: "Amount must be greater than 0", }; } try { // Get user's private key const privateKey = currentWalletData.paymentKey ? PrivateKey.fromWif(currentWalletData.paymentKey) : undefined; if (!privateKey) { throw { code: "MISSING_KEYS", message: "Payment private key is required", }; } // Get available UTXOs - prefer fresh data from blockchain service let utxos = currentWalletData.utxos || []; // If no cached UTXOs, try to fetch them if (!utxos.length && currentWalletData.fundingAddress) { try { const fetchedUtxos = await blockchainService.getUTXOs(currentWalletData.fundingAddress); utxos = fetchedUtxos; } catch (error) { console.warn("Failed to fetch UTXOs from blockchain service:", error); } } if (!utxos.length) { throw { code: "INSUFFICIENT_FUNDS", message: "No UTXOs available", }; } // Calculate total available const totalAvailable = utxos.reduce((sum, utxo) => sum + utxo.satoshis, 0); // Estimate transaction size and fee const estimatedSize = 148 + 34 + 10; // input + output + overhead (simplified) const fee = Math.ceil(estimatedSize * feeRate); const totalNeeded = satoshis + fee; if (totalNeeded > totalAvailable) { throw { code: "INSUFFICIENT_FUNDS", message: `Insufficient funds. Need ${totalNeeded} satoshis, have ${totalAvailable}`, }; } // Create transaction using BSV SDK const tx = new Transaction(); const p2pkh = new P2PKH(); // Add recipient output tx.addOutput({ lockingScript: p2pkh.lock(toAddress), satoshis: satoshis, }); // Select UTXOs and add inputs let inputTotal = 0; const usedUtxos = []; for (const utxo of utxos) { if (inputTotal >= totalNeeded) break; // Note: In a real implementation, you would need to fetch the full transaction // For now, we create a minimal transaction reference for the BSV SDK // This is a common pattern where the source transaction details are fetched separately // Create minimal source transaction for BSV SDK // Note: The SDK expects a Transaction object, but we can provide a minimal implementation const sourceTransaction = Object.create(Transaction.prototype); sourceTransaction.id = () => utxo.txid; sourceTransaction.outputs = [ { satoshis: utxo.satoshis, lockingScript: utxo.script || new P2PKH().lock(privateKey.toAddress()), }, ]; tx.addInput({ sourceTransaction, sourceOutputIndex: utxo.vout, unlockingScriptTemplate: p2pkh.unlock(privateKey), }); inputTotal += utxo.satoshis; usedUtxos.push(utxo); } // Add change output if needed const change = inputTotal - satoshis - fee; if (change > 546) { // Dust limit tx.addOutput({ lockingScript: p2pkh.lock(currentWalletData.fundingAddress || user.address), satoshis: change, }); } // Add OP_RETURN message if provided if (message) { const messageBytes = Utils.toArray(message, "utf8"); const opReturnScript = new Script([ { op: 106 }, // OP_RETURN { op: 0, data: messageBytes }, ]); tx.addOutput({ lockingScript: opReturnScript, satoshis: 0, }); } // Sign transaction await tx.sign(); // Broadcast transaction using blockchain service const broadcastResult = await blockchainService.broadcastTransaction(tx); if (!broadcastResult.success) { throw { code: "BROADCAST_FAILED", message: broadcastResult.error || "Failed to broadcast transaction", }; } const result = { txid: broadcastResult.txid ?? "", fee: fee, amount: satoshis, recipient: toAddress, message: message, size: tx.toHex().length / 2, // Convert hex length to bytes }; return result; } catch (error) { if (error.code) { throw error; } throw { code: "TRANSACTION_FAILED", message: "Failed to create or send transaction", details: error, }; } }, onSuccess: (data) => { onSuccess?.(data); }, onError: (error) => { console.error("Send BSV failed:", error); onError?.(error); }, }); // Calculate estimated fee for current inputs const estimatedFee = currentWalletData?.utxos?.length ? Math.ceil((148 + 34 + 10) * feeRate) // Basic estimation : 500; // Default fallback return { // Mutation functions sendBSV: sendMutation.mutate, sendBSVAsync: sendMutation.mutateAsync, // State isLoading: sendMutation.isPending, isError: sendMutation.isError, isSuccess: sendMutation.isSuccess, error: sendMutation.error, data: sendMutation.data, estimatedFee, // Reset reset: sendMutation.reset, }; }