import { useState, useCallback, useMemo } from "react"; import type { SelectableUTXO, VariableInput } from "../types.js"; import type { Invitation } from "../../../../services/invitation.js"; import { formatSatoshis } from "../../../theme.js"; import { autoSelectGreedyUtxos, mapUnspentOutputsToSelectable, } from "../../../../utils/invitation-flow.js"; /** * Manages UTXO selection state for the wizard's inputs step. * * Only active for transaction flows that require the creator * to provide funding inputs. */ export function useUtxoSelection() { const [availableUtxos, setAvailableUtxos] = useState([]); const [selectedUtxoIndex, setSelectedUtxoIndex] = useState(0); const [requiredAmount, setRequiredAmount] = useState(0n); const [fee, setFee] = useState(500n); const selectedAmount = useMemo( () => availableUtxos .filter((u) => u.selected) .reduce((sum, u) => sum + u.valueSatoshis, 0n), [availableUtxos], ); const changeAmount = useMemo( () => selectedAmount - requiredAmount - fee, [selectedAmount, requiredAmount, fee], ); /** Toggle the selected state of a single UTXO. */ const toggleSelection = useCallback((index: number) => { setAvailableUtxos((prev) => { const updated = [...prev]; const utxo = updated[index]; if (utxo) { updated[index] = { ...utxo, selected: !utxo.selected }; } return updated; }); }, []); /** Select all available UTXOs. */ const selectAll = useCallback(() => { setAvailableUtxos((prev) => prev.map((u) => ({ ...u, selected: true }))); }, []); /** Deselect all UTXOs. */ const deselectAll = useCallback(() => { setAvailableUtxos((prev) => prev.map((u) => ({ ...u, selected: false }))); }, []); /** * Query the invitation instance for suitable UTXOs and auto-select * greedily to meet the required amount. */ const loadUtxos = useCallback( async ( invitationInstance: Invitation, templateIdentifier: string, variables: VariableInput[], setStatus: (msg: string) => void, ): Promise => { setStatus("Finding suitable UTXOs..."); // Derive required amount from variables that look like satoshi/amount fields. const requestedVar = variables.find( (v) => v.id.toLowerCase().includes("satoshi") || v.id.toLowerCase().includes("amount"), ); const requested = requestedVar ? BigInt(requestedVar.value || "0") : 0n; setRequiredAmount(requested); const unspentOutputs = await invitationInstance.findSuitableResources({ templateIdentifier, }); const mapped = mapUnspentOutputsToSelectable(unspentOutputs); const autoSelected = autoSelectGreedyUtxos(mapped, requested + fee); setAvailableUtxos(autoSelected as SelectableUTXO[]); setStatus("Ready"); }, [fee], ); /** Validate that the selection meets the required amounts. */ const validate = useCallback((): string | null => { const selected = availableUtxos.filter((u) => u.selected); if (selected.length === 0) { return "Please select at least one UTXO"; } if (selectedAmount < requiredAmount + fee) { return `Insufficient funds. Need ${formatSatoshis(requiredAmount + fee)}, selected ${formatSatoshis(selectedAmount)}`; } if (changeAmount < 546n) { return `Change amount (${changeAmount}) is below dust threshold (546 sats)`; } return null; }, [availableUtxos, selectedAmount, requiredAmount, fee, changeAmount]); return { availableUtxos, setAvailableUtxos, selectedUtxoIndex, setSelectedUtxoIndex, requiredAmount, fee, selectedAmount, changeAmount, toggleSelection, selectAll, deselectAll, loadUtxos, validate, } as const; } export type UtxoSelectionState = ReturnType;