diff --git a/src/components/settings/FeeConfigModal.tsx b/src/components/settings/FeeConfigModal.tsx index 36490405b..581be1bcd 100644 --- a/src/components/settings/FeeConfigModal.tsx +++ b/src/components/settings/FeeConfigModal.tsx @@ -12,6 +12,7 @@ import ToggleSwitch from '../ToggleSwitch' import { isValidNumber, factorToPercentage, percentageToFactor } from '../../utils' import styles from './FeeConfigModal.module.css' import BitcoinAmountInput, { AmountValue, toAmountValue } from '../BitcoinAmountInput' +import { JM_MAX_SWEEP_FEE_CHANGE_DEFAULT } from '../../constants/config' const __dev_allowFeeValuesReset = isDebugFeatureEnabled('allowFeeValuesReset') @@ -27,6 +28,8 @@ 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 @@ -40,7 +43,7 @@ export type FeeConfigSectionKey = 'tx_fee' | 'cj_fee' const TX_FEE_SECTION_KEY: FeeConfigSectionKey = 'tx_fee' const CJ_FEE_SECTION_KEY: FeeConfigSectionKey = 'cj_fee' -type FeeFormValues = Pick & { +type FeeFormValues = Pick & { max_cj_fee_abs?: AmountValue enableValidation?: boolean } @@ -167,7 +170,7 @@ const FeeConfigForm = forwardRef( {t('settings.fees.title_general_fee_settings')} @@ -217,6 +220,55 @@ const FeeConfigForm = forwardRef( {errors.tx_fees_factor} + + + {t('settings.fees.label_max_sweep_fee_change', { + fee: isValidNumber(values.max_sweep_fee_change) + ? `(${factorToPercentage(values.max_sweep_fee_change!)}%)` + : '', + })} + + + {t('settings.fees.description_max_sweep_fee_change', { + defaultValue: `${factorToPercentage(JM_MAX_SWEEP_FEE_CHANGE_DEFAULT)}%`, + })} + + + + % + + { + const value = parseFloat(e.target.value) + setFieldValue('max_sweep_fee_change', percentageToFactor(value), true) + }} + isValid={touched.max_sweep_fee_change && !errors.max_sweep_fee_change} + isInvalid={touched.max_sweep_fee_change && !!errors.max_sweep_fee_change} + min={factorToPercentage(MAX_SWEEP_FEE_CHANGE_MIN)} + max={factorToPercentage(MAX_SWEEP_FEE_CHANGE_MAX)} + step={1} + /> + + {errors.max_sweep_fee_change} + + + @@ -323,6 +375,17 @@ export default function FeeConfigModal({ max: `${factorToPercentage(CJ_FEE_REL_MAX)}%`, }) } + + if ( + !isValidNumber(values.max_sweep_fee_change) || + values.max_sweep_fee_change! < MAX_SWEEP_FEE_CHANGE_MIN || + values.max_sweep_fee_change! > MAX_SWEEP_FEE_CHANGE_MAX + ) { + errors.max_sweep_fee_change = t('settings.fees.feedback_invalid_max_sweep_fee_change', { + min: `${factorToPercentage(MAX_SWEEP_FEE_CHANGE_MIN)}%`, + max: `${factorToPercentage(MAX_SWEEP_FEE_CHANGE_MAX)}%`, + }) + } return errors }, [t], @@ -346,6 +409,10 @@ export default function FeeConfigModal({ key: FEE_CONFIG_KEYS.max_cj_fee_rel, value: String(values.max_cj_fee_rel ?? ''), }, + { + key: FEE_CONFIG_KEYS.max_sweep_fee_change, + value: String(values.max_sweep_fee_change ?? ''), + }, ] setSaveErrorMessage(undefined) @@ -452,6 +519,7 @@ export default function FeeConfigModal({ formRef.current?.setFieldValue('max_cj_fee_rel', undefined, false) formRef.current?.setFieldValue('tx_fees', undefined, false) formRef.current?.setFieldValue('tx_fees_factor', undefined, false) + formRef.current?.setFieldValue('max_sweep_fee_change', undefined, false) setTimeout(() => formRef.current?.validateForm(), 4) }} disabled={isLoading || isSubmitting} diff --git a/src/constants/config.ts b/src/constants/config.ts index b91f47960..f80c07e50 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -23,3 +23,6 @@ export const JM_API_AUTH_TOKEN_EXPIRY: Milliseconds = Math.round(0.5 * 60 * 60 * // 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 + +// 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 diff --git a/src/hooks/Fees.ts b/src/hooks/Fees.ts index 339cc3b1c..fac10dbce 100644 --- a/src/hooks/Fees.ts +++ b/src/hooks/Fees.ts @@ -20,6 +20,7 @@ export const FEE_CONFIG_KEYS = { 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 { @@ -27,6 +28,7 @@ export interface FeeValues { tx_fees_factor?: number max_cj_fee_abs?: number max_cj_fee_rel?: number + max_sweep_fee_change?: number } export const useLoadFeeConfigValues = () => { @@ -45,6 +47,7 @@ export const useLoadFeeConfigValues = () => { 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) @@ -56,6 +59,7 @@ export const useLoadFeeConfigValues = () => { 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 }, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 1657b0003..d990fc0de 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -260,6 +260,9 @@ "label_max_cj_fee_rel": "Relative limit (per collaborator)", "description_max_cj_fee_rel": "The maximum fee you are willing to pay per collaborator, as a percentage of the transaction amount. Example: if you send 2 million sats and the maximum fee is set to 0.1%, you will at most pay 2,000 sats to a single collaborator.", "feedback_invalid_max_cj_fee_rel": "Please provide a valid maximum relative fee between {{ min }} and {{ max }}.", + "label_max_sweep_fee_change": "Collaborative sweep fee change", + "description_max_sweep_fee_change": "The maximum fee increase you are willing to pay on collaborative sweep transactions, as a percentage of the estimated transaction fee. Example: when set to 60% with 10 sats/vByte, the actual fee for collaborative sweeps can be between 4 and 16 sats/vByte. Default: {{ defaultValue }}", + "feedback_invalid_max_sweep_fee_change": "Please provide a valid maximum sweep fee change between {{ min }} and {{ max }}.", "text_button_cancel": "Cancel", "text_button_submit": "Save", "text_button_submitting": "Saving...",