From a604e3a1c6d37ff8c753f996cb33155b7750d62e Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 26 Oct 2023 14:21:03 +0200 Subject: [PATCH] refactor: add hook WaitForUtxosToBeSpent --- src/components/Send/index.tsx | 67 +++-------- src/components/fb/SpendFidelityBondModal.tsx | 115 +++++-------------- src/hooks/WaitForUtxosToBeSpent.ts | 64 +++++++++++ 3 files changed, 110 insertions(+), 136 deletions(-) create mode 100644 src/hooks/WaitForUtxosToBeSpent.ts diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index ad71aa1f..dc3b58c3 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -15,13 +15,15 @@ import { PaymentConfirmModal } from '../PaymentConfirmModal' import { CoinjoinPreconditionViolationAlert } from '../CoinjoinPreconditionViolationAlert' import CollaboratorsSelector from './CollaboratorsSelector' import Accordion from '../Accordion' +import FeeBreakdown from './FeeBreakdown' import FeeConfigModal, { FeeConfigSectionKey } from '../settings/FeeConfigModal' -import { useFeeConfigValues, useEstimatedMaxCollaboratorFee } from '../../hooks/Fees' import { useReloadCurrentWalletInfo, useCurrentWalletInfo, CurrentWallet } from '../../context/WalletContext' import { useServiceInfo, useReloadServiceInfo } from '../../context/ServiceInfoContext' import { useLoadConfigValue } from '../../context/ServiceConfigContext' import { buildCoinjoinRequirementSummary } from '../../hooks/CoinjoinRequirements' +import { useFeeConfigValues, useEstimatedMaxCollaboratorFee } from '../../hooks/Fees' +import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent' import { routes } from '../../constants/routes' import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/config' @@ -35,7 +37,6 @@ import { isValidNumCollaborators, } from './helpers' import styles from './Send.module.css' -import FeeBreakdown from './FeeBreakdown' const IS_COINJOIN_DEFAULT_VAL = true @@ -89,7 +90,7 @@ export default function Send({ wallet }: SendProps) { const [activeFeeConfigModalSection, setActiveFeeConfigModalSection] = useState() const [showFeeConfigModal, setShowFeeConfigModal] = useState(false) - const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([]) + const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([]) const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState() const isOperationDisabled = useMemo( @@ -187,54 +188,22 @@ export default function Send({ wallet }: SendProps) { useEffect(() => setAmount(isSweep ? 0 : null), [isSweep]) - // This callback is responsible for updating `waitForUtxosToBeSpent` while - // the wallet is synchronizing. The wallet needs some time after a tx is sent - // to reflect the changes internally. In order to show the actual balance, - // all outputs in `waitForUtxosToBeSpent` must have been removed from the - // wallet's utxo set. - useEffect( - function updateWaitForUtxosToBeSpentHook() { - if (waitForUtxosToBeSpent.length === 0) return - - const abortCtrl = new AbortController() - - // Delaying the poll requests gives the wallet some time to synchronize - // the utxo set and reduces amount of http requests - const initialDelayInMs = 250 - const timer = setTimeout(() => { - if (abortCtrl.signal.aborted) return - - reloadCurrentWalletInfo - .reloadUtxos({ signal: abortCtrl.signal }) - .then((res) => { - if (abortCtrl.signal.aborted) return - const outputs = res.utxos.map((it) => it.utxo) - const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) - setWaitForUtxosToBeSpent([...utxosStillPresent]) - }) - - .catch((err) => { - if (abortCtrl.signal.aborted) return - - // Stop waiting for wallet synchronization on errors, but inform - // the user that loading the wallet info failed - setWaitForUtxosToBeSpent([]) - - const message = t('global.errors.error_reloading_wallet_failed', { - reason: err.message || t('global.errors.reason_unknown'), - }) - setAlert({ variant: 'danger', message }) - }) - }, initialDelayInMs) - - return () => { - abortCtrl.abort() - clearTimeout(timer) - } - }, - [waitForUtxosToBeSpent, reloadCurrentWalletInfo, t], + const waitForUtxosToBeSpentContext = useMemo( + () => ({ + waitForUtxosToBeSpent, + setWaitForUtxosToBeSpent, + onError: (error: any) => { + const message = t('global.errors.error_reloading_wallet_failed', { + reason: error.message || t('global.errors.reason_unknown'), + }) + setAlert({ variant: 'danger', message }) + }, + }), + [waitForUtxosToBeSpent, t], ) + useWaitForUtxosToBeSpent(waitForUtxosToBeSpentContext) + useEffect( function initialize() { if (isOperationDisabled) { diff --git a/src/components/fb/SpendFidelityBondModal.tsx b/src/components/fb/SpendFidelityBondModal.tsx index 8e24d04f..b6d24865 100644 --- a/src/components/fb/SpendFidelityBondModal.tsx +++ b/src/components/fb/SpendFidelityBondModal.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as rb from 'react-bootstrap' import { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' -import { CurrentWallet, useReloadCurrentWalletInfo, Utxo, Utxos, WalletInfo } from '../../context/WalletContext' +import { CurrentWallet, Utxo, Utxos, WalletInfo } from '../../context/WalletContext' import * as Api from '../../libs/JmWalletApi' import * as fb from './utils' import Alert from '../Alert' @@ -14,6 +14,7 @@ import { useFeeConfigValues } from '../../hooks/Fees' import { isDebugFeatureEnabled } from '../../constants/debugFeatures' import { CopyButton } from '../CopyButton' import { LockInfoAlert } from './CreateFidelityBond' +import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent' import styles from './SpendFidelityBondModal.module.css' type Input = { @@ -226,7 +227,6 @@ const RenewFidelityBondModal = ({ ...modalProps }: RenewFidelityBondModalProps) => { const { t } = useTranslation() - const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const feeConfigValues = useFeeConfigValues()[0] const [alert, setAlert] = useState() @@ -250,50 +250,21 @@ const RenewFidelityBondModal = ({ return walletInfo.data.utxos.utxos.find((utxo) => utxo.utxo === fidelityBondId) }, [walletInfo, fidelityBondId]) - // This callback is responsible for updating the loading state when the - // the payment is made. The wallet needs some time after a tx is sent - // to reflect the changes internally. All outputs in - // `waitForUtxosToBeSpent` must have been removed from the wallet - // for the payment to be considered done. - useEffect(() => { - if (waitForUtxosToBeSpent.length === 0) return - - const abortCtrl = new AbortController() - - // Delaying the poll requests gives the wallet some time to synchronize - // the utxo set and reduces amount of http requests - const initialDelayInMs = 1_000 - const timer = setTimeout(() => { - if (abortCtrl.signal.aborted) return - - reloadCurrentWalletInfo - .reloadUtxos({ signal: abortCtrl.signal }) - .then((res) => { - if (abortCtrl.signal.aborted) return - - const outputs = res.utxos.map((it) => it.utxo) - const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) - setWaitForUtxosToBeSpent([...utxosStillPresent]) + const waitForUtxosToBeSpentContext = useMemo( + () => ({ + waitForUtxosToBeSpent, + setWaitForUtxosToBeSpent, + onError: (error: any) => { + const message = t('global.errors.error_reloading_wallet_failed', { + reason: error.message || t('global.errors.reason_unknown'), }) - .catch((err) => { - if (abortCtrl.signal.aborted) return - - // Stop waiting for wallet synchronization on errors, but inform - // the user that loading the wallet info failed - setWaitForUtxosToBeSpent([]) - - const message = t('global.errors.error_reloading_wallet_failed', { - reason: err.message || t('global.errors.reason_unknown'), - }) - setAlert({ variant: 'danger', message }) - }) - }, initialDelayInMs) + setAlert({ variant: 'danger', message }) + }, + }), + [waitForUtxosToBeSpent, t], + ) - return () => { - abortCtrl.abort() - clearTimeout(timer) - } - }, [waitForUtxosToBeSpent, reloadCurrentWalletInfo, t]) + useWaitForUtxosToBeSpent(waitForUtxosToBeSpentContext) const yearsRange = useMemo(() => { if (isDebugFeatureEnabled('allowCreatingExpiredFidelityBond')) { @@ -558,7 +529,6 @@ const SpendFidelityBondModal = ({ ...modalProps }: SpendFidelityBondModalProps) => { const { t } = useTranslation() - const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const feeConfigValues = useFeeConfigValues()[0] const [alert, setAlert] = useState() @@ -582,50 +552,21 @@ const SpendFidelityBondModal = ({ return walletInfo.data.utxos.utxos.find((utxo) => utxo.utxo === fidelityBondId) }, [walletInfo, fidelityBondId]) - // This callback is responsible for updating the loading state when the - // the payment is made. The wallet needs some time after a tx is sent - // to reflect the changes internally. All outputs in - // `waitForUtxosToBeSpent` must have been removed from the wallet - // for the payment to be considered done. - useEffect(() => { - if (waitForUtxosToBeSpent.length === 0) return - - const abortCtrl = new AbortController() - - // Delaying the poll requests gives the wallet some time to synchronize - // the utxo set and reduces amount of http requests - const initialDelayInMs = 1_000 - const timer = setTimeout(() => { - if (abortCtrl.signal.aborted) return - - reloadCurrentWalletInfo - .reloadUtxos({ signal: abortCtrl.signal }) - .then((res) => { - if (abortCtrl.signal.aborted) return - - const outputs = res.utxos.map((it) => it.utxo) - const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) - setWaitForUtxosToBeSpent([...utxosStillPresent]) + const waitForUtxosToBeSpentContext = useMemo( + () => ({ + waitForUtxosToBeSpent, + setWaitForUtxosToBeSpent, + onError: (error: any) => { + const message = t('global.errors.error_reloading_wallet_failed', { + reason: error.message || t('global.errors.reason_unknown'), }) - .catch((err) => { - if (abortCtrl.signal.aborted) return - - // Stop waiting for wallet synchronization on errors, but inform - // the user that loading the wallet info failed - setWaitForUtxosToBeSpent([]) - - const message = t('global.errors.error_reloading_wallet_failed', { - reason: err.message || t('global.errors.reason_unknown'), - }) - setAlert({ variant: 'danger', message }) - }) - }, initialDelayInMs) + setAlert({ variant: 'danger', message }) + }, + }), + [waitForUtxosToBeSpent, t], + ) - return () => { - abortCtrl.abort() - clearTimeout(timer) - } - }, [waitForUtxosToBeSpent, reloadCurrentWalletInfo, t]) + useWaitForUtxosToBeSpent(waitForUtxosToBeSpentContext) const onPrimaryButtonClicked = () => { if (isLoading) return diff --git a/src/hooks/WaitForUtxosToBeSpent.ts b/src/hooks/WaitForUtxosToBeSpent.ts new file mode 100644 index 00000000..b54da5cd --- /dev/null +++ b/src/hooks/WaitForUtxosToBeSpent.ts @@ -0,0 +1,64 @@ +import { useEffect } from 'react' +import { useReloadCurrentWalletInfo } from '../context/WalletContext' +import { UtxoId } from '../libs/JmWalletApi' + +// Delaying the poll requests gives the wallet some time to synchronize +// the utxo set and reduces amount of http requests +const DEFAUL_DELAY: Milliseconds = 1_000 + +interface WaitForUtxosToBeSpentArgs { + waitForUtxosToBeSpent: UtxoId[] + setWaitForUtxosToBeSpent: (utxos: UtxoId[]) => void + onError: (error: any) => void + delay?: Milliseconds + resetOnErrors?: boolean +} + +// This callback is responsible for updating the utxo array when a +// payment is made. The wallet needs some time after a tx is sent +// to reflect the changes internally. All outputs given in +// `waitForUtxosToBeSpent` must have been removed from the wallet +// for a payment to be considered done. +export const useWaitForUtxosToBeSpent = ({ + waitForUtxosToBeSpent, + setWaitForUtxosToBeSpent, + onError, + delay = DEFAUL_DELAY, + resetOnErrors = true, +}: WaitForUtxosToBeSpentArgs): void => { + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + return useEffect(() => { + if (waitForUtxosToBeSpent.length === 0) return + + const abortCtrl = new AbortController() + + const timer = setTimeout(() => { + if (abortCtrl.signal.aborted) return + + reloadCurrentWalletInfo + .reloadUtxos({ signal: abortCtrl.signal }) + .then((res) => { + if (abortCtrl.signal.aborted) return + + const outputs = res.utxos.map((it) => it.utxo) + const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) + + // updating the utxos array will trigger a recursive call + setWaitForUtxosToBeSpent([...utxosStillPresent]) + }) + .catch((error: any) => { + if (abortCtrl.signal.aborted) return + if (resetOnErrors) { + // Stop waiting for wallet synchronization on errors + setWaitForUtxosToBeSpent([]) + } + onError(error) + }) + }, delay) + + return () => { + abortCtrl.abort() + clearTimeout(timer) + } + }, [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent, resetOnErrors, onError, delay, reloadCurrentWalletInfo]) +}