diff --git a/src/components/Earn.tsx b/src/components/Earn.tsx index 4fa872a4..9b5da7bc 100644 --- a/src/components/Earn.tsx +++ b/src/components/Earn.tsx @@ -269,10 +269,10 @@ const EarnForm = ({ value: OFFERTYPE_REL, }, ]} - onChange={(tab, checked) => { - checked && setFieldValue('offertype', tab.value, true) + value={values.offertype} + onChange={(tab) => { + setFieldValue('offertype', tab.value, true) }} - initialValue={values.offertype} disabled={isLoading || isSubmitting} /> diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 70987a2f..6554ce4a 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -21,22 +21,18 @@ const feeRange: (txFee: TxFee, txFeeFactor: number) => [number, number] = (txFee return [minFeeSatsPerVByte, maxFeeSatsPerVByte] } -const useMiningFeeText = ({ feeConfigValues }: { feeConfigValues?: FeeValues }) => { +const useMiningFeeText = ({ tx_fees, tx_fees_factor }: Pick) => { const { t } = useTranslation() - const miningFeeText = useMemo(() => { - if (!feeConfigValues) return null - if (!isValidNumber(feeConfigValues.tx_fees?.value) || !isValidNumber(feeConfigValues.tx_fees_factor)) return null + return useMemo(() => { + if (!isValidNumber(tx_fees?.value) || !isValidNumber(tx_fees_factor)) return null - if (!feeConfigValues.tx_fees?.unit) { + if (!tx_fees?.unit) { return null - } else if (feeConfigValues.tx_fees.unit === 'blocks') { - return t('send.confirm_send_modal.text_miner_fee_in_targeted_blocks', { count: feeConfigValues.tx_fees.value }) + } else if (tx_fees.unit === 'blocks') { + return t('send.confirm_send_modal.text_miner_fee_in_targeted_blocks', { count: tx_fees.value }) } else { - const [minFeeSatsPerVByte, maxFeeSatsPerVByte] = feeRange( - feeConfigValues.tx_fees, - feeConfigValues.tx_fees_factor!, - ) + const [minFeeSatsPerVByte, maxFeeSatsPerVByte] = feeRange(tx_fees, tx_fees_factor!) const fractionDigits = 2 if (minFeeSatsPerVByte.toFixed(fractionDigits) === maxFeeSatsPerVByte.toFixed(fractionDigits)) { @@ -56,9 +52,7 @@ const useMiningFeeText = ({ feeConfigValues }: { feeConfigValues?: FeeValues }) }), }) } - }, [t, feeConfigValues]) - - return miningFeeText + }, [t, tx_fees, tx_fees_factor]) } interface PaymentDisplayInfo { @@ -92,7 +86,7 @@ export function PaymentConfirmModal({ const { t } = useTranslation() const settings = useSettings() - const miningFeeText = useMiningFeeText({ feeConfigValues }) + const miningFeeText = useMiningFeeText({ ...feeConfigValues }) const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ isCoinjoin, feeConfigValues, @@ -161,17 +155,6 @@ export function PaymentConfirmModal({ )} - - {miningFeeText && ( - - - {t('send.confirm_send_modal.label_miner_fee')} - - - {miningFeeText} - - - )} {isCoinjoin && ( @@ -213,6 +196,16 @@ export function PaymentConfirmModal({ )} + {miningFeeText && ( + + + {t('send.confirm_send_modal.label_miner_fee')} + + + {miningFeeText} + + + )} ) diff --git a/src/components/SegmentedTabs.module.css b/src/components/SegmentedTabs.module.css index 9d0b2006..8dee177b 100644 --- a/src/components/SegmentedTabs.module.css +++ b/src/components/SegmentedTabs.module.css @@ -1,27 +1,27 @@ -.segmented-tabs { +.segmentedTabs { background-color: var(--bs-gray-300); border-radius: 0.25rem; padding: 0.25rem; width: 100%; } -.segmented-tab > label { +.segmentedTab > label { height: 100%; display: flex; flex-direction: column; justify-content: center; } -:root[data-theme='dark'] .segmented-tabs { +:root[data-theme='dark'] .segmentedTabs { color: var(--bs-gray-100); background-color: var(--bs-gray-800); } -.segmented-tab { +.segmentedTab { flex: 1 1 0; } -.segmented-tab input[type='radio'] { +.segmentedTab input[type='radio'] { appearance: none; margin: 0; position: absolute; @@ -30,7 +30,7 @@ width: 0; } -.segmented-tab label { +.segmentedTab label { background-color: white; padding: 0.25rem 1rem; border-radius: 0.1rem; @@ -39,33 +39,33 @@ text-align: center; } -.segmented-tab input[type='radio']:disabled ~ label { +.segmentedTab input[type='radio']:disabled ~ label { background-color: transparent; color: var(--bs-gray-600); opacity: 0.5; } -:root[data-theme='dark'] .segmented-tab input[type='radio']:disabled ~ label { +:root[data-theme='dark'] .segmentedTab input[type='radio']:disabled ~ label { color: var(--bs-gray-600); opacity: 0.5; } -.segmented-tab input[type='radio']:not(:checked):not(:disabled) ~ label { +.segmentedTab input[type='radio']:not(:checked):not(:disabled) ~ label { background-color: transparent; } -.segmented-tab input[type='radio']:not(:disabled) ~ label { +.segmentedTab input[type='radio']:not(:disabled) ~ label { cursor: pointer; } -.segmented-tab input[type='radio']:checked:not(:disabled) ~ label { +.segmentedTab input[type='radio']:checked:not(:disabled) ~ label { background-color: var(--bs-gray-100); box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.1); } -:root[data-theme='dark'] .segmented-tab input[type='radio']:checked:not(:disabled) ~ label { +:root[data-theme='dark'] .segmentedTab input[type='radio']:checked:not(:disabled) ~ label { background-color: var(--bs-gray-600); } -.segmented-tab input[type='radio']:focus ~ label { +.segmentedTab input[type='radio']:focus ~ label { box-shadow: 0 0 0 0.25rem var(--bs-focus-ring-color) !important; } diff --git a/src/components/SegmentedTabs.tsx b/src/components/SegmentedTabs.tsx index d0350cd5..ba294b32 100644 --- a/src/components/SegmentedTabs.tsx +++ b/src/components/SegmentedTabs.tsx @@ -1,53 +1,37 @@ -import React from 'react' +import { ChangeEvent } from 'react' import * as rb from 'react-bootstrap' import styles from './SegmentedTabs.module.css' +type SegmentedTabValue = string interface SegmentedTab { label: string - value: string + value: SegmentedTabValue disabled?: boolean } -function SegmentedTabFormCheck({ id, name, value, label, disabled, checked, onChange }: rb.FormCheckProps) { - const _onChange = (e: React.ChangeEvent) => { - e.stopPropagation() - onChange && onChange(e) - } - - return ( - <> -
- - -
- - ) -} +const SegmentedTabFormCheck = ({ id, name, value, label, disabled, checked, onChange }: rb.FormCheckProps) => ( +
+ + +
+) interface SegmentedTabsProps { name: string tabs: SegmentedTab[] - onChange: (tab: SegmentedTab, checked: boolean) => void - initialValue?: string + onChange: (tab: SegmentedTab) => void + value?: SegmentedTabValue disabled?: boolean } -export default function SegmentedTabs({ name, tabs, onChange, initialValue, disabled = false }: SegmentedTabsProps) { - const _onChange = (e: React.ChangeEvent, tab: SegmentedTab) => { +export default function SegmentedTabs({ name, tabs, onChange, value, disabled = false }: SegmentedTabsProps) { + const _onChange = (e: ChangeEvent, tab: SegmentedTab) => { e.stopPropagation() - onChange(tab, e.currentTarget.checked) + onChange(tab) } return ( -
+
{tabs.map((tab, index) => { return ( @@ -56,9 +40,10 @@ export default function SegmentedTabs({ name, tabs, onChange, initialValue, disa id={`${name}-${index}`} name={name} label={tab.label} + value={tab.value} disabled={disabled || tab.disabled} + checked={value === tab.value} inline={true} - checked={initialValue === tab.value} onChange={(e) => _onChange(e, tab)} /> ) diff --git a/src/components/Send/AmountInputField.module.css b/src/components/Send/AmountInputField.module.css index 5d352d3d..90bcd72d 100644 --- a/src/components/Send/AmountInputField.module.css +++ b/src/components/Send/AmountInputField.module.css @@ -1,13 +1,3 @@ -.inputLoader { - height: 3.5rem; - border-radius: 0.25rem; -} - -.input { - height: 3.5rem; - width: 100%; -} - .button { font-size: 0.875rem !important; } diff --git a/src/components/Send/AmountInputField.tsx b/src/components/Send/AmountInputField.tsx index 15e24ce3..e16a8934 100644 --- a/src/components/Send/AmountInputField.tsx +++ b/src/components/Send/AmountInputField.tsx @@ -2,6 +2,7 @@ import { useRef } from 'react' import { useTranslation } from 'react-i18next' import * as rb from 'react-bootstrap' import { useField, useFormikContext } from 'formik' +import classNames from 'classnames' import Sprite from '../Sprite' import { AccountBalanceSummary } from '../../context/BalanceSummary' import { formatBtcDisplayValue } from '../../utils' @@ -11,6 +12,7 @@ import styles from './AmountInputField.module.css' export type AmountInputFieldProps = { name: string label: string + className?: string placeholder?: string isLoading: boolean disabled?: boolean @@ -21,6 +23,7 @@ export type AmountInputFieldProps = { export const AmountInputField = ({ name, label, + className, placeholder, isLoading, disabled = false, @@ -39,13 +42,13 @@ export const AmountInputField = ({ {isLoading ? ( - + ) : (
{label} {isLoading ? ( - + ) : ( <> {field.value.fromJar !== null ? ( { form.setFieldValue(field.name, form.initialValues[field.name], true) setTimeout(() => ref.current?.focus(), 4) @@ -124,7 +121,7 @@ export const DestinationInputField = ({ aria-label={label} name={field.name} placeholder={t('send.placeholder_recipient')} - className={classNames('slashed-zeroes', styles.input, className)} + className={classNames('slashed-zeroes', className)} value={field.value.value || ''} onBlur={field.onBlur} required @@ -144,7 +141,6 @@ export const DestinationInputField = ({ /> setDestinationJarPickerShown(true)} disabled={disabled || !walletInfo} > diff --git a/src/components/Send/SendForm.module.css b/src/components/Send/SendForm.module.css index e9cc4d50..fcaf7f05 100644 --- a/src/components/Send/SendForm.module.css +++ b/src/components/Send/SendForm.module.css @@ -1,3 +1,9 @@ .blurred { filter: blur(2px); } + +.input { + height: 3.5rem; + width: 100%; + border-radius: 0.25rem; +} diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 10d8ac17..7b16481f 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -1,6 +1,6 @@ import { useState, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { Formik, FormikErrors, FormikProps } from 'formik' +import { Field, Formik, FormikErrors, FormikProps } from 'formik' import * as rb from 'react-bootstrap' import * as Api from '../../libs/JmWalletApi' import ToggleSwitch from '../ToggleSwitch' @@ -16,7 +16,7 @@ import FeeBreakdown from './FeeBreakdown' import Accordion from '../Accordion' import Balance from '../Balance' import FeeConfigModal, { FeeConfigSectionKey } from '../settings/FeeConfigModal' -import { useEstimatedMaxCollaboratorFee, FeeValues } from '../../hooks/Fees' +import { useEstimatedMaxCollaboratorFee, FeeValues, TxFee } from '../../hooks/Fees' import { buildCoinjoinRequirementSummary } from '../../hooks/CoinjoinRequirements' import { MAX_NUM_COLLABORATORS, @@ -29,6 +29,9 @@ import { AccountBalanceSummary } from '../../context/BalanceSummary' import { WalletInfo } from '../../context/WalletContext' import { useSettings } from '../../context/SettingsContext' import styles from './SendForm.module.css' +import { TxFeeInputField, validateTxFee } from '../settings/TxFeeInputField' +import { useServiceInfo } from '../../context/ServiceInfoContext' +import { isFeatureEnabled } from '../../constants/features' type CollaborativeTransactionOptionsProps = { selectedAmount?: AmountValue @@ -53,8 +56,8 @@ function CollaborativeTransactionOptions({ feeConfigValues, reloadFeeConfigValues, }: CollaborativeTransactionOptionsProps) { - const settings = useSettings() const { t } = useTranslation() + const settings = useSettings() const [activeFeeConfigModalSection, setActiveFeeConfigModalSection] = useState() const [showFeeConfigModal, setShowFeeConfigModal] = useState(false) @@ -208,8 +211,9 @@ export interface SendFormValues { sourceJarIndex?: JarIndex destination?: DestinationValue amount?: AmountValue - numCollaborators?: number + txFee?: TxFee isCoinJoin: boolean + numCollaborators?: number } interface InnerSendFormProps { @@ -236,6 +240,7 @@ const InnerSendForm = ({ disabled = false, }: InnerSendFormProps) => { const { t } = useTranslation() + const serviceInfo = useServiceInfo() const jarBalances = useMemo(() => { if (!walletInfo) return [] @@ -283,6 +288,7 @@ const InnerSendForm = ({ +
- {/* direct-send options: empty on purpose */} + {/* direct-send only options: empty on purpose */}
+ + {serviceInfo && isFeatureEnabled('txFeeOnSend', serviceInfo) && ( +
+ +
+ )} + {(props) => { return ( { +const createInitialValues = (numCollaborators: number, feeConfigValues: FeeValues | undefined): SendFormValues => { return { sourceJarIndex: INITIAL_SOURCE_JAR_INDEX ?? undefined, destination: { @@ -71,8 +71,9 @@ const createInitialValues = (minNumCollaborators: number) => { value: INITIAL_AMOUNT, isSweep: false, }, + txFee: feeConfigValues?.tx_fees, isCoinJoin: INITIAL_IS_COINJOIN, - numCollaborators: initialNumCollaborators(minNumCollaborators), + numCollaborators, } } @@ -95,6 +96,7 @@ export default function Send({ wallet }: SendProps) { const [alert, setAlert] = useState() const [isSending, setIsSending] = useState(false) const [minNumCollaborators, setMinNumCollaborators] = useState(JM_MINIMUM_MAKERS_DEFAULT) + const initNumCollaborators = useMemo(() => initialNumCollaborators(minNumCollaborators), [minNumCollaborators]) const [feeConfigValues, reloadFeeConfigValues] = useFeeConfigValues() const maxFeesConfigMissing = useMemo( @@ -131,8 +133,10 @@ export default function Send({ wallet }: SendProps) { const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false) const [showConfirmSendModal, setShowConfirmSendModal] = useState() - const initialValues = useMemo(() => createInitialValues(minNumCollaborators), [minNumCollaborators]) - + const initialValues = useMemo( + () => createInitialValues(initNumCollaborators, feeConfigValues), + [initNumCollaborators, feeConfigValues], + ) const formRef = useRef>(null) const loadNewWalletAddress = useCallback( @@ -261,7 +265,12 @@ export default function Send({ wallet }: SendProps) { [isOperationDisabled, wallet, reloadCurrentWalletInfo, reloadServiceInfo, loadConfigValue, t], ) - const sendPayment = async (sourceJarIndex: JarIndex, destination: Api.BitcoinAddress, amountSats: Api.AmountSats) => { + const sendPayment = async ( + sourceJarIndex: JarIndex, + destination: Api.BitcoinAddress, + amountSats: Api.AmountSats, + txFee: TxFee, + ) => { setAlert(undefined) setPaymentSuccessfulInfoAlert(undefined) setIsSending(true) @@ -270,7 +279,7 @@ export default function Send({ wallet }: SendProps) { try { const res = await Api.postDirectSend( { ...wallet }, - { mixdepth: sourceJarIndex, amount_sats: amountSats, destination }, + { mixdepth: sourceJarIndex, amount_sats: amountSats, destination, txfee: txFee.value }, ) if (res.ok) { @@ -311,6 +320,7 @@ export default function Send({ wallet }: SendProps) { destination: Api.BitcoinAddress, amountSats: Api.AmountSats, counterparties: number, + txFee: TxFee, ) => { setAlert(undefined) setIsSending(true) @@ -324,6 +334,7 @@ export default function Send({ wallet }: SendProps) { amount_sats: amountSats, destination, counterparties, + txfee: txFee.value, }, ) @@ -368,10 +379,12 @@ export default function Send({ wallet }: SendProps) { setAlert(undefined) const abortCtrl = new AbortController() - return Api.getTakerStop({ ...wallet, signal: abortCtrl.signal }).catch((err) => { - if (abortCtrl.signal.aborted) return - setAlert({ variant: 'danger', message: err.message }) - }) + return Api.getTakerStop({ ...wallet, signal: abortCtrl.signal }) + .then((res) => (res.ok ? true : Api.Helper.throwError(res))) + .catch((err) => { + if (abortCtrl.signal.aborted) return + setAlert({ variant: 'danger', message: err.message }) + }) } const onSubmit = async (values: SendFormValues) => { @@ -380,7 +393,10 @@ export default function Send({ wallet }: SendProps) { setPaymentSuccessfulInfoAlert(undefined) const isValid = - values.amount !== undefined && values.sourceJarIndex !== undefined && values.destination !== undefined + values.amount !== undefined && + values.sourceJarIndex !== undefined && + values.destination !== undefined && + values.txFee !== undefined if (isValid) { if (values.amount!.isSweep === true && values.amount!.value !== 0) { @@ -409,8 +425,9 @@ export default function Send({ wallet }: SendProps) { values.destination!.value!, values.amount!.value, values.numCollaborators!, + values.txFee!, ) - : await sendPayment(values.sourceJarIndex!, values.destination!.value!, values.amount!.value) + : await sendPayment(values.sourceJarIndex!, values.destination!.value!, values.amount!.value, values.txFee!) if (success) { formRef.current?.resetForm({ values: initialValues }) @@ -528,7 +545,7 @@ export default function Send({ wallet }: SendProps) { isSweep: showConfirmSendModal.amount!.isSweep, isCoinjoin: showConfirmSendModal.isCoinJoin, numCollaborators: showConfirmSendModal.numCollaborators!, - feeConfigValues, + feeConfigValues: { ...feeConfigValues, tx_fees: showConfirmSendModal.txFee }, }} /> )} diff --git a/src/components/jar_details/JarDetailsOverlay.tsx b/src/components/jar_details/JarDetailsOverlay.tsx index b491dc09..f1970d72 100644 --- a/src/components/jar_details/JarDetailsOverlay.tsx +++ b/src/components/jar_details/JarDetailsOverlay.tsx @@ -324,8 +324,8 @@ const JarDetailsOverlay = (props: JarDetailsOverlayProps) => { checked && setSelectedTab(tab.value)} - initialValue={selectedTab} + onChange={(tab) => setSelectedTab(tab.value)} + value={selectedTab} />
diff --git a/src/components/settings/TxFeeInputField.tsx b/src/components/settings/TxFeeInputField.tsx index 81095a42..95a7117c 100644 --- a/src/components/settings/TxFeeInputField.tsx +++ b/src/components/settings/TxFeeInputField.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { TFunction } from 'i18next' @@ -11,34 +12,20 @@ type SatsPerKiloVByte = number const TX_FEES_BLOCKS_MIN = 1 const TX_FEES_BLOCKS_MAX = 1_000 +const TX_FEES_DEFAULT_UNIT: TxFeeValueUnit = 'blocks' const TX_FEES_SATSPERKILOVBYTE_MIN: SatsPerKiloVByte = 1_001 // actual min of `tx_fees` if unit is sats/kilo-vbyte // 350 sats/vbyte - no enforcement by JM - this should be a "sane" max value (taken default value of "absurd_fee_per_kb") const TX_FEES_SATSPERKILOVBYTE_MAX: SatsPerKiloVByte = 350_000 -const adjustTxFees = (val: TxFee) => { - if (val.unit === 'sats/kilo-vbyte') { - // There is one special case for value `tx_fees`: - // Users are allowed to specify the value in "sats/vbyte", but this might - // be interpreted by JM as "targeted blocks". This adaption makes sure - // that it is in fact closer to what the user actually expects, albeit it - // can be surprising that the value is slightly different as specified. - return { - ...val, - value: Math.max(val.value, TX_FEES_SATSPERKILOVBYTE_MIN), - } - } - return val -} - export const validateTxFee = (val: TxFee | undefined, t: TFunction): FormikErrors => { const errors = {} as FormikErrors if (val?.unit === 'sats/kilo-vbyte') { if ( !isValidNumber(val.value) || - val.value < TX_FEES_SATSPERKILOVBYTE_MIN || - val.value > TX_FEES_SATSPERKILOVBYTE_MAX + val.value! < TX_FEES_SATSPERKILOVBYTE_MIN || + val.value! > TX_FEES_SATSPERKILOVBYTE_MAX ) { errors.value = t('settings.fees.feedback_invalid_tx_fees_satspervbyte', { min: (TX_FEES_SATSPERKILOVBYTE_MIN / 1_000).toLocaleString(undefined, { @@ -63,18 +50,20 @@ export const validateTxFee = (val: TxFee | undefined, t: TFunction): FormikError type TxFeeInputFieldProps = FieldProps & { label: string + className?: string } -export const TxFeeInputField = ({ field, form, label }: TxFeeInputFieldProps) => { +export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFieldProps) => { const { t } = useTranslation() + const unitDisplayValue = useMemo(() => field.value?.unit || TX_FEES_DEFAULT_UNIT, [field.value?.unit]) + return ( <> {label} - - + value: 'sats/kilo-vbyte', }, ]} + value={unitDisplayValue} + disabled={form.isSubmitting} onChange={(tab) => { const unit = tab.value as TxFeeValueUnit - if (field.value) { - if (unit === 'sats/kilo-vbyte') { - form.setFieldValue( - field.name, - adjustTxFees({ - value: Math.round(field.value.value * 1_000), - unit, - }), - true, - ) - } else { - form.setFieldValue( - field.name, - adjustTxFees({ - value: Math.round(field.value.value / 1_000), - unit, - }), - true, - ) - } + form.setFieldTouched(field.name, true) + if (unit === 'sats/kilo-vbyte') { + form.setFieldValue( + field.name, + { + value: field.value?.value ? Math.round(field.value.value * 1_000) : field.value?.value, + unit, + }, + true, + ) + } else { + form.setFieldValue( + field.name, + { + value: field.value?.value ? Math.round(field.value.value / 1_000) : field.value?.value, + unit, + }, + true, + ) } }} - initialValue={field.value?.unit || 'blocks'} - disabled={form.isSubmitting} /> {t( - field.value?.unit === 'sats/kilo-vbyte' + unitDisplayValue === 'sats/kilo-vbyte' ? 'settings.fees.description_tx_fees_satspervbyte' : 'settings.fees.description_tx_fees_blocks', )} @@ -124,7 +112,7 @@ export const TxFeeInputField = ({ field, form, label }: TxFeeInputFieldProps) => - {field.value?.unit === 'sats/kilo-vbyte' ? ( + {unitDisplayValue === 'sats/kilo-vbyte' ? ( <> / vB @@ -133,24 +121,24 @@ export const TxFeeInputField = ({ field, form, label }: TxFeeInputFieldProps) => )} - {field.value?.unit === 'sats/kilo-vbyte' ? ( + {unitDisplayValue === 'sats/kilo-vbyte' ? ( { const value = parseFloat(e.target.value) form.setFieldValue( field.name, - adjustTxFees({ + { value: Math.round(value * 1_000), unit: 'sats/kilo-vbyte', - }), + }, true, ) }} @@ -163,7 +151,7 @@ export const TxFeeInputField = ({ field, form, label }: TxFeeInputFieldProps) => ) : ( onBlur={field.onBlur} onChange={(e) => { const value = parseInt(e.target.value, 10) - form.setFieldValue( - field.name, - adjustTxFees({ - value, - unit: 'blocks', - }), - true, - ) + form.setFieldValue(field.name, { value, unit: 'blocks' }, true) }} isValid={form.touched[field.name] && !form.errors[field.name]} isInvalid={form.touched[field.name] && !!form.errors[field.name]} diff --git a/src/constants/features.ts b/src/constants/features.ts index c3fb168d..adf938aa 100644 --- a/src/constants/features.ts +++ b/src/constants/features.ts @@ -3,13 +3,13 @@ import { ServiceInfo } from '../context/ServiceInfoContext' interface Features { importWallet: SemVer rescanChain: SemVer - directSendTxFee: SemVer + txFeeOnSend: SemVer } const features: Features = { importWallet: { major: 0, minor: 9, patch: 10 }, // added in https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1461 rescanChain: { major: 0, minor: 9, patch: 10 }, // added in https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1461 - directSendTxFee: { major: 0, minor: 9, patch: 10 }, // added in https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1597 + txFeeOnSend: { major: 0, minor: 9, patch: 11 }, // added in https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1597 } type Feature = keyof Features diff --git a/src/hooks/Fees.ts b/src/hooks/Fees.ts index 28d7b305..339cc3b1 100644 --- a/src/hooks/Fees.ts +++ b/src/hooks/Fees.ts @@ -6,8 +6,8 @@ import { isValidNumber } from '../utils' export type TxFeeValueUnit = 'blocks' | 'sats/kilo-vbyte' export type TxFeeValue = number export type TxFee = { - value: TxFeeValue - unit: TxFeeValueUnit + value?: TxFeeValue + unit?: TxFeeValueUnit } export const toTxFeeValueUnit = (val?: TxFeeValue): TxFeeValueUnit | undefined => { @@ -50,7 +50,7 @@ export const useLoadFeeConfigValues = () => { tx_fees: isValidNumber(parsedTxFees) ? { value: parsedTxFees, - unit: toTxFeeValueUnit(parsedTxFees) || 'blocks', + unit: toTxFeeValueUnit(parsedTxFees), } : undefined, tx_fees_factor: isValidNumber(parsedTxFeesFactor) ? parsedTxFeesFactor : undefined, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 5d046dde..05b0a228 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -296,6 +296,7 @@ "sending_options": "Sending options", "toggle_coinjoin": "Send as collaborative transaction", "toggle_coinjoin_subtitle": "Collaborative transactions improve the privacy of yourself and others.", + "label_tx_fees": "Mining fee", "button_send": "Send", "button_send_despite_warning": "Ignore warning & try send", "button_send_without_improved_privacy": "Send without privacy improvement", diff --git a/src/libs/JmWalletApi.ts b/src/libs/JmWalletApi.ts index c28b26f4..7db9c218 100644 --- a/src/libs/JmWalletApi.ts +++ b/src/libs/JmWalletApi.ts @@ -120,6 +120,7 @@ interface DoCoinjoinRequest { destination: BitcoinAddress amount_sats: AmountSats counterparties: number + txfee?: number } interface FreezeRequest { @@ -391,10 +392,7 @@ const postDirectSend = async ({ token, signal, walletFileName }: WalletRequestCo return await fetch(`${basePath()}/v1/wallet/${encodeURIComponent(walletFileName)}/taker/direct-send`, { method: 'POST', headers: { ...Helper.buildAuthHeader(token) }, - body: JSON.stringify({ - ...req, - txfee: req.txfee ? String(req.txfee) : undefined, - }), + body: JSON.stringify(req), signal, }) }