diff --git a/src/components/Jam.tsx b/src/components/Jam.tsx index 2314fa221..aa148303b 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,15 @@ export default function Jam({ wallet }: JamProps) { ) : ( <> + {maxFeesConfigMissing && ( + + {t('send.taker_error_message_max_fees_config_missing')} +   + setShowFeeConfigModal(true)}> + {t('settings.show_fee_config')} + + + )} )} @@ -498,6 +521,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 +535,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 +551,18 @@ export default function Jam({ wallet }: JamProps) { setShowingFeeConfig(true)} + onClick={() => setShowFeeConfigModal(true)} > {t('settings.show_fee_config')} - {showingFeeConfig && ( - setShowingFeeConfig(false)} /> + {showFeeConfigModal && ( + reloadFeeConfigValues()} + onHide={() => setShowFeeConfigModal(false)} + defaultActiveSectionKey={'cj_fee'} + /> )} 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 ( - + { @@ -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 5db10ceb8..ad71aa1f5 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, @@ -83,6 +81,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) @@ -90,8 +93,13 @@ export default function Send({ wallet }: SendProps) { const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState() const isOperationDisabled = useMemo( - () => isCoinjoinInProgress || isMakerRunning || isRescanningInProgress || waitForUtxosToBeSpent.length > 0, - [isCoinjoinInProgress, isMakerRunning, isRescanningInProgress, waitForUtxosToBeSpent], + () => + maxFeesConfigMissing || + isCoinjoinInProgress || + isMakerRunning || + isRescanningInProgress || + waitForUtxosToBeSpent.length > 0, + [maxFeesConfigMissing, isCoinjoinInProgress, isMakerRunning, isRescanningInProgress, waitForUtxosToBeSpent], ) const [isInitializing, setIsInitializing] = useState(!isOperationDisabled) const isLoading = useMemo( @@ -239,7 +247,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 @@ -264,10 +272,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)) }) @@ -299,18 +307,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 @@ -329,13 +336,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) @@ -350,7 +355,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) @@ -362,8 +367,8 @@ export default function Send({ wallet }: SendProps) { { ...wallet }, { mixdepth: sourceJarIndex, + amount_sats: amountSats, destination, - amount_sats, counterparties, }, ) @@ -374,14 +379,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) @@ -417,6 +415,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 }) }) } @@ -608,6 +607,20 @@ 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 && ( {alert.message} @@ -819,7 +832,7 @@ export default function Send({ wallet }: SendProps) { {isSweep && <>{frozenOrLockedWarning}} - + { +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], diff --git a/src/hooks/Fees.ts b/src/hooks/Fees.ts index cdc43e1f7..e4fe61a2d 100644 --- a/src/hooks/Fees.ts +++ b/src/hooks/Fees.ts @@ -64,6 +64,8 @@ export const useFeeConfigValues = (): [FeeValues | undefined, () => 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) })