Skip to content

Commit

Permalink
feat(widget): refresh stale quotes (#2763)
Browse files Browse the repository at this point in the history
* useBridgeQuoteUpdater

* Abstract fetchAndStoreBridgeQuote()

* Implement useBridgeQuoteUpdater to refresh stale bridge quotes

* Clean

* Add missing default prop

* Ensure quote refresher only fires single callback

* Clean logs
  • Loading branch information
bigboydiamonds authored Jun 28, 2024
1 parent 02767e6 commit 2651119
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 26 deletions.
77 changes: 52 additions & 25 deletions packages/widget/src/components/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import {
useRef,
useState,
} from 'react'
import { BridgeableToken, Chain, CustomThemeVariables } from 'types'
import {
type BridgeableToken,
type Chain,
type CustomThemeVariables,
} from 'types'
import { ZeroAddress } from 'ethers'

import { Web3Context } from '@/providers/Web3Provider'
Expand All @@ -28,9 +32,11 @@ import {
setProtocolName,
} from '@/state/slices/bridge/reducer'
import { useBridgeState } from '@/state/slices/bridge/hooks'
import { setIsWalletPending } from '@/state/slices/wallet/reducer'
import {
fetchAndStoreAllowance,
fetchAndStoreTokenBalances,
useWalletState,
} from '@/state/slices/wallet/hooks'
import { BridgeButton } from '@/components/BridgeButton'
import { AvailableBalance } from '@/components/AvailableBalance'
Expand Down Expand Up @@ -61,6 +67,8 @@ import { getFromTokens } from '@/utils/routeMaker/getFromTokens'
import { getSymbol } from '@/utils/routeMaker/generateRoutePossibilities'
import { findTokenByRouteSymbol } from '@/utils/findTokenByRouteSymbol'
import { useMaintenance } from '@/components/Maintenance/Maintenance'
import { getTimeMinutesFromNow } from '@/utils/getTimeMinutesFromNow'
import { useBridgeQuoteUpdater } from '@/hooks/useBridgeQuoteUpdater'

interface WidgetProps {
customTheme: CustomThemeVariables
Expand Down Expand Up @@ -114,8 +122,8 @@ export const Widget = ({
}, [originChainId])

const { bridgeQuote, isLoading } = useBridgeQuoteState()

const { isInputValid, hasValidSelections } = useValidations()
const { isWalletPending } = useWalletState()

const { bridgeTxnStatus } = useBridgeTransactionState()
const { approveTxnStatus } = useApproveTransactionState()
Expand Down Expand Up @@ -191,32 +199,38 @@ export const Widget = ({
bridgeQuote?.routerAddress,
])

/** Handle refreshing quotes */
useEffect(() => {
if (isInputValid && hasValidSelections) {
currentSDKRequestID.current += 1
const thisRequestId = currentSDKRequestID.current
const fetchAndStoreBridgeQuote = async () => {
currentSDKRequestID.current += 1
const thisRequestId = currentSDKRequestID.current

dispatch(resetQuote())
dispatch(resetQuote())
const currentTimestamp: number = getTimeMinutesFromNow(0)

if (thisRequestId === currentSDKRequestID.current) {
dispatch(
fetchBridgeQuote({
originChainId,
destinationChainId,
originToken,
destinationToken,
amount: stringToBigInt(
debouncedInputAmount,
originToken.decimals[originChainId]
),
if (thisRequestId === currentSDKRequestID.current) {
dispatch(
fetchBridgeQuote({
originChainId,
destinationChainId,
originToken,
destinationToken,
amount: stringToBigInt(
debouncedInputAmount,
synapseSDK,
requestId: thisRequestId,
pausedModules: pausedModulesList,
})
)
}
originToken.decimals[originChainId]
),
debouncedInputAmount,
synapseSDK,
requestId: thisRequestId,
pausedModules: pausedModulesList,
timestamp: currentTimestamp,
})
)
}
}

/** Handle refreshing quotes */
useEffect(() => {
if (isInputValid && hasValidSelections) {
fetchAndStoreBridgeQuote()
} else {
dispatch(resetQuote())
}
Expand All @@ -230,6 +244,13 @@ export const Widget = ({
hasValidSelections,
])

useBridgeQuoteUpdater(
bridgeQuote,
fetchAndStoreBridgeQuote,
isLoading,
isWalletPending
)

const handleUserInput = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = cleanNumberInput(event.target.value)
Expand Down Expand Up @@ -268,6 +289,7 @@ export const Widget = ({

const executeApproval = async () => {
try {
dispatch(setIsWalletPending(true))
const tx = await dispatch(
executeApproveTxn({
spenderAddress: bridgeQuote?.routerAddress,
Expand All @@ -294,11 +316,14 @@ export const Widget = ({
}
} catch (error) {
console.error(`[Synapse Widget] Error while approving token: `, error)
} finally {
dispatch(setIsWalletPending(false))
}
}

const executeBridge = async () => {
try {
dispatch(setIsWalletPending(true))
const action = await dispatch(
executeBridgeTxn({
destinationAddress: connectedAddress,
Expand Down Expand Up @@ -348,6 +373,8 @@ export const Widget = ({
}
} catch (error) {
console.error('[Synapse Widget] Error bridging: ', error)
} finally {
dispatch(setIsWalletPending(false))
}
}

Expand Down
47 changes: 47 additions & 0 deletions packages/widget/src/hooks/useBridgeQuoteUpdater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { isNull, isNumber } from 'lodash'
import { useEffect, useRef } from 'react'

import { type BridgeQuote } from '@/state/slices/bridgeQuote/reducer'
import { calculateTimeBetween } from '@/utils/calculateTimeBetween'
import { useIntervalTimer } from '@/hooks/useIntervalTimer'

/**
* Refreshes quotes based on selected stale timeout duration.
* Will refresh quote when browser is active and wallet prompt is not pending.
*/
export const useBridgeQuoteUpdater = (
quote: BridgeQuote,
refreshQuoteCallback: () => Promise<void>,
isQuoteLoading: boolean,
isWalletPending: boolean,
staleTimeout: number = 15000 // 15_000ms or 15s
) => {
const quoteTime = quote?.timestamp
const isValidQuote = isNumber(quoteTime) && !isNull(quoteTime)
const currentTime = useIntervalTimer(staleTimeout, !isValidQuote)
const eventListenerRef = useRef<null | (() => void)>(null)

useEffect(() => {
if (isValidQuote && !isQuoteLoading && !isWalletPending) {
const timeDifference = calculateTimeBetween(currentTime, quoteTime)
const isStaleQuote = timeDifference >= staleTimeout

if (isStaleQuote) {
if (eventListenerRef.current) {
document.removeEventListener('mousemove', eventListenerRef.current)
}

const newEventListener = () => {
refreshQuoteCallback()
eventListenerRef.current = null
}

document.addEventListener('mousemove', newEventListener, {
once: true,
})

eventListenerRef.current = newEventListener
}
}
}, [currentTime, staleTimeout])
}
3 changes: 3 additions & 0 deletions packages/widget/src/state/slices/bridgeQuote/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const fetchBridgeQuote = createAsyncThunk(
synapseSDK,
requestId,
pausedModules,
timestamp,
}: {
originChainId: number
destinationChainId: number
Expand All @@ -36,6 +37,7 @@ export const fetchBridgeQuote = createAsyncThunk(
synapseSDK: any
requestId: number
pausedModules: any
timestamp: number
}) => {
const allQuotes = await synapseSDK.allBridgeQuotes(
originChainId,
Expand Down Expand Up @@ -120,6 +122,7 @@ export const fetchBridgeQuote = createAsyncThunk(
estimatedTime,
bridgeModuleName,
requestId,
timestamp,
}
}
)
2 changes: 2 additions & 0 deletions packages/widget/src/state/slices/bridgeQuote/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type BridgeQuote = {
estimatedTime: number
bridgeModuleName: string
requestId: number
timestamp: number
}

export const EMPTY_BRIDGE_QUOTE = {
Expand All @@ -43,6 +44,7 @@ export const EMPTY_BRIDGE_QUOTE = {
estimatedTime: null,
bridgeModuleName: null,
requestId: null,
timestamp: null,
}

export interface BridgeQuoteState {
Expand Down
10 changes: 9 additions & 1 deletion packages/widget/src/state/slices/wallet/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,25 @@ export interface WalletState {
allowance: string
status: FetchState
error?: any
isWalletPending: boolean
}

const initialState: WalletState = {
balances: [],
allowance: null,
status: FetchState.IDLE,
error: null,
isWalletPending: false,
}

export const walletSlice = createSlice({
name: 'wallet',
initialState,
reducers: {},
reducers: {
setIsWalletPending: (state, action: PayloadAction<boolean>) => {
state.isWalletPending = action.payload
},
},
extraReducers: (builder) => {
builder
.addCase(fetchAndStoreTokenBalances.pending, (state) => {
Expand Down Expand Up @@ -66,4 +72,6 @@ export const walletSlice = createSlice({
},
})

export const { setIsWalletPending } = walletSlice.actions

export default walletSlice.reducer
6 changes: 6 additions & 0 deletions packages/widget/src/utils/calculateTimeBetween.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const calculateTimeBetween = (
timeBefore: number,
timeAfter: number
): number => {
return Math.abs(timeBefore - timeAfter) * 1000
}

0 comments on commit 2651119

Please sign in to comment.