diff --git a/src/components/Earn.tsx b/src/components/Earn.tsx index 772d3864..17c2683a 100644 --- a/src/components/Earn.tsx +++ b/src/components/Earn.tsx @@ -4,7 +4,7 @@ import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { TFunction } from 'i18next' import { useSettings } from '../context/SettingsContext' -import { CurrentWallet, useCurrentWalletInfo, useReloadCurrentWalletInfo } from '../context/WalletContext' +import { CurrentWallet, useCurrentWalletInfo, useReloadCurrentWalletInfo, WalletInfo } from '../context/WalletContext' import { useServiceInfo, useReloadServiceInfo, Offer } from '../context/ServiceInfoContext' import { factorToPercentage, isAbsoluteOffer, isRelativeOffer, isValidNumber, percentageToFactor } from '../utils' import * as Api from '../libs/JmWalletApi' @@ -22,6 +22,7 @@ import Accordion from './Accordion' import BitcoinAmountInput, { AmountValue, toAmountValue } from './BitcoinAmountInput' import { isValidAmount } from './Send/helpers' import styles from './Earn.module.css' +import { JM_DUST_THRESHOLD } from '../constants/config' // In order to prevent state mismatch, the 'maker stop' response is delayed shortly. // Even though the API response suggests that the maker has started or stopped immediately, it seems that this is not always the case. @@ -203,6 +204,7 @@ interface EarnFormProps { onSubmit: (values: EarnFormValues) => Promise isLoading: boolean disabled?: boolean + walletInfo?: WalletInfo } const EarnForm = ({ @@ -211,9 +213,27 @@ const EarnForm = ({ onSubmit, isLoading, disabled = false, + walletInfo, }: EarnFormProps) => { const { t } = useTranslation() + const maxAvailableBalanceInJar = useMemo(() => { + return Math.max( + 0, + Math.max( + ...Object.values(walletInfo?.balanceSummary.accountBalances || []).map( + (it) => it.calculatedAvailableBalanceInSats, + ), + ), + ) + }, [walletInfo]) + + const offerMinsizeMin = JM_DUST_THRESHOLD + + const offerMinsizeMax = useMemo(() => { + return Math.max(0, maxAvailableBalanceInJar - JM_DUST_THRESHOLD) + }, [maxAvailableBalanceInJar]) + const validate = (values: EarnFormValues) => { const errors = {} as FormikErrors const isRelOffer = isRelativeOffer(values.offertype) @@ -241,6 +261,16 @@ const EarnForm = ({ if (!isValidAmount(values.minsize?.value ?? null, false)) { errors.minsize = t('earn.feedback_invalid_min_amount') + } else { + const minsize = values.minsize?.value || 0 + if (offerMinsizeMin > offerMinsizeMax) { + errors.minsize = t('earn.feedback_invalid_min_amount_insufficient_funds') + } else if (minsize < offerMinsizeMin || minsize > offerMinsizeMax) { + errors.minsize = t('earn.feedback_invalid_min_amount_range', { + minAmountMin: offerMinsizeMin.toLocaleString(), + minAmountMax: offerMinsizeMax.toLocaleString(), + }) + } } return errors } @@ -426,6 +456,15 @@ export default function Earn({ wallet }: EarnProps) { const [moveToJarFidelityBondId, setMoveToJarFidelityBondId] = useState() const [renewFidelityBondId, setRenewFidelityBondId] = useState() + const isSufficientFundsAvailable = useMemo( + () => (currentWalletInfo?.balanceSummary.calculatedAvailableBalanceInSats ?? 0) > 0, + [currentWalletInfo], + ) + + const isOperationDisabled = useMemo(() => { + return !isSufficientFundsAvailable || serviceInfo?.rescanning === true || isWaitingMakerStart || isWaitingMakerStop + }, [isSufficientFundsAvailable, serviceInfo, isWaitingMakerStart, isWaitingMakerStop]) + const startMakerService = useCallback( (values: EarnFormValues) => { setIsSending(true) @@ -701,8 +740,9 @@ export default function Earn({ wallet }: EarnProps) { { return ( <> diff --git a/src/constants/config.ts b/src/constants/config.ts index c6716566..b91f4796 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -19,3 +19,7 @@ export const CJ_STATE_MAKER_RUNNING = 1 export const CJ_STATE_NONE_RUNNING = 2 export const JM_API_AUTH_TOKEN_EXPIRY: Milliseconds = Math.round(0.5 * 60 * 60 * 1_000) + +// cap of dusty offer minsizes ("has dusty minsize, capping at 27300") +// See: https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/v0.9.11/src/jmclient/configure.py#L70 (last check on 2024-04-22 of v0.9.11) +export const JM_DUST_THRESHOLD = 27_300 diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 59f306ed..fe6ce379 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -394,6 +394,8 @@ "label_min_amount_input": "Minimum amount", "placeholder_min_amount_input": "Enter a minimum amount in sats or BTC...", "feedback_invalid_min_amount": "Please provide a minimum amount.", + "feedback_invalid_min_amount_range": "Please provide a minimum amount in sats between {{ minAmountMin }} and {{ minAmountMax }}.", + "feedback_invalid_min_amount_insufficient_funds": "Invalid minimum amount: Insufficient funds available.", "button_start": "Start Earning!", "button_stop": "Stop", "text_starting": "Starting",