Skip to content

Commit

Permalink
fix: show warning on missing fee config values (#674)
Browse files Browse the repository at this point in the history
* fix(send): show warning on missing fee config values

* fix(fee): allow missing fee config values to be updated

* feat(fee): open fee config modal from fee warning alert
  • Loading branch information
theborakompanioni authored Oct 12, 2023
1 parent fcf23a9 commit 5f20ee4
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 92 deletions.
43 changes: 36 additions & 7 deletions src/components/Jam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -155,18 +156,31 @@ export default function Jam({ wallet }: JamProps) {

const [alert, setAlert] = useState<SimpleAlert>()
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<Schedule | null>(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],
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -375,6 +389,15 @@ export default function Jam({ wallet }: JamProps) {
</div>
) : (
<>
{maxFeesConfigMissing && (
<rb.Alert className="slashed-zeroes" variant="danger">
{t('send.taker_error_message_max_fees_config_missing')}
&nbsp;
<rb.Alert.Link onClick={() => setShowFeeConfigModal(true)}>
{t('settings.show_fee_config')}
</rb.Alert.Link>
</rb.Alert>
)}
<rb.Fade
in={!schedulerPreconditionSummary.isFulfilled}
mountOnEnter={true}
Expand Down Expand Up @@ -480,7 +503,7 @@ export default function Jam({ wallet }: JamProps) {
})
}
}}
disabled={isSubmitting}
disabled={isOperationDisabled || isSubmitting}
/>
</rb.Form.Group>
)}
Expand All @@ -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}
/>
<rb.Form.Control.Feedback type="invalid">{errors[key]}</rb.Form.Control.Feedback>
</rb.Form.Group>
Expand All @@ -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}
>
<div className="d-flex justify-content-center align-items-center">
{t('scheduler.button_start')}
Expand All @@ -527,13 +551,18 @@ export default function Jam({ wallet }: JamProps) {
<rb.Button
variant="outline-dark"
className="border-0 mb-2 d-inline-flex align-items-center"
onClick={() => setShowingFeeConfig(true)}
onClick={() => setShowFeeConfigModal(true)}
>
<Sprite symbol="coins" width="24" height="24" className="me-1" />
{t('settings.show_fee_config')}
</rb.Button>
{showingFeeConfig && (
<FeeConfigModal show={showingFeeConfig} onHide={() => setShowingFeeConfig(false)} />
{showFeeConfigModal && (
<FeeConfigModal
show={showFeeConfigModal}
onSuccess={() => reloadFeeConfigValues()}
onHide={() => setShowFeeConfigModal(false)}
defaultActiveSectionKey={'cj_fee'}
/>
)}
</rb.Col>
</rb.Row>
Expand Down
6 changes: 5 additions & 1 deletion src/components/Send/FeeBreakdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ const FeeCard = ({ amount, feeConfigValue, highlight, subtitle, onClick }: FeeCa
const { t } = useTranslation()

return (
<rb.Card onClick={onClick} border={highlight ? (settings.theme === 'dark' ? 'light' : 'dark') : undefined}>
<rb.Card
className={onClick ? 'cursor-pointer' : undefined}
onClick={onClick}
border={highlight ? (settings.theme === 'dark' ? 'light' : 'dark') : undefined}
>
<rb.Card.Body
className={classNames('text-center py-2', {
'text-muted': !highlight,
Expand Down
47 changes: 0 additions & 47 deletions src/components/Send/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ServiceConfigContextEntry } from '../../context/ServiceConfigContext'
import { isValidNumber } from '../../utils'

export const initialNumCollaborators = (minValue: number) => {
Expand Down Expand Up @@ -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
}
75 changes: 44 additions & 31 deletions src/components/Send/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -83,15 +81,25 @@ 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<FeeConfigSectionKey>()
const [showFeeConfigModal, setShowFeeConfigModal] = useState(false)

const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([])
const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState<SimpleAlert>()

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(
Expand Down Expand Up @@ -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
Expand All @@ -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))
})
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -362,8 +367,8 @@ export default function Send({ wallet }: SendProps) {
{ ...wallet },
{
mixdepth: sourceJarIndex,
amount_sats: amountSats,
destination,
amount_sats,
counterparties,
},
)
Expand All @@ -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)
Expand Down Expand Up @@ -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 })
})
}
Expand Down Expand Up @@ -608,6 +607,20 @@ export default function Send({ wallet }: SendProps) {
)}
</>
</rb.Fade>
{maxFeesConfigMissing && (
<rb.Alert className="slashed-zeroes" variant="danger">
{t('send.taker_error_message_max_fees_config_missing')}
&nbsp;
<rb.Alert.Link
onClick={() => {
setActiveFeeConfigModalSection('cj_fee')
setShowFeeConfigModal(true)
}}
>
{t('settings.show_fee_config')}
</rb.Alert.Link>
</rb.Alert>
)}
{alert && (
<rb.Alert className="slashed-zeroes" variant={alert.variant}>
{alert.message}
Expand Down Expand Up @@ -819,7 +832,7 @@ export default function Send({ wallet }: SendProps) {
</rb.Form.Control.Feedback>
{isSweep && <>{frozenOrLockedWarning}</>}
</rb.Form.Group>
<Accordion title={t('send.sending_options')}>
<Accordion title={t('send.sending_options')} disabled={isOperationDisabled}>
<rb.Form.Group controlId="isCoinjoin" className="mb-3">
<ToggleSwitch
label={t('send.toggle_coinjoin')}
Expand Down
Loading

0 comments on commit 5f20ee4

Please sign in to comment.