bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
188 lines (187 loc) • 8.23 kB
JavaScript
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,
};
}