124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
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<SelectableUTXO[]>([]);
|
|
const [selectedUtxoIndex, setSelectedUtxoIndex] = useState(0);
|
|
const [requiredAmount, setRequiredAmount] = useState<bigint>(0n);
|
|
const [fee, setFee] = useState<bigint>(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<void> => {
|
|
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<typeof useUtxoSelection>;
|