Skip to content

Commit

Permalink
refactor: RecentTransactionProvider into hook
Browse files Browse the repository at this point in the history
  • Loading branch information
agualis committed Sep 3, 2024
1 parent 86acbf7 commit d95e628
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function AddLiquidityModal({
})

function onModalClose() {
recentTransactions.recoverUnconfirmedTransactions()
recentTransactions.recheckUnconfirmedTransactions()
onClose()
}

Expand Down
4 changes: 2 additions & 2 deletions lib/modules/portfolio/PortfolioProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { useUserAccount } from '../web3/UserAccountProvider'
import { getProjectConfig } from '@/lib/config/getProjectConfig'
import { useOnchainUserPoolBalances } from '../pool/queries/useOnchainUserPoolBalances'
import { Pool } from '../pool/PoolProvider'
import { useRecentTransactions } from '../transactions/RecentTransactionsProvider'
import { isAfter } from 'date-fns'
import { compact, uniq, uniqBy } from 'lodash'
import {
Expand All @@ -22,6 +21,7 @@ import {
getUserTotalBalanceUsd,
} from '../pool/user-balance.helpers'
import { getTimestamp } from '@/lib/shared/utils/time'
import { useTransactionState } from '../transactions/transaction-steps/TransactionStateProvider'

export interface ClaimableBalanceResult {
status: 'success' | 'error'
Expand All @@ -40,7 +40,7 @@ export type UsePortfolio = ReturnType<typeof _usePortfolio>

function _usePortfolio() {
const { userAddress, isConnected } = useUserAccount()
const { transactions } = useRecentTransactions()
const { transactions } = useTransactionState()

const fiveMinutesAgo = getTimestamp().minsAgo(5)
const chainIn = getProjectConfig().supportedNetworks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { createContext, PropsWithChildren, useState } from 'react'
import { ManagedResult } from './lib'
import { useMandatoryContext } from '@/lib/shared/utils/contexts'
import { TransactionResult } from '../../web3/contracts/contract.types'
import { RecentTransactionsResponse, useRecentTransactions } from '../useRecentTransactions'

export function _useTransactionState() {
export function _useTransactionState(recentTransactions: RecentTransactionsResponse) {
const [transactionMap, setTransactionMap] = useState<Map<string, ManagedResult>>(new Map())

function updateTransaction(k: string, v: ManagedResult) {
Expand All @@ -23,25 +24,30 @@ export function _useTransactionState() {
}

function getTransaction(id: string) {
return transactionMap.get(id)
const transaction = transactionMap.get(id)
return transaction
}

function resetTransactionState() {
setTransactionMap(new Map())
recentTransactions.recheckUnconfirmedTransactions()
}

return {
getTransaction,
updateTransaction,
resetTransactionState,
transactions: recentTransactions.transactions,
recentTransactions,
}
}

export type TransactionStateResponse = ReturnType<typeof _useTransactionState>
export const TransactionStateContext = createContext<TransactionStateResponse | null>(null)

export function TransactionStateProvider({ children }: PropsWithChildren) {
const hook = _useTransactionState()
const recentTransactions = useRecentTransactions()
const hook = _useTransactionState(recentTransactions)

return (
<TransactionStateContext.Provider value={hook}>{children}</TransactionStateContext.Provider>
Expand All @@ -51,7 +57,7 @@ export function TransactionStateProvider({ children }: PropsWithChildren) {
export const useTransactionState = (): TransactionStateResponse =>
useMandatoryContext(TransactionStateContext, 'TransactionState')

function resetTransaction(v: ManagedResult) {
function resetTransaction(v: ManagedResult): ManagedResult {
// Resetting the execution transaction does not immediately reset execution and result statuses so we need to reset them manually
v.execution.status = 'pending'
v.result = { status: 'pending', isSuccess: false, data: undefined } as TransactionResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import { getChainId } from '@/lib/config/app.config'
import { Toast } from '@/lib/shared/components/toasts/Toast'
import { getBlockExplorerTxUrl } from '@/lib/shared/hooks/useBlockExplorer'
import { GqlChain } from '@/lib/shared/services/api/generated/graphql'
import { useMandatoryContext } from '@/lib/shared/utils/contexts'
import { ensureError } from '@/lib/shared/utils/errors'
import { captureFatalError } from '@/lib/shared/utils/query-errors'
import { secs } from '@/lib/shared/utils/time'
import { AlertStatus, ToastId, useToast } from '@chakra-ui/react'
import { keyBy, orderBy, take } from 'lodash'
import React, { ReactNode, createContext, useCallback, useEffect, useState } from 'react'
import React, { createContext, useCallback, useEffect, useState } from 'react'
import { Hash } from 'viem'
import { useConfig, usePublicClient } from 'wagmi'
import { waitForTransactionReceipt } from 'wagmi/actions'
import { getWaitForReceiptTimeout } from '../web3/contracts/wagmi-helpers'
import { useNetworkConfig } from '@/lib/config/useNetworkConfig'

export type RecentTransactionsResponse = ReturnType<typeof _useRecentTransactions>
export type RecentTransactionsResponse = ReturnType<typeof useRecentTransactions>
export const TransactionsContext = createContext<RecentTransactionsResponse | null>(null)
const NUM_RECENT_TRANSACTIONS = 20

Expand Down Expand Up @@ -47,12 +47,12 @@ export type TrackedTransaction = {
poolId?: string
}

type UpdateTrackedTransaction = Pick<
export type UpdateTrackedTransaction = Pick<
TrackedTransaction,
'label' | 'description' | 'status' | 'duration'
>

const TransactionStatusToastStatusMapping: Record<TransactionStatus, AlertStatus> = {
export const TransactionStatusToastStatusMapping: Record<TransactionStatus, AlertStatus> = {
confirmed: 'success',
confirming: 'loading',
reverted: 'error',
Expand All @@ -61,18 +61,24 @@ const TransactionStatusToastStatusMapping: Record<TransactionStatus, AlertStatus
unknown: 'warning',
}

export function _useRecentTransactions() {
export function useRecentTransactions() {
const [transactions, setTransactions] = useState<Record<string, TrackedTransaction>>({})
const toast = useToast()
const publicClient = usePublicClient()
const config = useConfig()
const { minConfirmations } = useNetworkConfig()

// load from localStorage on mount
useEffect(() => {
loadTransactionsFromLocalStorage()
}, [])

// when loading transactions from the localStorage cache and we identify any unconfirmed
// transactions, we should fetch the receipt of the transactions
const waitForUnconfirmedTransactions = useCallback(
async (transactions: Record<string, TrackedTransaction>) => {
const unconfirmedTransactions = Object.values(transactions).filter(
tx => tx.status === 'confirming'
tx => tx.status === 'confirming' || tx.status === 'timeout'
)

const updatePayload = {
Expand All @@ -87,6 +93,7 @@ export function _useRecentTransactions() {
hash: tx.hash,
chainId: getChainId(tx.chain),
timeout: getWaitForReceiptTimeout(getChainId(tx.chain)),
confirmations: minConfirmations,
})
if (receipt?.status === 'success') {
updatePayload[tx.hash] = {
Expand All @@ -99,9 +106,10 @@ export function _useRecentTransactions() {
status: 'reverted',
}
}
updateToast(updatePayload[tx.hash])
setTransactions(updatePayload)
} catch (error) {
console.error('Error in RecentTransactionsProvider: ', error)
console.error('Error in useRecentTransactions: ', error)

/* This is an edge-case that we found randomly happening in polygon.
Debug tip:
Expand All @@ -111,14 +119,15 @@ export function _useRecentTransactions() {
captureFatalError(
error,
'waitForTransactionReceiptError',
'Error in waitForTransactionReceipt inside RecentTransactionsProvider',
'Error in waitForTransactionReceipt inside useRecentTransactions',
{ txHash: tx.hash }
)
const isTimeoutError = ensureError(error).name === 'WaitForTransactionReceiptTimeoutError'
updatePayload[tx.hash] = {
...tx,
status: isTimeoutError ? 'timeout' : 'unknown',
}
updateToast(updatePayload[tx.hash])
setTransactions(updatePayload)
}
}
Expand All @@ -128,19 +137,6 @@ export function _useRecentTransactions() {
[publicClient]
)

// fetch recent transactions from local storage
useEffect(() => {
const _recentTransactions = localStorage.getItem('balancer.recentTransactions')
if (_recentTransactions) {
const recentTransactions = JSON.parse(_recentTransactions)
setTransactions(recentTransactions)
// confirm the status of any past confirming transactions
// on load
waitForUnconfirmedTransactions(recentTransactions)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function handleTransactionAdded(trackedTransaction: TrackedTransaction) {
// add a toast for this transaction, rather than emitting a new toast
Expand Down Expand Up @@ -206,33 +202,28 @@ export function _useRecentTransactions() {

setTransactions(updatedCache)
updateLocalStorage(updatedCache)
updateToast(updatedCachedTransaction)
}

const duration = updatePayload.duration

// update the relevant toast too
if (updatedCachedTransaction.toastId) {
if (updatePayload.status === 'timeout' || updatePayload.status === 'unknown') {
// Close the toast as these errors are shown as alerts inside the TransactionStepButton
return toast.close(updatedCachedTransaction.toastId)
}
function updateToast(transaction: TrackedTransaction) {
if (!transaction.toastId) return
const duration = transaction.duration

toast.update(updatedCachedTransaction.toastId, {
status: TransactionStatusToastStatusMapping[updatePayload.status],
title: updatedCachedTransaction.label,
description: updatedCachedTransaction.description,
isClosable: true,
duration: duration || duration === null ? duration : secs(10).toMs(),
render: ({ ...rest }) => (
<Toast
linkUrl={getBlockExplorerTxUrl(
updatedCachedTransaction.hash,
updatedCachedTransaction.chain
)}
{...rest}
/>
),
})
if (transaction.status === 'timeout' || transaction.status === 'unknown') {
// Close the toast as these errors are shown as alerts inside the TransactionStepButton
return toast.close(transaction.toastId)
}

toast.update(transaction.toastId, {
status: TransactionStatusToastStatusMapping[transaction.status],
title: transaction.label,
description: transaction.description,
isClosable: true,
duration: duration || duration === null ? duration : secs(10).toMs(),
render: ({ ...rest }) => (
<Toast linkUrl={getBlockExplorerTxUrl(transaction.hash, transaction.chain)} {...rest} />
),
})
}

function updateLocalStorage(customUpdate?: Record<string, TrackedTransaction>) {
Expand All @@ -242,24 +233,34 @@ export function _useRecentTransactions() {
)
}

function recheckUnconfirmedTransactions() {
waitForUnconfirmedTransactions(transactions)
}

function addTrackedTransaction(trackedTransaction: TrackedTransaction) {
handleTransactionAdded(trackedTransaction)
}

function loadTransactionsFromLocalStorage() {
const _recentTransactions = localStorage.getItem('balancer.recentTransactions')
if (_recentTransactions) {
const recentTransactions = JSON.parse(_recentTransactions)
setTransactions(recentTransactions)
waitForUnconfirmedTransactions(recentTransactions)
}
}

function clearTransactions() {
updateLocalStorage({})
setTransactions({})
}

return { transactions, addTrackedTransaction, updateTrackedTransaction, clearTransactions }
}

export function RecentTransactionsProvider({ children }: { children: ReactNode }) {
const transactions = _useRecentTransactions()
return (
<TransactionsContext.Provider value={transactions}>{children}</TransactionsContext.Provider>
)
return {
loadTransactionsFromLocalStorage,
addTrackedTransaction,
updateTrackedTransaction,
recheckUnconfirmedTransactions,
clearTransactions,
transactions,
}
}

export const useRecentTransactions = () =>
useMandatoryContext(TransactionsContext, 'RecentTransactionsProvider')
35 changes: 6 additions & 29 deletions lib/modules/web3/contracts/useManagedSendTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import { ManagedResult, TransactionLabels } from '@/lib/modules/transactions/tra
import { useEffect } from 'react'
import { useEstimateGas, useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'
import { TransactionConfig, TransactionExecution, TransactionSimulation } from './contract.types'
import { useOnTransactionConfirmation } from './useOnTransactionConfirmation'
import { useOnTransactionSubmission } from './useOnTransactionSubmission'
import { getGqlChain } from '@/lib/config/app.config'
import { useChainSwitch } from '../useChainSwitch'
import {
captureWagmiExecutionError,
sentryMetaForWagmiExecution,
} from '@/lib/shared/utils/query-errors'
import { useNetworkConfig } from '@/lib/config/useNetworkConfig'
import { useRecentTransactions } from '../../transactions/RecentTransactionsProvider'
import { mainnet } from 'viem/chains'
import { useTxHash } from '../safe.hooks'
import { getWaitForReceiptTimeout } from './wagmi-helpers'
import { useTransactionState } from '../../transactions/transaction-steps/TransactionStateProvider'
import { useOnTransactionSubmission } from './useOnTransactionSubmission'
import { getGqlChain } from '@/lib/config/app.config'
import { useOnTransactionConfirmation } from './useOnTransactionConfirmation'

export type ManagedSendTransactionInput = {
labels: TransactionLabels
Expand All @@ -34,7 +34,7 @@ export function useManagedSendTransaction({
const chainId = txConfig?.chainId || mainnet.id
const { shouldChangeNetwork } = useChainSwitch(chainId)
const { minConfirmations } = useNetworkConfig()
const { updateTrackedTransaction } = useRecentTransactions()
const { recentTransactions } = useTransactionState()

const estimateGasQuery = useEstimateGas({
...txConfig,
Expand Down Expand Up @@ -71,33 +71,10 @@ export function useManagedSendTransaction({
isSafeTxLoading,
}

// when the transaction is successfully submitted to the chain
// start monitoring the hash
//
// when the transaction has an execution error, update that within
// the global transaction cache too
useEffect(() => {
if (bundle?.execution?.data) {
// add transaction here
}
}, [bundle.execution?.data])

// when the transaction has an execution error, update that within
// the global transaction cache
// this can either be an execution error or a confirmation error
useEffect(() => {
if (bundle?.execution?.error) {
// monitor execution error here
}
if (bundle?.result?.error) {
// monitor confirmation error here
}
}, [bundle.execution?.error, bundle.result?.error])

useEffect(() => {
if (transactionStatusQuery.error) {
if (txHash) {
updateTrackedTransaction(txHash, {
recentTransactions.updateTrackedTransaction(txHash, {
status: 'timeout',
label: 'Transaction timeout',
duration: null,
Expand Down
Loading

0 comments on commit d95e628

Please sign in to comment.