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: relative fee greater than zero #862

Merged
merged 7 commits into from
Oct 31, 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
66 changes: 27 additions & 39 deletions src/components/Earn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,21 @@ import { TFunction } from 'i18next'
import { useSettings } from '../context/SettingsContext'
import { CurrentWallet, useCurrentWalletInfo, useReloadCurrentWalletInfo, WalletInfo } from '../context/WalletContext'
import { useServiceInfo, useReloadServiceInfo, Offer } from '../context/ServiceInfoContext'
import { factorToPercentage, isAbsoluteOffer, isRelativeOffer, isValidNumber, percentageToFactor } from '../utils'
import {
calcOfferMinsizeMax,
factorToPercentage,
isAbsoluteOffer,
isRelativeOffer,
isValidNumber,
percentageToFactor,
} from '../utils'
import {
OFFER_FEE_ABS_MIN,
OFFER_FEE_REL_MAX,
OFFER_FEE_REL_MIN,
OFFER_FEE_REL_STEP,
OFFER_MINSIZE_MIN,
} from '../constants/jam'
import * as Api from '../libs/JmWalletApi'
import * as fb from './fb/utils'
import Sprite from './Sprite'
Expand All @@ -22,7 +36,6 @@ 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 @@ -194,10 +207,6 @@ function CurrentOffer({ offer, nickname }: CurrentOfferProps) {
)
}

const feeRelMin = 0.0
const feeRelMax = 0.1 // 10%
const feeRelPercentageStep = 0.0001

interface EarnFormProps {
initialValues?: EarnFormValues
submitButtonText: (isSubmitting: boolean) => React.ReactNode | string
Expand All @@ -217,22 +226,9 @@ const EarnForm = ({
}: 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])
return walletInfo === undefined ? 0 : calcOfferMinsizeMax(walletInfo.balanceSummary.accountBalances)
}, [walletInfo])

const validate = (values: EarnFormValues) => {
const errors = {} as FormikErrors<EarnFormValues>
Expand All @@ -245,16 +241,16 @@ const EarnForm = ({
}

if (isRelOffer) {
if (!isValidNumber(values.feeRel) || values.feeRel < feeRelMin || values.feeRel > feeRelMax) {
if (!isValidNumber(values.feeRel) || values.feeRel < OFFER_FEE_REL_MIN || values.feeRel > OFFER_FEE_REL_MAX) {
errors.feeRel = t('earn.feedback_invalid_rel_fee', {
feeRelPercentageMin: `${factorToPercentage(feeRelMin)}%`,
feeRelPercentageMax: `${factorToPercentage(feeRelMax)}%`,
feeRelPercentageMin: `${factorToPercentage(OFFER_FEE_REL_MIN)}%`,
feeRelPercentageMax: `${factorToPercentage(OFFER_FEE_REL_MAX)}%`,
})
}
}

if (isAbsOffer) {
if (!isValidNumber(values.feeAbs?.value) || values.feeAbs!.value! < 0) {
if (!isValidNumber(values.feeAbs?.value) || values.feeAbs!.value! < OFFER_FEE_ABS_MIN) {
errors.feeAbs = t('earn.feedback_invalid_abs_fee')
}
}
Expand All @@ -263,11 +259,11 @@ const EarnForm = ({
errors.minsize = t('earn.feedback_invalid_min_amount')
} else {
const minsize = values.minsize?.value || 0
if (offerMinsizeMin > offerMinsizeMax) {
if (OFFER_MINSIZE_MIN > offerMinsizeMax) {
errors.minsize = t('earn.feedback_invalid_min_amount_insufficient_funds')
} else if (minsize < offerMinsizeMin || minsize > offerMinsizeMax) {
} else if (minsize < OFFER_MINSIZE_MIN || minsize > offerMinsizeMax) {
errors.minsize = t('earn.feedback_invalid_min_amount_range', {
minAmountMin: offerMinsizeMin.toLocaleString(),
minAmountMin: OFFER_MINSIZE_MIN.toLocaleString(),
minAmountMax: offerMinsizeMax.toLocaleString(),
})
}
Expand All @@ -276,15 +272,7 @@ const EarnForm = ({
}

return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={onSubmit}
validateOnMount={true}
initialTouched={{
minsize: true,
}}
>
<Formik initialValues={initialValues} validate={validate} onSubmit={onSubmit}>
{(props) => {
const { handleSubmit, setFieldValue, handleBlur, values, touched, errors, isSubmitting } = props
const minsizeField = props.getFieldProps<AmountValue>('minsize')
Expand Down Expand Up @@ -347,8 +335,8 @@ const EarnForm = ({
value={typeof values.feeRel === 'number' ? factorToPercentage(values.feeRel) : ''}
isValid={touched.feeRel && !errors.feeRel}
isInvalid={touched.feeRel && !!errors.feeRel}
min={0}
step={feeRelPercentageStep}
min={factorToPercentage(OFFER_FEE_REL_MIN)}
step={factorToPercentage(OFFER_FEE_REL_STEP)}
/>
<rb.Form.Control.Feedback type="invalid">{errors.feeRel}</rb.Form.Control.Feedback>
</rb.InputGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ImportWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
isValidNumber,
walletDisplayNameToFileName,
} from '../utils'
import { JM_GAPLIMIT_DEFAULT, JM_GAPLIMIT_CONFIGKEY } from '../constants/config'
import { JM_GAPLIMIT_DEFAULT, JM_GAPLIMIT_CONFIGKEY } from '../constants/jm'

type ImportWalletDetailsFormValues = {
mnemonicPhrase: MnemonicPhrase
Expand Down
2 changes: 1 addition & 1 deletion src/components/Orderbook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { BTC, factorToPercentage, isAbsoluteOffer, isRelativeOffer } from '../ut
import { isDebugFeatureEnabled, isDevMode } from '../constants/debugFeatures'
import ToggleSwitch from './ToggleSwitch'
import { pseudoRandomNumber } from './Send/helpers'
import { JM_DUST_THRESHOLD } from '../constants/config'
import { JM_DUST_THRESHOLD } from '../constants/jm'
import * as fb from './fb/utils'
import styles from './Orderbook.module.css'

Expand Down
2 changes: 1 addition & 1 deletion src/components/Send/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useServiceInfo, useReloadServiceInfo } from '../../context/ServiceInfoC
import { useLoadConfigValue } from '../../context/ServiceConfigContext'
import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent'
import { routes } from '../../constants/routes'
import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/config'
import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/jm'
import { initialNumCollaborators } from './helpers'

const INITIAL_DESTINATION = null
Expand Down
29 changes: 12 additions & 17 deletions src/components/settings/FeeConfigModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,27 @@ import { Formik, FormikErrors, FormikProps, Field } from 'formik'
import classNames from 'classnames'
import Sprite from '../Sprite'
import { TxFeeInputField, validateTxFee } from './TxFeeInputField'
import { FEE_CONFIG_KEYS, FeeValues, useLoadFeeConfigValues } from '../../hooks/Fees'
import { FeeValues, useLoadFeeConfigValues } from '../../hooks/Fees'
import { useUpdateConfigValues } from '../../context/ServiceConfigContext'
import { isDebugFeatureEnabled } from '../../constants/debugFeatures'
import { FEE_CONFIG_KEYS, JM_MAX_SWEEP_FEE_CHANGE_DEFAULT } from '../../constants/jm'
import {
CJ_FEE_ABS_MAX,
CJ_FEE_ABS_MIN,
CJ_FEE_REL_MAX,
CJ_FEE_REL_MIN,
MAX_SWEEP_FEE_CHANGE_MAX,
MAX_SWEEP_FEE_CHANGE_MIN,
TX_FEES_FACTOR_MAX,
TX_FEES_FACTOR_MIN,
} from '../../constants/jam'
import ToggleSwitch from '../ToggleSwitch'
import { isValidNumber, factorToPercentage, percentageToFactor } from '../../utils'
import BitcoinAmountInput, { AmountValue, toAmountValue } from '../BitcoinAmountInput'
import { JM_MAX_SWEEP_FEE_CHANGE_DEFAULT } from '../../constants/config'
import styles from './FeeConfigModal.module.css'

const __dev_allowFeeValuesReset = isDebugFeatureEnabled('allowFeeValuesReset')

const TX_FEES_FACTOR_MIN = 0 // 0%
/**
* For the same reasons as stated above (comment for `TX_FEES_SATSPERKILOVBYTE_MIN`),
* the maximum randomization factor must not be too high.
* Settling on 50% as a reasonable compromise until this problem is addressed.
* Once resolved, this can be set to 100% again.
*/
const TX_FEES_FACTOR_MAX = 0.5 // 50%
const CJ_FEE_ABS_MIN = 1
const CJ_FEE_ABS_MAX = 1_000_000 // 0.01 BTC - no enforcement by JM - this should be a "sane" max value
const CJ_FEE_REL_MIN = 0.000001 // 0.0001%
const CJ_FEE_REL_MAX = 0.05 // 5% - no enforcement by JM - this should be a "sane" max value
const MAX_SWEEP_FEE_CHANGE_MIN = 0.5 // 50%
const MAX_SWEEP_FEE_CHANGE_MAX = 1 // 100%

interface FeeConfigModalProps {
show: boolean
onHide: () => void
Expand Down
25 changes: 25 additions & 0 deletions src/constants/jam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { percentageToFactor } from '../utils'
import { JM_DUST_THRESHOLD } from './jm'

export const TX_FEES_FACTOR_MIN = 0 // 0%
/**
* For the same reasons as stated above (comment for `TX_FEES_SATSPERKILOVBYTE_MIN`),
* the maximum randomization factor must not be too high.
* Settling on 50% as a reasonable compromise until this problem is addressed.
* Once resolved, this can be set to 100% again.
*/
export const TX_FEES_FACTOR_MAX = percentageToFactor(50) // 50%
export const CJ_FEE_ABS_MIN = 1
export const CJ_FEE_ABS_MAX = 1_000_000 // 0.01 BTC - no enforcement by JM - this should be a "sane" max value
export const CJ_FEE_REL_MIN = percentageToFactor(0.0001)
export const CJ_FEE_REL_MAX = percentageToFactor(5) // no enforcement by JM - this should be a "sane" max value
export const MAX_SWEEP_FEE_CHANGE_MIN = percentageToFactor(50)
export const MAX_SWEEP_FEE_CHANGE_MAX = percentageToFactor(100)

export const OFFER_FEE_REL_MIN = percentageToFactor(0.0001)
export const OFFER_FEE_REL_MAX = percentageToFactor(10)
export const OFFER_FEE_REL_STEP = percentageToFactor(0.0001)

export const OFFER_FEE_ABS_MIN = 0

export const OFFER_MINSIZE_MIN = JM_DUST_THRESHOLD
8 changes: 8 additions & 0 deletions src/constants/config.ts → src/constants/jm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ export const JM_DUST_THRESHOLD = 27_300

// See: https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/v0.9.11/src/jmclient/configure.py#L321 (last check on 2024-07-09 of v0.9.11)
export const JM_MAX_SWEEP_FEE_CHANGE_DEFAULT = 0.8

export const FEE_CONFIG_KEYS: Record<string, ConfigKey> = {
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' },
}
2 changes: 1 addition & 1 deletion src/context/ServiceInfoContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { useCurrentWallet, useClearCurrentWallet } from './WalletContext'
import { useWebsocket } from './WebsocketContext'
import { clearSession } from '../session'
import { CJ_STATE_TAKER_RUNNING, CJ_STATE_MAKER_RUNNING } from '../constants/config'
import { CJ_STATE_TAKER_RUNNING, CJ_STATE_MAKER_RUNNING } from '../constants/jm'
import { noop, setIntervalDebounced, toSemVer, UNKNOWN_VERSION } from '../utils'

import * as Api from '../libs/JmWalletApi'
Expand Down
2 changes: 1 addition & 1 deletion src/context/WalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getSession, setSession } from '../session'
import * as fb from '../components/fb/utils'
import * as Api from '../libs/JmWalletApi'
import { WalletBalanceSummary, toBalanceSummary } from './BalanceSummary'
import { JM_API_AUTH_TOKEN_EXPIRY } from '../constants/config'
import { JM_API_AUTH_TOKEN_EXPIRY } from '../constants/jm'
import { isDevMode } from '../constants/debugFeatures'
import { setIntervalDebounced, walletDisplayName } from '../utils'

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/CoinjoinRequirements.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fb from '../components/fb/utils'
import { groupByJar, Utxos } from '../context/WalletContext'
import { JM_TAKER_UTXO_AGE_DEFAULT } from '../constants/config'
import { JM_TAKER_UTXO_AGE_DEFAULT } from '../constants/jm'

export type CoinjoinRequirementOptions = {
minNumberOfUtxos: number // min amount of utxos available
Expand Down
9 changes: 1 addition & 8 deletions src/hooks/Fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState, useMemo } from 'react'
import { useRefreshConfigValues } from '../context/ServiceConfigContext'
import { AmountSats } from '../libs/JmWalletApi'
import { isValidNumber } from '../utils'
import { FEE_CONFIG_KEYS } from '../constants/jm'

export type TxFeeValueUnit = 'blocks' | 'sats/kilo-vbyte'
export type TxFeeValue = number
Expand All @@ -15,14 +16,6 @@ export const toTxFeeValueUnit = (val?: TxFeeValue): TxFeeValueUnit | 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
Expand Down
39 changes: 39 additions & 0 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
formatSats,
formatBtc,
formatBtcDisplayValue,
calcOfferMinsizeMax,
} from './utils'

describe('shortenStringMiddle', () => {
Expand Down Expand Up @@ -180,3 +181,41 @@ describe('formatBtcDisplayValue', () => {
expect(formatBtcDisplayValue(123456789, { withSymbol: true })).toBe('₿ 1.23 456 789')
})
})

describe('calcOfferMinsizeMax', () => {
it('should calc offer minsize based on wallet balance', () => {
expect(calcOfferMinsizeMax({})).toBe(0)
expect(
calcOfferMinsizeMax(
{
'0': {
accountIndex: 0,
calculatedTotalBalanceInSats: 21,
calculatedAvailableBalanceInSats: 21,
calculatedFrozenOrLockedBalanceInSats: 0,
},
},
0,
),
).toBe(21)
expect(
calcOfferMinsizeMax(
{
'0': {
accountIndex: 0,
calculatedTotalBalanceInSats: 42,
calculatedAvailableBalanceInSats: 41,
calculatedFrozenOrLockedBalanceInSats: 1,
},
'1': {
accountIndex: 1,
calculatedTotalBalanceInSats: 42_000,
calculatedAvailableBalanceInSats: 1,
calculatedFrozenOrLockedBalanceInSats: 41_999,
},
},
21,
),
).toBe(20)
})
})
16 changes: 15 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AmountSats, OfferType, WalletFileName } from './libs/JmWalletApi'
import { JM_DUST_THRESHOLD } from './constants/jm'
import type { AccountBalances } from './context/BalanceSummary'
import type { AmountSats, OfferType, WalletFileName } from './libs/JmWalletApi'

const BTC_FORMATTER = new Intl.NumberFormat('en-US', {
minimumIntegerDigits: 1,
Expand Down Expand Up @@ -135,3 +137,15 @@ export const setIntervalDebounced = (
)
})()
}

const calcMaxAvailableBalanceInJar = (accountBalances: AccountBalances) => {
return Math.max(0, Math.max(...Object.values(accountBalances || []).map((it) => it.calculatedAvailableBalanceInSats)))
}

export const calcOfferMinsizeMax = (
accountBalances: AccountBalances,
minBufferAmount: AmountSats = JM_DUST_THRESHOLD,
) => {
const maxAvailableBalanceInJar = calcMaxAvailableBalanceInJar(accountBalances)
return Math.max(0, maxAvailableBalanceInJar - minBufferAmount)
}