import { useCallback, useEffect, useState, useMemo } from 'react' import { useRefreshConfigValues } from '../context/ServiceConfigContext' import { AmountSats } from '../libs/JmWalletApi' import { isValidNumber } from '../utils' export type TxFeeValueUnit = 'blocks' | 'sats/kilo-vbyte' export type TxFeeValue = number export type TxFee = { value?: TxFeeValue unit?: TxFeeValueUnit } export const toTxFeeValueUnit = (val?: TxFeeValue): TxFeeValueUnit | undefined => { if (val === undefined || !Number.isInteger(val) || val < 1) return undefined return val <= 1_000 ? 'blocks' : 'sats/kilo-vbyte' } export const FEE_CONFIG_KEYS = { tx_fees: { section: 'POLICY', field: 'tx_fees' }, tx_fees_factor: { section: 'POLICY', field: 'tx_fees_factor' }, max_cj_fee_abs: { section: 'POLICY', field: 'max_cj_fee_abs' }, max_cj_fee_rel: { section: 'POLICY', field: 'max_cj_fee_rel' }, max_sweep_fee_change: { section: 'POLICY', field: 'max_sweep_fee_change' }, } export interface FeeValues { tx_fees?: TxFee tx_fees_factor?: number max_cj_fee_abs?: number max_cj_fee_rel?: number max_sweep_fee_change?: number } export const useLoadFeeConfigValues = () => { const refreshConfigValues = useRefreshConfigValues() return useCallback( async (signal?: AbortSignal) => { const serviceConfig = await refreshConfigValues({ signal, keys: Object.values(FEE_CONFIG_KEYS), }) const policy = serviceConfig['POLICY'] || {} const parsedTxFees = parseInt(policy.tx_fees || '', 10) const parsedTxFeesFactor = parseFloat(policy.tx_fees_factor || '') const parsedMaxFeeAbs = parseInt(policy.max_cj_fee_abs || '', 10) const parsedMaxFeeRel = parseFloat(policy.max_cj_fee_rel || '') const parsedMaxSweepFeeChange = parseFloat(policy.max_sweep_fee_change || '') const feeValues: FeeValues = { tx_fees: isValidNumber(parsedTxFees) ? { value: parsedTxFees, unit: toTxFeeValueUnit(parsedTxFees), } : undefined, tx_fees_factor: isValidNumber(parsedTxFeesFactor) ? parsedTxFeesFactor : undefined, max_cj_fee_abs: isValidNumber(parsedMaxFeeAbs) ? parsedMaxFeeAbs : undefined, max_cj_fee_rel: isValidNumber(parsedMaxFeeRel) ? parsedMaxFeeRel : undefined, max_sweep_fee_change: isValidNumber(parsedMaxSweepFeeChange) ? parsedMaxSweepFeeChange : undefined, } return feeValues }, [refreshConfigValues], ) } export const useFeeConfigValues = (): [FeeValues | undefined, () => void] => { const loadFeeConfigValues = useLoadFeeConfigValues() const [values, setValues] = useState<FeeValues>() const [reloadCounter, setReloadCounter] = useState(0) useEffect(() => { const abortCtrl = new AbortController() loadFeeConfigValues(abortCtrl.signal) .then((val) => setValues(val)) .catch((e) => { if (abortCtrl.signal.aborted) return console.log('Unable lo load fee config: ', e) setValues(undefined) }) return () => { abortCtrl.abort() } }, [loadFeeConfigValues, reloadCounter]) return [values, () => setReloadCounter((val) => val + 1)] } interface EstimatMaxCollaboratorFeeProps { amount: AmountSats collaborators: number maxFeeAbs: AmountSats maxFeeRel: number // e.g. 0.001 for 0.1% } export const estimateMaxCollaboratorFee = ({ amount, collaborators, maxFeeAbs, maxFeeRel, }: EstimatMaxCollaboratorFeeProps): AmountSats => { const maxFeePerCollaborator = Math.max(Math.ceil(amount * maxFeeRel), maxFeeAbs) return collaborators > 0 ? Math.min(maxFeePerCollaborator * collaborators, amount) : 0 } interface EstimatedMaxCollaboratorFeeArgs { isCoinjoin: boolean amount: AmountSats | null numCollaborators: number | null feeConfigValues?: FeeValues } export const useEstimatedMaxCollaboratorFee = ({ isCoinjoin, amount, numCollaborators, feeConfigValues, }: EstimatedMaxCollaboratorFeeArgs): AmountSats | null => { return useMemo(() => { if (!isCoinjoin || !feeConfigValues || !amount) return null if (!isValidNumber(amount) || !isValidNumber(numCollaborators ?? undefined)) return null if (!isValidNumber(feeConfigValues.max_cj_fee_abs) || !isValidNumber(feeConfigValues.max_cj_fee_rel)) return null return estimateMaxCollaboratorFee({ amount, collaborators: numCollaborators!, maxFeeAbs: feeConfigValues.max_cj_fee_abs!, maxFeeRel: feeConfigValues.max_cj_fee_rel!, }) }, [amount, isCoinjoin, numCollaborators, feeConfigValues]) }