Skip to content

Commit

Permalink
refactor: add hook WaitForUtxosToBeSpent
Browse files Browse the repository at this point in the history
  • Loading branch information
theborakompanioni committed Oct 26, 2023
1 parent cf7b86b commit a604e3a
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 136 deletions.
67 changes: 18 additions & 49 deletions src/components/Send/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -35,7 +37,6 @@ import {
isValidNumCollaborators,
} from './helpers'
import styles from './Send.module.css'
import FeeBreakdown from './FeeBreakdown'

const IS_COINJOIN_DEFAULT_VAL = true

Expand Down Expand Up @@ -89,7 +90,7 @@ export default function Send({ wallet }: SendProps) {
const [activeFeeConfigModalSection, setActiveFeeConfigModalSection] = useState<FeeConfigSectionKey>()
const [showFeeConfigModal, setShowFeeConfigModal] = useState(false)

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

const isOperationDisabled = useMemo(
Expand Down Expand Up @@ -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) {
Expand Down
115 changes: 28 additions & 87 deletions src/components/fb/SpendFidelityBondModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 = {
Expand Down Expand Up @@ -226,7 +227,6 @@ const RenewFidelityBondModal = ({
...modalProps
}: RenewFidelityBondModalProps) => {
const { t } = useTranslation()
const reloadCurrentWalletInfo = useReloadCurrentWalletInfo()
const feeConfigValues = useFeeConfigValues()[0]

const [alert, setAlert] = useState<SimpleAlert>()
Expand All @@ -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')) {
Expand Down Expand Up @@ -558,7 +529,6 @@ const SpendFidelityBondModal = ({
...modalProps
}: SpendFidelityBondModalProps) => {
const { t } = useTranslation()
const reloadCurrentWalletInfo = useReloadCurrentWalletInfo()
const feeConfigValues = useFeeConfigValues()[0]

const [alert, setAlert] = useState<SimpleAlert>()
Expand All @@ -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
Expand Down
64 changes: 64 additions & 0 deletions src/hooks/WaitForUtxosToBeSpent.ts
Original file line number Diff line number Diff line change
@@ -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])
}

0 comments on commit a604e3a

Please sign in to comment.