From 1123af92f2be03830c5e27dce62a9474bd4d40ec Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Tue, 10 Oct 2023 22:50:43 +0200 Subject: [PATCH 1/6] fix(send): show warning on missing fee config values --- src/components/Send/index.tsx | 34 ++++++++++++++++++++++------------ src/hooks/Fees.ts | 2 ++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 1898219b9..436625c35 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -90,8 +90,13 @@ export default function Send({ wallet }: SendProps) { const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState() const isOperationDisabled = useMemo( - () => isCoinjoinInProgress || isMakerRunning || isRescanningInProgress || waitForUtxosToBeSpent.length > 0, - [isCoinjoinInProgress, isMakerRunning, isRescanningInProgress, waitForUtxosToBeSpent], + () => + !feeConfigValues || + isCoinjoinInProgress || + isMakerRunning || + isRescanningInProgress || + waitForUtxosToBeSpent.length > 0, + [feeConfigValues, isCoinjoinInProgress, isMakerRunning, isRescanningInProgress, waitForUtxosToBeSpent], ) const [isInitializing, setIsInitializing] = useState(!isOperationDisabled) const isLoading = useMemo( @@ -255,7 +260,7 @@ export default function Send({ wallet }: SendProps) { setAlert(undefined) setIsInitializing(true) - // reloading service info is important, is it must be known as soon as possible + // reloading service info is important, as it must be known as soon as possible // if the operation is even allowed, i.e. if no other service is running const loadingServiceInfo = reloadServiceInfo({ signal: abortCtrl.signal }).catch((err) => { if (abortCtrl.signal.aborted) return @@ -315,18 +320,17 @@ export default function Send({ wallet }: SendProps) { [walletInfo, destination], ) - const sendPayment = async ( - sourceJarIndex: JarIndex, - destination: Api.BitcoinAddress, - amount_sats: Api.AmountSats, - ) => { + const sendPayment = async (sourceJarIndex: JarIndex, destination: Api.BitcoinAddress, amountSats: Api.AmountSats) => { setAlert(undefined) setPaymentSuccessfulInfoAlert(undefined) setIsSending(true) let success = false try { - const res = await Api.postDirectSend({ ...wallet }, { mixdepth: sourceJarIndex, destination, amount_sats }) + const res = await Api.postDirectSend( + { ...wallet }, + { mixdepth: sourceJarIndex, amount_sats: amountSats, destination }, + ) if (res.ok) { // TODO: add type for json response @@ -366,7 +370,7 @@ export default function Send({ wallet }: SendProps) { const startCoinjoin = async ( sourceJarIndex: JarIndex, destination: Api.BitcoinAddress, - amount_sats: Api.AmountSats, + amountSats: Api.AmountSats, counterparties: number, ) => { setAlert(undefined) @@ -378,8 +382,8 @@ export default function Send({ wallet }: SendProps) { { ...wallet }, { mixdepth: sourceJarIndex, + amount_sats: amountSats, destination, - amount_sats, counterparties, }, ) @@ -433,6 +437,7 @@ export default function Send({ wallet }: SendProps) { const abortCtrl = new AbortController() return Api.getTakerStop({ ...wallet, signal: abortCtrl.signal }).catch((err) => { + if (abortCtrl.signal.aborted) return setAlert({ variant: 'danger', message: err.message }) }) } @@ -623,6 +628,11 @@ export default function Send({ wallet }: SendProps) { )} + {!isLoading && !feeConfigValues && ( + + {t('send.taker_error_message_max_fees_config_missing')} + + )} {alert && ( {alert.message} @@ -834,7 +844,7 @@ export default function Send({ wallet }: SendProps) { {isSweep && <>{frozenOrLockedWarning}} - + void] => { loadFeeConfigValues(abortCtrl.signal) .then((val) => setValues(val)) .catch((e) => { + if (abortCtrl.signal.aborted) return + console.log('Unable lo load fee config: ', e) setValues(undefined) }) From 06ab7746a5dcfd332cf606467774eca4eb31d737 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 11 Oct 2023 22:05:17 +0200 Subject: [PATCH 2/6] fix(fee): allow missing fee config values to be updated --- src/components/Send/index.tsx | 15 ++++++++++----- src/context/ServiceConfigContext.tsx | 26 ++++++++++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 436625c35..36d4cec28 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -83,6 +83,11 @@ export default function Send({ wallet }: SendProps) { const [destinationIsReusedAddress, setDestinationIsReusedAddress] = useState(false) const [feeConfigValues, reloadFeeConfigValues] = useFeeConfigValues() + const maxFeesConfigMissing = useMemo( + () => + feeConfigValues && (feeConfigValues.max_cj_fee_abs === undefined || feeConfigValues.max_cj_fee_rel === undefined), + [feeConfigValues], + ) const [activeFeeConfigModalSection, setActiveFeeConfigModalSection] = useState() const [showFeeConfigModal, setShowFeeConfigModal] = useState(false) @@ -91,12 +96,12 @@ export default function Send({ wallet }: SendProps) { const isOperationDisabled = useMemo( () => - !feeConfigValues || + maxFeesConfigMissing || isCoinjoinInProgress || isMakerRunning || isRescanningInProgress || waitForUtxosToBeSpent.length > 0, - [feeConfigValues, isCoinjoinInProgress, isMakerRunning, isRescanningInProgress, waitForUtxosToBeSpent], + [maxFeesConfigMissing, isCoinjoinInProgress, isMakerRunning, isRescanningInProgress, waitForUtxosToBeSpent], ) const [isInitializing, setIsInitializing] = useState(!isOperationDisabled) const isLoading = useMemo( @@ -285,10 +290,10 @@ export default function Send({ wallet }: SendProps) { signal: abortCtrl.signal, key: { section: 'POLICY', field: 'minimum_makers' }, }) - .then((data) => { + .then((data) => (data.value !== null ? parseInt(data.value, 10) : JM_MINIMUM_MAKERS_DEFAULT)) + .then((minimumMakers) => { if (abortCtrl.signal.aborted) return - const minimumMakers = parseInt(data.value, 10) setMinNumCollaborators(minimumMakers) setNumCollaborators(initialNumCollaborators(minimumMakers)) }) @@ -628,7 +633,7 @@ export default function Send({ wallet }: SendProps) { )} - {!isLoading && !feeConfigValues && ( + {maxFeesConfigMissing && ( {t('send.taker_error_message_max_fees_config_missing')} diff --git a/src/context/ServiceConfigContext.tsx b/src/context/ServiceConfigContext.tsx index f06775cd5..8bf158353 100644 --- a/src/context/ServiceConfigContext.tsx +++ b/src/context/ServiceConfigContext.tsx @@ -23,6 +23,11 @@ export interface ServiceConfigUpdate { value: string } +export interface ServiceConfigValue { + key: ConfigKey + value: string | null +} + type LoadConfigValueProps = { signal?: AbortSignal key: ConfigKey @@ -40,7 +45,7 @@ type UpdateConfigValuesProps = { wallet?: MinimalWalletContext } -const configReducer = (state: ServiceConfig, obj: ServiceConfigUpdate): ServiceConfig => { +const configReducer = (state: ServiceConfig, obj: ServiceConfigValue): ServiceConfig => { const data = { ...state } data[obj.key.section] = { ...data[obj.key.section], [obj.key.field]: obj.value } return data @@ -55,14 +60,23 @@ const fetchConfigValues = async ({ wallet: MinimalWalletContext configKeys: ConfigKey[] }) => { - const fetches: Promise[] = configKeys.map((configKey) => { + const fetches: Promise[] = configKeys.map((configKey) => { return Api.postConfigGet({ ...wallet, signal }, { section: configKey.section, field: configKey.field }) .then((res) => (res.ok ? res.json() : Api.Helper.throwError(res))) .then((data: JmConfigData) => { return { key: configKey, value: data.configvalue, - } as ServiceConfigUpdate + } as ServiceConfigValue + }) + .catch((e) => { + if (e instanceof Api.JmApiError && e.response.status === 409) { + return { + key: configKey, + value: null, + } as ServiceConfigValue + } + throw e }) }) @@ -95,7 +109,7 @@ const pushConfigValues = async ({ } export interface ServiceConfigContextEntry { - loadConfigValueIfAbsent: (props: LoadConfigValueProps) => Promise + loadConfigValueIfAbsent: (props: LoadConfigValueProps) => Promise refreshConfigValues: (props: RefreshConfigValuesProps) => Promise updateConfigValues: (props: UpdateConfigValuesProps) => Promise } @@ -138,7 +152,7 @@ const ServiceConfigProvider = ({ children }: PropsWithChildren<{}>) => { return { key, value: serviceConfig.current[key.section][key.field], - } as ServiceConfigUpdate + } as ServiceConfigValue } } @@ -146,7 +160,7 @@ const ServiceConfigProvider = ({ children }: PropsWithChildren<{}>) => { return { key, value: conf[key.section][key.field], - } as ServiceConfigUpdate + } as ServiceConfigValue }) }, [refreshConfigValues], From b7dea3f65793395c4325bd432b3402bc06224d0b Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 11 Oct 2023 22:24:18 +0200 Subject: [PATCH 3/6] chore(send): remove unnecessary helper methods --- src/components/Send/helpers.ts | 47 ---------------------------------- src/components/Send/index.tsx | 23 +++++------------ 2 files changed, 6 insertions(+), 64 deletions(-) diff --git a/src/components/Send/helpers.ts b/src/components/Send/helpers.ts index 2b9868672..501593684 100644 --- a/src/components/Send/helpers.ts +++ b/src/components/Send/helpers.ts @@ -1,4 +1,3 @@ -import { ServiceConfigContextEntry } from '../../context/ServiceConfigContext' import { isValidNumber } from '../../utils' export const initialNumCollaborators = (minValue: number) => { @@ -28,49 +27,3 @@ export const isValidAmount = (candidate: number | null, isSweep: boolean) => { export const isValidNumCollaborators = (candidate: number | null, minNumCollaborators: number) => { return candidate !== null && isValidNumber(candidate) && candidate >= minNumCollaborators && candidate <= 99 } - -export const enhanceDirectPaymentErrorMessageIfNecessary = async ( - httpStatus: number, - errorMessage: string, - onBadRequest: (errorMessage: string) => string, -) => { - const tryEnhanceMessage = httpStatus === 400 - if (tryEnhanceMessage) { - return onBadRequest(errorMessage) - } - - return errorMessage -} - -export const enhanceTakerErrorMessageIfNecessary = async ( - loadConfigValue: ServiceConfigContextEntry['loadConfigValueIfAbsent'], - httpStatus: number, - errorMessage: string, - onMaxFeeSettingsMissing: (errorMessage: string) => string, -) => { - const tryEnhanceMessage = httpStatus === 409 - if (tryEnhanceMessage) { - const abortCtrl = new AbortController() - - const configExists = (section: string, field: string) => - loadConfigValue({ - signal: abortCtrl.signal, - key: { section, field }, - }) - .then((val) => val.value !== null) - .catch(() => false) - - const maxFeeSettingsPresent = await Promise.all([ - configExists('POLICY', 'max_cj_fee_rel'), - configExists('POLICY', 'max_cj_fee_abs'), - ]) - .then((arr) => arr.every((e) => e)) - .catch(() => false) - - if (!maxFeeSettingsPresent) { - return onMaxFeeSettingsMissing(errorMessage) - } - } - - return errorMessage -} diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 36d4cec28..de9eb8fe1 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -28,8 +28,6 @@ import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/config' import { SATS, formatSats, isValidNumber, scrollToTop } from '../../utils' import { - enhanceDirectPaymentErrorMessageIfNecessary, - enhanceTakerErrorMessageIfNecessary, initialNumCollaborators, isValidAddress, isValidAmount, @@ -354,13 +352,11 @@ export default function Send({ wallet }: SendProps) { setWaitForUtxosToBeSpent(inputs.map((it: any) => it.outpoint)) success = true } else { - const message = await Api.Helper.extractErrorMessage(res) - const displayMessage = await enhanceDirectPaymentErrorMessageIfNecessary( - res.status, - message, - (errorMessage) => `${errorMessage} ${t('send.direct_payment_error_message_bad_request')}`, - ) - setAlert({ variant: 'danger', message: displayMessage }) + const errorMessage = await Api.Helper.extractErrorMessage(res) + const message = `${errorMessage} ${ + res.status === 400 ? t('send.direct_payment_error_message_bad_request') : '' + }` + setAlert({ variant: 'danger', message }) } setIsSending(false) @@ -399,14 +395,7 @@ export default function Send({ wallet }: SendProps) { success = true } else { const message = await Api.Helper.extractErrorMessage(res) - const displayMessage = await enhanceTakerErrorMessageIfNecessary( - loadConfigValue, - res.status, - message, - (errorMessage) => `${errorMessage} ${t('send.taker_error_message_max_fees_config_missing')}`, - ) - - setAlert({ variant: 'danger', message: displayMessage }) + setAlert({ variant: 'danger', message }) } setIsSending(false) From 1c03c0ab5a1d50fc6658e0a6697ee72bbf63716c Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 12 Oct 2023 15:43:22 +0200 Subject: [PATCH 4/6] fix(jam): show warning on missing fee config values --- src/components/Jam.tsx | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/components/Jam.tsx b/src/components/Jam.tsx index 2314fa221..0fe448d51 100644 --- a/src/components/Jam.tsx +++ b/src/components/Jam.tsx @@ -17,6 +17,7 @@ import ScheduleProgress from './ScheduleProgress' import FeeConfigModal from './settings/FeeConfigModal' import styles from './Jam.module.css' +import { useFeeConfigValues } from '../hooks/Fees' const DEST_ADDRESS_COUNT_PROD = 3 const DEST_ADDRESS_COUNT_TEST = 1 @@ -155,18 +156,31 @@ export default function Jam({ wallet }: JamProps) { const [alert, setAlert] = useState() const [isLoading, setIsLoading] = useState(true) - const [showingFeeConfig, setShowingFeeConfig] = useState(false) + const [showFeeConfigModal, setShowFeeConfigModal] = useState(false) const [isWaitingSchedulerStart, setIsWaitingSchedulerStart] = useState(false) const [isWaitingSchedulerStop, setIsWaitingSchedulerStop] = useState(false) const [currentSchedule, setCurrentSchedule] = useState(null) const [lastKnownSchedule, resetLastKnownSchedule] = useLatestTruthy(currentSchedule ?? undefined) const [isShowSuccessMessage, setIsShowSuccessMessage] = useState(false) + const [feeConfigValues, reloadFeeConfigValues] = useFeeConfigValues() + const maxFeesConfigMissing = useMemo( + () => + feeConfigValues && (feeConfigValues.max_cj_fee_abs === undefined || feeConfigValues.max_cj_fee_rel === undefined), + [feeConfigValues], + ) + + const isRescanningInProgress = useMemo(() => serviceInfo?.rescanning === true, [serviceInfo]) const collaborativeOperationRunning = useMemo( () => serviceInfo?.coinjoinInProgress || serviceInfo?.makerRunning || false, [serviceInfo], ) + const isOperationDisabled = useMemo( + () => maxFeesConfigMissing || collaborativeOperationRunning || isRescanningInProgress, + [maxFeesConfigMissing, collaborativeOperationRunning, isRescanningInProgress], + ) + const schedulerPreconditionSummary = useMemo( () => buildCoinjoinRequirementSummary(walletInfo?.data.utxos.utxos || []), [walletInfo], @@ -262,7 +276,7 @@ export default function Jam({ wallet }: JamProps) { }, [currentSchedule, lastKnownSchedule, isWaitingSchedulerStop, walletInfo]) const startSchedule = async (values: FormikValues) => { - if (isLoading || collaborativeOperationRunning || serviceInfo?.rescanning === true) { + if (isLoading || collaborativeOperationRunning || isOperationDisabled) { return } @@ -375,6 +389,11 @@ export default function Jam({ wallet }: JamProps) { ) : ( <> + {maxFeesConfigMissing && ( + + {t('send.taker_error_message_max_fees_config_missing')} + + )} )} @@ -498,6 +517,7 @@ export default function Jam({ wallet }: JamProps) { onBlur={handleBlur} isInvalid={touched[key] && !!errors[key]} className={`${styles.input} slashed-zeroes`} + disabled={isOperationDisabled || isSubmitting} /> {errors[key]} @@ -511,7 +531,7 @@ export default function Jam({ wallet }: JamProps) { variant="dark" size="lg" type="submit" - disabled={isSubmitting || !isValid || serviceInfo?.rescanning === true} + disabled={isOperationDisabled || isSubmitting || !isValid} >
{t('scheduler.button_start')} @@ -527,13 +547,17 @@ export default function Jam({ wallet }: JamProps) { setShowingFeeConfig(true)} + onClick={() => setShowFeeConfigModal(true)} > {t('settings.show_fee_config')} - {showingFeeConfig && ( - setShowingFeeConfig(false)} /> + {showFeeConfigModal && ( + reloadFeeConfigValues()} + onHide={() => setShowFeeConfigModal(false)} + /> )} From 7f5c057f96bb71aa8bb6e1ae35a45c6fb178809c Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 12 Oct 2023 15:53:10 +0200 Subject: [PATCH 5/6] feat(fee): open fee config modal from fee warning alert --- src/components/Jam.tsx | 5 +++++ src/components/Send/index.tsx | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/components/Jam.tsx b/src/components/Jam.tsx index 0fe448d51..aa148303b 100644 --- a/src/components/Jam.tsx +++ b/src/components/Jam.tsx @@ -392,6 +392,10 @@ export default function Jam({ wallet }: JamProps) { {maxFeesConfigMissing && ( {t('send.taker_error_message_max_fees_config_missing')} +   + setShowFeeConfigModal(true)}> + {t('settings.show_fee_config')} + )} reloadFeeConfigValues()} onHide={() => setShowFeeConfigModal(false)} + defaultActiveSectionKey={'cj_fee'} /> )} diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index de9eb8fe1..7ba8a5d28 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -625,6 +625,15 @@ export default function Send({ wallet }: SendProps) { {maxFeesConfigMissing && ( {t('send.taker_error_message_max_fees_config_missing')} +   + { + setActiveFeeConfigModalSection('cj_fee') + setShowFeeConfigModal(true) + }} + > + {t('settings.show_fee_config')} + )} {alert && ( From 5b17af3c12f097bdc0fc4c7df50de0cc025ea66d Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 12 Oct 2023 16:18:02 +0200 Subject: [PATCH 6/6] ui(send): show cursor as pointer over fee breakdown cards --- 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 5dce93137..a44242806 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -27,7 +27,11 @@ const FeeCard = ({ amount, feeConfigValue, highlight, subtitle, onClick }: FeeCa const { t } = useTranslation() return ( - +