From 02729244bfbac9a2f8db6edf5649b6d98a8dfc31 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Mon, 30 Jan 2023 20:56:55 +0100 Subject: [PATCH 01/31] Add `AccordionInfo` component --- src/components/AccordionInfo.tsx | 33 ++++++++++++++++++++++++++++ src/components/Send/index.tsx | 2 ++ src/i18n/locales/en/translation.json | 1 + 3 files changed, 36 insertions(+) create mode 100644 src/components/AccordionInfo.tsx diff --git a/src/components/AccordionInfo.tsx b/src/components/AccordionInfo.tsx new file mode 100644 index 000000000..58629ef3b --- /dev/null +++ b/src/components/AccordionInfo.tsx @@ -0,0 +1,33 @@ +import { PropsWithChildren, useState } from 'react' +import { useSettings } from '../context/SettingsContext' +import * as rb from 'react-bootstrap' +import Sprite from './Sprite' + +interface AccordionInfoProps { + title: string + defaultOpen?: boolean +} + +const AccordionInfo = ({ title, defaultOpen = false, children }: PropsWithChildren) => { + const settings = useSettings() + const [isOpen, setIsOpen] = useState(defaultOpen) + + return ( +
+ setIsOpen((current) => !current)} + > + + {title} + + +
{children}
+
+
+ ) +} + +export default AccordionInfo diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index b46a6925e..27af2af18 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -36,6 +36,7 @@ import { } from './helpers' import { SATS, isValidNumber } from '../../utils' import styles from './Send.module.css' +import AccordionInfo from '../AccordionInfo' const IS_COINJOIN_DEFAULT_VAL = true // initial value for `minimum_makers` from the default joinmarket.cfg (last check on 2022-02-20 of v0.9.5) @@ -832,6 +833,7 @@ export default function Send({ wallet }: SendProps) { minNumCollaborators={minNumCollaborators} disabled={isLoading || isOperationDisabled} /> + Fee breakdown... diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 3a3af5cbe..70b4228a3 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -248,6 +248,7 @@ "sending_options": "Sending options", "toggle_coinjoin": "Send as collaborative transaction", "toggle_coinjoin_subtitle": "Collaborative transactions improve the privacy of yourself and others.", + "collaborators_number_accordion_info": "How much effect does this number has?", "button_send": "Send", "button_send_despite_warning": "Ignore warning & try send", "button_send_without_improved_privacy": "Send without privacy improvement", From 79165231943a7f6e68ad94fc175e8046e3e8f695 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Sat, 4 Feb 2023 23:23:44 +0100 Subject: [PATCH 02/31] Add fee breakdown UI --- src/components/Send/FeeBreakdown.tsx | 76 ++++++++++++++++++++++++++++ src/components/Send/index.tsx | 5 +- src/i18n/locales/en/translation.json | 11 ++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/components/Send/FeeBreakdown.tsx diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx new file mode 100644 index 000000000..308adc4b8 --- /dev/null +++ b/src/components/Send/FeeBreakdown.tsx @@ -0,0 +1,76 @@ +import classNames from 'classnames' +import { PropsWithChildren } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import { routes } from '../../constants/routes' +import Balance from '../Balance' + +interface FeeBreakdownProps { + numCollaborators: number | null +} + +const FeeBreakdown = ({ numCollaborators }: PropsWithChildren) => { + const rowClass = 'border-bottom pb-2' + const valueClass = 'text-end' + const { t } = useTranslation() + + return ( +
+
+ }} /> +
+
{numCollaborators}
+ +
+
{t('send.fee_breakdown.random_absolute_limit')}
+
+ +
+ +
{t('send.fee_breakdown.or')}
+
+ , a: }} + /> +
+
+ +
+ +
{t('send.fee_breakdown.or')}
+
{t('send.fee_breakdown.random_relative_limit')}
+
10%
+ +
{t('send.fee_breakdown.or')}
+
+ , a: }} + /> +
+
10%
+ +
{t('send.fee_breakdown.plus')}
+
+ }} + values={{ amount: '1.0' }} + /> +
+
+ +
+ +
+ {t('send.fee_breakdown.total_estimate')} +
+
+ +
+
+ ) +} + +export default FeeBreakdown diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 27af2af18..1ee35d7b6 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -37,6 +37,7 @@ import { import { SATS, isValidNumber } from '../../utils' import styles from './Send.module.css' import AccordionInfo from '../AccordionInfo' +import FeeBreakdown from './FeeBreakdown' const IS_COINJOIN_DEFAULT_VAL = true // initial value for `minimum_makers` from the default joinmarket.cfg (last check on 2022-02-20 of v0.9.5) @@ -833,7 +834,9 @@ export default function Send({ wallet }: SendProps) { minNumCollaborators={minNumCollaborators} disabled={isLoading || isOperationDisabled} /> - Fee breakdown... + + +
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 70b4228a3..4e7e1382e 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -260,6 +260,17 @@ "sweep_amount_breakdown_frozen_balance": "Frozen or locked balance", "sweep_amount_breakdown_estimated_amount": "Estimated amount to be sent", "sweep_amount_breakdown_explanation": "A sweep transaction will consume all UTXOs of a mixdepth leaving no coins behind except those that have been <1>frozen or <3>time-locked. Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", + "fee_breakdown": { + "or": "or", + "plus": "plus", + "counterparties_multiplied": "Counterparties multiplied by...", + "random_absolute_limit": "Absolute fee limit, chosen randomly", + "personal_absolute_limit": "Your personal absolute fee limit (if set, change here)", + "random_relative_limit": "Relative fee limit, chosen randomly", + "personal_relative_limit": "Your personal relative fee limit (if set, change here)", + "miner_fee": "Miner fee ({{amount}} sat/VB, change here)", + "total_estimate": "Very rough estimated total collaborative transaction fees" + }, "confirm_send_modal": { "title": "Confirm payment", "label_amount": "Amount", From 33809b08123619714abdfc9a27ca8684ea929578 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Sun, 19 Feb 2023 00:47:12 +0100 Subject: [PATCH 03/31] Wrap `loadFeeConfigValues` in hook I'm not sure about the correctness of using useEffect inside the useFeeConfigValues hook, in particular regarding the effectiveness of the cleanup function (including `abortCtrl.abort()`) --- src/hooks/Fees.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/hooks/Fees.ts b/src/hooks/Fees.ts index e772b0ca4..f05c21b56 100644 --- a/src/hooks/Fees.ts +++ b/src/hooks/Fees.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useRefreshConfigValues } from '../context/ServiceConfigContext' import { AmountSats } from '../libs/JmWalletApi' import { isValidNumber } from '../utils' @@ -53,6 +53,27 @@ export const useLoadFeeConfigValues = () => { ) } +export const useFeeConfigValues = () => { + const loadFeeConfigValues = useLoadFeeConfigValues() + const [values, setValues] = useState() + + useEffect(() => { + const abortCtrl = new AbortController() + + loadFeeConfigValues(abortCtrl.signal) + .then((val) => setValues(val)) + .catch((e) => { + console.log('Unable lo load fee config: ', e) + setValues(null) + }) + + return () => { + abortCtrl.abort() + } + }, [setValues, loadFeeConfigValues]) + return values +} + interface EstimatMaxCollaboratorFeeProps { amount: AmountSats collaborators: number From ae493fab3d2e7c301f45d78f2607bc3a6e0488e5 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Sun, 19 Feb 2023 00:54:15 +0100 Subject: [PATCH 04/31] Wrap mining fee text function in hook --- src/components/PaymentConfirmModal.tsx | 35 ++------------------- src/hooks/Fees.ts | 43 +++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index feb308068..682109b67 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -4,7 +4,7 @@ import * as rb from 'react-bootstrap' import Sprite from './Sprite' import Balance from './Balance' import { useSettings } from '../context/SettingsContext' -import { estimateMaxCollaboratorFee, FeeValues, toTxFeeValueUnit } from '../hooks/Fees' +import { estimateMaxCollaboratorFee, FeeValues, useMiningFeeText } from '../hooks/Fees' import { isValidNumber } from '../utils' import { ConfirmModal, ConfirmModalProps } from './Modal' @@ -56,38 +56,7 @@ export function PaymentConfirmModal({ }) }, [amount, isCoinjoin, numCollaborators, feeConfigValues]) - const miningFeeText = useMemo(() => { - if (!feeConfigValues) return null - if (!isValidNumber(feeConfigValues.tx_fees) || !isValidNumber(feeConfigValues.tx_fees_factor)) return null - - const unit = toTxFeeValueUnit(feeConfigValues.tx_fees) - if (!unit) { - return null - } else if (unit === 'blocks') { - return t('send.confirm_send_modal.text_miner_fee_in_targeted_blocks', { count: feeConfigValues.tx_fees }) - } else { - const feeTargetInSatsPerVByte = feeConfigValues.tx_fees! / 1_000 - if (feeConfigValues.tx_fees_factor === 0) { - return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_exact', { - value: feeTargetInSatsPerVByte.toLocaleString(undefined, { - maximumFractionDigits: Math.log10(1_000), - }), - }) - } - - const minFeeSatsPerVByte = Math.max(1, feeTargetInSatsPerVByte * (1 - feeConfigValues.tx_fees_factor!)) - const maxFeeSatsPerVByte = feeTargetInSatsPerVByte * (1 + feeConfigValues.tx_fees_factor!) - - return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_randomized', { - min: minFeeSatsPerVByte.toLocaleString(undefined, { - maximumFractionDigits: 1, - }), - max: maxFeeSatsPerVByte.toLocaleString(undefined, { - maximumFractionDigits: 1, - }), - }) - } - }, [t, feeConfigValues]) + const miningFeeText = useMiningFeeText() return ( diff --git a/src/hooks/Fees.ts b/src/hooks/Fees.ts index f05c21b56..77eb9366c 100644 --- a/src/hooks/Fees.ts +++ b/src/hooks/Fees.ts @@ -1,4 +1,5 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useRefreshConfigValues } from '../context/ServiceConfigContext' import { AmountSats } from '../libs/JmWalletApi' import { isValidNumber } from '../utils' @@ -90,3 +91,43 @@ export const estimateMaxCollaboratorFee = ({ const maxFeePerCollaborator = Math.max(Math.ceil(amount * maxFeeRel), maxFeeAbs) return collaborators > 0 ? Math.min(maxFeePerCollaborator * collaborators, amount) : 0 } + +export const useMiningFeeText = () => { + const feeConfigValues = useFeeConfigValues() + const { t } = useTranslation() + + const miningFeeText = useMemo(() => { + if (!feeConfigValues) return null + if (!isValidNumber(feeConfigValues.tx_fees) || !isValidNumber(feeConfigValues.tx_fees_factor)) return null + + const unit = toTxFeeValueUnit(feeConfigValues.tx_fees) + if (!unit) { + return null + } else if (unit === 'blocks') { + return t('send.confirm_send_modal.text_miner_fee_in_targeted_blocks', { count: feeConfigValues.tx_fees }) + } else { + const feeTargetInSatsPerVByte = feeConfigValues.tx_fees! / 1_000 + if (feeConfigValues.tx_fees_factor === 0) { + return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_exact', { + value: feeTargetInSatsPerVByte.toLocaleString(undefined, { + maximumFractionDigits: Math.log10(1_000), + }), + }) + } + + const minFeeSatsPerVByte = Math.max(1, feeTargetInSatsPerVByte * (1 - feeConfigValues.tx_fees_factor!)) + const maxFeeSatsPerVByte = feeTargetInSatsPerVByte * (1 + feeConfigValues.tx_fees_factor!) + + return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_randomized', { + min: minFeeSatsPerVByte.toLocaleString(undefined, { + maximumFractionDigits: 1, + }), + max: maxFeeSatsPerVByte.toLocaleString(undefined, { + maximumFractionDigits: 1, + }), + }) + } + }, [t, feeConfigValues]) + + return miningFeeText +} From 344e18d79c5adc4ffb4e222523a933cf1156a205 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Sun, 19 Feb 2023 01:07:57 +0100 Subject: [PATCH 05/31] Wrap estimated max collaborator fee in hook --- src/components/PaymentConfirmModal.tsx | 18 ++--------------- src/hooks/Fees.ts | 27 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 682109b67..d901e0c15 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -1,14 +1,10 @@ -import { useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import * as rb from 'react-bootstrap' import Sprite from './Sprite' import Balance from './Balance' import { useSettings } from '../context/SettingsContext' -import { estimateMaxCollaboratorFee, FeeValues, useMiningFeeText } from '../hooks/Fees' - -import { isValidNumber } from '../utils' +import { FeeValues, useEstimatedMaxCollaboratorFee, useMiningFeeText } from '../hooks/Fees' import { ConfirmModal, ConfirmModalProps } from './Modal' - import styles from './PaymentConfirmModal.module.css' import { AmountSats } from '../libs/JmWalletApi' import { jarInitial } from './jars/Jar' @@ -44,17 +40,7 @@ export function PaymentConfirmModal({ const { t } = useTranslation() const settings = useSettings() - const estimatedMaxCollaboratorFee = useMemo(() => { - if (!isCoinjoin || !feeConfigValues) return null - if (!isValidNumber(amount) || !isValidNumber(numCollaborators)) return null - if (!isValidNumber(feeConfigValues.max_cj_fee_abs) || !isValidNumber(feeConfigValues.max_cj_fee_rel)) return null - return estimateMaxCollaboratorFee({ - amount, - collaborators: numCollaborators!, - maxFeeAbs: feeConfigValues.max_cj_fee_abs!, - maxFeeRel: feeConfigValues.max_cj_fee_rel!, - }) - }, [amount, isCoinjoin, numCollaborators, feeConfigValues]) + const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ amount, numCollaborators, isCoinjoin }) const miningFeeText = useMiningFeeText() diff --git a/src/hooks/Fees.ts b/src/hooks/Fees.ts index 77eb9366c..216b5bbb6 100644 --- a/src/hooks/Fees.ts +++ b/src/hooks/Fees.ts @@ -131,3 +131,30 @@ export const useMiningFeeText = () => { return miningFeeText } + +interface useEstimatedMaxCollaboratorFeeArgs { + isCoinjoin: boolean + amount: number | null + numCollaborators?: number | null +} +export const useEstimatedMaxCollaboratorFee = ({ + isCoinjoin, + amount, + numCollaborators, +}: useEstimatedMaxCollaboratorFeeArgs) => { + const feeConfigValues = useFeeConfigValues() + + const estimatedMaxCollaboratorFee = useMemo(() => { + if (!isCoinjoin || !feeConfigValues || !amount) return null + if (!isValidNumber(amount) || !isValidNumber(numCollaborators ?? undefined)) return null + if (!isValidNumber(feeConfigValues.max_cj_fee_abs) || !isValidNumber(feeConfigValues.max_cj_fee_rel)) return null + return estimateMaxCollaboratorFee({ + amount, + collaborators: numCollaborators!, + maxFeeAbs: feeConfigValues.max_cj_fee_abs!, + maxFeeRel: feeConfigValues.max_cj_fee_rel!, + }) + }, [amount, isCoinjoin, numCollaborators, feeConfigValues]) + + return estimatedMaxCollaboratorFee +} From 4a1dcb55bb0bf61874793b385f4b3761be5de3cb Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Sun, 19 Feb 2023 01:09:44 +0100 Subject: [PATCH 06/31] Get real fee values --- src/components/Send/FeeBreakdown.tsx | 31 +++++++++++++++++++--------- src/components/Send/index.tsx | 2 +- src/i18n/locales/en/translation.json | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 308adc4b8..bc2efb69c 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -3,17 +3,31 @@ import { PropsWithChildren } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { routes } from '../../constants/routes' +import { useEstimatedMaxCollaboratorFee, useFeeConfigValues, useMiningFeeText } from '../../hooks/Fees' import Balance from '../Balance' interface FeeBreakdownProps { numCollaborators: number | null + amount: number | null + isCoinjoin: boolean } -const FeeBreakdown = ({ numCollaborators }: PropsWithChildren) => { +const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildren) => { const rowClass = 'border-bottom pb-2' const valueClass = 'text-end' const { t } = useTranslation() + const feesConfig = useFeeConfigValues() + const maxCjRelativeFee = feesConfig?.max_cj_fee_rel + ? `${feesConfig.max_cj_fee_rel * 100}%` + : t('send.fee_breakdown.not_set') + const maxCjAbsoluteFee = feesConfig?.max_cj_fee_abs + ? feesConfig?.max_cj_fee_abs.toString() + : t('send.fee_breakdown.not_set') + + const miningFeeText = useMiningFeeText() + const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ amount, numCollaborators, isCoinjoin }) + return (
@@ -24,7 +38,7 @@ const FeeBreakdown = ({ numCollaborators }: PropsWithChildren
{t('send.fee_breakdown.random_absolute_limit')}
- +
{t('send.fee_breakdown.or')}
@@ -35,12 +49,12 @@ const FeeBreakdown = ({ numCollaborators }: PropsWithChildren />
- +
{t('send.fee_breakdown.or')}
{t('send.fee_breakdown.random_relative_limit')}
-
10%
+
{maxCjRelativeFee}
{t('send.fee_breakdown.or')}
@@ -49,25 +63,22 @@ const FeeBreakdown = ({ numCollaborators }: PropsWithChildren components={{ b: , a: }} />
-
10%
+
{maxCjRelativeFee}
{t('send.fee_breakdown.plus')}
}} - values={{ amount: '1.0' }} />
-
- -
+
{miningFeeText}
{t('send.fee_breakdown.total_estimate')}
- +
) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 1ee35d7b6..7d039e493 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -835,7 +835,7 @@ export default function Send({ wallet }: SendProps) { disabled={isLoading || isOperationDisabled} /> - +
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 4e7e1382e..25dd9ad1d 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -268,7 +268,7 @@ "personal_absolute_limit": "Your personal absolute fee limit (if set, change here)", "random_relative_limit": "Relative fee limit, chosen randomly", "personal_relative_limit": "Your personal relative fee limit (if set, change here)", - "miner_fee": "Miner fee ({{amount}} sat/VB, change here)", + "miner_fee": "Miner fee, change here", "total_estimate": "Very rough estimated total collaborative transaction fees" }, "confirm_send_modal": { From 69a961e0061821f9ef36016b757fb337cfa6cb8d Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Sun, 26 Feb 2023 21:14:29 +0100 Subject: [PATCH 07/31] Fix TS error "TFunction is not generic" --- src/components/jar_details/UtxoList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/jar_details/UtxoList.tsx b/src/components/jar_details/UtxoList.tsx index 76bc387ce..0ab2d6f6f 100644 --- a/src/components/jar_details/UtxoList.tsx +++ b/src/components/jar_details/UtxoList.tsx @@ -37,7 +37,7 @@ const ADDRESS_STATUS_COLORS: { [key: string]: string } = { type Tag = { tag: string; color: string } -const utxoTags = (utxo: Utxo, walletInfo: WalletInfo, t: TFunction<'translation', undefined>): Tag[] => { +const utxoTags = (utxo: Utxo, walletInfo: WalletInfo, t: TFunction): Tag[] => { const rawStatus = walletInfo.addressSummary[utxo.address]?.status let status: string | null = null @@ -62,7 +62,7 @@ const utxoTags = (utxo: Utxo, walletInfo: WalletInfo, t: TFunction<'translation' return tags } -const utxoIcon = (utxo: Utxo, t: TFunction<'translation', undefined>) => { +const utxoIcon = (utxo: Utxo, t: TFunction) => { if (fb.utxo.isFidelityBond(utxo)) { return ( <> From 4d0327d2f828e07c3338005cfe8528889d56b8bb Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Sat, 27 May 2023 19:02:38 +0200 Subject: [PATCH 08/31] Improve clarity --- src/components/Send/FeeBreakdown.tsx | 133 ++++++++++++++++----------- src/components/Send/index.tsx | 10 +- src/i18n/locales/en/translation.json | 20 ++-- 3 files changed, 100 insertions(+), 63 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index bc2efb69c..5381ad359 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -1,10 +1,9 @@ -import classNames from 'classnames' import { PropsWithChildren } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { Link } from 'react-router-dom' -import { routes } from '../../constants/routes' import { useEstimatedMaxCollaboratorFee, useFeeConfigValues, useMiningFeeText } from '../../hooks/Fees' import Balance from '../Balance' +import * as rb from 'react-bootstrap' +import Sprite from '../Sprite' interface FeeBreakdownProps { numCollaborators: number | null @@ -13,72 +12,102 @@ interface FeeBreakdownProps { } const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildren) => { - const rowClass = 'border-bottom pb-2' - const valueClass = 'text-end' const { t } = useTranslation() - const feesConfig = useFeeConfigValues() const maxCjRelativeFee = feesConfig?.max_cj_fee_rel ? `${feesConfig.max_cj_fee_rel * 100}%` : t('send.fee_breakdown.not_set') + + const maxEstimatedRelativeFee = + feesConfig?.max_cj_fee_rel && numCollaborators && amount + ? amount * feesConfig.max_cj_fee_rel * numCollaborators >= 1 + ? Math.ceil(amount * feesConfig.max_cj_fee_rel) * numCollaborators + : null + : null + const maxCjAbsoluteFee = feesConfig?.max_cj_fee_abs ? feesConfig?.max_cj_fee_abs.toString() : t('send.fee_breakdown.not_set') + const maxEstimatedAbsoluteFee = + feesConfig?.max_cj_fee_abs && numCollaborators ? feesConfig.max_cj_fee_abs * numCollaborators : null const miningFeeText = useMiningFeeText() const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ amount, numCollaborators, isCoinjoin }) return ( -
-
- }} /> -
-
{numCollaborators}
- -
-
{t('send.fee_breakdown.random_absolute_limit')}
-
- -
- -
{t('send.fee_breakdown.or')}
-
- , a: }} - /> -
-
- -
- -
{t('send.fee_breakdown.or')}
-
{t('send.fee_breakdown.random_relative_limit')}
-
{maxCjRelativeFee}
- -
{t('send.fee_breakdown.or')}
-
- , a: }} - /> -
-
{maxCjRelativeFee}
+
+ {maxEstimatedAbsoluteFee && ( +
+
+ , + }} + values={{ num: numCollaborators }} + /> +
+
+ +
+
+ )} -
{t('send.fee_breakdown.plus')}
-
- }} - /> +
+
+ +
+
+ {amount ? ( + maxEstimatedRelativeFee ? ( + + ) : ( + t('send.fee_breakdown.too_low') + ) + ) : ( + '-' + )} +
-
{miningFeeText}
-
- {t('send.fee_breakdown.total_estimate')} +
+
{t('send.fee_breakdown.total_estimate')}
+
+ {estimatedMaxCollaboratorFee ? ( + + ) : ( + '-' + )} +
-
- +
+
+ {t('send.fee_breakdown.plus_mining_fee')} + + {t('send.fee_breakdown.why_cant_estimate_mining_fee')} + + }} /> + + + } + > +
+ +
+
+
+
{miningFeeText}
) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 7d039e493..0caa3ef44 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -834,7 +834,15 @@ export default function Send({ wallet }: SendProps) { minNumCollaborators={minNumCollaborators} disabled={isLoading || isOperationDisabled} /> - + +
+ , + }} + /> +
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 25dd9ad1d..d98476456 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -248,7 +248,8 @@ "sending_options": "Sending options", "toggle_coinjoin": "Send as collaborative transaction", "toggle_coinjoin_subtitle": "Collaborative transactions improve the privacy of yourself and others.", - "collaborators_number_accordion_info": "How much effect does this number has?", + "collaborators_fee_question": "How much is the maximum collaborators fee?", + "collaborators_fee_info": "Here you can find the maximum collaborators fee you will pay (this does not include mining fees). You can change your fee limits in the settings page.", "button_send": "Send", "button_send_despite_warning": "Ignore warning & try send", "button_send_without_improved_privacy": "Send without privacy improvement", @@ -261,15 +262,14 @@ "sweep_amount_breakdown_estimated_amount": "Estimated amount to be sent", "sweep_amount_breakdown_explanation": "A sweep transaction will consume all UTXOs of a mixdepth leaving no coins behind except those that have been <1>frozen or <3>time-locked. Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", "fee_breakdown": { - "or": "or", - "plus": "plus", - "counterparties_multiplied": "Counterparties multiplied by...", - "random_absolute_limit": "Absolute fee limit, chosen randomly", - "personal_absolute_limit": "Your personal absolute fee limit (if set, change here)", - "random_relative_limit": "Relative fee limit, chosen randomly", - "personal_relative_limit": "Your personal relative fee limit (if set, change here)", - "miner_fee": "Miner fee, change here", - "total_estimate": "Very rough estimated total collaborative transaction fees" + "absolute_limit": "Absolute fee limit () * {{ num }} collaborators", + "or_relative_limit": "OR relative fee limit ({{ percentage }}) * {{ num }} collaborators", + "not_set": "Not set", + "too_low": "Too low", + "total_estimate": "Max collaborators fee", + "plus_mining_fee": "Plus mining fees", + "why_cant_estimate_mining_fee": "Why can't we estimate mining fees?", + "cant_estimate_mining_fee_info": "The amount of inputs and outputs is not know beforehand, so it is not possible to know the transaction size, hence the transaction fees cannot be estimated.
In addition to this, if you have set a \"Block target\" as mining fee (read: \"Include my tx in the next n blocks\", instead of a sats/vByte value), we cannot estimate the fee needed for your transaction since Jam does not have access to the current mempool conditions." }, "confirm_send_modal": { "title": "Confirm payment", From 32ba78a286ec6954ee1cc4d3b3615225fcb3b586 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Mon, 5 Jun 2023 19:30:43 +0200 Subject: [PATCH 09/31] Fix popover header visibility in dark mode --- src/components/Send/FeeBreakdown.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 5381ad359..05bd5b9b0 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -4,6 +4,7 @@ import { useEstimatedMaxCollaboratorFee, useFeeConfigValues, useMiningFeeText } import Balance from '../Balance' import * as rb from 'react-bootstrap' import Sprite from '../Sprite' +import { useSettings } from '../../context/SettingsContext' interface FeeBreakdownProps { numCollaborators: number | null @@ -33,6 +34,7 @@ const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildre const miningFeeText = useMiningFeeText() const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ amount, numCollaborators, isCoinjoin }) + const settings = useSettings() return (
@@ -90,7 +92,9 @@ const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildre placement="right" overlay={ - {t('send.fee_breakdown.why_cant_estimate_mining_fee')} + + {t('send.fee_breakdown.why_cant_estimate_mining_fee')} + }} /> From 4e1f69d558db137dd71dfdfa561a6a091cbe0a81 Mon Sep 17 00:00:00 2001 From: Nicola Elia <36515569+httpiga@users.noreply.github.com> Date: Mon, 5 Jun 2023 17:35:37 +0000 Subject: [PATCH 10/31] Use passive voice in "cannot estimate mining fee" text Co-authored-by: Thebora Kompanioni --- src/i18n/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index d98476456..135bacaae 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -269,7 +269,7 @@ "total_estimate": "Max collaborators fee", "plus_mining_fee": "Plus mining fees", "why_cant_estimate_mining_fee": "Why can't we estimate mining fees?", - "cant_estimate_mining_fee_info": "The amount of inputs and outputs is not know beforehand, so it is not possible to know the transaction size, hence the transaction fees cannot be estimated.
In addition to this, if you have set a \"Block target\" as mining fee (read: \"Include my tx in the next n blocks\", instead of a sats/vByte value), we cannot estimate the fee needed for your transaction since Jam does not have access to the current mempool conditions." + "cant_estimate_mining_fee_info": "The amount of inputs and outputs is not know beforehand, so it is not possible to know the transaction size, hence the transaction fees cannot be estimated.
In addition to this, if you have set a \"Block target\" as mining fee (read: \"Include my tx in the next n blocks\", instead of a sats/vByte value), the transaction fee cannot be calculated since Jam does not have access to the current mempool conditions." }, "confirm_send_modal": { "title": "Confirm payment", From 4a6f6fbe522e707eacf96675ff221603e2361ef6 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Mon, 5 Jun 2023 19:38:55 +0200 Subject: [PATCH 11/31] Fix past usage in "cannot estimate mining fee" text --- src/i18n/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 135bacaae..2f79382d9 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -269,7 +269,7 @@ "total_estimate": "Max collaborators fee", "plus_mining_fee": "Plus mining fees", "why_cant_estimate_mining_fee": "Why can't we estimate mining fees?", - "cant_estimate_mining_fee_info": "The amount of inputs and outputs is not know beforehand, so it is not possible to know the transaction size, hence the transaction fees cannot be estimated.
In addition to this, if you have set a \"Block target\" as mining fee (read: \"Include my tx in the next n blocks\", instead of a sats/vByte value), the transaction fee cannot be calculated since Jam does not have access to the current mempool conditions." + "cant_estimate_mining_fee_info": "The amount of inputs and outputs is not known beforehand, so it is not possible to know the transaction size, hence the transaction fees cannot be estimated.
In addition to this, if you have set a \"Block target\" as mining fee (read: \"Include my tx in the next n blocks\", instead of a sats/vByte value), we cannot estimate the fee needed for your transaction since Jam does not have access to the current mempool conditions." }, "confirm_send_modal": { "title": "Confirm payment", From bc8d276ca22b4501d15ccc12f470f1d4fc44ee47 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Mon, 12 Jun 2023 19:35:36 +0200 Subject: [PATCH 12/31] Fix popover dark mode colors --- src/components/Send/FeeBreakdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 05bd5b9b0..5ec199451 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -91,11 +91,11 @@ const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildre + {t('send.fee_breakdown.why_cant_estimate_mining_fee')} - + }} /> From 5ce49125ec8d527710f219f158f9cdcaf184110b Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Sat, 12 Aug 2023 13:59:02 +0200 Subject: [PATCH 13/31] Align UI to the new proposal from editwentyone --- src/components/Send/FeeBreakdown.tsx | 166 ++++++++++++++------------- src/components/Send/index.tsx | 14 ++- src/i18n/locales/en/translation.json | 15 +-- src/index.css | 2 +- 4 files changed, 99 insertions(+), 98 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 5ec199451..bb977b84a 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -1,10 +1,12 @@ import { PropsWithChildren } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { useEstimatedMaxCollaboratorFee, useFeeConfigValues, useMiningFeeText } from '../../hooks/Fees' +import { useFeeConfigValues } from '../../hooks/Fees' import Balance from '../Balance' import * as rb from 'react-bootstrap' -import Sprite from '../Sprite' import { useSettings } from '../../context/SettingsContext' +import { Link } from 'react-router-dom' +import { routes } from '../../constants/routes' +import { formatSats } from '../../utils' interface FeeBreakdownProps { numCollaborators: number | null @@ -12,13 +14,43 @@ interface FeeBreakdownProps { isCoinjoin: boolean } +type FeeCardProps = { + amount: number | null + highlight?: boolean + subtitle?: React.ReactNode +} +const FeeCard = ({ amount, highlight, subtitle }: FeeCardProps) => { + const settings = useSettings() + const { t } = useTranslation() + + return ( + + +
+ {amount ? ( + + ) : ( + t('send.fee_breakdown.too_low') + )} +
+
+ {subtitle} +
+
+
+ ) +} + const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildren) => { const { t } = useTranslation() const feesConfig = useFeeConfigValues() - const maxCjRelativeFee = feesConfig?.max_cj_fee_rel + + /** eg: "0.03%" */ + const maxSettingsRelativeFee = feesConfig?.max_cj_fee_rel ? `${feesConfig.max_cj_fee_rel * 100}%` : t('send.fee_breakdown.not_set') + /** eg: 44658 (expressed in sats) */ const maxEstimatedRelativeFee = feesConfig?.max_cj_fee_rel && numCollaborators && amount ? amount * feesConfig.max_cj_fee_rel * numCollaborators >= 1 @@ -26,94 +58,64 @@ const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildre : null : null - const maxCjAbsoluteFee = feesConfig?.max_cj_fee_abs - ? feesConfig?.max_cj_fee_abs.toString() + /** eg: "8,636 sats" */ + const maxSettingsAbsoluteFee = feesConfig?.max_cj_fee_abs + ? `${formatSats(feesConfig.max_cj_fee_abs)} sats` : t('send.fee_breakdown.not_set') + + /** eg: 77724 (expressed in sats) */ const maxEstimatedAbsoluteFee = feesConfig?.max_cj_fee_abs && numCollaborators ? feesConfig.max_cj_fee_abs * numCollaborators : null - const miningFeeText = useMiningFeeText() - const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ amount, numCollaborators, isCoinjoin }) - const settings = useSettings() - return ( -
- {maxEstimatedAbsoluteFee && ( -
-
+ + + {t('send.fee_breakdown.absolute_limit')} + , + a: , + }} + values={{ + numCollaborators, + maxFee: maxSettingsAbsoluteFee, }} - values={{ num: numCollaborators }} /> -
-
- -
-
- )} - -
-
- -
-
- {amount ? ( - maxEstimatedRelativeFee ? ( - - ) : ( - t('send.fee_breakdown.too_low') - ) - ) : ( - '-' - )} -
-
- -
-
{t('send.fee_breakdown.total_estimate')}
-
- {estimatedMaxCollaboratorFee ? ( - - ) : ( - '-' - )} -
-
-
-
- {t('send.fee_breakdown.plus_mining_fee')} - - - {t('send.fee_breakdown.why_cant_estimate_mining_fee')} - - - }} /> - - - } - > -
- -
-
-
-
{miningFeeText}
-
-
+ } + highlight={ + maxEstimatedAbsoluteFee && maxEstimatedRelativeFee + ? maxEstimatedAbsoluteFee > maxEstimatedRelativeFee + : false + } + /> + + + {t('send.fee_breakdown.relative_limit')} + , + }} + values={{ + numCollaborators, + maxFee: maxSettingsRelativeFee, + }} + /> + } + highlight={ + maxEstimatedAbsoluteFee && maxEstimatedRelativeFee + ? maxEstimatedRelativeFee > maxEstimatedAbsoluteFee + : false + } + /> + + ) } diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 0caa3ef44..48ea0c8fd 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -834,17 +834,19 @@ export default function Send({ wallet }: SendProps) { minNumCollaborators={minNumCollaborators} disabled={isLoading || isOperationDisabled} /> - -
+ + {t('send.fee_breakdown.title')} +
+ , }} /> -
- - + +
+
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 2f79382d9..0ec5c187d 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -248,8 +248,6 @@ "sending_options": "Sending options", "toggle_coinjoin": "Send as collaborative transaction", "toggle_coinjoin_subtitle": "Collaborative transactions improve the privacy of yourself and others.", - "collaborators_fee_question": "How much is the maximum collaborators fee?", - "collaborators_fee_info": "Here you can find the maximum collaborators fee you will pay (this does not include mining fees). You can change your fee limits in the settings page.", "button_send": "Send", "button_send_despite_warning": "Ignore warning & try send", "button_send_without_improved_privacy": "Send without privacy improvement", @@ -262,14 +260,13 @@ "sweep_amount_breakdown_estimated_amount": "Estimated amount to be sent", "sweep_amount_breakdown_explanation": "A sweep transaction will consume all UTXOs of a mixdepth leaving no coins behind except those that have been <1>frozen or <3>time-locked. Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", "fee_breakdown": { - "absolute_limit": "Absolute fee limit () * {{ num }} collaborators", - "or_relative_limit": "OR relative fee limit ({{ percentage }}) * {{ num }} collaborators", + "title": "Fee Estimation", + "subtitle": "Depending on the amount, the number of collaborators and the preset limits, you can see the maximum fees for the upcoming collaborative transaction. This does not include regular mining fees.", + "absolute_limit": "Absolute fee limit", + "relative_limit": "Relative fee limit", + "fee_card_subtitle": "{{ maxFee }} * {{ numCollaborators }}", "not_set": "Not set", - "too_low": "Too low", - "total_estimate": "Max collaborators fee", - "plus_mining_fee": "Plus mining fees", - "why_cant_estimate_mining_fee": "Why can't we estimate mining fees?", - "cant_estimate_mining_fee_info": "The amount of inputs and outputs is not known beforehand, so it is not possible to know the transaction size, hence the transaction fees cannot be estimated.
In addition to this, if you have set a \"Block target\" as mining fee (read: \"Include my tx in the next n blocks\", instead of a sats/vByte value), we cannot estimate the fee needed for your transaction since Jam does not have access to the current mempool conditions." + "too_low": "Amount too low..." }, "confirm_send_modal": { "title": "Confirm payment", diff --git a/src/index.css b/src/index.css index dab640b0b..71a6624f5 100644 --- a/src/index.css +++ b/src/index.css @@ -590,7 +590,7 @@ h2 { } :root[data-theme='dark'] - .card:not(.border-success):not(.border-danger):not(.border-warning):not(.border-primary):not(.border-info) { + .card:not(.border-success):not(.border-danger):not(.border-warning):not(.border-primary):not(.border-info):not(.border-light):not(.border-dark) { border-color: var(--bs-gray-800) !important; } From 52b9d90a83679feb1dd7643d991d8f1c2354d8e3 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 12:56:24 +0200 Subject: [PATCH 14/31] ui: de-emphasize non-effective fee card content --- src/components/Send/FeeBreakdown.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index bb977b84a..4a37f7ad1 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -1,5 +1,6 @@ import { PropsWithChildren } from 'react' import { Trans, useTranslation } from 'react-i18next' +import classNames from 'classnames' import { useFeeConfigValues } from '../../hooks/Fees' import Balance from '../Balance' import * as rb from 'react-bootstrap' @@ -16,7 +17,7 @@ interface FeeBreakdownProps { type FeeCardProps = { amount: number | null - highlight?: boolean + highlight: boolean subtitle?: React.ReactNode } const FeeCard = ({ amount, highlight, subtitle }: FeeCardProps) => { @@ -25,7 +26,11 @@ const FeeCard = ({ amount, highlight, subtitle }: FeeCardProps) => { return ( - +
{amount ? ( From 476b586cd28250a24900d54ad45dfa6b773a68bf Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 13:06:08 +0200 Subject: [PATCH 15/31] chore: fix browser warning for fee breakdown subtitle validateDOMNesting(...): cannot appear as a descendant of . --- src/components/Send/index.tsx | 1 - src/i18n/locales/en/translation.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 48ea0c8fd..fbba04fb2 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -36,7 +36,6 @@ import { } from './helpers' import { SATS, isValidNumber } from '../../utils' import styles from './Send.module.css' -import AccordionInfo from '../AccordionInfo' import FeeBreakdown from './FeeBreakdown' const IS_COINJOIN_DEFAULT_VAL = true diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 0ec5c187d..6d9595a4a 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -261,7 +261,7 @@ "sweep_amount_breakdown_explanation": "A sweep transaction will consume all UTXOs of a mixdepth leaving no coins behind except those that have been <1>frozen or <3>time-locked. Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", "fee_breakdown": { "title": "Fee Estimation", - "subtitle": "Depending on the amount, the number of collaborators and the preset limits, you can see the maximum fees for the upcoming collaborative transaction. This does not include regular mining fees.", + "subtitle": "Depending on the amount, the number of collaborators and the preset limits, you can see the maximum fees for the upcoming collaborative transaction. This does not include regular mining fees.", "absolute_limit": "Absolute fee limit", "relative_limit": "Relative fee limit", "fee_card_subtitle": "{{ maxFee }} * {{ numCollaborators }}", From 40b0a72c230d09c6449a244b46a627a02a09bf72 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 13:16:57 +0200 Subject: [PATCH 16/31] chore(wording): Fee Estimation -> Maximum collaborator fee limit --- src/components/Send/FeeBreakdown.tsx | 6 +++--- src/i18n/locales/en/translation.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 4a37f7ad1..95d0865ae 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -73,9 +73,9 @@ const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildre feesConfig?.max_cj_fee_abs && numCollaborators ? feesConfig.max_cj_fee_abs * numCollaborators : null return ( - + - {t('send.fee_breakdown.absolute_limit')} + {t('send.fee_breakdown.absolute_limit')} - {t('send.fee_breakdown.relative_limit')} + {t('send.fee_breakdown.relative_limit')} frozen or <3>time-locked. Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", "fee_breakdown": { - "title": "Fee Estimation", - "subtitle": "Depending on the amount, the number of collaborators and the preset limits, you can see the maximum fees for the upcoming collaborative transaction. This does not include regular mining fees.", + "title": "Maximum collaborator fee limit", + "subtitle": "Depending on the amount, the number of collaborators and the preset limits, you can see the maximum collaborator fees for the upcoming collaborative transaction. This does not include regular mining fees.", "absolute_limit": "Absolute fee limit", "relative_limit": "Relative fee limit", "fee_card_subtitle": "{{ maxFee }} * {{ numCollaborators }}", From de80e6a8518da0dac6a5bd8c3d22190a0c0939d5 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 13:36:46 +0200 Subject: [PATCH 17/31] chore: use css class text-small for fee breakdown labels --- src/components/Jam.module.css | 4 ---- src/components/Jam.tsx | 4 +--- src/components/ScheduleProgress.jsx | 2 +- src/components/ScheduleProgress.module.css | 4 ---- src/components/Send/FeeBreakdown.tsx | 17 +++++++---------- src/components/Send/index.tsx | 10 +++++----- src/components/ToggleSwitch.module.css | 2 +- src/components/ToggleSwitch.tsx | 2 +- src/index.css | 4 ++++ 9 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/components/Jam.module.css b/src/components/Jam.module.css index 08a206b22..6f2500007 100644 --- a/src/components/Jam.module.css +++ b/src/components/Jam.module.css @@ -18,7 +18,3 @@ text-decoration: none; color: var(--bs-body-color); } - -.small-text { - font-size: 0.8rem; -} diff --git a/src/components/Jam.tsx b/src/components/Jam.tsx index 5cf944805..8ac5c0b7a 100644 --- a/src/components/Jam.tsx +++ b/src/components/Jam.tsx @@ -391,9 +391,7 @@ export default function Jam({ wallet }: JamProps) {
{t('scheduler.complete_wallet_title')}
-
- {t('scheduler.complete_wallet_subtitle')} -
+
{t('scheduler.complete_wallet_subtitle')}
<> diff --git a/src/components/ScheduleProgress.jsx b/src/components/ScheduleProgress.jsx index 1f74502ca..a543562ea 100644 --- a/src/components/ScheduleProgress.jsx +++ b/src/components/ScheduleProgress.jsx @@ -101,7 +101,7 @@ const ScheduleProgress = ({ schedule }) => {
)}

-

{t('scheduler.progress_description')}

+

{t('scheduler.progress_description')}

diff --git a/src/components/ScheduleProgress.module.css b/src/components/ScheduleProgress.module.css index eda1eaa3f..3affd856e 100644 --- a/src/components/ScheduleProgress.module.css +++ b/src/components/ScheduleProgress.module.css @@ -130,7 +130,3 @@ width: 100%; } } - -.text-small { - font-size: 0.8rem; -} diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 95d0865ae..735fdda95 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -7,12 +7,11 @@ import * as rb from 'react-bootstrap' import { useSettings } from '../../context/SettingsContext' import { Link } from 'react-router-dom' import { routes } from '../../constants/routes' -import { formatSats } from '../../utils' +import { SATS, formatSats } from '../../utils' interface FeeBreakdownProps { numCollaborators: number | null amount: number | null - isCoinjoin: boolean } type FeeCardProps = { @@ -31,22 +30,20 @@ const FeeCard = ({ amount, highlight, subtitle }: FeeCardProps) => { 'text-muted': !highlight, })} > -
+
{amount ? ( - + ) : ( t('send.fee_breakdown.too_low') )}
-
- {subtitle} -
+
{subtitle}
) } -const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildren) => { +const FeeBreakdown = ({ numCollaborators, amount }: PropsWithChildren) => { const { t } = useTranslation() const feesConfig = useFeeConfigValues() @@ -75,7 +72,7 @@ const FeeBreakdown = ({ numCollaborators, amount, isCoinjoin }: PropsWithChildre return ( - {t('send.fee_breakdown.absolute_limit')} + {t('send.fee_breakdown.absolute_limit')} - {t('send.fee_breakdown.relative_limit')} + {t('send.fee_breakdown.relative_limit')} - {t('send.fee_breakdown.title')} -
- + + {t('send.fee_breakdown.title')} + -
- + +
diff --git a/src/components/ToggleSwitch.module.css b/src/components/ToggleSwitch.module.css index 707db917e..39536851b 100644 --- a/src/components/ToggleSwitch.module.css +++ b/src/components/ToggleSwitch.module.css @@ -61,5 +61,5 @@ } .subtitle { - font-size: 0.8rem; + font-size: 0.875rem; } diff --git a/src/components/ToggleSwitch.tsx b/src/components/ToggleSwitch.tsx index a280758bc..2f83f9a69 100644 --- a/src/components/ToggleSwitch.tsx +++ b/src/components/ToggleSwitch.tsx @@ -33,7 +33,7 @@ export default function ToggleSwitch({
{label}
- {subtitle &&
{subtitle}
} + {subtitle &&
{subtitle}
}
) diff --git a/src/index.css b/src/index.css index 71a6624f5..7bb32d838 100644 --- a/src/index.css +++ b/src/index.css @@ -289,6 +289,10 @@ main { color: inherit !important; } +.text-small { + font-size: 0.875rem; /* var(--bs-font-size-sm); */ +} + /* Fullscreen Overlays */ .offcanvas-fullscreen { width: 100vw !important; From d11afe6aaabdd20a950de1e93060c77fa05cb708 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 13:55:24 +0200 Subject: [PATCH 18/31] chore: use useMemo in FeeBreakdown component --- src/components/Send/FeeBreakdown.tsx | 78 ++++++++++++++++++---------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 735fdda95..32fddd0e0 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from 'react' +import { useMemo, PropsWithChildren } from 'react' import { Trans, useTranslation } from 'react-i18next' import classNames from 'classnames' import { useFeeConfigValues } from '../../hooks/Fees' @@ -48,32 +48,59 @@ const FeeBreakdown = ({ numCollaborators, amount }: PropsWithChildren (feesConfig?.max_cj_fee_rel ? `${feesConfig.max_cj_fee_rel * 100}%` : t('send.fee_breakdown.not_set')), + [feesConfig, t] + ) /** eg: 44658 (expressed in sats) */ - const maxEstimatedRelativeFee = - feesConfig?.max_cj_fee_rel && numCollaborators && amount - ? amount * feesConfig.max_cj_fee_rel * numCollaborators >= 1 - ? Math.ceil(amount * feesConfig.max_cj_fee_rel) * numCollaborators - : null - : null + const maxEstimatedRelativeFee = useMemo( + () => + feesConfig?.max_cj_fee_rel && numCollaborators && amount + ? amount * feesConfig.max_cj_fee_rel * numCollaborators >= 1 + ? Math.ceil(amount * feesConfig.max_cj_fee_rel) * numCollaborators + : null + : null, + [feesConfig, amount, numCollaborators] + ) /** eg: "8,636 sats" */ - const maxSettingsAbsoluteFee = feesConfig?.max_cj_fee_abs - ? `${formatSats(feesConfig.max_cj_fee_abs)} sats` - : t('send.fee_breakdown.not_set') + const maxSettingsAbsoluteFee = useMemo( + () => + feesConfig?.max_cj_fee_abs ? `${formatSats(feesConfig.max_cj_fee_abs)} sats` : t('send.fee_breakdown.not_set'), + [feesConfig, t] + ) /** eg: 77724 (expressed in sats) */ - const maxEstimatedAbsoluteFee = - feesConfig?.max_cj_fee_abs && numCollaborators ? feesConfig.max_cj_fee_abs * numCollaborators : null + const maxEstimatedAbsoluteFee = useMemo( + () => (feesConfig?.max_cj_fee_abs && numCollaborators ? feesConfig.max_cj_fee_abs * numCollaborators : null), + [feesConfig, numCollaborators] + ) + + const isAbsoluteFeeHighlighted = useMemo( + () => + maxEstimatedAbsoluteFee && maxEstimatedRelativeFee ? maxEstimatedAbsoluteFee > maxEstimatedRelativeFee : false, + [maxEstimatedAbsoluteFee, maxEstimatedRelativeFee] + ) + + const isRelativeFeeHighlighted = useMemo( + () => + maxEstimatedAbsoluteFee && maxEstimatedRelativeFee ? maxEstimatedRelativeFee > maxEstimatedAbsoluteFee : false, + [maxEstimatedAbsoluteFee, maxEstimatedRelativeFee] + ) return ( - {t('send.fee_breakdown.absolute_limit')} + + {t('send.fee_breakdown.absolute_limit')} + } - highlight={ - maxEstimatedAbsoluteFee && maxEstimatedRelativeFee - ? maxEstimatedAbsoluteFee > maxEstimatedRelativeFee - : false - } /> - {t('send.fee_breakdown.relative_limit')} + + {t('send.fee_breakdown.relative_limit')} + } - highlight={ - maxEstimatedAbsoluteFee && maxEstimatedRelativeFee - ? maxEstimatedRelativeFee > maxEstimatedAbsoluteFee - : false - } /> From 9cf74c57f28fef55e5d839f41f0d0df7cea2cbab Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 15:38:16 +0200 Subject: [PATCH 19/31] feat(FeeBreakdown): open fee modal on click --- src/components/PaymentConfirmModal.tsx | 81 ++++++++++++++++++- src/components/Send/FeeBreakdown.tsx | 43 ++++++---- src/components/Send/index.tsx | 39 +++++----- src/components/fb/SpendFidelityBondModal.tsx | 25 +----- src/components/settings/FeeConfigModal.tsx | 12 ++- src/hooks/Fees.ts | 82 ++------------------ 6 files changed, 143 insertions(+), 139 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index d901e0c15..8a21ffc7b 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -1,13 +1,82 @@ +import { useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import * as rb from 'react-bootstrap' import Sprite from './Sprite' import Balance from './Balance' import { useSettings } from '../context/SettingsContext' -import { FeeValues, useEstimatedMaxCollaboratorFee, useMiningFeeText } from '../hooks/Fees' +import { estimateMaxCollaboratorFee, FeeValues, toTxFeeValueUnit } from '../hooks/Fees' import { ConfirmModal, ConfirmModalProps } from './Modal' import styles from './PaymentConfirmModal.module.css' import { AmountSats } from '../libs/JmWalletApi' import { jarInitial } from './jars/Jar' +import { isValidNumber } from '../utils' + +const useMiningFeeText = ({ feeConfigValues }: { feeConfigValues?: FeeValues }) => { + const { t } = useTranslation() + + const miningFeeText = useMemo(() => { + if (!feeConfigValues) return null + if (!isValidNumber(feeConfigValues.tx_fees) || !isValidNumber(feeConfigValues.tx_fees_factor)) return null + + const unit = toTxFeeValueUnit(feeConfigValues.tx_fees) + if (!unit) { + return null + } else if (unit === 'blocks') { + return t('send.confirm_send_modal.text_miner_fee_in_targeted_blocks', { count: feeConfigValues.tx_fees }) + } else { + const feeTargetInSatsPerVByte = feeConfigValues.tx_fees! / 1_000 + if (feeConfigValues.tx_fees_factor === 0) { + return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_exact', { + value: feeTargetInSatsPerVByte.toLocaleString(undefined, { + maximumFractionDigits: Math.log10(1_000), + }), + }) + } + + const minFeeSatsPerVByte = Math.max(1, feeTargetInSatsPerVByte * (1 - feeConfigValues.tx_fees_factor!)) + const maxFeeSatsPerVByte = feeTargetInSatsPerVByte * (1 + feeConfigValues.tx_fees_factor!) + + return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_randomized', { + min: minFeeSatsPerVByte.toLocaleString(undefined, { + maximumFractionDigits: 1, + }), + max: maxFeeSatsPerVByte.toLocaleString(undefined, { + maximumFractionDigits: 1, + }), + }) + } + }, [t, feeConfigValues]) + + return miningFeeText +} + +interface EstimatedMaxCollaboratorFeeArgs { + feeConfigValues?: FeeValues + isCoinjoin: boolean + amount: AmountSats + numCollaborators?: number +} + +const useEstimatedMaxCollaboratorFee = ({ + feeConfigValues, + isCoinjoin, + amount, + numCollaborators, +}: EstimatedMaxCollaboratorFeeArgs) => { + const estimatedMaxCollaboratorFee = useMemo(() => { + if (!isCoinjoin || !feeConfigValues || !amount) return null + if (!isValidNumber(amount) || !isValidNumber(numCollaborators ?? undefined)) return null + if (!isValidNumber(feeConfigValues.max_cj_fee_abs) || !isValidNumber(feeConfigValues.max_cj_fee_rel)) return null + return estimateMaxCollaboratorFee({ + amount, + collaborators: numCollaborators!, + maxFeeAbs: feeConfigValues.max_cj_fee_abs!, + maxFeeRel: feeConfigValues.max_cj_fee_rel!, + }) + }, [amount, isCoinjoin, numCollaborators, feeConfigValues]) + + return estimatedMaxCollaboratorFee +} interface PaymentDisplayInfo { sourceJarIndex?: JarIndex @@ -40,9 +109,13 @@ export function PaymentConfirmModal({ const { t } = useTranslation() const settings = useSettings() - const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ amount, numCollaborators, isCoinjoin }) - - const miningFeeText = useMiningFeeText() + const miningFeeText = useMiningFeeText({ feeConfigValues }) + const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ + feeConfigValues, + amount, + numCollaborators, + isCoinjoin, + }) return ( diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 32fddd0e0..aecc99446 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -1,30 +1,33 @@ import { useMemo, PropsWithChildren } from 'react' import { Trans, useTranslation } from 'react-i18next' import classNames from 'classnames' -import { useFeeConfigValues } from '../../hooks/Fees' import Balance from '../Balance' import * as rb from 'react-bootstrap' import { useSettings } from '../../context/SettingsContext' import { Link } from 'react-router-dom' import { routes } from '../../constants/routes' import { SATS, formatSats } from '../../utils' +import { FeeValues } from '../../hooks/Fees' interface FeeBreakdownProps { + feeConfigValues?: FeeValues numCollaborators: number | null amount: number | null + onClick?: () => void } type FeeCardProps = { amount: number | null highlight: boolean subtitle?: React.ReactNode + onClick: () => void } -const FeeCard = ({ amount, highlight, subtitle }: FeeCardProps) => { +const FeeCard = ({ amount, highlight, subtitle, onClick }: FeeCardProps) => { const settings = useSettings() const { t } = useTranslation() return ( - + { ) } -const FeeBreakdown = ({ numCollaborators, amount }: PropsWithChildren) => { +const FeeBreakdown = ({ + feeConfigValues, + numCollaborators, + amount, + onClick = () => {}, +}: PropsWithChildren) => { const { t } = useTranslation() - const feesConfig = useFeeConfigValues() /** eg: "0.03%" */ const maxSettingsRelativeFee = useMemo( - () => (feesConfig?.max_cj_fee_rel ? `${feesConfig.max_cj_fee_rel * 100}%` : t('send.fee_breakdown.not_set')), - [feesConfig, t] + () => + feeConfigValues?.max_cj_fee_rel ? `${feeConfigValues.max_cj_fee_rel * 100}%` : t('send.fee_breakdown.not_set'), + [feeConfigValues, t] ) /** eg: 44658 (expressed in sats) */ const maxEstimatedRelativeFee = useMemo( () => - feesConfig?.max_cj_fee_rel && numCollaborators && amount - ? amount * feesConfig.max_cj_fee_rel * numCollaborators >= 1 - ? Math.ceil(amount * feesConfig.max_cj_fee_rel) * numCollaborators + feeConfigValues?.max_cj_fee_rel && numCollaborators && amount + ? amount * feeConfigValues.max_cj_fee_rel * numCollaborators >= 1 + ? Math.ceil(amount * feeConfigValues.max_cj_fee_rel) * numCollaborators : null : null, - [feesConfig, amount, numCollaborators] + [feeConfigValues, amount, numCollaborators] ) /** eg: "8,636 sats" */ const maxSettingsAbsoluteFee = useMemo( () => - feesConfig?.max_cj_fee_abs ? `${formatSats(feesConfig.max_cj_fee_abs)} sats` : t('send.fee_breakdown.not_set'), - [feesConfig, t] + feeConfigValues?.max_cj_fee_abs + ? `${formatSats(feeConfigValues.max_cj_fee_abs)} sats` + : t('send.fee_breakdown.not_set'), + [feeConfigValues, t] ) /** eg: 77724 (expressed in sats) */ const maxEstimatedAbsoluteFee = useMemo( - () => (feesConfig?.max_cj_fee_abs && numCollaborators ? feesConfig.max_cj_fee_abs * numCollaborators : null), - [feesConfig, numCollaborators] + () => + feeConfigValues?.max_cj_fee_abs && numCollaborators ? feeConfigValues.max_cj_fee_abs * numCollaborators : null, + [feeConfigValues, numCollaborators] ) const isAbsoluteFeeHighlighted = useMemo( @@ -114,6 +125,7 @@ const FeeBreakdown = ({ numCollaborators, amount }: PropsWithChildren } + onClick={onClick} /> @@ -139,6 +151,7 @@ const FeeBreakdown = ({ numCollaborators, amount }: PropsWithChildren } + onClick={onClick} /> diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index b3b842c27..f692b7abe 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -3,7 +3,6 @@ import { Link } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' import * as rb from 'react-bootstrap' import classNames from 'classnames' - import PageTitle from '../PageTitle' import ToggleSwitch from '../ToggleSwitch' import Sprite from '../Sprite' @@ -15,11 +14,12 @@ import { PaymentConfirmModal } from '../PaymentConfirmModal' import { CoinjoinPreconditionViolationAlert } from '../CoinjoinPreconditionViolationAlert' import CollaboratorsSelector from './CollaboratorsSelector' import Accordion from '../Accordion' +import FeeConfigModal from '../settings/FeeConfigModal' +import { useFeeConfigValues } from '../../hooks/Fees' import { useReloadCurrentWalletInfo, useCurrentWalletInfo, CurrentWallet } from '../../context/WalletContext' import { useServiceInfo, useReloadServiceInfo } from '../../context/ServiceInfoContext' import { useLoadConfigValue } from '../../context/ServiceConfigContext' -import { FeeValues, useLoadFeeConfigValues } from '../../hooks/Fees' import { buildCoinjoinRequirementSummary } from '../../hooks/CoinjoinRequirements' import * as Api from '../../libs/JmWalletApi' @@ -74,7 +74,6 @@ export default function Send({ wallet }: SendProps) { const serviceInfo = useServiceInfo() const reloadServiceInfo = useReloadServiceInfo() const loadConfigValue = useLoadConfigValue() - const loadFeeConfigValues = useLoadFeeConfigValues() const isCoinjoinInProgress = useMemo(() => serviceInfo && serviceInfo.coinjoinInProgress, [serviceInfo]) const isMakerRunning = useMemo(() => serviceInfo && serviceInfo.makerRunning, [serviceInfo]) @@ -88,7 +87,8 @@ export default function Send({ wallet }: SendProps) { const [destinationJar, setDestinationJar] = useState(null) const [destinationIsReusedAddress, setDestinationIsReusedAddress] = useState(false) - const [feeConfigValues, setFeeConfigValues] = useState() + const [feeConfigValues, reloadFeeConfigValues] = useFeeConfigValues() + const [showingFeeConfig, setShowingFeeConfig] = useState(false) const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([]) const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState() @@ -288,25 +288,13 @@ export default function Send({ wallet }: SendProps) { setAlert({ variant: 'danger', message: err.message }) }) - const loadFeeValues = loadFeeConfigValues(abortCtrl.signal) - .then((data) => { - if (abortCtrl.signal.aborted) return - setFeeConfigValues(data) - }) - .catch(() => { - if (abortCtrl.signal.aborted) return - // As fee config is not essential, don't raise an error on purpose. - // Fee settings cannot be displayed, but making a payment is still possible. - setFeeConfigValues(undefined) - }) - - Promise.all([loadingServiceInfo, loadingWalletInfoAndUtxos, loadingMinimumMakerConfig, loadFeeValues]).finally( + Promise.all([loadingServiceInfo, loadingWalletInfoAndUtxos, loadingMinimumMakerConfig]).finally( () => !abortCtrl.signal.aborted && setIsInitializing(false) ) return () => abortCtrl.abort() }, - [isOperationDisabled, wallet, reloadCurrentWalletInfo, reloadServiceInfo, loadConfigValue, loadFeeConfigValues, t] + [isOperationDisabled, wallet, reloadCurrentWalletInfo, reloadServiceInfo, loadConfigValue, t] ) useEffect( @@ -844,7 +832,20 @@ export default function Send({ wallet }: SendProps) { }} /> - + setShowingFeeConfig(true)} + /> + + {showingFeeConfig && ( + reloadFeeConfigValues()} + onHide={() => setShowingFeeConfig(false)} + /> + )}
diff --git a/src/components/fb/SpendFidelityBondModal.tsx b/src/components/fb/SpendFidelityBondModal.tsx index 742f060c1..b8a8d2e9f 100644 --- a/src/components/fb/SpendFidelityBondModal.tsx +++ b/src/components/fb/SpendFidelityBondModal.tsx @@ -10,7 +10,7 @@ import Sprite from '../Sprite' import { SelectJar } from './FidelityBondSteps' import { PaymentConfirmModal } from '../PaymentConfirmModal' import { jarInitial } from '../jars/Jar' -import { FeeValues, useLoadFeeConfigValues } from '../../hooks/Fees' +import { useFeeConfigValues } from '../../hooks/Fees' import styles from './SpendFidelityBondModal.module.css' @@ -156,7 +156,7 @@ const SpendFidelityBondModal = ({ }: SpendFidelityBondModalProps) => { const { t } = useTranslation() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() - const loadFeeConfigValues = useLoadFeeConfigValues() + const feeConfigValues = useFeeConfigValues()[0] const [alert, setAlert] = useState() const [selectedDestinationJarIndex, setSelectedDestinationJarIndex] = useState( @@ -172,7 +172,6 @@ const SpendFidelityBondModal = ({ const enableDestinationJarSelection = useMemo(() => destinationJarIndex === undefined, [destinationJarIndex]) const [showConfirmSendModal, setShowConfirmSendModal] = useState(!enableDestinationJarSelection) - const [feeConfigValues, setFeeConfigValues] = useState() const submitButtonRef = useRef(null) @@ -180,26 +179,6 @@ const SpendFidelityBondModal = ({ return walletInfo.data.utxos.utxos.find((utxo) => utxo.utxo === fidelityBondId) }, [walletInfo, fidelityBondId]) - useEffect(() => { - const abortCtrl = new AbortController() - - loadFeeConfigValues(abortCtrl.signal) - .then((data) => { - if (abortCtrl.signal.aborted) return - setFeeConfigValues(data) - }) - .catch((e) => { - if (abortCtrl.signal.aborted) return - // As fee config is not essential, don't raise an error on purpose. - // Fee settings cannot be displayed, but making a payment is still possible. - setFeeConfigValues(undefined) - }) - - return () => { - abortCtrl.abort() - } - }, [loadFeeConfigValues]) - // This callback is responsible for updating the loading state when the // the payment is made. The wallet needs some time after a tx is sent // to reflect the changes internally. All outputs in diff --git a/src/components/settings/FeeConfigModal.tsx b/src/components/settings/FeeConfigModal.tsx index 472597de3..2d448dd56 100644 --- a/src/components/settings/FeeConfigModal.tsx +++ b/src/components/settings/FeeConfigModal.tsx @@ -44,6 +44,8 @@ const CJ_FEE_REL_MAX = 0.05 // 5% - no enforcement by JM - this should be a "san interface FeeConfigModalProps { show: boolean onHide: () => void + onSuccess?: () => void + onCancel?: () => void } interface FeeConfigFormProps { @@ -306,7 +308,7 @@ const FeeConfigForm = forwardRef( } ) -export default function FeeConfigModal({ show, onHide }: FeeConfigModalProps) { +export default function FeeConfigModal({ show, onHide, onSuccess, onCancel }: FeeConfigModalProps) { const { t } = useTranslation() const updateConfigValues = useUpdateConfigValues() const loadFeeConfigValues = useLoadFeeConfigValues() @@ -383,6 +385,7 @@ export default function FeeConfigModal({ show, onHide }: FeeConfigModalProps) { await updateConfigValues({ updates }) setIsSubmitting(false) + onSuccess && onSuccess() onHide() } catch (err) { setIsSubmitting(false) @@ -463,16 +466,17 @@ export default function FeeConfigModal({ show, onHide }: FeeConfigModalProps) { [t] ) - const cancel = () => { + const cancel = useCallback(() => { + onCancel && onCancel() onHide() - } + }, [onCancel, onHide]) return ( { ) } -export const useFeeConfigValues = () => { +export const useFeeConfigValues = (): [FeeValues | undefined, () => void] => { const loadFeeConfigValues = useLoadFeeConfigValues() - const [values, setValues] = useState() + const [values, setValues] = useState() + const [reloadCounter, setReloadCounter] = useState(0) useEffect(() => { const abortCtrl = new AbortController() @@ -65,14 +65,15 @@ export const useFeeConfigValues = () => { .then((val) => setValues(val)) .catch((e) => { console.log('Unable lo load fee config: ', e) - setValues(null) + setValues(undefined) }) return () => { abortCtrl.abort() } - }, [setValues, loadFeeConfigValues]) - return values + }, [loadFeeConfigValues, reloadCounter]) + + return [values, () => setReloadCounter((val) => val + 1)] } interface EstimatMaxCollaboratorFeeProps { @@ -91,70 +92,3 @@ export const estimateMaxCollaboratorFee = ({ const maxFeePerCollaborator = Math.max(Math.ceil(amount * maxFeeRel), maxFeeAbs) return collaborators > 0 ? Math.min(maxFeePerCollaborator * collaborators, amount) : 0 } - -export const useMiningFeeText = () => { - const feeConfigValues = useFeeConfigValues() - const { t } = useTranslation() - - const miningFeeText = useMemo(() => { - if (!feeConfigValues) return null - if (!isValidNumber(feeConfigValues.tx_fees) || !isValidNumber(feeConfigValues.tx_fees_factor)) return null - - const unit = toTxFeeValueUnit(feeConfigValues.tx_fees) - if (!unit) { - return null - } else if (unit === 'blocks') { - return t('send.confirm_send_modal.text_miner_fee_in_targeted_blocks', { count: feeConfigValues.tx_fees }) - } else { - const feeTargetInSatsPerVByte = feeConfigValues.tx_fees! / 1_000 - if (feeConfigValues.tx_fees_factor === 0) { - return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_exact', { - value: feeTargetInSatsPerVByte.toLocaleString(undefined, { - maximumFractionDigits: Math.log10(1_000), - }), - }) - } - - const minFeeSatsPerVByte = Math.max(1, feeTargetInSatsPerVByte * (1 - feeConfigValues.tx_fees_factor!)) - const maxFeeSatsPerVByte = feeTargetInSatsPerVByte * (1 + feeConfigValues.tx_fees_factor!) - - return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_randomized', { - min: minFeeSatsPerVByte.toLocaleString(undefined, { - maximumFractionDigits: 1, - }), - max: maxFeeSatsPerVByte.toLocaleString(undefined, { - maximumFractionDigits: 1, - }), - }) - } - }, [t, feeConfigValues]) - - return miningFeeText -} - -interface useEstimatedMaxCollaboratorFeeArgs { - isCoinjoin: boolean - amount: number | null - numCollaborators?: number | null -} -export const useEstimatedMaxCollaboratorFee = ({ - isCoinjoin, - amount, - numCollaborators, -}: useEstimatedMaxCollaboratorFeeArgs) => { - const feeConfigValues = useFeeConfigValues() - - const estimatedMaxCollaboratorFee = useMemo(() => { - if (!isCoinjoin || !feeConfigValues || !amount) return null - if (!isValidNumber(amount) || !isValidNumber(numCollaborators ?? undefined)) return null - if (!isValidNumber(feeConfigValues.max_cj_fee_abs) || !isValidNumber(feeConfigValues.max_cj_fee_rel)) return null - return estimateMaxCollaboratorFee({ - amount, - collaborators: numCollaborators!, - maxFeeAbs: feeConfigValues.max_cj_fee_abs!, - maxFeeRel: feeConfigValues.max_cj_fee_rel!, - }) - }, [amount, isCoinjoin, numCollaborators, feeConfigValues]) - - return estimatedMaxCollaboratorFee -} From 98bdcc1916aaebc0ef1b92db00e1e344d3859bb1 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 15:43:39 +0200 Subject: [PATCH 20/31] ui(FeeBreakdown): add less-or-equal sign --- src/components/Send/FeeBreakdown.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index aecc99446..dbb89e78f 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -35,7 +35,10 @@ const FeeCard = ({ amount, highlight, subtitle, onClick }: FeeCardProps) => { >
{amount ? ( - + <> + ≤ + + ) : ( t('send.fee_breakdown.too_low') )} From 33c9a03e12974468a10dff1bcb1fd3bf8a74dfcd Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 16:17:55 +0200 Subject: [PATCH 21/31] ui(FeeBreakdown): show max collaborator fee value in title --- src/components/PaymentConfirmModal.tsx | 34 +++----------------------- src/components/Send/index.tsx | 19 +++++++++++--- src/hooks/Fees.ts | 30 +++++++++++++++++++++-- src/i18n/locales/en/translation.json | 2 +- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 8a21ffc7b..3a7ac8f97 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -4,7 +4,7 @@ import * as rb from 'react-bootstrap' import Sprite from './Sprite' import Balance from './Balance' import { useSettings } from '../context/SettingsContext' -import { estimateMaxCollaboratorFee, FeeValues, toTxFeeValueUnit } from '../hooks/Fees' +import { FeeValues, useEstimatedMaxCollaboratorFee, toTxFeeValueUnit } from '../hooks/Fees' import { ConfirmModal, ConfirmModalProps } from './Modal' import styles from './PaymentConfirmModal.module.css' import { AmountSats } from '../libs/JmWalletApi' @@ -50,34 +50,6 @@ const useMiningFeeText = ({ feeConfigValues }: { feeConfigValues?: FeeValues }) return miningFeeText } -interface EstimatedMaxCollaboratorFeeArgs { - feeConfigValues?: FeeValues - isCoinjoin: boolean - amount: AmountSats - numCollaborators?: number -} - -const useEstimatedMaxCollaboratorFee = ({ - feeConfigValues, - isCoinjoin, - amount, - numCollaborators, -}: EstimatedMaxCollaboratorFeeArgs) => { - const estimatedMaxCollaboratorFee = useMemo(() => { - if (!isCoinjoin || !feeConfigValues || !amount) return null - if (!isValidNumber(amount) || !isValidNumber(numCollaborators ?? undefined)) return null - if (!isValidNumber(feeConfigValues.max_cj_fee_abs) || !isValidNumber(feeConfigValues.max_cj_fee_rel)) return null - return estimateMaxCollaboratorFee({ - amount, - collaborators: numCollaborators!, - maxFeeAbs: feeConfigValues.max_cj_fee_abs!, - maxFeeRel: feeConfigValues.max_cj_fee_rel!, - }) - }, [amount, isCoinjoin, numCollaborators, feeConfigValues]) - - return estimatedMaxCollaboratorFee -} - interface PaymentDisplayInfo { sourceJarIndex?: JarIndex destination: String @@ -111,10 +83,10 @@ export function PaymentConfirmModal({ const miningFeeText = useMiningFeeText({ feeConfigValues }) const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ + isCoinjoin, feeConfigValues, amount, - numCollaborators, - isCoinjoin, + numCollaborators: numCollaborators || null, }) return ( diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index f692b7abe..98ce5700b 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -15,7 +15,7 @@ import { CoinjoinPreconditionViolationAlert } from '../CoinjoinPreconditionViola import CollaboratorsSelector from './CollaboratorsSelector' import Accordion from '../Accordion' import FeeConfigModal from '../settings/FeeConfigModal' -import { useFeeConfigValues } from '../../hooks/Fees' +import { useFeeConfigValues, useEstimatedMaxCollaboratorFee } from '../../hooks/Fees' import { useReloadCurrentWalletInfo, useCurrentWalletInfo, CurrentWallet } from '../../context/WalletContext' import { useServiceInfo, useReloadServiceInfo } from '../../context/ServiceInfoContext' @@ -24,6 +24,7 @@ import { buildCoinjoinRequirementSummary } from '../../hooks/CoinjoinRequirement import * as Api from '../../libs/JmWalletApi' import { routes } from '../../constants/routes' +import { SATS, formatSats, isValidNumber } from '../../utils' import { enhanceDirectPaymentErrorMessageIfNecessary, @@ -34,7 +35,6 @@ import { isValidJarIndex, isValidNumCollaborators, } from './helpers' -import { SATS, isValidNumber } from '../../utils' import styles from './Send.module.css' import FeeBreakdown from './FeeBreakdown' @@ -116,6 +116,13 @@ export default function Send({ wallet }: SendProps) { ) }, [walletInfo]) + const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ + feeConfigValues, + amount, + numCollaborators, + isCoinjoin, + }) + useEffect( function preSelectSourceJarIfPossible() { if (isLoading) return @@ -823,7 +830,13 @@ export default function Send({ wallet }: SendProps) { /> - {t('send.fee_breakdown.title')} + + {t('send.fee_breakdown.title', { + maxCollaboratorFee: estimatedMaxCollaboratorFee + ? `≤${formatSats(estimatedMaxCollaboratorFee)} sats` + : '...', + })} + { +}: EstimatMaxCollaboratorFeeProps): AmountSats => { const maxFeePerCollaborator = Math.max(Math.ceil(amount * maxFeeRel), maxFeeAbs) return collaborators > 0 ? Math.min(maxFeePerCollaborator * collaborators, amount) : 0 } + +interface EstimatedMaxCollaboratorFeeArgs { + isCoinjoin: boolean + amount: AmountSats | null + numCollaborators: number | null + feeConfigValues?: FeeValues +} + +export const useEstimatedMaxCollaboratorFee = ({ + isCoinjoin, + amount, + numCollaborators, + feeConfigValues, +}: EstimatedMaxCollaboratorFeeArgs): AmountSats | null => { + return useMemo(() => { + if (!isCoinjoin || !feeConfigValues || !amount) return null + if (!isValidNumber(amount) || !isValidNumber(numCollaborators ?? undefined)) return null + if (!isValidNumber(feeConfigValues.max_cj_fee_abs) || !isValidNumber(feeConfigValues.max_cj_fee_rel)) return null + return estimateMaxCollaboratorFee({ + amount, + collaborators: numCollaborators!, + maxFeeAbs: feeConfigValues.max_cj_fee_abs!, + maxFeeRel: feeConfigValues.max_cj_fee_rel!, + }) + }, [amount, isCoinjoin, numCollaborators, feeConfigValues]) +} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index b2cada08d..71473530a 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -260,7 +260,7 @@ "sweep_amount_breakdown_estimated_amount": "Estimated amount to be sent", "sweep_amount_breakdown_explanation": "A sweep transaction will consume all UTXOs of a mixdepth leaving no coins behind except those that have been <1>frozen or <3>time-locked. Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", "fee_breakdown": { - "title": "Maximum collaborator fee limit", + "title": "Maximum collaborator fees: {{ maxCollaboratorFee }}", "subtitle": "Depending on the amount, the number of collaborators and the preset limits, you can see the maximum collaborator fees for the upcoming collaborative transaction. This does not include regular mining fees.", "absolute_limit": "Absolute fee limit", "relative_limit": "Relative fee limit", From 0146241644bbee19b3ab7779c47ef70aa29ccb61 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 23 Aug 2023 16:48:57 +0200 Subject: [PATCH 22/31] ui(FeeBreakdown): open cj fee section by default --- src/components/Send/FeeBreakdown.tsx | 24 +++++++-------- src/components/Send/index.tsx | 36 +++++++++++++++++----- src/components/settings/FeeConfigModal.tsx | 33 ++++++++++++++++---- src/i18n/locales/en/translation.json | 8 ++--- src/index.css | 6 +++- 5 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index dbb89e78f..6955113be 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -4,9 +4,7 @@ import classNames from 'classnames' import Balance from '../Balance' import * as rb from 'react-bootstrap' import { useSettings } from '../../context/SettingsContext' -import { Link } from 'react-router-dom' -import { routes } from '../../constants/routes' -import { SATS, formatSats } from '../../utils' +import { SATS, formatSats, factorToPercentage } from '../../utils' import { FeeValues } from '../../hooks/Fees' interface FeeBreakdownProps { @@ -20,7 +18,7 @@ type FeeCardProps = { amount: number | null highlight: boolean subtitle?: React.ReactNode - onClick: () => void + onClick?: () => void } const FeeCard = ({ amount, highlight, subtitle, onClick }: FeeCardProps) => { const settings = useSettings() @@ -34,13 +32,13 @@ const FeeCard = ({ amount, highlight, subtitle, onClick }: FeeCardProps) => { })} >
- {amount ? ( + {amount !== null ? ( <> ≤ ) : ( - t('send.fee_breakdown.too_low') + t('send.fee_breakdown.placeholder_amount') )}
{subtitle}
@@ -60,17 +58,17 @@ const FeeBreakdown = ({ /** eg: "0.03%" */ const maxSettingsRelativeFee = useMemo( () => - feeConfigValues?.max_cj_fee_rel ? `${feeConfigValues.max_cj_fee_rel * 100}%` : t('send.fee_breakdown.not_set'), + feeConfigValues?.max_cj_fee_rel + ? `${factorToPercentage(feeConfigValues.max_cj_fee_rel)}%` + : t('send.fee_breakdown.not_set'), [feeConfigValues, t] ) /** eg: 44658 (expressed in sats) */ const maxEstimatedRelativeFee = useMemo( () => - feeConfigValues?.max_cj_fee_rel && numCollaborators && amount - ? amount * feeConfigValues.max_cj_fee_rel * numCollaborators >= 1 - ? Math.ceil(amount * feeConfigValues.max_cj_fee_rel) * numCollaborators - : null + feeConfigValues?.max_cj_fee_rel && numCollaborators && amount && amount > 0 + ? Math.ceil(amount * feeConfigValues.max_cj_fee_rel) * numCollaborators : null, [feeConfigValues, amount, numCollaborators] ) @@ -120,7 +118,7 @@ const FeeBreakdown = ({ , + 1: , }} values={{ numCollaborators, @@ -146,7 +144,7 @@ const FeeBreakdown = ({ , + 1: , }} values={{ numCollaborators, diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 98ce5700b..52848662d 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -14,7 +14,7 @@ import { PaymentConfirmModal } from '../PaymentConfirmModal' import { CoinjoinPreconditionViolationAlert } from '../CoinjoinPreconditionViolationAlert' import CollaboratorsSelector from './CollaboratorsSelector' import Accordion from '../Accordion' -import FeeConfigModal from '../settings/FeeConfigModal' +import FeeConfigModal, { FeeConfigSectionKey } from '../settings/FeeConfigModal' import { useFeeConfigValues, useEstimatedMaxCollaboratorFee } from '../../hooks/Fees' import { useReloadCurrentWalletInfo, useCurrentWalletInfo, CurrentWallet } from '../../context/WalletContext' @@ -88,7 +88,8 @@ export default function Send({ wallet }: SendProps) { const [destinationIsReusedAddress, setDestinationIsReusedAddress] = useState(false) const [feeConfigValues, reloadFeeConfigValues] = useFeeConfigValues() - const [showingFeeConfig, setShowingFeeConfig] = useState(false) + const [activeFeeConfigModalSection, setActiveFeeConfigModalSection] = useState() + const [showFeeConfigModal, setShowFeeConfigModal] = useState(false) const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([]) const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState() @@ -841,7 +842,24 @@ export default function Send({ wallet }: SendProps) { , + 1: ( + { + setActiveFeeConfigModalSection('cj_fee') + setShowFeeConfigModal(true) + }} + className="text-decoration-underline link-secondary" + /> + ), + 2: ( + { + setActiveFeeConfigModalSection('tx_fee') + setShowFeeConfigModal(true) + }} + className="text-decoration-underline link-secondary" + /> + ), }} />
@@ -849,14 +867,18 @@ export default function Send({ wallet }: SendProps) { feeConfigValues={feeConfigValues} numCollaborators={numCollaborators} amount={amount} - onClick={() => setShowingFeeConfig(true)} + onClick={() => { + setActiveFeeConfigModalSection('cj_fee') + setShowFeeConfigModal(true) + }} /> - {showingFeeConfig && ( + {showFeeConfigModal && ( reloadFeeConfigValues()} - onHide={() => setShowingFeeConfig(false)} + onHide={() => setShowFeeConfigModal(false)} + defaultActiveSectionKey={activeFeeConfigModalSection} /> )}
diff --git a/src/components/settings/FeeConfigModal.tsx b/src/components/settings/FeeConfigModal.tsx index 2d448dd56..744c2f50c 100644 --- a/src/components/settings/FeeConfigModal.tsx +++ b/src/components/settings/FeeConfigModal.tsx @@ -46,16 +46,25 @@ interface FeeConfigModalProps { onHide: () => void onSuccess?: () => void onCancel?: () => void + defaultActiveSectionKey?: FeeConfigSectionKey } +export type FeeConfigSectionKey = 'tx_fee' | 'cj_fee' +const TX_FEE_SECTION_KEY: FeeConfigSectionKey = 'tx_fee' +const CJ_FEE_SECTION_KEY: FeeConfigSectionKey = 'cj_fee' + interface FeeConfigFormProps { initialValues: FeeValues validate: (values: FeeValues, txFeesUnit: TxFeeValueUnit) => FormikErrors onSubmit: (values: FeeValues, txFeesUnit: TxFeeValueUnit) => void + defaultActiveSectionKey?: FeeConfigSectionKey } const FeeConfigForm = forwardRef( - ({ onSubmit, validate, initialValues }: FeeConfigFormProps, ref: React.Ref) => { + ( + { onSubmit, validate, initialValues, defaultActiveSectionKey }: FeeConfigFormProps, + ref: React.Ref + ) => { const { t, i18n } = useTranslation() const [txFeesUnit, setTxFeesUnit] = useState(toTxFeeValueUnit(initialValues.tx_fees) || 'blocks') @@ -68,8 +77,8 @@ const FeeConfigForm = forwardRef( > {({ handleSubmit, setFieldValue, handleBlur, validateForm, values, touched, errors, isSubmitting }) => ( - - + + - + {feeConfigValues && ( - + )} )} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 71473530a..f2936bc6b 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -260,13 +260,13 @@ "sweep_amount_breakdown_estimated_amount": "Estimated amount to be sent", "sweep_amount_breakdown_explanation": "A sweep transaction will consume all UTXOs of a mixdepth leaving no coins behind except those that have been <1>frozen or <3>time-locked. Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", "fee_breakdown": { - "title": "Maximum collaborator fees: {{ maxCollaboratorFee }}", - "subtitle": "Depending on the amount, the number of collaborators and the preset limits, you can see the maximum collaborator fees for the upcoming collaborative transaction. This does not include regular mining fees.", + "title": "Collaborator fees: {{ maxCollaboratorFee }}", + "subtitle": "Depending on the amount, the number of collaborators and the <1>preset limits, you can see the maximum collaborator fees for the upcoming collaborative transaction. This does not include regular <2>mining fees.", "absolute_limit": "Absolute fee limit", "relative_limit": "Relative fee limit", - "fee_card_subtitle": "{{ maxFee }} * {{ numCollaborators }}", + "fee_card_subtitle": "<1>{{ maxFee }} * {{ numCollaborators }}", "not_set": "Not set", - "too_low": "Amount too low..." + "placeholder_amount": "Enter amount..." }, "confirm_send_modal": { "title": "Confirm payment", diff --git a/src/index.css b/src/index.css index 7bb32d838..e2c60c325 100644 --- a/src/index.css +++ b/src/index.css @@ -290,7 +290,7 @@ main { } .text-small { - font-size: 0.875rem; /* var(--bs-font-size-sm); */ + font-size: 0.875rem; } /* Fullscreen Overlays */ @@ -488,6 +488,10 @@ h2 { z-index: 1200; } +.link-secondary:hover { + cursor: pointer; +} + /* Wallets Styles */ .wallets a.wallet-name { From 054bc22123a3600f058620ba2b1a5226a8dce698 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 24 Aug 2023 22:16:12 +0200 Subject: [PATCH 23/31] ui(FeeSettings): switch position of mining and collaborator fees --- src/components/settings/FeeConfigModal.tsx | 166 +++++++++++---------- src/i18n/locales/en/translation.json | 4 +- 2 files changed, 87 insertions(+), 83 deletions(-) diff --git a/src/components/settings/FeeConfigModal.tsx b/src/components/settings/FeeConfigModal.tsx index 744c2f50c..3098144a2 100644 --- a/src/components/settings/FeeConfigModal.tsx +++ b/src/components/settings/FeeConfigModal.tsx @@ -78,6 +78,89 @@ const FeeConfigForm = forwardRef( {({ handleSubmit, setFieldValue, handleBlur, validateForm, values, touched, errors, isSubmitting }) => ( + + + + {t('settings.fees.title_max_cj_fee_settings')} + + + +
{t('settings.fees.description_max_cj_fee_settings')}
+ + + + + + {t('settings.fees.label_max_cj_fee_abs')} + {t('settings.fees.description_max_cj_fee_abs')} + + + + + { + const value = parseInt(e.target.value, 10) + setFieldValue('max_cj_fee_abs', value, true) + }} + isValid={touched.max_cj_fee_abs && !errors.max_cj_fee_abs} + isInvalid={touched.max_cj_fee_abs && !!errors.max_cj_fee_abs} + min={CJ_FEE_ABS_MIN} + max={CJ_FEE_ABS_MAX} + step={1} + /> + {errors.max_cj_fee_abs} + + + + + + {t('settings.fees.label_max_cj_fee_rel', { + fee: isValidNumber(values.max_cj_fee_rel) + ? `(${factorToPercentage(values.max_cj_fee_rel!)}%)` + : '', + })} + + {t('settings.fees.description_max_cj_fee_rel')} + + + % + + { + const value = parseFloat(e.target.value) + setFieldValue('max_cj_fee_rel', percentageToFactor(value), true) + }} + isValid={touched.max_cj_fee_rel && !errors.max_cj_fee_rel} + isInvalid={touched.max_cj_fee_rel && !!errors.max_cj_fee_rel} + min={factorToPercentage(CJ_FEE_REL_MIN)} + max={factorToPercentage(CJ_FEE_REL_MAX)} + step={0.0001} + /> + {errors.max_cj_fee_rel} + + +
+
+
{t('settings.fees.description_general_fee_settings')}
{t('settings.fees.label_tx_fees')} {txFeesUnit && ( @@ -227,88 +311,6 @@ const FeeConfigForm = forwardRef(
- - - - {t('settings.fees.title_max_cj_fee_settings')} - - - - - - {t('settings.fees.subtitle_max_cj_fee')} - - - {t('settings.fees.label_max_cj_fee_abs')} - {t('settings.fees.description_max_cj_fee_abs')} - - - - - { - const value = parseInt(e.target.value, 10) - setFieldValue('max_cj_fee_abs', value, true) - }} - isValid={touched.max_cj_fee_abs && !errors.max_cj_fee_abs} - isInvalid={touched.max_cj_fee_abs && !!errors.max_cj_fee_abs} - min={CJ_FEE_ABS_MIN} - max={CJ_FEE_ABS_MAX} - step={1} - /> - {errors.max_cj_fee_abs} - - - - - - {t('settings.fees.label_max_cj_fee_rel', { - fee: isValidNumber(values.max_cj_fee_rel) - ? `(${factorToPercentage(values.max_cj_fee_rel!)}%)` - : '', - })} - - {t('settings.fees.description_max_cj_fee_rel')} - - - % - - { - const value = parseFloat(e.target.value) - setFieldValue('max_cj_fee_rel', percentageToFactor(value), true) - }} - isValid={touched.max_cj_fee_rel && !errors.max_cj_fee_rel} - isInvalid={touched.max_cj_fee_rel && !!errors.max_cj_fee_rel} - min={factorToPercentage(CJ_FEE_REL_MIN)} - max={factorToPercentage(CJ_FEE_REL_MAX)} - step={0.0001} - /> - {errors.max_cj_fee_rel} - - - -
)} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index f2936bc6b..cfea59e86 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -192,8 +192,9 @@ "confirm_locking_modal_body_jam": "A collaborative transaction is being executed. Locking the wallet will stop the service. You can close the browser window to let it run in the background.", "fees": { "title": "Fee Limits", - "description": "Adjust mining fees and collaborator fees according to your needs. Mining fees relate to transaction priority and depend on mempool conditions. Collaborator fees relate to liquidity price and depend on market conditions. Total fees paid for each transaction depend on the amount of collaborators. Additional collaborators increase privacy, but also fees. These settings will be reset to default values when the JoinMarket service restarts, e.g. on a system reboot. For more information, <1>see the documentation on fees.", + "description": "Adjust mining fees and collaborator fees according to your needs. These settings will be reset to default values when the JoinMarket service restarts, e.g. on a system reboot. For more information, <1>see the documentation on fees.", "title_general_fee_settings": "Mining fees", + "description_general_fee_settings": "Mining fees relate to transaction priority and depend on mempool conditions.", "radio_tx_fees_blocks": "Block target", "radio_tx_fees_satspervbyte": "sats/vByte", "label_tx_fees": "Transaction base fee", @@ -205,6 +206,7 @@ "description_tx_fees_factor": "Random fees improve privacy. The percentage is to be understood as a +/- around the base fee. Example: If you set the base fee to 10 sats/vByte and the randomization to 30%, a value between 7 and 13 sats/vByte will be used. Default: 20%.", "feedback_invalid_tx_fees_factor": "Please provide a valid fee randomization value between {{ min }} and {{ max }}.", "title_max_cj_fee_settings": "Collaborator fees", + "description_max_cj_fee_settings": "Collaborator fees relate to liquidity price and depend on market conditions. Total fees paid for each transaction depend on the amount of collaborators. Additional collaborators increase privacy, but also fees.", "subtitle_max_cj_fee": "Note: An offer will be rejected only if both limits are exceeded.", "label_max_cj_fee_abs": "Absolute limit (per collaborator)", "description_max_cj_fee_abs": "The maximum fee you are willing to pay per collaborator. Example: when set to 3,000 sats, the maximum fee in total would be 27,000 sats when using the default of 9 collaborators.", From 86c98461b5f9ae0540fddb05254c7ee0e8e1d641 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 24 Aug 2023 22:35:17 +0200 Subject: [PATCH 24/31] ui(FeeBreakdown): now works with sweep transactions --- src/components/Send/index.tsx | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 52848662d..66c6683ee 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -117,13 +117,6 @@ export default function Send({ wallet }: SendProps) { ) }, [walletInfo]) - const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ - feeConfigValues, - amount, - numCollaborators, - isCoinjoin, - }) - useEffect( function preSelectSourceJarIfPossible() { if (isLoading) return @@ -155,6 +148,13 @@ export default function Send({ wallet }: SendProps) { return buildCoinjoinRequirementSummary(sourceJarUtxos) }, [sourceJarUtxos]) + const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({ + feeConfigValues, + amount: isSweep && accountBalance ? accountBalance.calculatedAvailableBalanceInSats : amount, + numCollaborators, + isCoinjoin, + }) + const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false) const [showConfirmSendModal, setShowConfirmSendModal] = useState(false) const submitButtonRef = useRef(null) @@ -863,15 +863,18 @@ export default function Send({ wallet }: SendProps) { }} /> - { - setActiveFeeConfigModalSection('cj_fee') - setShowFeeConfigModal(true) - }} - /> + + {accountBalance && ( + { + setActiveFeeConfigModalSection('cj_fee') + setShowFeeConfigModal(true) + }} + /> + )} {showFeeConfigModal && ( Date: Thu, 24 Aug 2023 22:40:56 +0200 Subject: [PATCH 25/31] ui(FeeBreakdown): market maker fees -> collaborator fees --- src/components/Send/index.tsx | 7 +++---- src/i18n/locales/en/translation.json | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 66c6683ee..780a9a1e3 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -556,10 +556,9 @@ export default function Send({ wallet }: SendProps) { > time-locked - . Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero - change. The exact transaction amount can only be calculated by JoinMarket at the point when the - transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. - Refer to the + . Mining fees and collaborator fees will be deducted from the amount so as to leave zero change. The + exact transaction amount can only be calculated by JoinMarket at the point when the transaction is + made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the frozen or <3>time-locked. Onchain transaction fees and market maker fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", + "sweep_amount_breakdown_explanation": "A sweep transaction will consume all UTXOs of a mixdepth leaving no coins behind except those that have been <1>frozen or <3>time-locked. Mining fees and collaborator fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", "fee_breakdown": { "title": "Collaborator fees: {{ maxCollaboratorFee }}", "subtitle": "Depending on the amount, the number of collaborators and the <1>preset limits, you can see the maximum collaborator fees for the upcoming collaborative transaction. This does not include regular <2>mining fees.", From 4dbe9754c6ac5f16afccffc87e17a767041138a9 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 24 Aug 2023 23:02:39 +0200 Subject: [PATCH 26/31] fix(actions): temporarily print linter debug messages --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bacbada42..906799139 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: run: npm ci # Checks - name: Lint - run: npm run lint + run: npm run lint -- --version && npm run lint -- --loglevel debug # Test - name: Test run: npm test From 0c3dc380816c428c3378dd94391c73e218749ecd Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 24 Aug 2023 23:39:40 +0200 Subject: [PATCH 27/31] build(deps): update to prettier v2.8.7 --- .github/workflows/build.yml | 2 +- package-lock.json | 14 +++++++------- package.json | 2 +- src/index.css | 6 +----- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 906799139..b1da30b99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: run: npm ci # Checks - name: Lint - run: npm run lint -- --version && npm run lint -- --loglevel debug + run: npm run lint -- --version && npm run lint # Test - name: Test run: npm test diff --git a/package-lock.json b/package-lock.json index 949c91315..2622d92a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "jest-watch-typeahead": "^0.6.5", "jest-websocket-mock": "^2.4.0", "lint-staged": "^13.0.3", - "prettier": "^2.7.1", + "prettier": "^2.8.7", "react-scripts": "^5.0.1", "typescript": "^4.8.4" }, @@ -17589,9 +17589,9 @@ } }, "node_modules/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -35131,9 +35131,9 @@ "dev": true }, "prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", "dev": true }, "pretty-bytes": { diff --git a/package.json b/package.json index 121058eb0..611f659d0 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "jest-watch-typeahead": "^0.6.5", "jest-websocket-mock": "^2.4.0", "lint-staged": "^13.0.3", - "prettier": "^2.7.1", + "prettier": "^2.8.7", "react-scripts": "^5.0.1", "typescript": "^4.8.4" }, diff --git a/src/index.css b/src/index.css index e2c60c325..6cb2b998d 100644 --- a/src/index.css +++ b/src/index.css @@ -579,6 +579,7 @@ h2 { --bs-code-color: var(--bs-gray-300); --bs-gray-dark: #16191c; --bs-black: #000; + --bs-border-color-translucent: var(--bs-gray-800); } :root[data-theme='dark'] .accordion { @@ -597,11 +598,6 @@ h2 { background-color: var(--bs-gray-900); } -:root[data-theme='dark'] - .card:not(.border-success):not(.border-danger):not(.border-warning):not(.border-primary):not(.border-info):not(.border-light):not(.border-dark) { - border-color: var(--bs-gray-800) !important; -} - :root[data-theme='dark'] .btn:not(.btn-light) { --bs-btn-color: var(--bs-white); } From c7f9b0d36a5deecdb3771f77dc83038c794ad0dd Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Fri, 25 Aug 2023 00:32:28 +0200 Subject: [PATCH 28/31] ui(FeeBreakdown): emphasize collaborator fee note --- src/components/Send/index.tsx | 11 ++++++++++- src/components/settings/FeeConfigModal.module.css | 5 ----- src/components/settings/FeeConfigModal.tsx | 4 ++-- src/i18n/locales/en/translation.json | 3 ++- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 780a9a1e3..a86bf426a 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -850,7 +850,16 @@ export default function Send({ wallet }: SendProps) { className="text-decoration-underline link-secondary" /> ), - 2: ( + }} + /> + + + + { setActiveFeeConfigModalSection('tx_fee') diff --git a/src/components/settings/FeeConfigModal.module.css b/src/components/settings/FeeConfigModal.module.css index 94194bab6..e89e26f78 100644 --- a/src/components/settings/FeeConfigModal.module.css +++ b/src/components/settings/FeeConfigModal.module.css @@ -16,11 +16,6 @@ margin-bottom: 0.5rem !important; } -.infoIcon { - border: 1px solid; - border-radius: 50%; -} - .inputGroupText { width: 5ch; display: inline-flex; diff --git a/src/components/settings/FeeConfigModal.tsx b/src/components/settings/FeeConfigModal.tsx index 3098144a2..27a566d4f 100644 --- a/src/components/settings/FeeConfigModal.tsx +++ b/src/components/settings/FeeConfigModal.tsx @@ -91,8 +91,8 @@ const FeeConfigForm = forwardRef(
{t('settings.fees.description_max_cj_fee_settings')}
- - + + {t('settings.fees.label_max_cj_fee_abs')} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index c27225904..8c4119f6b 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -263,7 +263,8 @@ "sweep_amount_breakdown_explanation": "A sweep transaction will consume all UTXOs of a mixdepth leaving no coins behind except those that have been <1>frozen or <3>time-locked. Mining fees and collaborator fees will be deducted from the amount so as to leave zero change. The exact transaction amount can only be calculated by JoinMarket at the point when the transaction is made. Therefore the estimated amount shown might deviate from the actually sent amount. Refer to the <5>JoinMarket documentation for more details.", "fee_breakdown": { "title": "Collaborator fees: {{ maxCollaboratorFee }}", - "subtitle": "Depending on the amount, the number of collaborators and the <1>preset limits, you can see the maximum collaborator fees for the upcoming collaborative transaction. This does not include regular <2>mining fees.", + "subtitle": "Depending on the amount, the number of collaborators and the <1>preset limits, you can see the maximum collaborator fees for the upcoming collaborative transaction.", + "alert_collaborator_fee_note": "This does not include regular <1>mining fees.", "absolute_limit": "Absolute fee limit", "relative_limit": "Relative fee limit", "fee_card_subtitle": "<1>{{ maxFee }} * {{ numCollaborators }}", From 5432b6d1ff9bea4cfaafb8e917ead2cd568610b0 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Fri, 25 Aug 2023 02:07:34 +0200 Subject: [PATCH 29/31] fix(FeeBreakdown): improve handling of missing config values --- src/components/Send/FeeBreakdown.tsx | 28 +++++++++++++++++++--------- src/i18n/locales/en/translation.json | 6 +++--- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index 6955113be..04ca351fc 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -6,6 +6,7 @@ import * as rb from 'react-bootstrap' import { useSettings } from '../../context/SettingsContext' import { SATS, formatSats, factorToPercentage } from '../../utils' import { FeeValues } from '../../hooks/Fees' +import { AmountSats } from '../../libs/JmWalletApi' interface FeeBreakdownProps { feeConfigValues?: FeeValues @@ -15,12 +16,13 @@ interface FeeBreakdownProps { } type FeeCardProps = { - amount: number | null + amount: AmountSats | null + feeConfigValue: number | undefined highlight: boolean subtitle?: React.ReactNode onClick?: () => void } -const FeeCard = ({ amount, highlight, subtitle, onClick }: FeeCardProps) => { +const FeeCard = ({ amount, feeConfigValue, highlight, subtitle, onClick }: FeeCardProps) => { const settings = useSettings() const { t } = useTranslation() @@ -32,13 +34,19 @@ const FeeCard = ({ amount, highlight, subtitle, onClick }: FeeCardProps) => { })} >
- {amount !== null ? ( + {feeConfigValue === undefined ? ( + t('send.fee_breakdown.placeholder_config_value_not_present') + ) : ( <> - ≤ - + {amount === null ? ( + t('send.fee_breakdown.placeholder_amount_missing_amount') + ) : ( + <> + ≤ + + + )} - ) : ( - t('send.fee_breakdown.placeholder_amount') )}
{subtitle}
@@ -60,7 +68,7 @@ const FeeBreakdown = ({ () => feeConfigValues?.max_cj_fee_rel ? `${factorToPercentage(feeConfigValues.max_cj_fee_rel)}%` - : t('send.fee_breakdown.not_set'), + : t('send.fee_breakdown.placeholder_config_value_not_present'), [feeConfigValues, t] ) @@ -78,7 +86,7 @@ const FeeBreakdown = ({ () => feeConfigValues?.max_cj_fee_abs ? `${formatSats(feeConfigValues.max_cj_fee_abs)} sats` - : t('send.fee_breakdown.not_set'), + : t('send.fee_breakdown.placeholder_config_value_not_present'), [feeConfigValues, t] ) @@ -114,6 +122,7 @@ const FeeBreakdown = ({ preset limits, you can see the maximum collaborator fees for the upcoming collaborative transaction.", - "alert_collaborator_fee_note": "This does not include regular <1>mining fees.", + "alert_collaborator_fee_note": "This value does not include regular <1>mining fees.", "absolute_limit": "Absolute fee limit", "relative_limit": "Relative fee limit", "fee_card_subtitle": "<1>{{ maxFee }} * {{ numCollaborators }}", - "not_set": "Not set", - "placeholder_amount": "Enter amount..." + "placeholder_config_value_not_present": "Undefined", + "placeholder_amount_missing_amount": "Enter amount..." }, "confirm_send_modal": { "title": "Confirm payment", From 6f8efef00e3cd61919bbb16240e0d081cdfcbf9c Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Mon, 28 Aug 2023 12:53:49 +0200 Subject: [PATCH 30/31] chore(FeeBreakdown): Remove unused `AccordionInfo` --- src/components/AccordionInfo.tsx | 33 -------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 src/components/AccordionInfo.tsx diff --git a/src/components/AccordionInfo.tsx b/src/components/AccordionInfo.tsx deleted file mode 100644 index 58629ef3b..000000000 --- a/src/components/AccordionInfo.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { PropsWithChildren, useState } from 'react' -import { useSettings } from '../context/SettingsContext' -import * as rb from 'react-bootstrap' -import Sprite from './Sprite' - -interface AccordionInfoProps { - title: string - defaultOpen?: boolean -} - -const AccordionInfo = ({ title, defaultOpen = false, children }: PropsWithChildren) => { - const settings = useSettings() - const [isOpen, setIsOpen] = useState(defaultOpen) - - return ( -
- setIsOpen((current) => !current)} - > - - {title} - - -
{children}
-
-
- ) -} - -export default AccordionInfo From b331c5875138ef6d774b826c9e75506c8b4fe7b5 Mon Sep 17 00:00:00 2001 From: httpiga <36515569+httpiga@users.noreply.github.com> Date: Mon, 28 Aug 2023 12:58:48 +0200 Subject: [PATCH 31/31] revert: temporarily print linter debug messages --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1da30b99..bacbada42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: run: npm ci # Checks - name: Lint - run: npm run lint -- --version && npm run lint + run: npm run lint # Test - name: Test run: npm test