Skip to content

Commit

Permalink
fix: precondition for collaborative transactions (#485)
Browse files Browse the repository at this point in the history
* fix: min confs of preconditions for all utxos instead of single

* fix: i18n phrases of collaborative transaction preconditions

* wip: use new scheduler preconditions in Jam component

* fix: only report utxos without retries of no retry is left within jar

* feat(regtest): set taker_utxo_retries and maker_timeout_sec config vars for improved testability

* test(preconditions): add test for CoinjoinRequirements

* fix: enable starting scheduler even if preconditions are not fulfilled

* review: fix wording

Co-authored-by: Gigi <109058+dergigi@users.noreply.github.com>
  • Loading branch information
theborakompanioni and dergigi committed Sep 15, 2022
1 parent 09c1d30 commit db29235
Show file tree
Hide file tree
Showing 11 changed files with 504 additions and 284 deletions.
2 changes: 2 additions & 0 deletions docker/regtest/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ services:
jm_rpc_password: joinmarket2
jm_directory_nodes: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file}
jm_socks5: "false" # will _not_ connect to local irc over tor
jm_taker_utxo_retries: 1 # easier testing of commitment failures on regtest; default is 3
maker_timeout_sec: 10 # easier testing of maker timeouts on regtest (and "Stall Monitor" retries); default is 60
expose:
- 80 # nginx
- 62601 # obwatch
Expand Down
100 changes: 100 additions & 0 deletions src/components/CoinjoinPreconditionViolationAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { forwardRef } from 'react'
import * as rb from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSettings } from '../context/SettingsContext'
import { CoinjoinRequirementSummary } from '../hooks/CoinjoinRequirements'
import { jarInitial } from './jars/Jar'
import { shortenStringMiddle } from '../utils'
import Sprite from './Sprite'
import Balance from './Balance'

interface CoinjoinPreconditionViolationAlertProps {
summary: CoinjoinRequirementSummary
i18nPrefix?: string
}

export const CoinjoinPreconditionViolationAlert = forwardRef(
({ summary, i18nPrefix = '' }: CoinjoinPreconditionViolationAlertProps, ref: React.Ref<HTMLDivElement>) => {
const { t } = useTranslation()
const settings = useSettings()

if (summary.isFulfilled) return <></>

if (summary.numberOfMissingUtxos > 0) {
return (
<rb.Alert variant="warning" ref={ref}>
{t(`${i18nPrefix}hint_missing_utxos`, {
minConfirmations: summary.options.minConfirmations,
})}
</rb.Alert>
)
}

if (summary.numberOfMissingConfirmations > 0) {
return (
<rb.Alert variant="warning" ref={ref}>
{t(`${i18nPrefix}hint_missing_confirmations`, {
minConfirmations: summary.options.minConfirmations,
amountOfMissingConfirmations: summary.numberOfMissingConfirmations,
})}
</rb.Alert>
)
}

const utxosViolatingRetriesLeft = summary.violations
.map((it) => it.utxosViolatingRetriesLeft)
.reduce((acc, utxos) => acc.concat(utxos), [])

if (utxosViolatingRetriesLeft.length > 0) {
return (
<rb.Alert variant="warning" ref={ref}>
<>
<Trans i18nKey={`${i18nPrefix}hint_missing_retries`}>
You tried too many times. See
<a
href="https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/v0.9.7/docs/SOURCING-COMMITMENTS.md"
target="_blank"
rel="noopener noreferrer"
>
the docs
</a>{' '}
for more info.
</Trans>
<br />
<br />
<Trans i18nKey={`${i18nPrefix}hint_missing_retries_detail`} count={utxosViolatingRetriesLeft.length}>
Following utxos have been used unsuccessfully too many times:
<ul className="mt-2 mb-0 ps-2">
{utxosViolatingRetriesLeft.map((utxo, index) => (
<li key={index} className="mb-2 slashed-zeroes small" style={{ display: 'inline-flex' }}>
<span className="pe-1" style={{ display: 'inline-flex' }}>
<Sprite symbol="jar-closed-fill-50" width="20" height="20" />
<span className="slashed-zeroes">
<strong>{jarInitial(utxo.mixdepth)}</strong>
</span>
:
</span>
<div>
<span>{utxo.address}</span>
&nbsp;(
<Balance
valueString={`${utxo.value}`}
convertToUnit={settings.unit}
showBalance={settings.showBalance}
/>
)
<br />
<small>{shortenStringMiddle(utxo.utxo, 32)}</small>
</div>
</li>
))}
</ul>
</Trans>
</>
</rb.Alert>
)
}

return <></>
}
)
79 changes: 14 additions & 65 deletions src/components/Jam.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import * as rb from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { Formik, useFormikContext } from 'formik'
import * as Api from '../libs/JmWalletApi'
import { useSettings } from '../context/SettingsContext'
import { useServiceInfo, useReloadServiceInfo } from '../context/ServiceInfoContext'
import { useCurrentWallet, useCurrentWalletInfo, useReloadCurrentWalletInfo } from '../context/WalletContext'
import { isDebugFeatureEnabled } from '../constants/debugFeatures'
import { COINJOIN_PRECONDITIONS, useCoinjoinPreconditionSummary } from '../hooks/CoinjoinPrecondition'
import { buildCoinjoinRequirementSummary } from '../hooks/CoinjoinRequirements'
import { CoinjoinPreconditionViolationAlert } from './CoinjoinPreconditionViolationAlert'
import PageTitle from './PageTitle'
import ToggleSwitch from './ToggleSwitch'
import Sprite from './Sprite'
Expand All @@ -21,8 +22,6 @@ import styles from './Jam.module.css'
// Length of this array must be 3 for now.
const INTERNAL_DEST_ACCOUNTS = [0, 1, 2]

const SCHEDULER_START_ACCOUNT = 0

const ValuesListener = ({ handler }) => {
const { values } = useFormikContext()

Expand All @@ -48,16 +47,9 @@ export default function Jam() {
const [isLoading, setIsLoading] = useState(true)
const [collaborativeOperationRunning, setCollaborativeOperationRunning] = useState(false)

const startJarUtxos = useMemo(() => {
if (!walletInfo) return null

return walletInfo.data.utxos.utxos.filter((it) => it.mixdepth === SCHEDULER_START_ACCOUNT)
}, [walletInfo])

const schedulerPreconditionSummary = useCoinjoinPreconditionSummary(startJarUtxos || [])
const isSchedulerPreconditionsFulfilled = useMemo(
() => schedulerPreconditionSummary.isFulfilled,
[schedulerPreconditionSummary]
const schedulerPreconditionSummary = useMemo(
() => buildCoinjoinRequirementSummary(walletInfo?.data.utxos.utxos || []),
[walletInfo]
)

// Returns one fresh address for each requested mixdepth.
Expand Down Expand Up @@ -135,7 +127,7 @@ export default function Jam() {
}, [serviceInfo])

const startSchedule = async (values) => {
if (isLoading || collaborativeOperationRunning || !isSchedulerPreconditionsFulfilled) {
if (isLoading || collaborativeOperationRunning) {
return
}

Expand Down Expand Up @@ -219,54 +211,15 @@ export default function Jam() {
</rb.Alert>
)}
<rb.Fade
in={!collaborativeOperationRunning && !isSchedulerPreconditionsFulfilled}
in={!collaborativeOperationRunning && !schedulerPreconditionSummary.isFulfilled}
mountOnEnter={true}
unmountOnExit={true}
className="mb-4"
>
<rb.Alert variant="warning" className="mb-4">
<>
{schedulerPreconditionSummary.numberOfMissingUtxos > 0 ? (
<Trans i18nKey="scheduler.precondition.hint_missing_utxos">
To run the scheduler you need at least one UTXO with{' '}
<strong>{{ minConfirmations: COINJOIN_PRECONDITIONS.MIN_CONFIRMATIONS_OF_SINGLE_UTXO }}</strong>{' '}
confirmations. Fund your wallet and wait for{' '}
<strong>{{ minConfirmations: COINJOIN_PRECONDITIONS.MIN_CONFIRMATIONS_OF_SINGLE_UTXO }}</strong>{' '}
blocks.
</Trans>
) : schedulerPreconditionSummary.amountOfMissingConfirmations > 0 ? (
<Trans i18nKey="scheduler.precondition.hint_missing_confirmations">
The scheduler requires one of your UTXOs to have{' '}
<strong>
{{
/* this comment is a hack for "prettier" and prevents the removal of "{' '}"
(which is essential for parameterized translations to work). */
minConfirmations: COINJOIN_PRECONDITIONS.MIN_CONFIRMATIONS_OF_SINGLE_UTXO,
}}
</strong>{' '}
or more confirmations. Wait for{' '}
<strong>
{{ amountOfMissingConfirmations: schedulerPreconditionSummary.amountOfMissingConfirmations }}
</strong>{' '}
more block(s).
</Trans>
) : (
schedulerPreconditionSummary.amountOfMissingOverallRetries > 0 && (
<Trans i18nKey="scheduler.precondition.hint_missing_overall_retries">
You've tried running the scheduler unsuccessfully too many times in a row. For security reasons,
you need a fresh UTXO to try again. See{' '}
<a
href="https://github.com/JoinMarket-Org/joinmarket/wiki/Sourcing-commitments-for-joins#sourcing-external-commitments"
target="_blank"
rel="noopener noreferrer"
>
the docs
</a>{' '}
for more information.
</Trans>
)
)}
</>
</rb.Alert>
<CoinjoinPreconditionViolationAlert
summary={schedulerPreconditionSummary}
i18nPrefix="scheduler.precondition."
/>
</rb.Fade>
{!collaborativeOperationRunning && walletInfo && (
<>
Expand Down Expand Up @@ -421,11 +374,7 @@ export default function Jam() {
className={styles.submit}
variant="dark"
type="submit"
disabled={
isSubmitting ||
isLoading ||
(!collaborativeOperationRunning && (!isValid || !isSchedulerPreconditionsFulfilled))
}
disabled={isSubmitting || isLoading || (!collaborativeOperationRunning && !isValid)}
>
<div className="d-flex justify-content-center align-items-center">
{collaborativeOperationRunning ? t('scheduler.button_stop') : t('scheduler.button_start')}
Expand Down
63 changes: 12 additions & 51 deletions src/components/Send.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import { useReloadCurrentWalletInfo, useCurrentWallet, useCurrentWalletInfo } fr
import { useServiceInfo, useReloadServiceInfo } from '../context/ServiceInfoContext'
import { useLoadConfigValue } from '../context/ServiceConfigContext'
import { useSettings } from '../context/SettingsContext'
import { COINJOIN_PRECONDITIONS, useCoinjoinPreconditionSummary } from '../hooks/CoinjoinPrecondition'
import { buildCoinjoinRequirementSummary } from '../hooks/CoinjoinRequirements'

import * as Api from '../libs/JmWalletApi'
import { SATS, formatBtc, formatSats } from '../utils'
import { routes } from '../constants/routes'
import styles from './Send.module.css'
import { ConfirmModal } from './Modal'
import { CoinjoinPreconditionViolationAlert } from './CoinjoinPreconditionViolationAlert'
import { jarInitial, jarName } from './jars/Jar'

const IS_COINJOIN_DEFAULT_VAL = true
Expand Down Expand Up @@ -213,54 +214,6 @@ function SweepAccordionToggle({ eventKey }) {
)
}

function CoinjoinPreconditionFailedAlert({ coinjoinPreconditionSummary }) {
return (
<rb.Alert variant="warning" className="mb-4">
<>
{coinjoinPreconditionSummary.numberOfMissingUtxos > 0 ? (
<Trans i18nKey="send.coinjoin_precondition.hint_missing_utxos">
To execute a collaborative transaction you need at least one UTXO with{' '}
<strong>{{ minConfirmations: COINJOIN_PRECONDITIONS.MIN_CONFIRMATIONS_OF_SINGLE_UTXO }}</strong>{' '}
confirmations in the source jar. Select another jar to send from or fund this jar and wait for{' '}
<strong>{{ minConfirmations: COINJOIN_PRECONDITIONS.MIN_CONFIRMATIONS_OF_SINGLE_UTXO }}</strong> blocks.
</Trans>
) : coinjoinPreconditionSummary.amountOfMissingConfirmations > 0 ? (
<Trans i18nKey="send.coinjoin_precondition.hint_missing_confirmations">
A collaborative transaction requires one of your UTXOs to have{' '}
<strong>
{{
/* this comment is a hack for "prettier" and prevents the removal of "{' '}"
(which is essential for parameterized translations to work). */
minConfirmations: COINJOIN_PRECONDITIONS.MIN_CONFIRMATIONS_OF_SINGLE_UTXO,
}}
</strong>{' '}
or more confirmations. Select another jar to send from or wait for{' '}
<strong>
{{ amountOfMissingConfirmations: coinjoinPreconditionSummary.amountOfMissingConfirmations }}
</strong>{' '}
more block(s).
</Trans>
) : (
coinjoinPreconditionSummary.amountOfMissingOverallRetries > 0 && (
<Trans i18nKey="send.coinjoin_precondition.hint_missing_overall_retries">
You've tried executing a collaborative transaction from this jar unsuccessfully too many times in a row.
For security reasons, you need a fresh UTXO to try again. See{' '}
<a
href="https://github.com/JoinMarket-Org/joinmarket/wiki/Sourcing-commitments-for-joins#sourcing-external-commitments"
target="_blank"
rel="noopener noreferrer"
>
the docs
</a>{' '}
for more information.
</Trans>
)
)}
</>
</rb.Alert>
)
}

export default function Send() {
const { t } = useTranslation()
const wallet = useCurrentWallet()
Expand Down Expand Up @@ -324,7 +277,10 @@ export default function Send() {
return walletInfo.data.utxos.utxos.filter((it) => it.mixdepth === account)
}, [walletInfo, account])

const coinjoinPreconditionSummary = useCoinjoinPreconditionSummary(sourceJarUtxos || [])
const coinjoinPreconditionSummary = useMemo(
() => buildCoinjoinRequirementSummary(sourceJarUtxos || []),
[sourceJarUtxos]
)

useEffect(() => {
if (
Expand Down Expand Up @@ -717,7 +673,12 @@ export default function Send() {
)}

{!isLoading && !isOperationDisabled && isCoinjoin && !coinjoinPreconditionSummary.isFulfilled && (
<CoinjoinPreconditionFailedAlert coinjoinPreconditionSummary={coinjoinPreconditionSummary} />
<div className="mb-4">
<CoinjoinPreconditionViolationAlert
summary={coinjoinPreconditionSummary}
i18nPrefix="send.coinjoin_precondition."
/>
</div>
)}

{!isLoading && walletInfo && (
Expand Down

0 comments on commit db29235

Please sign in to comment.