feat: abort collaborative transaction (#497)

* feat(taker): ability to abort single collaborative transaction

* feat(taker): show confirm modal before aborting collaborative transaction

* fix: disable abort button if abort confirm modal is already shown

* fix: let normal init process reload wallet and service info

* doc(send): fix typo in comment

Co-authored-by: Gigi <109058+dergigi@users.noreply.github.com>
This commit is contained in:
Thebora Kompanioni 2022-09-16 10:15:08 +02:00 committed by GitHub
parent db292356f5
commit 80e40ff510
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 74 additions and 21 deletions

View file

@ -174,7 +174,7 @@ export default function Jam() {
setIsLoading(true)
const abortCtrl = new AbortController()
return Api.getSchedulerStop({ signal: abortCtrl.signal, walletName: wallet.name, token: wallet.token })
return Api.getTakerStop({ signal: abortCtrl.signal, walletName: wallet.name, token: wallet.token })
.then((res) => (res.ok ? true : Api.Helper.throwError(res, t('scheduler.error_stopping_schedule_failed'))))
.then((_) => setCollaborativeOperationRunning(false))
.then((_) =>

View file

@ -252,7 +252,8 @@ export default function Send() {
[isInitializing, waitForUtxosToBeSpent]
)
const [showConfirmInputsModal, setShowConfirmInputsModal] = useState(false)
const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false)
const [showConfirmSendModal, setShowConfirmSendModal] = useState(false)
const submitButtonRef = useRef(null)
useEffect(() => {
@ -498,6 +499,34 @@ export default function Send() {
return success
}
useEffect(() => {
// hide the abort modal, if a user wants to abort a running transaction,
// but the transaction failed or succeeded in the meantime
if (showConfirmAbortModal && !isCoinjoinInProgress) {
setShowConfirmAbortModal(false)
}
}, [isCoinjoinInProgress, showConfirmAbortModal])
const abortCoinjoin = async () => {
if (!isCoinjoinInProgress) {
setShowConfirmAbortModal(false)
return
}
if (!showConfirmAbortModal) {
setShowConfirmAbortModal(true)
return
}
setShowConfirmAbortModal(false)
setAlert(null)
const abortCtrl = new AbortController()
return Api.getTakerStop({ signal: abortCtrl.signal, walletName: wallet.name, token: wallet.token }).catch((err) => {
setAlert({ variant: 'danger', message: err.message })
})
}
const onSubmit = async (e) => {
e.preventDefault()
@ -509,12 +538,12 @@ export default function Send() {
const isValid = formIsValid
if (isValid) {
if (!showConfirmInputsModal) {
setShowConfirmInputsModal(true)
if (!showConfirmSendModal) {
setShowConfirmSendModal(true)
return
}
setShowConfirmInputsModal(false)
setShowConfirmSendModal(false)
const counterparties = parseInt(numCollaborators, 10)
@ -652,8 +681,17 @@ export default function Send() {
</Link>
)}
{isCoinjoinInProgress && (
<rb.Alert variant="info" className="mb-4">
<rb.Alert variant="info" className="mb-4 d-flex align-items-center">
{t('send.text_coinjoin_already_running')}
<rb.Button
variant={'outline-light'}
className="ms-auto"
disabled={showConfirmAbortModal}
onClick={() => abortCoinjoin()}
>
{t('global.abort')}
</rb.Button>
</rb.Alert>
)}
</>
@ -893,10 +931,20 @@ export default function Send() {
t('send.button_send_without_improved_privacy')
)}
</rb.Button>
<ConfirmModal
isShown={showConfirmInputsModal}
title={t('send.confirm_modal.title')}
onCancel={() => setShowConfirmInputsModal(false)}
isShown={showConfirmAbortModal}
title={t('send.confirm_abort_modal.title')}
onCancel={() => setShowConfirmAbortModal(false)}
onConfirm={() => abortCoinjoin()}
>
{t('send.confirm_abort_modal.text_body')}
</ConfirmModal>
<ConfirmModal
isShown={showConfirmSendModal}
title={t('send.confirm_send_modal.title')}
onCancel={() => setShowConfirmSendModal(false)}
onConfirm={() => {
submitButtonRef.current?.click()
}}
@ -905,23 +953,23 @@ export default function Send() {
<rb.Row className="mt-2 mb-3">
<rb.Col xs={12} className="text-center">
{isCoinjoin ? (
<strong className="text-success">{t('send.confirm_modal.text_collaborative_tx_enabled')}</strong>
<strong className="text-success">{t('send.confirm_send_modal.text_collaborative_tx_enabled')}</strong>
) : (
<strong className="text-danger">{t('send.confirm_modal.text_collaborative_tx_disabled')}</strong>
<strong className="text-danger">{t('send.confirm_send_modal.text_collaborative_tx_disabled')}</strong>
)}
</rb.Col>
</rb.Row>
<rb.Row>
<rb.Col xs={3} className="text-end">
<strong>{t('send.confirm_modal.label_source_jar')}</strong>
<strong>{t('send.confirm_send_modal.label_source_jar')}</strong>
</rb.Col>
<rb.Col xs={9} className="text-start">
{t('send.confirm_modal.text_source_jar', { jarId: jarInitial(account) })}
{t('send.confirm_send_modal.text_source_jar', { jarId: jarInitial(account) })}
</rb.Col>
</rb.Row>
<rb.Row>
<rb.Col xs={3} className="text-end">
<strong>{t('send.confirm_modal.label_recipient')}</strong>
<strong>{t('send.confirm_send_modal.label_recipient')}</strong>
</rb.Col>
<rb.Col xs={9} className="text-start text-break slashed-zeroes">
{destination}
@ -929,12 +977,12 @@ export default function Send() {
</rb.Row>
<rb.Row>
<rb.Col xs={3} className="text-end">
<strong>{t('send.confirm_modal.label_amount')}</strong>
<strong>{t('send.confirm_send_modal.label_amount')}</strong>
</rb.Col>
<rb.Col xs={9} className="text-start">
{isSweep ? (
<div className="d-flex justify-content-start align-items-center">
<Trans i18nKey="send.confirm_modal.text_sweep_balance">
<Trans i18nKey="send.confirm_send_modal.text_sweep_balance">
Sweep
<Balance
valueString={amountFieldValue().toString()}
@ -946,7 +994,7 @@ export default function Send() {
placement="right"
overlay={
<rb.Popover>
<rb.Popover.Body>{t('send.confirm_modal.text_sweep_info_popover')}</rb.Popover.Body>
<rb.Popover.Body>{t('send.confirm_send_modal.text_sweep_info_popover')}</rb.Popover.Body>
</rb.Popover>
}
>
@ -967,7 +1015,7 @@ export default function Send() {
{isCoinjoin && (
<rb.Row>
<rb.Col xs={3} className="text-end">
<strong>{t('send.confirm_modal.label_num_collaborators')}</strong>
<strong>{t('send.confirm_send_modal.label_num_collaborators')}</strong>
</rb.Col>
<rb.Col xs={9} className="text-start">
{numCollaborators}

View file

@ -6,6 +6,7 @@
"next": "Next",
"back": "Back",
"close": "Close",
"abort": "Abort",
"table": {
"pagination": {
"items_per_page": {
@ -208,7 +209,7 @@
"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</1> or <3>time-locked</3>. 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</5> for more details.",
"confirm_modal": {
"confirm_send_modal": {
"title": "Confirm payment",
"label_amount": "Amount",
"text_sweep_balance": "Sweep whole jar (<1></1>)",
@ -220,6 +221,10 @@
"text_collaborative_tx_enabled": "Payment with privacy improvement",
"text_collaborative_tx_disabled": "Payment without privacy improvement"
},
"confirm_abort_modal": {
"title": "Abort payment",
"text_body": "Are you sure you want to abort the collaborative transaction?"
},
"coinjoin_precondition": {
"hint_missing_utxos": "To execute a collaborative transaction you need UTXOs with {{ minConfirmations }} or more confirmations in the source jar. $t(send.coinjoin_precondition.nested_hint_fund_jar, {\"count\": {{ minConfirmations }} })",
"hint_missing_confirmations": "A collaborative transaction requires your UTXOs to have {{ minConfirmations }} or more confirmations. $t(send.coinjoin_precondition.nested_hint_wait_for_block, {\"count\": {{ amountOfMissingConfirmations }} })",

View file

@ -358,7 +358,7 @@ const postSchedulerStart = async ({ token, signal, walletName }: WalletRequestCo
})
}
const getSchedulerStop = async ({ token, signal, walletName }: WalletRequestContext) => {
const getTakerStop = async ({ token, signal, walletName }: WalletRequestContext) => {
return await fetch(`${basePath()}/v1/wallet/${encodeURIComponent(walletName)}/taker/stop`, {
headers: { ...Helper.buildAuthHeader(token) },
signal,
@ -414,7 +414,7 @@ export {
postConfigGet,
getWalletSeed,
postSchedulerStart,
getSchedulerStop,
getTakerStop,
getSchedule,
Helper,
}