bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
170 lines (169 loc) • 7.58 kB
JavaScript
import { PrivateKey, } from "@bsv/sdk";
// useBuyListing Hook - Purchase marketplace listings
import { useMutation } from "@tanstack/react-query";
import { TokenType, purchaseOrdListing, purchaseOrdTokenListing, } from "js-1sat-ord";
import { useBitcoinAuth } from "../../../hooks/useBitcoinAuth.js";
import { broadcastTransaction } from "../../../lib/broadcast";
import { getWalletProps, } from "../../wallet/types/extended-user.js";
import { AssetType, } from "../types/market.js";
export function useBuyListing(options = {}) {
const { app: _app = "bigblocks", feeRate = 0.5, onSuccess, onError, } = options;
const { user } = useBitcoinAuth();
const buyMutation = useMutation({
mutationFn: async (listing) => {
if (!user) {
throw {
code: "UNAUTHORIZED",
message: "User must be authenticated to buy listings",
};
}
try {
// Get wallet properties with defaults
const walletProps = getWalletProps(user);
// Get user's private keys
const paymentPk = walletProps.paymentKey
? PrivateKey.fromWif(walletProps.paymentKey)
: undefined;
const _ordPk = walletProps.ordinalKey
? PrivateKey.fromWif(walletProps.ordinalKey)
: undefined;
if (!paymentPk) {
throw {
code: "MISSING_KEYS",
message: "Payment private key is required for purchasing",
};
}
// Get payment UTXOs
const paymentUtxos = walletProps.utxos;
if (!paymentUtxos.length) {
throw {
code: "INSUFFICIENT_FUNDS",
message: "No UTXOs available for payment",
};
}
// Check available balance
const availableBalance = paymentUtxos.reduce((sum, utxo) => sum + utxo.satoshis, 0);
const estimatedFee = 500; // Basic fee estimate
const totalCost = listing.priceSats + estimatedFee;
if (totalCost > availableBalance) {
throw {
code: "INSUFFICIENT_FUNDS",
message: `Insufficient funds. Need ${totalCost} satoshis, have ${availableBalance}`,
};
}
let tx;
let _spentOutpoints;
if (listing.assetType === AssetType.Ordinals) {
// Buy ordinal/NFT listing - need to convert to proper format
const config = {
utxos: paymentUtxos,
listing: {
payout: listing.payAddress, // Assuming payAddress is the payout script
listingUtxo: {
txid: listing.txid,
vout: listing.vout,
satoshis: listing.priceSats,
script: listing.ordAddress, // Assuming ordAddress contains script
},
},
paymentPk,
ordAddress: walletProps.ordinalAddress,
changeAddress: walletProps.fundingAddress,
satsPerKb: Math.floor(feeRate * 1000),
};
const result = await purchaseOrdListing(config);
tx = result.tx;
_spentOutpoints = result.spentOutpoints;
}
else {
// Buy token listing (BSV20/BSV21)
if (!listing.tokenId || !listing.tokenAmount) {
throw {
code: "INVALID_INPUT",
message: "Token ID and amount are required for token purchases",
};
}
const config = {
protocol: listing.assetType === AssetType.BSV20
? TokenType.BSV20
: TokenType.BSV21,
tokenID: listing.tokenId,
utxos: paymentUtxos,
paymentPk,
listingUtxo: {
txid: listing.txid,
vout: listing.vout,
satoshis: 1, // Token UTXOs always contain exactly 1 satoshi in 1Sat Ordinals protocol
script: listing.ordAddress,
amt: listing.tokenAmount,
id: listing.tokenId,
},
ordAddress: walletProps.ordinalAddress,
changeAddress: walletProps.fundingAddress,
satsPerKb: Math.floor(feeRate * 1000),
};
const result = await purchaseOrdTokenListing(config);
tx = result.tx;
_spentOutpoints = result.spentOutpoints;
}
// Broadcast transaction
const broadcastResult = await broadcastTransaction(tx);
if (!broadcastResult.success) {
throw {
code: "BROADCAST_FAILED",
message: broadcastResult.error ||
"Failed to broadcast purchase transaction",
};
}
if (!broadcastResult.txid) {
throw {
code: "BROADCAST_FAILED",
message: "Transaction broadcast succeeded but no transaction ID returned",
};
}
const result = {
txid: broadcastResult.txid,
fee: tx.inputs.reduce((sum, input) => {
const sourceOutput = input.sourceTransaction?.outputs?.[input.sourceOutputIndex];
return sum + (sourceOutput?.satoshis || 0);
}, 0) -
tx.outputs.reduce((sum, output) => sum + (output.satoshis || 0), 0),
price: listing.priceSats,
assetType: listing.assetType,
tokenAmount: listing.tokenAmount,
};
return result;
}
catch (error) {
if (error.code) {
throw error;
}
throw {
code: "TRANSACTION_FAILED",
message: "Failed to purchase listing",
details: error,
};
}
},
onSuccess: (data) => {
onSuccess?.({ success: true, txid: data.txid });
},
onError: (error) => {
console.error("Buy listing failed:", error);
onError?.(error);
},
});
return {
// Mutation functions
buyListing: buyMutation.mutate,
buyListingAsync: buyMutation.mutateAsync,
// State
isLoading: buyMutation.isPending,
isError: buyMutation.isError,
isSuccess: buyMutation.isSuccess,
error: buyMutation.error,
data: buyMutation.data,
// Reset
reset: buyMutation.reset,
};
}