From efe4b2e5e7af2144f70ff212dd0cda1938e7ee7c Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 23 Apr 2026 21:39:34 +0200 Subject: [PATCH] chore(fee-dialog): add nested TxFeeForm --- src/components/send/SendForm.schema.ts | 2 +- src/components/send/SendForm.tsx | 79 +------ src/components/send/TxFeeForm.schema.ts | 6 +- src/components/send/TxFeeForm.tsx | 9 +- ...hema.ts => CollaboratorFeesForm.schema.ts} | 15 +- .../settings/CollaboratorFeesForm.tsx | 2 +- src/components/settings/FeeLimitDialog.tsx | 51 +++-- .../settings/MiningFeesForm.schema.ts | 52 +++++ src/components/settings/MiningFeesForm.tsx | 204 +++++++----------- .../settings/MiningFeesFormSchema.ts | 88 -------- src/components/settings/TxFeeInputField.tsx | 90 -------- src/constants/jam.ts | 6 +- src/constants/jm.ts | 7 + 13 files changed, 194 insertions(+), 417 deletions(-) rename src/components/settings/{CollaboratorFeesFormSchema.ts => CollaboratorFeesForm.schema.ts} (81%) create mode 100644 src/components/settings/MiningFeesForm.schema.ts delete mode 100644 src/components/settings/MiningFeesFormSchema.ts delete mode 100644 src/components/settings/TxFeeInputField.tsx diff --git a/src/components/send/SendForm.schema.ts b/src/components/send/SendForm.schema.ts index c61d18c4..4e470dfe 100644 --- a/src/components/send/SendForm.schema.ts +++ b/src/components/send/SendForm.schema.ts @@ -161,7 +161,7 @@ export const createSendFormSchema = ( }), }) // eslint-disable-next-line unicorn/prefer-spread -- false positive - .concat(createTxFeeFormSchema(t)) + .concat(createTxFeeFormSchema({ t })) .required() .test('address-not-from-source-jar-test', function (root) { // Note: `fromJar` might still be `undefined` at this point diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx index 77a07b4e..ff67f4d0 100644 --- a/src/components/send/SendForm.tsx +++ b/src/components/send/SendForm.tsx @@ -155,12 +155,19 @@ export function SendForm({ SendFormValues >({ mode: 'onSubmit', - + disabled, defaultValues, // force type (see https://github.com/react-hook-form/resolvers/issues/807) resolver: yupResolver(schema) as Resolver, }) - const { errors, isSubmitting, isValid } = formState + const { errors, isValid, isSubmitting } = useMemo( + () => ({ + errors: formState.errors, + isValid: formState.isValid, + isSubmitting: formState.isSubmitting, + }), + [formState.errors, formState.isValid, formState.isSubmitting], + ) const values = useWatch({ control }) const sourceJarIndex = useWatch({ control, name: 'source.fromJar' }) @@ -598,75 +605,7 @@ export function SendForm({ )} )} - - - {/*
- - -
- - {t('send.label_tx_fees')} - {t('settings.fees.description_tx_fees_blocks')} - - - - - - - - {errors.txFeeInBlocks?.message && ( -
{errors.txFeeInBlocks.message}
- )} -
-
- -
- - {t('send.label_tx_fees')} - {t('settings.fees.description_tx_fees_satspervbyte')} - - - - - / vB - - - - {errors.txFeeInSatsPerVbyte?.message && ( -
{errors.txFeeInSatsPerVbyte.message}
- )} -
-
-
- { - setValue('txFeeUnit', value as TxFeeUnit, { - shouldValidate: true, // trigger validation - shouldTouch: true, // update touched fields form state - shouldDirty: true, // update dirty and dirty fields form state - }) - }} - /> -
*/} diff --git a/src/components/send/TxFeeForm.schema.ts b/src/components/send/TxFeeForm.schema.ts index 8d084d2b..cf649671 100644 --- a/src/components/send/TxFeeForm.schema.ts +++ b/src/components/send/TxFeeForm.schema.ts @@ -29,7 +29,11 @@ export interface TxFeeFormValues { } } -export function createTxFeeFormSchema(t: TFunction): yup.ObjectSchema { +export function createTxFeeFormSchema({ + t, +}: { + t: TFunction<'translation', undefined> +}): yup.ObjectSchema { const feedbackInvalidTxFeesBlocks = t('settings.fees.feedback_invalid_tx_fees_blocks', { min: MIN_TX_FEE_IN_BLOCKS, max: MAX_TX_FEE_IN_BLOCKS, diff --git a/src/components/send/TxFeeForm.tsx b/src/components/send/TxFeeForm.tsx index 9fdc3170..816940dc 100644 --- a/src/components/send/TxFeeForm.tsx +++ b/src/components/send/TxFeeForm.tsx @@ -44,10 +44,7 @@ const TxFeeUnitInput = (props: React.ComponentProps) => { className="order-1 size-5 cursor-pointer after:absolute after:inset-0 [&_svg]:size-3" />
- - - / vB - + @@ -87,6 +84,7 @@ export function TxFeeForm({ className }: TxFeeFormProps) { id="txFeeInBlocks" {...register('txFee.txFeeInBlocks', { disabled, + valueAsNumber: true, })} type="number" min={MIN_TX_FEE_IN_BLOCKS} @@ -113,13 +111,14 @@ export function TxFeeForm({ className }: TxFeeFormProps) { id="txFeeInSatsPerVbyte" {...register('txFee.txFeeInSatsPerVbyte', { disabled, + valueAsNumber: true, })} type="number" min={MIN_TX_FEE_IN_SATS_PER_VBYTE} max={MAX_TX_FEE_IN_SATS_PER_VBYTE} step={1} /> - + / vB diff --git a/src/components/settings/CollaboratorFeesFormSchema.ts b/src/components/settings/CollaboratorFeesForm.schema.ts similarity index 81% rename from src/components/settings/CollaboratorFeesFormSchema.ts rename to src/components/settings/CollaboratorFeesForm.schema.ts index 4cb5e06e..c6c5a0c3 100644 --- a/src/components/settings/CollaboratorFeesFormSchema.ts +++ b/src/components/settings/CollaboratorFeesForm.schema.ts @@ -9,16 +9,11 @@ export type CollaboratorFeesFormValues = { maxCjFeeRelInPercent?: number } -export function collaboratorFeesFormSchema(enableFormValidation: boolean, t: TFunction<'translation', undefined>) { - if (!enableFormValidation) { - return yup - .object({ - maxCjFeeAbs: yup.string().default(''), - maxCjFeeRelInPercent: yup.string().default(''), - }) - .required() - } - +export function createCollaboratorFeesFormSchema({ + t, +}: { + t: TFunction<'translation', undefined> +}): yup.ObjectSchema { const maxCjFeeAbsoluteMessage = t('settings.fees.feedback_invalid_max_cj_fee_abs', { min: formatSats(CJ_FEE_ABS_MIN), max: formatSats(CJ_FEE_ABS_MAX), diff --git a/src/components/settings/CollaboratorFeesForm.tsx b/src/components/settings/CollaboratorFeesForm.tsx index 05b8bd8e..1bdf1cce 100644 --- a/src/components/settings/CollaboratorFeesForm.tsx +++ b/src/components/settings/CollaboratorFeesForm.tsx @@ -6,7 +6,7 @@ import { cn } from '@/lib/utils' import { Field, FieldDescription, FieldLabel } from '../ui/field' import { InputGroup, InputGroupAddon, InputGroupInput } from '../ui/input-group' import { SatSymbol } from '../ui/jam/CurrencySymbol' -import type { CollaboratorFeesFormValues } from './CollaboratorFeesFormSchema' +import type { CollaboratorFeesFormValues } from './CollaboratorFeesForm.schema' const FieldPrefixSatSymbol = ( , 'children'>, @@ -57,29 +57,24 @@ export const FeeLimitDialog = ({ open, onOpenChange, walletFileName, ...dialogPr const client = useApiClient() - const miningFeeFormSchema = useMemo(() => { - return miningFeesFormSchema(enableFormValidation, t) - }, [enableFormValidation, t]) + const miningFeeFormSchema = useMemo(() => createMiningFeesFormSchema({ t }), [t]) - const miningFeeFormInitialValues: MiningFeesFormValues = useMemo(() => { - const txFee = toTxFee(feeConfigValues) + const miningFeeFormDefaultValues: MiningFeesFormValues = useMemo(() => { const txFeesFactor = Number.parseFloat(feeConfigValues.tx_fees_factor || '') const maxSweepChangeFactor = Number.parseFloat(feeConfigValues.max_sweep_fee_change || '') return { - feeType: txFee.unit, - txFeesBlocks: txFee.unit === txFeeUnit.BLOCKS ? txFee.value : undefined, - txFeesSatsPerVbyte: txFee.unit === txFeeUnit.SATS_PER_KILO_VBYTE ? txFee.value / 1_000 : undefined, txFeesFactorInPercent: Number.isFinite(txFeesFactor) ? factorToPercentage(txFeesFactor) : undefined, maxSweepFeeChangeInPercent: Number.isFinite(maxSweepChangeFactor) ? factorToPercentage(maxSweepChangeFactor) : undefined, + ...toTxFeeFormDefaultValues(feeConfigValues), } }, [feeConfigValues]) const miningFeesForm = useForm({ mode: 'onChange', disabled: isSubmitting || isLoadingConfig, - values: miningFeeFormInitialValues, + defaultValues: miningFeeFormDefaultValues, resolver: yupResolver(miningFeeFormSchema as yup.AnyObjectSchema) as Resolver< MiningFeesFormValues, unknown, @@ -87,11 +82,9 @@ export const FeeLimitDialog = ({ open, onOpenChange, walletFileName, ...dialogPr >, }) - const collaboratorFormSchema = useMemo(() => { - return collaboratorFeesFormSchema(enableFormValidation, t) - }, [enableFormValidation, t]) + const collaboratorFormSchema = useMemo(() => createCollaboratorFeesFormSchema({ t }), [t]) - const collaboratorFeesFormInitialValues: CollaboratorFeesFormValues = useMemo(() => { + const collaboratorFeesFormDefaultValues: CollaboratorFeesFormValues = useMemo(() => { const maxCjFeeAbsolute = Number.parseInt(feeConfigValues?.max_cj_fee_abs || '', 10) const maxCjFeeRelative = Number.parseFloat(feeConfigValues?.max_cj_fee_rel || '') return { @@ -103,7 +96,7 @@ export const FeeLimitDialog = ({ open, onOpenChange, walletFileName, ...dialogPr const collaboratorFeesForm = useForm({ mode: 'onChange', disabled: isSubmitting || isLoadingConfig, - values: collaboratorFeesFormInitialValues, + defaultValues: collaboratorFeesFormDefaultValues, resolver: yupResolver(collaboratorFormSchema as yup.AnyObjectSchema) as Resolver< CollaboratorFeesFormValues, unknown, @@ -117,12 +110,14 @@ export const FeeLimitDialog = ({ open, onOpenChange, walletFileName, ...dialogPr setIsSubmitting(true) try { - // Trigger validation on both forms before submission - const collaboratorValid = await collaboratorFeesForm.trigger() - const miningValid = await miningFeesForm.trigger() + if (enableFormValidation) { + // Trigger validation on both nested forms before submission + const collaboratorValid = await collaboratorFeesForm.trigger() + const miningValid = await miningFeesForm.trigger() - if (!collaboratorValid || !miningValid) { - return + if (!collaboratorValid || !miningValid) { + return + } } const collaboratorData = collaboratorFeesForm.getValues() @@ -136,12 +131,14 @@ export const FeeLimitDialog = ({ open, onOpenChange, walletFileName, ...dialogPr collaboratorData.maxCjFeeRelInPercent !== undefined && Number.isFinite(collaboratorData.maxCjFeeRelInPercent) ? String(percentageToFactor(collaboratorData.maxCjFeeRelInPercent)) : '' - const txFeesBlocksValue = Number.isSafeInteger(miningData.txFeesBlocks) ? String(miningData.txFeesBlocks) : '' + const txFeesBlocksValue = Number.isSafeInteger(miningData.txFee.txFeeInBlocks) + ? String(miningData.txFee.txFeeInBlocks) + : '' const txFeesSatsPerKvByteValue = - miningData.txFeesSatsPerVbyte !== undefined && Number.isFinite(miningData.txFeesSatsPerVbyte) - ? String(Math.round(miningData.txFeesSatsPerVbyte * 1_000)) + miningData.txFee.txFeeInSatsPerVbyte !== undefined && Number.isFinite(miningData.txFee.txFeeInSatsPerVbyte) + ? String(Math.ceil(miningData.txFee.txFeeInSatsPerVbyte * 1_000)) : '' - const txFeesValue = miningData.feeType === txFeeUnit.BLOCKS ? txFeesBlocksValue : txFeesSatsPerKvByteValue + const txFeesValue = miningData.txFee.txFeeUnit === txFeeUnit.BLOCKS ? txFeesBlocksValue : txFeesSatsPerKvByteValue const txFeesFactorValue = miningData.txFeesFactorInPercent !== undefined && Number.isFinite(miningData.txFeesFactorInPercent) ? String(percentageToFactor(miningData.txFeesFactorInPercent)) @@ -211,7 +208,7 @@ export const FeeLimitDialog = ({ open, onOpenChange, walletFileName, ...dialogPr -
+
{isDeveloperMode && ( <>
diff --git a/src/components/settings/MiningFeesForm.schema.ts b/src/components/settings/MiningFeesForm.schema.ts new file mode 100644 index 00000000..3dcf4f88 --- /dev/null +++ b/src/components/settings/MiningFeesForm.schema.ts @@ -0,0 +1,52 @@ +import type { TFunction } from 'i18next' +import * as yup from 'yup' +import { + TX_FEES_FACTOR_MIN, + TX_FEES_FACTOR_MAX, + MAX_SWEEP_FEE_CHANGE_MIN, + MAX_SWEEP_FEE_CHANGE_MAX, +} from '@/constants/jam' +import { factorToPercentage } from '@/lib/utils' +import { createTxFeeFormSchema, type TxFeeFormValues } from '../send/TxFeeForm.schema' + +export type MiningFeesFormValues = { + txFeesFactorInPercent?: number + maxSweepFeeChangeInPercent?: number + txFee: TxFeeFormValues['txFee'] +} + +export function createMiningFeesFormSchema({ + t, +}: { + t: TFunction<'translation', undefined> +}): yup.ObjectSchema { + const txFeesFactorMessage = t('settings.fees.feedback_invalid_tx_fees_factor', { + min: factorToPercentage(TX_FEES_FACTOR_MIN).toLocaleString(), + max: factorToPercentage(TX_FEES_FACTOR_MAX).toLocaleString(), + }) + const maxSweepFeeChangeMessage = t('settings.fees.feedback_invalid_max_sweep_fee_change', { + min: factorToPercentage(MAX_SWEEP_FEE_CHANGE_MIN).toLocaleString(), + max: factorToPercentage(MAX_SWEEP_FEE_CHANGE_MAX).toLocaleString(), + }) + + return ( + yup + .object({ + txFeesFactorInPercent: yup + .number() + .transform((value) => (Number.isFinite(value) ? Number(value) : null)) + .min(factorToPercentage(TX_FEES_FACTOR_MIN), txFeesFactorMessage) + .max(factorToPercentage(TX_FEES_FACTOR_MAX), txFeesFactorMessage) + .required(txFeesFactorMessage), + maxSweepFeeChangeInPercent: yup + .number() + .transform((value) => (Number.isFinite(value) ? Number(value) : null)) + .min(factorToPercentage(MAX_SWEEP_FEE_CHANGE_MIN), maxSweepFeeChangeMessage) + .max(factorToPercentage(MAX_SWEEP_FEE_CHANGE_MAX), maxSweepFeeChangeMessage) + .required(maxSweepFeeChangeMessage), + }) + // eslint-disable-next-line unicorn/prefer-spread -- false positive + .concat(createTxFeeFormSchema({ t })) + .required() + ) +} diff --git a/src/components/settings/MiningFeesForm.tsx b/src/components/settings/MiningFeesForm.tsx index 665a6e8e..91e8ed36 100644 --- a/src/components/settings/MiningFeesForm.tsx +++ b/src/components/settings/MiningFeesForm.tsx @@ -1,13 +1,13 @@ +import { useMemo } from 'react' import { PercentIcon } from 'lucide-react' -import { useWatch, type UseFormReturn } from 'react-hook-form' +import { FormProvider, type UseFormReturn } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { Label } from '@/components/ui/label' -import { JM_MAX_SWEEP_FEE_CHANGE_DEFAULT, txFeeUnit, type TxFeeUnit } from '@/constants/jm' +import { JM_MAX_SWEEP_FEE_CHANGE_DEFAULT, JM_TX_FEES_FACTOR_DEFAULT } from '@/constants/jm' import { cn, factorToPercentage } from '@/lib/utils' +import { TxFeeForm } from '../send/TxFeeForm' import { Field, FieldDescription, FieldLabel } from '../ui/field' import { InputGroup, InputGroupAddon, InputGroupInput } from '../ui/input-group' -import type { MiningFeesFormValues } from './MiningFeesFormSchema' -import { TxFeeInputField } from './TxFeeInputField' +import type { MiningFeesFormValues } from './MiningFeesForm.schema' interface MiningFeesFormProps { className?: string @@ -16,130 +16,92 @@ interface MiningFeesFormProps { export const MiningFeesForm = ({ className, - form: { - register, - control, - setValue, - formState: { errors, disabled }, - }, + form: { register, control, setValue, formState, ...formMethods }, }: MiningFeesFormProps) => { const { t } = useTranslation() - - const feeType = useWatch({ control, name: 'feeType' }) - const txFeesBlocks = useWatch({ control, name: 'txFeesBlocks' }) - const txFeesSatsPerVbyte = useWatch({ control, name: 'txFeesSatsPerVbyte' }) - - const handleTxFeeUnitChange = (newUnit: TxFeeUnit) => { - if (newUnit !== feeType) { - const currentValue = feeType === txFeeUnit.BLOCKS ? txFeesBlocks : txFeesSatsPerVbyte - - if (currentValue) { - const numberValue = Number(currentValue) - if (!Number.isNaN(numberValue)) { - if (newUnit === txFeeUnit.SATS_PER_KILO_VBYTE && feeType === txFeeUnit.BLOCKS) { - setValue('txFeesSatsPerVbyte', Math.round(numberValue * 1_000) / 1_000, { - shouldDirty: true, - shouldValidate: true, - }) - } else if (newUnit === txFeeUnit.BLOCKS && feeType === txFeeUnit.SATS_PER_KILO_VBYTE) { - const converted = Math.round(numberValue * 1_000) - setValue('txFeesBlocks', Math.round(converted / 1_000), { - shouldDirty: true, - shouldValidate: true, - }) - } - } - } - } - - setValue('feeType', newUnit, { shouldDirty: true, shouldValidate: true }) - } - - const handleTxFeeValueChange = (value: string) => { - const fieldName = feeType === txFeeUnit.BLOCKS ? 'txFeesBlocks' : 'txFeesSatsPerVbyte' - setValue(fieldName, Number(value), { shouldDirty: true, shouldValidate: true }) - } - - const txFeesError = feeType === txFeeUnit.BLOCKS ? errors.txFeesBlocks?.message : errors.txFeesSatsPerVbyte?.message + const { errors, disabled } = useMemo( + () => ({ + errors: formState.errors, + disabled: formState.disabled, + }), + [formState.errors, formState.disabled], + ) return ( -
-

{t('settings.fees.description_general_fee_settings')}

+ +
+

{t('settings.fees.description_general_fee_settings')}

-
- - -
+ -
- - {t('settings.fees.label_tx_fees_factor')} - - {t('settings.fees.description_tx_fees_factor_^0.9.10')} - - - + + + {t('settings.fees.label_tx_fees_factor', { + // TODO: i18n - change variable name.. `defaultValue` is used by the library! + defaultValue: `${factorToPercentage(JM_TX_FEES_FACTOR_DEFAULT)}%`, })} - type="number" - inputMode="decimal" - min="0" - max="100" - step="1" - /> - - - - - - {errors.txFeesFactorInPercent?.message && ( -
{errors.txFeesFactorInPercent.message}
- )} -
- -
- - - {t('settings.fees.label_max_sweep_fee_change')} - - - {t('settings.fees.description_max_sweep_fee_change', { - // TODO: i18n - change variable name.. `defaultValue` is used by the library! - defaultValue: `${factorToPercentage(JM_MAX_SWEEP_FEE_CHANGE_DEFAULT)}%`, - })} - - - - - - - - {errors.maxSweepFeeChangeInPercent?.message && ( -
{errors.maxSweepFeeChangeInPercent.message}
+ + + {t('settings.fees.description_tx_fees_factor_^0.9.10')} + + + + + + + +
+ {errors.txFeesFactorInPercent?.message && ( +
{errors.txFeesFactorInPercent.message}
)} - +
+ +
+ + + {t('settings.fees.label_max_sweep_fee_change')} + + + {t('settings.fees.description_max_sweep_fee_change', { + // TODO: i18n - change variable name.. `defaultValue` is used by the library! + defaultValue: `${factorToPercentage(JM_MAX_SWEEP_FEE_CHANGE_DEFAULT)}%`, + })} + + + + + + + + {errors.maxSweepFeeChangeInPercent?.message && ( +
{errors.maxSweepFeeChangeInPercent.message}
+ )} +
+
-
+ ) } diff --git a/src/components/settings/MiningFeesFormSchema.ts b/src/components/settings/MiningFeesFormSchema.ts deleted file mode 100644 index fcc42864..00000000 --- a/src/components/settings/MiningFeesFormSchema.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { TFunction } from 'i18next' -import * as yup from 'yup' -import { - TX_FEES_FACTOR_MIN, - TX_FEES_FACTOR_MAX, - MAX_SWEEP_FEE_CHANGE_MIN, - MAX_SWEEP_FEE_CHANGE_MAX, -} from '@/constants/jam' -import { txFeeUnit, type TxFeeUnit } from '@/constants/jm' -import { factorToPercentage } from '@/lib/utils' - -export type MiningFeesFormValues = { - feeType: TxFeeUnit - txFeesBlocks?: number - txFeesSatsPerVbyte?: number - txFeesFactorInPercent?: number - maxSweepFeeChangeInPercent?: number -} - -export function miningFeesFormSchema(enableFormValidation: boolean, t: TFunction<'translation', undefined>) { - if (!enableFormValidation) { - return yup - .object({ - feeType: yup.string().oneOf([txFeeUnit.BLOCKS, txFeeUnit.SATS_PER_KILO_VBYTE]).required(), - txFeesBlocks: yup.string().default(''), - txFeesSatsPerVbyte: yup.string().default(''), - txFeesFactorInPercent: yup.string().default(''), - maxSweepFeeChange: yup.string().default(''), - }) - .required() - } - - const txFeesBlocksMessage = t('settings.fees.feedback_invalid_tx_fees_blocks', { - min: Number(1).toLocaleString(), - max: Number(1_000).toLocaleString(), - }) - const txFeesSatsMessage = t('settings.fees.feedback_invalid_tx_fees_satspervbyte', { - min: Number(1.001).toLocaleString(), - max: Number(350).toLocaleString(), - }) - const txFeesFactorMessage = t('settings.fees.feedback_invalid_tx_fees_factor', { - min: factorToPercentage(TX_FEES_FACTOR_MIN).toLocaleString(), - max: factorToPercentage(TX_FEES_FACTOR_MAX).toLocaleString(), - }) - const maxSweepFeeChangeMessage = t('settings.fees.feedback_invalid_max_sweep_fee_change', { - min: factorToPercentage(MAX_SWEEP_FEE_CHANGE_MIN).toLocaleString(), - max: factorToPercentage(MAX_SWEEP_FEE_CHANGE_MAX).toLocaleString(), - }) - - return yup - .object({ - feeType: yup.string().oneOf([txFeeUnit.BLOCKS, txFeeUnit.SATS_PER_KILO_VBYTE]).required(), - txFeesBlocks: yup.number().when('feeType', { - is: txFeeUnit.BLOCKS, - then: (schema) => - schema - .integer(txFeesBlocksMessage) - .transform((value) => (Number.isSafeInteger(value) ? Number(value) : null)) - .min(1, txFeesBlocksMessage) - .max(1_000, txFeesBlocksMessage) - .required(txFeesBlocksMessage), - otherwise: (schema) => schema.nullable().optional(), - }), - txFeesSatsPerVbyte: yup.number().when('feeType', { - is: txFeeUnit.SATS_PER_KILO_VBYTE, - then: (schema) => - schema - .transform((value) => (Number.isFinite(value) ? Number(value) : null)) - .min(1.001, txFeesSatsMessage) - .max(350, txFeesSatsMessage) - .required(txFeesSatsMessage), - otherwise: (schema) => schema.nullable().optional(), - }), - txFeesFactorInPercent: yup - .number() - .transform((value) => (Number.isFinite(value) ? Number(value) : null)) - .min(factorToPercentage(TX_FEES_FACTOR_MIN), txFeesFactorMessage) - .max(factorToPercentage(TX_FEES_FACTOR_MAX), txFeesFactorMessage) - .required(txFeesFactorMessage), - maxSweepFeeChangeInPercent: yup - .number() - .transform((value) => (Number.isFinite(value) ? Number(value) : null)) - .min(factorToPercentage(MAX_SWEEP_FEE_CHANGE_MIN), maxSweepFeeChangeMessage) - .max(factorToPercentage(MAX_SWEEP_FEE_CHANGE_MAX), maxSweepFeeChangeMessage) - .required(maxSweepFeeChangeMessage), - }) - .required() -} diff --git a/src/components/settings/TxFeeInputField.tsx b/src/components/settings/TxFeeInputField.tsx deleted file mode 100644 index 0b68b664..00000000 --- a/src/components/settings/TxFeeInputField.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useMemo } from 'react' -import { cx } from 'class-variance-authority' -import { BlocksIcon } from 'lucide-react' -import { useTranslation } from 'react-i18next' -import { Input } from '@/components/ui/input' -import { CurrencySymbol } from '@/components/ui/jam/CurrencySymbol' -import { txFeeUnit, type TxFeeUnit } from '@/constants/jm' - -export interface TxFeeInputFieldProps { - value: string - unit: TxFeeUnit - onUnitChange: (unit: TxFeeUnit) => void - onValueChange: (value: string) => void - error?: string - disabled?: boolean -} - -export const TxFeeInputField = ({ - value, - unit, - onUnitChange, - onValueChange, - error, - disabled, -}: TxFeeInputFieldProps) => { - const { t } = useTranslation() - const unitTabs = useMemo( - () => [ - { label: t('settings.fees.radio_tx_fees_blocks'), value: txFeeUnit.BLOCKS }, - { label: t('settings.fees.radio_tx_fees_satspervbyte'), value: txFeeUnit.SATS_PER_KILO_VBYTE }, - ], - [t], - ) - - const handleUnitChange = (newUnit: TxFeeUnit) => { - if (newUnit !== unit) { - onUnitChange(newUnit) - } - } - - return ( -
-
- {unitTabs.map((tab) => ( - - ))} -
-

- {unit === txFeeUnit.BLOCKS - ? t('settings.fees.description_tx_fees_blocks') - : t('settings.fees.description_tx_fees_satspervbyte')} -

-
-
- {unit === txFeeUnit.BLOCKS ? ( - - ) : ( - <> - - / vB - - )} -
- onValueChange(event.target.value)} - placeholder={unit === txFeeUnit.BLOCKS ? '3' : '1.0'} - className="h-full rounded-l-none" - disabled={disabled} - /> -
- {error &&
{error}
} -
- ) -} diff --git a/src/constants/jam.ts b/src/constants/jam.ts index 174e2b34..0d9bdf25 100644 --- a/src/constants/jam.ts +++ b/src/constants/jam.ts @@ -20,13 +20,13 @@ export const TX_FEES_FACTOR_MIN = 0 // 0% * 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 TX_FEES_FACTOR_MAX = percentageToFactor(50) // TODO: since JM 0.9.11, this is only applied to the upside, so it can also be 110%, why limit it to 50%? export const CJ_FEE_ABS_MIN: AmountSats = 1 export const CJ_FEE_ABS_MAX: AmountSats = 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 MAX_SWEEP_FEE_CHANGE_MIN = percentageToFactor(50) // no enforcement by JM - should be a "sane" min vaue (too low and users might run into problems on sweeps) +export const MAX_SWEEP_FEE_CHANGE_MAX = percentageToFactor(100) // TODO: this can also be 200%, why limit it to 100%? export const OFFER_FEE_REL_MIN = percentageToFactor(0.0001) export const OFFER_FEE_REL_MAX = percentageToFactor(10) diff --git a/src/constants/jm.ts b/src/constants/jm.ts index 35686839..94918d9f 100644 --- a/src/constants/jm.ts +++ b/src/constants/jm.ts @@ -19,6 +19,7 @@ export const JM_API_AUTH_TOKEN_EXPIRY: Milliseconds = Math.max( ), ) +// initial value for `max_sweep_fee_change` from the default joinmarket.cfg (last check on 2026-04-22 of v0.9.12) export const JM_MAX_SWEEP_FEE_CHANGE_DEFAULT = 0.8 export const JM_DUST_THRESHOLD: AmountSats = 27_300 @@ -26,6 +27,12 @@ export const JM_DUST_THRESHOLD: AmountSats = 27_300 // initial value for `minimum_makers` from the default joinmarket.cfg (last check on 2022-02-20 of v0.9.5) export const JM_MINIMUM_MAKERS_DEFAULT = 4 +// initial value for `tx_fees` from the default joinmarket.cfg (last check on 2026-04-22 of v0.9.12) +export const JM_TX_FEES_DEFAULT = 3 + +// initial value for `tx_fees_factor` from the default joinmarket.cfg (last check on 2026-04-22 of v0.9.12) +export const JM_TX_FEES_FACTOR_DEFAULT = 0.2 + export const JM_GAPLIMIT_DEFAULT = 6 export const JM_GAPLIMIT_CONFIGKEY: ConfigKey = {