From 316d66b08bb529a3d78d06014874741f91041e80 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 2 Dec 2023 23:42:08 +0100 Subject: [PATCH 01/12] chore: use kristapsk branch for testing in regtest --- .../regtest/dockerfile-deps/joinmarket/latest/Dockerfile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/regtest/dockerfile-deps/joinmarket/latest/Dockerfile b/docker/regtest/dockerfile-deps/joinmarket/latest/Dockerfile index 871c2875..18958e97 100644 --- a/docker/regtest/dockerfile-deps/joinmarket/latest/Dockerfile +++ b/docker/regtest/dockerfile-deps/joinmarket/latest/Dockerfile @@ -7,9 +7,12 @@ RUN apt-get update \ tor \ && rm -rf /var/lib/apt/lists/* -ENV REPO https://github.com/JoinMarket-Org/joinmarket-clientserver -ENV REPO_BRANCH master -ENV REPO_REF master +#ENV REPO https://github.com/JoinMarket-Org/joinmarket-clientserver +#ENV REPO_BRANCH master +#ENV REPO_REF master +ENV REPO https://github.com/kristapsk/joinmarket-clientserver +ENV REPO_BRANCH wallet_rpc-coinjoin-txfee +ENV REPO_REF wallet_rpc-coinjoin-txfee WORKDIR /src RUN git clone "$REPO" . --depth=10 --branch "$REPO_BRANCH" && git checkout "$REPO_REF" From a67debcb85c9d7d209163c6dd51c97dc51196693 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 6 Dec 2023 13:19:38 +0100 Subject: [PATCH 02/12] feat(send): ability to specify tx fee in blocks or sats/vbyte --- src/components/PaymentConfirmModal.tsx | 24 +++++-------- .../Send/AmountInputField.module.css | 10 ------ src/components/Send/AmountInputField.tsx | 2 +- .../Send/DestinationInputField.module.css | 9 ----- src/components/Send/DestinationInputField.tsx | 10 ++---- src/components/Send/SendForm.module.css | 6 ++++ src/components/Send/SendForm.tsx | 36 ++++++++++++++++--- src/components/Send/index.tsx | 35 ++++++++++++------ src/components/settings/TxFeeInputField.tsx | 12 +++---- src/libs/JmWalletApi.ts | 6 ++-- 10 files changed, 83 insertions(+), 67 deletions(-) delete mode 100644 src/components/Send/DestinationInputField.module.css diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 70987a2f..420c51f4 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, 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..664c2a6a 100644 --- a/src/components/Send/AmountInputField.tsx +++ b/src/components/Send/AmountInputField.tsx @@ -39,7 +39,7 @@ export const AmountInputField = ({ {isLoading ? ( - + ) : (
diff --git a/src/components/Send/DestinationInputField.module.css b/src/components/Send/DestinationInputField.module.css deleted file mode 100644 index 419e00af..00000000 --- a/src/components/Send/DestinationInputField.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.inputLoader { - height: 3.5rem; - border-radius: 0.25rem; -} - -.input { - height: 3.5rem; - width: 100%; -} diff --git a/src/components/Send/DestinationInputField.tsx b/src/components/Send/DestinationInputField.tsx index 990a0507..ddd37a7c 100644 --- a/src/components/Send/DestinationInputField.tsx +++ b/src/components/Send/DestinationInputField.tsx @@ -7,10 +7,8 @@ import * as Api from '../../libs/JmWalletApi' import Sprite from '../Sprite' import { jarName } from '../jars/Jar' import JarSelectorModal from '../JarSelectorModal' - import { WalletInfo } from '../../context/WalletContext' import { noop } from '../../utils' -import styles from './DestinationInputField.module.css' export type DestinationValue = { value: Api.BitcoinAddress | null @@ -88,14 +86,14 @@ export const DestinationInputField = ({ {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..27f5f2de 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,7 @@ 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' type CollaborativeTransactionOptionsProps = { selectedAmount?: AmountValue @@ -208,8 +209,9 @@ export interface SendFormValues { sourceJarIndex?: JarIndex destination?: DestinationValue amount?: AmountValue - numCollaborators?: number + txFee?: TxFee isCoinJoin: boolean + numCollaborators?: number } interface InnerSendFormProps { @@ -283,6 +285,7 @@ const InnerSendForm = ({ +
- {/* direct-send options: empty on purpose */} + {/* direct-send only options: empty on purpose */}
+ +
+ +
+ {(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, }, ) @@ -380,7 +391,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 +423,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 +543,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/settings/TxFeeInputField.tsx b/src/components/settings/TxFeeInputField.tsx index 81095a42..282c5093 100644 --- a/src/components/settings/TxFeeInputField.tsx +++ b/src/components/settings/TxFeeInputField.tsx @@ -1,7 +1,7 @@ import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { TFunction } from 'i18next' -import { FieldProps, FormikErrors } from 'formik' +import { FieldAttributes, FieldProps, FormikErrors } from 'formik' import { TxFeeValueUnit, TxFee } from '../../hooks/Fees' import { isValidNumber } from '../../utils' import Sprite from '../Sprite' @@ -63,16 +63,16 @@ 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() return ( <> {label} - - + {field.value?.unit === 'sats/kilo-vbyte' ? ( ) : ( Date: Wed, 6 Dec 2023 14:07:21 +0100 Subject: [PATCH 03/12] chore: fix SegmentedTabs active state --- src/components/Earn.tsx | 65 +++++++++++++++++++ src/components/SegmentedTabs.tsx | 22 +++++-- .../jar_details/JarDetailsOverlay.tsx | 2 +- src/components/settings/TxFeeInputField.tsx | 27 +++++--- 4 files changed, 102 insertions(+), 14 deletions(-) diff --git a/src/components/Earn.tsx b/src/components/Earn.tsx index 4fa872a4..bfb57d84 100644 --- a/src/components/Earn.tsx +++ b/src/components/Earn.tsx @@ -247,6 +247,7 @@ const EarnForm = ({ return ( +<<<<<<< HEAD {(props) => { const { handleSubmit, setFieldValue, handleBlur, values, touched, errors, isSubmitting } = props const minsizeField = props.getFieldProps('minsize') @@ -275,6 +276,70 @@ const EarnForm = ({ initialValue={values.offertype} disabled={isLoading || isSubmitting} /> +======= + {({ handleSubmit, setFieldValue, handleChange, handleBlur, values, touched, errors, isSubmitting }) => ( + <> + + + <> + + { + checked && setFieldValue('offertype', tab.value, true) + }} + value={values.offertype} + disabled={isLoading || isSubmitting} + /> + + {values.offertype === OFFERTYPE_REL ? ( + + + {t('earn.label_rel_fee', { + fee: typeof values.feeRel === 'number' ? `(${factorToPercentage(values.feeRel)}%)` : '', + })} + + {t('earn.description_rel_fee')} + {isLoading ? ( + + + + ) : ( + + + % + + { + const value = e.target.value || '' + setFieldValue('feeRel', value !== '' ? percentageToFactor(parseFloat(value)) : '', true) + }} + onBlur={handleBlur} + value={typeof values.feeRel === 'number' ? factorToPercentage(values.feeRel) : ''} + isValid={touched.feeRel && !errors.feeRel} + isInvalid={touched.feeRel && !!errors.feeRel} + min={0} + step={feeRelPercentageStep} + /> + {errors.feeRel} + + )} +>>>>>>> 6149e4a (chore: fix SegmentedTabs active state) {values.offertype === OFFERTYPE_REL ? ( diff --git a/src/components/SegmentedTabs.tsx b/src/components/SegmentedTabs.tsx index d0350cd5..b1f06056 100644 --- a/src/components/SegmentedTabs.tsx +++ b/src/components/SegmentedTabs.tsx @@ -2,20 +2,26 @@ import React 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 } +<<<<<<< HEAD function SegmentedTabFormCheck({ id, name, value, label, disabled, checked, onChange }: rb.FormCheckProps) { +======= +function SegmentedTabFormCheck(props: rb.FormCheckProps) { +>>>>>>> 6149e4a (chore: fix SegmentedTabs active state) const _onChange = (e: React.ChangeEvent) => { e.stopPropagation() - onChange && onChange(e) + props.onChange && props.onChange(e) } return ( <> +<<<<<<< HEAD
+======= + +>>>>>>> 6149e4a (chore: fix SegmentedTabs active state) ) } @@ -36,11 +45,11 @@ interface SegmentedTabsProps { name: string tabs: SegmentedTab[] onChange: (tab: SegmentedTab, checked: boolean) => void - initialValue?: string + value?: SegmentedTabValue disabled?: boolean } -export default function SegmentedTabs({ name, tabs, onChange, initialValue, disabled = false }: SegmentedTabsProps) { +export default function SegmentedTabs({ name, tabs, onChange, value, disabled = false }: SegmentedTabsProps) { const _onChange = (e: React.ChangeEvent, tab: SegmentedTab) => { e.stopPropagation() onChange(tab, e.currentTarget.checked) @@ -56,9 +65,14 @@ 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} +<<<<<<< HEAD checked={initialValue === tab.value} +======= +>>>>>>> 6149e4a (chore: fix SegmentedTabs active state) onChange={(e) => _onChange(e, tab)} /> ) diff --git a/src/components/jar_details/JarDetailsOverlay.tsx b/src/components/jar_details/JarDetailsOverlay.tsx index b491dc09..c873ae25 100644 --- a/src/components/jar_details/JarDetailsOverlay.tsx +++ b/src/components/jar_details/JarDetailsOverlay.tsx @@ -325,7 +325,7 @@ const JarDetailsOverlay = (props: JarDetailsOverlayProps) => { name="jarDetailsTab" tabs={tabs} onChange={(tab, checked) => checked && setSelectedTab(tab.value)} - initialValue={selectedTab} + value={selectedTab} />
diff --git a/src/components/settings/TxFeeInputField.tsx b/src/components/settings/TxFeeInputField.tsx index 282c5093..0b9fef33 100644 --- a/src/components/settings/TxFeeInputField.tsx +++ b/src/components/settings/TxFeeInputField.tsx @@ -1,7 +1,8 @@ +import { useEffect, useState } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { TFunction } from 'i18next' -import { FieldAttributes, FieldProps, FormikErrors } from 'formik' +import { FieldProps, FormikErrors } from 'formik' import { TxFeeValueUnit, TxFee } from '../../hooks/Fees' import { isValidNumber } from '../../utils' import Sprite from '../Sprite' @@ -11,6 +12,7 @@ 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") @@ -69,12 +71,18 @@ type TxFeeInputFieldProps = FieldProps & { export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFieldProps) => { const { t } = useTranslation() + const [unitDisplayValue, setUnitDisplayValue] = useState(field.value?.unit || TX_FEES_DEFAULT_UNIT) + + useEffect(() => { + setUnitDisplayValue(field.value?.unit || TX_FEES_DEFAULT_UNIT) + }, [field.value]) + return ( <> {label} - + { const unit = tab.value as TxFeeValueUnit + setUnitDisplayValue(unit) if (field.value) { if (unit === 'sats/kilo-vbyte') { @@ -110,13 +121,11 @@ export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFie } } }} - 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 +133,7 @@ export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFie - {field.value?.unit === 'sats/kilo-vbyte' ? ( + {unitDisplayValue === 'sats/kilo-vbyte' ? ( <> / vB @@ -133,14 +142,14 @@ export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFie )} - {field.value?.unit === 'sats/kilo-vbyte' ? ( + {unitDisplayValue === 'sats/kilo-vbyte' ? ( { From 3f16ece1852bf4f8bb77141bb0d1af3090c3729e Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 6 Dec 2023 16:08:12 +0100 Subject: [PATCH 04/12] chore: tx fee value and unit can be undefined --- src/components/settings/TxFeeInputField.tsx | 82 +++++++-------------- src/hooks/Fees.ts | 6 +- 2 files changed, 30 insertions(+), 58 deletions(-) diff --git a/src/components/settings/TxFeeInputField.tsx b/src/components/settings/TxFeeInputField.tsx index 0b9fef33..95a7117c 100644 --- a/src/components/settings/TxFeeInputField.tsx +++ b/src/components/settings/TxFeeInputField.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useMemo } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { TFunction } from 'i18next' @@ -18,29 +18,14 @@ const TX_FEES_SATSPERKILOVBYTE_MIN: SatsPerKiloVByte = 1_001 // actual min of `t // 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, { @@ -71,11 +56,7 @@ type TxFeeInputFieldProps = FieldProps & { export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFieldProps) => { const { t } = useTranslation() - const [unitDisplayValue, setUnitDisplayValue] = useState(field.value?.unit || TX_FEES_DEFAULT_UNIT) - - useEffect(() => { - setUnitDisplayValue(field.value?.unit || TX_FEES_DEFAULT_UNIT) - }, [field.value]) + const unitDisplayValue = useMemo(() => field.value?.unit || TX_FEES_DEFAULT_UNIT, [field.value?.unit]) return ( <> @@ -97,28 +78,26 @@ export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFie disabled={form.isSubmitting} onChange={(tab) => { const unit = tab.value as TxFeeValueUnit - setUnitDisplayValue(unit) - 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, + ) } }} /> @@ -149,17 +128,17 @@ export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFie name={field.name} type="number" placeholder="1" - value={isValidNumber(field.value?.value) ? field.value!.value / 1_000 : ''} + value={isValidNumber(field.value?.value) ? field.value!.value! / 1_000 : ''} disabled={form.isSubmitting} onBlur={field.onBlur} onChange={(e) => { const value = parseFloat(e.target.value) form.setFieldValue( field.name, - adjustTxFees({ + { value: Math.round(value * 1_000), unit: 'sats/kilo-vbyte', - }), + }, true, ) }} @@ -181,14 +160,7 @@ export const TxFeeInputField = ({ field, form, label, className }: TxFeeInputFie 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/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, From 76ded60bf0b5dfa309223cb5202a1bcd692cd788 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 6 Dec 2023 16:54:06 +0100 Subject: [PATCH 05/12] refactor: SegmentedTab onChange handler props --- src/components/Earn.tsx | 4 +- src/components/SegmentedTabs.module.css | 26 ++++---- src/components/SegmentedTabs.tsx | 59 ++++++------------- .../jar_details/JarDetailsOverlay.tsx | 2 +- 4 files changed, 34 insertions(+), 57 deletions(-) diff --git a/src/components/Earn.tsx b/src/components/Earn.tsx index bfb57d84..73d76ac1 100644 --- a/src/components/Earn.tsx +++ b/src/components/Earn.tsx @@ -295,9 +295,7 @@ const EarnForm = ({ value: OFFERTYPE_REL, }, ]} - onChange={(tab, checked) => { - checked && setFieldValue('offertype', tab.value, true) - }} + onChange={(tab) => setFieldValue('offertype', tab.value, true)} value={values.offertype} disabled={isLoading || isSubmitting} /> 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 b1f06056..ee7e6103 100644 --- a/src/components/SegmentedTabs.tsx +++ b/src/components/SegmentedTabs.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import { ChangeEvent } from 'react' import * as rb from 'react-bootstrap' import styles from './SegmentedTabs.module.css' @@ -9,54 +9,37 @@ interface SegmentedTab { disabled?: boolean } -<<<<<<< HEAD -function SegmentedTabFormCheck({ id, name, value, label, disabled, checked, onChange }: rb.FormCheckProps) { -======= -function SegmentedTabFormCheck(props: rb.FormCheckProps) { ->>>>>>> 6149e4a (chore: fix SegmentedTabs active state) - const _onChange = (e: React.ChangeEvent) => { - e.stopPropagation() - props.onChange && props.onChange(e) - } - - return ( - <> -<<<<<<< HEAD -
- - -
-======= - ->>>>>>> 6149e4a (chore: fix SegmentedTabs active state) - - ) -} +const SegmentedTabFormCheck = ({ id, name, value, label, disabled, checked, onChange }: rb.FormCheckProps) => ( +
+ + +
+) interface SegmentedTabsProps { name: string tabs: SegmentedTab[] - onChange: (tab: SegmentedTab, checked: boolean) => void + onChange: (tab: SegmentedTab) => void value?: SegmentedTabValue disabled?: boolean } export default function SegmentedTabs({ name, tabs, onChange, value, disabled = false }: SegmentedTabsProps) { - const _onChange = (e: React.ChangeEvent, tab: SegmentedTab) => { + const _onChange = (e: ChangeEvent, tab: SegmentedTab) => { e.stopPropagation() - onChange(tab, e.currentTarget.checked) + onChange(tab) } return ( -
+
{tabs.map((tab, index) => { return ( @@ -69,10 +52,6 @@ export default function SegmentedTabs({ name, tabs, onChange, value, disabled = disabled={disabled || tab.disabled} checked={value === tab.value} inline={true} -<<<<<<< HEAD - checked={initialValue === tab.value} -======= ->>>>>>> 6149e4a (chore: fix SegmentedTabs active state) onChange={(e) => _onChange(e, tab)} /> ) diff --git a/src/components/jar_details/JarDetailsOverlay.tsx b/src/components/jar_details/JarDetailsOverlay.tsx index c873ae25..f1970d72 100644 --- a/src/components/jar_details/JarDetailsOverlay.tsx +++ b/src/components/jar_details/JarDetailsOverlay.tsx @@ -324,7 +324,7 @@ const JarDetailsOverlay = (props: JarDetailsOverlayProps) => { checked && setSelectedTab(tab.value)} + onChange={(tab) => setSelectedTab(tab.value)} value={selectedTab} />
From 42ad9d1d25d16dff02214bfd1175ef7121b24c35 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 17 Jan 2024 12:22:48 +0100 Subject: [PATCH 06/12] dev: fix after rebase on master --- src/components/Earn.tsx | 69 ++---------------------- src/components/SegmentedTabs.tsx | 10 +--- src/components/Send/AmountInputField.tsx | 7 ++- src/components/Send/SendForm.tsx | 1 + 4 files changed, 10 insertions(+), 77 deletions(-) diff --git a/src/components/Earn.tsx b/src/components/Earn.tsx index 73d76ac1..9b5da7bc 100644 --- a/src/components/Earn.tsx +++ b/src/components/Earn.tsx @@ -247,7 +247,6 @@ const EarnForm = ({ return ( -<<<<<<< HEAD {(props) => { const { handleSubmit, setFieldValue, handleBlur, values, touched, errors, isSubmitting } = props const minsizeField = props.getFieldProps('minsize') @@ -270,74 +269,12 @@ 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} /> -======= - {({ handleSubmit, setFieldValue, handleChange, handleBlur, values, touched, errors, isSubmitting }) => ( - <> - - - <> - - setFieldValue('offertype', tab.value, true)} - value={values.offertype} - disabled={isLoading || isSubmitting} - /> - - {values.offertype === OFFERTYPE_REL ? ( - - - {t('earn.label_rel_fee', { - fee: typeof values.feeRel === 'number' ? `(${factorToPercentage(values.feeRel)}%)` : '', - })} - - {t('earn.description_rel_fee')} - {isLoading ? ( - - - - ) : ( - - - % - - { - const value = e.target.value || '' - setFieldValue('feeRel', value !== '' ? percentageToFactor(parseFloat(value)) : '', true) - }} - onBlur={handleBlur} - value={typeof values.feeRel === 'number' ? factorToPercentage(values.feeRel) : ''} - isValid={touched.feeRel && !errors.feeRel} - isInvalid={touched.feeRel && !!errors.feeRel} - min={0} - step={feeRelPercentageStep} - /> - {errors.feeRel} - - )} ->>>>>>> 6149e4a (chore: fix SegmentedTabs active state) {values.offertype === OFFERTYPE_REL ? ( diff --git a/src/components/SegmentedTabs.tsx b/src/components/SegmentedTabs.tsx index ee7e6103..ba294b32 100644 --- a/src/components/SegmentedTabs.tsx +++ b/src/components/SegmentedTabs.tsx @@ -11,15 +11,7 @@ interface SegmentedTab { const SegmentedTabFormCheck = ({ id, name, value, label, disabled, checked, onChange }: rb.FormCheckProps) => (
- +
) diff --git a/src/components/Send/AmountInputField.tsx b/src/components/Send/AmountInputField.tsx index 664c2a6a..4d5c0aaf 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 ? ( - + ) : (
Date: Wed, 17 Jan 2024 12:27:39 +0100 Subject: [PATCH 07/12] ui(Send): transaction fee label --- src/components/Send/SendForm.tsx | 7 +------ src/i18n/locales/en/translation.json | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index b9a54c7b..38328089 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -344,12 +344,7 @@ const InnerSendForm = ({
- +
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", From fd051ca47172ed589382d339e1e9ba40f34ca2aa Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 17 Jan 2024 13:05:58 +0100 Subject: [PATCH 08/12] ui(Confirm): move mining fee section below collaborator fee section --- src/components/PaymentConfirmModal.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 420c51f4..6554ce4a 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -155,17 +155,6 @@ export function PaymentConfirmModal({ )} - - {miningFeeText && ( - - - {t('send.confirm_send_modal.label_miner_fee')} - - - {miningFeeText} - - - )} {isCoinjoin && ( @@ -207,6 +196,16 @@ export function PaymentConfirmModal({ )} + {miningFeeText && ( + + + {t('send.confirm_send_modal.label_miner_fee')} + + + {miningFeeText} + + + )} ) From b17c0bbd80ff06096f375b0a98fd22ef706dd94b Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 17 Jan 2024 13:42:59 +0100 Subject: [PATCH 09/12] feat(send): display tx fee input field only with supported backends --- src/components/Send/SendForm.tsx | 18 ++++++++++++++---- src/constants/features.ts | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 38328089..7b16481f 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -30,6 +30,8 @@ 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 @@ -54,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) @@ -238,6 +240,7 @@ const InnerSendForm = ({ disabled = false, }: InnerSendFormProps) => { const { t } = useTranslation() + const serviceInfo = useServiceInfo() const jarBalances = useMemo(() => { if (!walletInfo) return [] @@ -343,9 +346,16 @@ const InnerSendForm = ({ />
-
- -
+ {serviceInfo && isFeatureEnabled('txFeeOnSend', serviceInfo) && ( +
+ +
+ )} Date: Wed, 17 Jan 2024 13:44:16 +0100 Subject: [PATCH 10/12] fix(ui): amount input loading state style --- src/components/Send/AmountInputField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Send/AmountInputField.tsx b/src/components/Send/AmountInputField.tsx index 4d5c0aaf..e16a8934 100644 --- a/src/components/Send/AmountInputField.tsx +++ b/src/components/Send/AmountInputField.tsx @@ -42,7 +42,7 @@ export const AmountInputField = ({ {isLoading ? ( - + ) : (
From 36a9459b7b635ccc35722f7b0a4c38d66dd10942 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Fri, 26 Jan 2024 22:36:13 +0100 Subject: [PATCH 11/12] fix(send): add alert if aborting taker operation fails --- src/components/Send/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 0758c6f9..553b14b0 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -379,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) => { From 14942829650b2e7530af72fed8a230a1d834931a Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 27 Jan 2024 13:31:36 +0100 Subject: [PATCH 12/12] Revert "chore: use kristapsk branch for testing in regtest" This reverts commit 316d66b08bb529a3d78d06014874741f91041e80. --- .../regtest/dockerfile-deps/joinmarket/latest/Dockerfile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docker/regtest/dockerfile-deps/joinmarket/latest/Dockerfile b/docker/regtest/dockerfile-deps/joinmarket/latest/Dockerfile index 18958e97..871c2875 100644 --- a/docker/regtest/dockerfile-deps/joinmarket/latest/Dockerfile +++ b/docker/regtest/dockerfile-deps/joinmarket/latest/Dockerfile @@ -7,12 +7,9 @@ RUN apt-get update \ tor \ && rm -rf /var/lib/apt/lists/* -#ENV REPO https://github.com/JoinMarket-Org/joinmarket-clientserver -#ENV REPO_BRANCH master -#ENV REPO_REF master -ENV REPO https://github.com/kristapsk/joinmarket-clientserver -ENV REPO_BRANCH wallet_rpc-coinjoin-txfee -ENV REPO_REF wallet_rpc-coinjoin-txfee +ENV REPO https://github.com/JoinMarket-Org/joinmarket-clientserver +ENV REPO_BRANCH master +ENV REPO_REF master WORKDIR /src RUN git clone "$REPO" . --depth=10 --branch "$REPO_BRANCH" && git checkout "$REPO_REF"