From 26511191fe8788688d38d80be6823feb4a22c34e Mon Sep 17 00:00:00 2001 From: bigboydiamonds <57741810+bigboydiamonds@users.noreply.github.com> Date: Fri, 28 Jun 2024 06:43:05 -0700 Subject: [PATCH] feat(widget): refresh stale quotes (#2763) * useBridgeQuoteUpdater * Abstract fetchAndStoreBridgeQuote() * Implement useBridgeQuoteUpdater to refresh stale bridge quotes * Clean * Add missing default prop * Ensure quote refresher only fires single callback * Clean logs --- packages/widget/src/components/Widget.tsx | 77 +++++++++++++------ .../widget/src/hooks/useBridgeQuoteUpdater.ts | 47 +++++++++++ .../src/state/slices/bridgeQuote/hooks.ts | 3 + .../src/state/slices/bridgeQuote/reducer.ts | 2 + .../widget/src/state/slices/wallet/reducer.ts | 10 ++- .../widget/src/utils/calculateTimeBetween.ts | 6 ++ 6 files changed, 119 insertions(+), 26 deletions(-) create mode 100644 packages/widget/src/hooks/useBridgeQuoteUpdater.ts create mode 100644 packages/widget/src/utils/calculateTimeBetween.ts diff --git a/packages/widget/src/components/Widget.tsx b/packages/widget/src/components/Widget.tsx index 63783081ba..2c0a89e035 100644 --- a/packages/widget/src/components/Widget.tsx +++ b/packages/widget/src/components/Widget.tsx @@ -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' @@ -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' @@ -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 @@ -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() @@ -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()) } @@ -230,6 +244,13 @@ export const Widget = ({ hasValidSelections, ]) + useBridgeQuoteUpdater( + bridgeQuote, + fetchAndStoreBridgeQuote, + isLoading, + isWalletPending + ) + const handleUserInput = useCallback( (event: React.ChangeEvent) => { const value = cleanNumberInput(event.target.value) @@ -268,6 +289,7 @@ export const Widget = ({ const executeApproval = async () => { try { + dispatch(setIsWalletPending(true)) const tx = await dispatch( executeApproveTxn({ spenderAddress: bridgeQuote?.routerAddress, @@ -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, @@ -348,6 +373,8 @@ export const Widget = ({ } } catch (error) { console.error('[Synapse Widget] Error bridging: ', error) + } finally { + dispatch(setIsWalletPending(false)) } } diff --git a/packages/widget/src/hooks/useBridgeQuoteUpdater.ts b/packages/widget/src/hooks/useBridgeQuoteUpdater.ts new file mode 100644 index 0000000000..af015b4ad7 --- /dev/null +++ b/packages/widget/src/hooks/useBridgeQuoteUpdater.ts @@ -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, + 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 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]) +} diff --git a/packages/widget/src/state/slices/bridgeQuote/hooks.ts b/packages/widget/src/state/slices/bridgeQuote/hooks.ts index 7e49f05e9d..3e40f38195 100644 --- a/packages/widget/src/state/slices/bridgeQuote/hooks.ts +++ b/packages/widget/src/state/slices/bridgeQuote/hooks.ts @@ -26,6 +26,7 @@ export const fetchBridgeQuote = createAsyncThunk( synapseSDK, requestId, pausedModules, + timestamp, }: { originChainId: number destinationChainId: number @@ -36,6 +37,7 @@ export const fetchBridgeQuote = createAsyncThunk( synapseSDK: any requestId: number pausedModules: any + timestamp: number }) => { const allQuotes = await synapseSDK.allBridgeQuotes( originChainId, @@ -120,6 +122,7 @@ export const fetchBridgeQuote = createAsyncThunk( estimatedTime, bridgeModuleName, requestId, + timestamp, } } ) diff --git a/packages/widget/src/state/slices/bridgeQuote/reducer.ts b/packages/widget/src/state/slices/bridgeQuote/reducer.ts index 698faf8a12..b2c90ac97b 100644 --- a/packages/widget/src/state/slices/bridgeQuote/reducer.ts +++ b/packages/widget/src/state/slices/bridgeQuote/reducer.ts @@ -29,6 +29,7 @@ export type BridgeQuote = { estimatedTime: number bridgeModuleName: string requestId: number + timestamp: number } export const EMPTY_BRIDGE_QUOTE = { @@ -43,6 +44,7 @@ export const EMPTY_BRIDGE_QUOTE = { estimatedTime: null, bridgeModuleName: null, requestId: null, + timestamp: null, } export interface BridgeQuoteState { diff --git a/packages/widget/src/state/slices/wallet/reducer.ts b/packages/widget/src/state/slices/wallet/reducer.ts index ed9318f105..6548f3ace7 100644 --- a/packages/widget/src/state/slices/wallet/reducer.ts +++ b/packages/widget/src/state/slices/wallet/reducer.ts @@ -15,6 +15,7 @@ export interface WalletState { allowance: string status: FetchState error?: any + isWalletPending: boolean } const initialState: WalletState = { @@ -22,12 +23,17 @@ const initialState: WalletState = { allowance: null, status: FetchState.IDLE, error: null, + isWalletPending: false, } export const walletSlice = createSlice({ name: 'wallet', initialState, - reducers: {}, + reducers: { + setIsWalletPending: (state, action: PayloadAction) => { + state.isWalletPending = action.payload + }, + }, extraReducers: (builder) => { builder .addCase(fetchAndStoreTokenBalances.pending, (state) => { @@ -66,4 +72,6 @@ export const walletSlice = createSlice({ }, }) +export const { setIsWalletPending } = walletSlice.actions + export default walletSlice.reducer diff --git a/packages/widget/src/utils/calculateTimeBetween.ts b/packages/widget/src/utils/calculateTimeBetween.ts new file mode 100644 index 0000000000..cf2d332229 --- /dev/null +++ b/packages/widget/src/utils/calculateTimeBetween.ts @@ -0,0 +1,6 @@ +export const calculateTimeBetween = ( + timeBefore: number, + timeAfter: number +): number => { + return Math.abs(timeBefore - timeAfter) * 1000 +}