Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Earn): validate offer minsize #745

Merged
merged 1 commit into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions src/components/Earn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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.
Expand Down Expand Up @@ -203,6 +204,7 @@ interface EarnFormProps {
onSubmit: (values: EarnFormValues) => Promise<void>
isLoading: boolean
disabled?: boolean
walletInfo?: WalletInfo
}

const EarnForm = ({
Expand All @@ -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<EarnFormValues>
const isRelOffer = isRelativeOffer(values.offertype)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -426,6 +456,15 @@ export default function Earn({ wallet }: EarnProps) {
const [moveToJarFidelityBondId, setMoveToJarFidelityBondId] = useState<Api.UtxoId>()
const [renewFidelityBondId, setRenewFidelityBondId] = useState<Api.UtxoId>()

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)
Expand Down Expand Up @@ -701,8 +740,9 @@ export default function Earn({ wallet }: EarnProps) {
<EarnForm
initialValues={initialValues}
onSubmit={onSubmitStart}
walletInfo={currentWalletInfo}
isLoading={isLoading}
disabled={serviceInfo?.rescanning === true || isWaitingMakerStart || isWaitingMakerStop}
disabled={isOperationDisabled}
submitButtonText={(_) => {
return (
<>
Expand Down
4 changes: 4 additions & 0 deletions src/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading