Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(synapse-interface): confirm new price [SLT-150] #3084

Merged
merged 59 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
38dec50
bridge quote history middleware
abtestingalpha Aug 29, 2024
f44b62c
skip resetting quote at beginning of fetch
bigboydiamonds Aug 30, 2024
3e38869
bridge button requests user to confirm new bridge price when detected
bigboydiamonds Aug 30, 2024
bd29349
conditions for displaying confirm price
bigboydiamonds Aug 30, 2024
76663ba
confirm prices based on initial triggered ref
bigboydiamonds Aug 30, 2024
18c8176
nit
bigboydiamonds Aug 30, 2024
4e37c9a
Merge branch 'master' into fe/confirm-new-price
bigboydiamonds Aug 30, 2024
6d14aca
request User confirm price if change greater than 1 bps
bigboydiamonds Sep 3, 2024
cd75683
callback functions to handle creating/accepting/reset confirm flow
bigboydiamonds Sep 3, 2024
febd306
request user confirm after first accepted
bigboydiamonds Sep 4, 2024
e7df238
fe/updating-quote (#3104)
bigboydiamonds Sep 5, 2024
69f4034
Merge branch 'fe/confirm-new-price' of https://github.com/synapsecns/…
bigboydiamonds Sep 5, 2024
571b391
clean logic
bigboydiamonds Sep 5, 2024
c7b2981
[WIP] stale quote animation (#3105)
bigboydiamonds Sep 7, 2024
62d7735
Merge branch 'master' into fe/confirm-new-price
bigboydiamonds Sep 9, 2024
93dc9da
prevent refetch during pending wallet
bigboydiamonds Sep 9, 2024
e7e2f31
update reset animation
bigboydiamonds Sep 9, 2024
32de57c
disable bridge button when quote stale
bigboydiamonds Sep 9, 2024
d04b69a
Merge branch 'fe/confirm-new-price' of https://github.com/synapsecns/…
bigboydiamonds Sep 9, 2024
f1118b1
yarn install
bigboydiamonds Sep 9, 2024
f682d73
update confirm button tet
bigboydiamonds Sep 9, 2024
cd83681
display greyed out output when stale quote
bigboydiamonds Sep 9, 2024
c47a4b6
store threshold as constant
bigboydiamonds Sep 10, 2024
03549a6
auto refresher duration, resets on mouse move
bigboydiamonds Sep 10, 2024
e8eb2a0
persist animation after User confirms new quote
bigboydiamonds Sep 10, 2024
5ad3190
update animation
bigboydiamonds Sep 10, 2024
0d729b1
show animation condition
bigboydiamonds Sep 11, 2024
e8000fc
Merge branch 'master' into fe/confirm-new-price
bigboydiamonds Sep 11, 2024
ef5a504
reset bps threshold
bigboydiamonds Sep 11, 2024
9abf1b4
Merge branch 'fe/confirm-new-price' of https://github.com/synapsecns/…
bigboydiamonds Sep 11, 2024
5c42768
bridge button conditions
bigboydiamonds Sep 11, 2024
10fd34c
Add new i8n phrases
bigboydiamonds Sep 11, 2024
eea56c7
rm unused prop
bigboydiamonds Sep 11, 2024
835003c
conditions to show countdown animation
bigboydiamonds Sep 11, 2024
c2c185b
remove pulse effect, add i8n for en-US
bigboydiamonds Sep 11, 2024
194799b
button outline to prevent shift, disable update when approval required
bigboydiamonds Sep 11, 2024
1112571
require approval before confirm modal shows
bigboydiamonds Sep 11, 2024
54a9e01
require wallet connected for updater to be active
bigboydiamonds Sep 11, 2024
fae7078
update button visuals
bigboydiamonds Sep 11, 2024
c4701d8
retain same button colors as previous state when refreshing
bigboydiamonds Sep 11, 2024
b383e86
clean
bigboydiamonds Sep 11, 2024
e14285d
mv
bigboydiamonds Sep 11, 2024
c7e3206
refactor
bigboydiamonds Sep 12, 2024
6f57fd6
refactor
bigboydiamonds Sep 12, 2024
bd4294a
add translations
bigboydiamonds Sep 12, 2024
c80c956
Request confirm if bridge module changes
bigboydiamonds Sep 12, 2024
d580509
remove updating quote button state, match loading texts
bigboydiamonds Sep 12, 2024
080c5ff
update spinner animation
bigboydiamonds Sep 12, 2024
1c5414f
mv util
bigboydiamonds Sep 12, 2024
5623a0d
loader animation for quote refresh
bigboydiamonds Sep 12, 2024
6436cab
rm flag
bigboydiamonds Sep 12, 2024
6266b40
Merge branch 'master' into fe/confirm-new-price
bigboydiamonds Sep 12, 2024
82d447a
Confirm new quote
bigboydiamonds Sep 21, 2024
383519d
remove icon pointer events
bigboydiamonds Sep 23, 2024
f74ca92
trigger request user confirm on negative price shift
bigboydiamonds Sep 23, 2024
c63eb5d
fix compare logic
bigboydiamonds Sep 23, 2024
8627728
suggestions
bigboydiamonds Sep 23, 2024
8dbadb9
fix quote comparison
bigboydiamonds Sep 23, 2024
907d0e1
lint
bigboydiamonds Sep 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { BridgeQuote } from '@/utils/types'
import { useState, useEffect } from 'react'

export const BridgeQuoteResetTimer = ({
bridgeQuote,
hasValidQuote,
duration, // in ms
}: {
bridgeQuote: BridgeQuote
hasValidQuote: boolean
duration: number
}) => {
if (hasValidQuote) {
return (
<AnimatedProgressCircle animateKey={bridgeQuote.id} duration={duration} />
)
}
}

const AnimatedProgressCircle = ({
animateKey,
duration,
}: {
animateKey: string
duration: number
}) => {
const [animationKey, setAnimationKey] = useState(0)

useEffect(() => {
setAnimationKey((prevKey) => prevKey + 1)
}, [animateKey])

return (
<svg
key={animationKey}
width="24"
height="24"
viewBox="-12 -12 24 24"
stroke="currentcolor"
strokeOpacity=".33"
fill="none"
className="absolute -rotate-90 -scale-y-100 right-4"
>
<circle r="8" />
<circle r="8" strokeDasharray="1" pathLength="1">
<animate
attributeName="stroke-dashoffset"
values="1; 2"
dur={`${convertMsToSeconds(duration)}s`}
/>
</circle>
</svg>
)
}

const convertMsToSeconds = (ms: number) => {
return Math.ceil(ms / 1000)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import { useBridgeDisplayState, useBridgeState } from '@/slices/bridge/hooks'
import { TransactionButton } from '@/components/buttons/TransactionButton'
import { useBridgeValidations } from './hooks/useBridgeValidations'
import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider'
import { useConfirmNewBridgePrice } from './hooks/useConfirmNewBridgePrice'
import { BridgeQuoteResetTimer } from './AnimatedProgressCircle'

export const BridgeTransactionButton = ({
approveTxn,
executeBridge,
isApproved,
isBridgePaused,
isTyping,
isQuoteStale,
quoteTimeout,
}) => {
const dispatch = useAppDispatch()
const { openConnectModal } = useConnectModal()
Expand Down Expand Up @@ -45,6 +49,12 @@ export const BridgeTransactionButton = ({
debouncedFromValue,
} = useBridgeState()
const { bridgeQuote, isLoading } = useBridgeQuoteState()
const {
hasSameSelectionsAsPreviousQuote,
hasQuoteOutputChanged,
hasUserConfirmedChange,
onUserAcceptChange,
} = useConfirmNewBridgePrice()

const { isWalletPending } = useWalletState()
const { showDestinationWarning, isDestinationWarningAccepted } =
Expand All @@ -62,6 +72,7 @@ export const BridgeTransactionButton = ({

const isButtonDisabled =
isBridgePaused ||
isQuoteStale ||
isTyping ||
isLoading ||
isWalletPending ||
Expand Down Expand Up @@ -94,6 +105,16 @@ export const BridgeTransactionButton = ({
label: `Please select an Origin token`,
onClick: null,
}
} else if (isConnected && !hasSufficientBalance) {
buttonProperties = {
label: 'Insufficient balance',
onClick: null,
}
} else if (isLoading && hasSameSelectionsAsPreviousQuote) {
buttonProperties = {
label: 'Updating quote',
onClick: null,
}
} else if (isLoading) {
buttonProperties = {
label: `Bridge ${fromToken?.symbol}`,
Expand Down Expand Up @@ -141,11 +162,6 @@ export const BridgeTransactionButton = ({
label: 'Invalid bridge quote',
onClick: null,
}
} else if (!isLoading && isConnected && !hasSufficientBalance) {
buttonProperties = {
label: 'Insufficient balance',
onClick: null,
}
} else if (destinationAddress && !isAddress(destinationAddress)) {
buttonProperties = {
label: 'Invalid Destination address',
Expand All @@ -162,6 +178,13 @@ export const BridgeTransactionButton = ({
onClick: () => switchChain({ chainId: fromChainId }),
pendingLabel: 'Switching chains',
}
} else if (hasQuoteOutputChanged && !hasUserConfirmedChange) {
buttonProperties = {
label: 'Confirm new price',
Copy link
Collaborator

@abtestingalpha abtestingalpha Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bridgeModule could change as well. Example: User first gets RFQ quote, and then inventory level changes. The quote is now through CCTP. Now the price/the module/ the fees are all different.

"Confirm new quote" (instead of price) covers both cases. Does the plumbing in the hook cover this as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - confirming that the hook covers the scenario that bridgeModule changes while bridge selections remain the same as previous quote.

The useConfirmNewPrice() hook will check for changes greater than 1bps given current and previous quotes share the same origin input amount, origin chainId, origin token, destination chainId, and destination token.

Updated button text in f682d73

onClick: () => onUserAcceptChange(),
className:
'!border !border-synapsePurple !from-bgLight !to-bgLight !animate-pulse',
}
} else if (!isApproved && hasValidInput && hasValidQuote) {
buttonProperties = {
onClick: approveTxn,
Expand All @@ -173,6 +196,13 @@ export const BridgeTransactionButton = ({
onClick: executeBridge,
label: `Bridge ${fromToken?.symbol}`,
pendingLabel: 'Bridging',
labelAnimation: (
<BridgeQuoteResetTimer
bridgeQuote={bridgeQuote}
hasValidQuote={hasValidQuote}
duration={quoteTimeout}
/>
),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const useBridgeValidations = () => {
}
}

const constructStringifiedBridgeSelections = (
export const constructStringifiedBridgeSelections = (
originAmount,
originChainId,
originToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useState, useEffect, useMemo, useRef } from 'react'

import { useBridgeState } from '@/slices/bridge/hooks'
import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
import { constructStringifiedBridgeSelections } from './useBridgeValidations'
import { BridgeQuote } from '@/utils/types'

export const useConfirmNewBridgePrice = () => {
const quoteRef = useRef<any>(null)

const [hasQuoteOutputChanged, setHasQuoteOutputChanged] =
useState<boolean>(false)
const [hasUserConfirmedChange, setHasUserConfirmedChange] =
useState<boolean>(false)

const { bridgeQuote, previousBridgeQuote } = useBridgeQuoteState()
const { debouncedFromValue, fromToken, toToken, fromChainId, toChainId } =
useBridgeState()

const currentBridgeQuoteSelections = useMemo(
() =>
constructStringifiedBridgeSelections(
debouncedFromValue,
fromChainId,
fromToken,
toChainId,
toToken
),
[debouncedFromValue, fromChainId, fromToken, toChainId, toToken]
)

const previousBridgeQuoteSelections = useMemo(
() =>
constructStringifiedBridgeSelections(
previousBridgeQuote?.inputAmountForQuote,
previousBridgeQuote?.originChainId,
previousBridgeQuote?.originTokenForQuote,
previousBridgeQuote?.destChainId,
previousBridgeQuote?.destTokenForQuote
),
[previousBridgeQuote]
)

const hasSameSelectionsAsPreviousQuote = useMemo(
() => currentBridgeQuoteSelections === previousBridgeQuoteSelections,
[currentBridgeQuoteSelections, previousBridgeQuoteSelections]
)

useEffect(() => {
const validQuotes =
bridgeQuote?.outputAmount && previousBridgeQuote?.outputAmount

const outputAmountDiffMoreThan1bps = validQuotes
? calculateOutputRelativeDifference(
bridgeQuote,
quoteRef.current ?? previousBridgeQuote
) > 0.0001
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth putting this into a constant if we need to update

: false

if (
validQuotes &&
outputAmountDiffMoreThan1bps &&
hasSameSelectionsAsPreviousQuote
) {
requestUserConfirmChange(previousBridgeQuote)
} else {
resetConfirm()
}
}, [bridgeQuote, previousBridgeQuote, hasSameSelectionsAsPreviousQuote])

const requestUserConfirmChange = (previousQuote: BridgeQuote) => {
if (!hasQuoteOutputChanged && !hasUserConfirmedChange) {
quoteRef.current = previousQuote
setHasQuoteOutputChanged(true)
}
setHasUserConfirmedChange(false)
}

const resetConfirm = () => {
if (hasUserConfirmedChange) {
quoteRef.current = null
setHasQuoteOutputChanged(false)
setHasUserConfirmedChange(false)
}
}

const onUserAcceptChange = () => {
quoteRef.current = null
setHasUserConfirmedChange(true)
}

return {
hasSameSelectionsAsPreviousQuote,
hasQuoteOutputChanged,
hasUserConfirmedChange,
onUserAcceptChange,
}
}

const calculateOutputRelativeDifference = (
quoteA?: BridgeQuote,
quoteB?: BridgeQuote
) => {
if (!quoteA?.outputAmountString || !quoteB?.outputAmountString) return null

const outputA = parseFloat(quoteA.outputAmountString)
const outputB = parseFloat(quoteB.outputAmountString)

return Math.abs(outputA - outputB) / outputB
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const TransactionButton = ({
onClick,
pendingLabel,
label,
labelAnimation,
onSuccess,
disabled,
chainId,
Expand All @@ -29,6 +30,7 @@ export const TransactionButton = ({
onClick: () => Promise<TransactionResponse | any>
pendingLabel: string
label: string
labelAnimation?: React.ReactNode
onSuccess?: () => void
chainId?: number
style?: CSSProperties
Expand Down Expand Up @@ -63,7 +65,9 @@ export const TransactionButton = ({
<span className="opacity-30">{pendingLabel}</span>{' '}
</>
) : (
<>{label}</>
<>
{label} {labelAnimation}
</>
)}
</Button>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
import { resetBridgeQuote } from '@/slices/bridgeQuote/reducer'
import { fetchBridgeQuote } from '@/slices/bridgeQuote/thunks'
import { useIsBridgeApproved } from '@/utils/hooks/useIsBridgeApproved'
import { isTransactionUserRejectedError } from '@/utils/isTransactionUserRejectedError'

const StateManagedBridge = () => {
const dispatch = useAppDispatch()
Expand Down Expand Up @@ -136,8 +137,6 @@ const StateManagedBridge = () => {

// will have to handle deadlineMinutes here at later time, gets passed as optional last arg in .bridgeQuote()

/* clear stored bridge quote before requesting new bridge quote */
dispatch(resetBridgeQuote())
const currentTimestamp: number = getUnixTimeMinutesFromNow(0)

try {
Expand Down Expand Up @@ -198,7 +197,7 @@ const StateManagedBridge = () => {
}
}

useStaleQuoteUpdater(
const isStale = useStaleQuoteUpdater(
bridgeQuote,
getAndSetBridgeQuote,
isLoading,
Expand Down Expand Up @@ -398,6 +397,10 @@ const StateManagedBridge = () => {
)
}

if (isTransactionUserRejectedError) {
getAndSetBridgeQuote()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix incorrect condition in error handling

The condition if (isTransactionUserRejectedError) is incorrect. It should be a function call to check if the error is a user-rejected transaction error. The correct usage would be if (isTransactionUserRejectedError(error)).

Apply this diff to fix the condition:

-if (isTransactionUserRejectedError) {
+if (isTransactionUserRejectedError(error)) {
    getAndSetBridgeQuote()
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isTransactionUserRejectedError) {
getAndSetBridgeQuote()
}
if (isTransactionUserRejectedError(error)) {
getAndSetBridgeQuote()
}


return txErrorHandler(error)
} finally {
dispatch(setIsWalletPending(false))
Expand Down Expand Up @@ -452,6 +455,8 @@ const StateManagedBridge = () => {
approveTxn={approveTxn}
executeBridge={executeBridge}
isBridgePaused={isBridgePaused}
isQuoteStale={isStale}
quoteTimeout={quoteTimeout}
/>
</>
)}
Expand Down
8 changes: 7 additions & 1 deletion packages/synapse-interface/slices/bridgeQuote/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { fetchBridgeQuote } from './thunks'

export interface BridgeQuoteState {
bridgeQuote: BridgeQuote
previousBridgeQuote: BridgeQuote | null
isLoading: boolean
}

export const initialState: BridgeQuoteState = {
bridgeQuote: EMPTY_BRIDGE_QUOTE,
previousBridgeQuote: null,
isLoading: false,
}

Expand All @@ -24,6 +26,9 @@ export const bridgeQuoteSlice = createSlice({
resetBridgeQuote: (state) => {
state.bridgeQuote = initialState.bridgeQuote
},
setPreviousBridgeQuote: (state, action: PayloadAction<any>) => {
state.previousBridgeQuote = action.payload
},
},
extraReducers: (builder) => {
builder
Expand All @@ -44,6 +49,7 @@ export const bridgeQuoteSlice = createSlice({
},
})

export const { resetBridgeQuote, setIsLoading } = bridgeQuoteSlice.actions
export const { resetBridgeQuote, setIsLoading, setPreviousBridgeQuote } =
bridgeQuoteSlice.actions

export default bridgeQuoteSlice.reducer
Loading
Loading