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 all 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,113 @@
import { useState, useEffect, useMemo } from 'react'

import { BridgeQuote } from '@/utils/types'
import { convertMsToSeconds } from '@/utils/time'

export const BridgeQuoteResetTimer = ({
bridgeQuote,
isLoading,
isActive,
duration, // in ms
}: {
bridgeQuote: BridgeQuote
isLoading: boolean
isActive: boolean
duration: number
}) => {
const memoizedTimer = useMemo(() => {
if (!isActive) return null

if (isLoading) {
return <AnimatedLoadingCircle />
} else {
return (
<AnimatedProgressCircle
animateKey={bridgeQuote.id}
duration={duration}
/>
)
}
}, [bridgeQuote, duration, isActive])

return memoizedTimer
}

const AnimatedLoadingCircle = () => {
return (
<svg
width="24"
height="24"
viewBox="-12 -12 24 24"
stroke="currentcolor"
fill="none"
className="absolute block -rotate-90"
>
<circle r="8" pathLength="1" stroke-dashArray="0.05" stroke-opacity=".5">
<animate
attributeName="stroke-dashoffset"
to="-1"
dur="2.5s"
repeatCount="indefinite"
/>
</circle>
</svg>
)
}

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"
fill="none"
className="absolute block -rotate-90"
>
<circle r="8" pathLength="1" stroke-opacity=".25">
<animate
attributeName="stroke-dashoffset"
to="-1"
dur="2.5s"
repeatCount="indefinite"
/>
<set
attributeName="stroke-dasharray"
to="0.05"
begin={`${convertMsToSeconds(duration)}s`}
/>
<set
attributeName="stroke-opacity"
to="0.5"
begin={`${convertMsToSeconds(duration)}s`}
/>
</circle>
<circle r="8" stroke-dasharray="1" pathLength="1">
<animate
attributeName="stroke-dashoffset"
values="2; 1"
dur={`${convertMsToSeconds(duration)}s`}
fill="freeze"
/>
<animate
attributeName="stroke-opacity"
values="0; 1"
dur={`${convertMsToSeconds(duration)}s`}
/>
</circle>
</svg>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ 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'

export const BridgeTransactionButton = ({
approveTxn,
executeBridge,
isApproved,
isBridgePaused,
isTyping,
isQuoteStale,
}) => {
const dispatch = useAppDispatch()
const { openConnectModal } = useConnectModal()
Expand Down Expand Up @@ -48,6 +50,8 @@ export const BridgeTransactionButton = ({
debouncedFromValue,
} = useBridgeState()
const { bridgeQuote, isLoading } = useBridgeQuoteState()
const { isPendingConfirmChange, onUserAcceptChange } =
useConfirmNewBridgePrice()

const { isWalletPending } = useWalletState()
const { showDestinationWarning, isDestinationWarningAccepted } =
Expand All @@ -73,6 +77,7 @@ export const BridgeTransactionButton = ({
isBridgeQuoteAmountGreaterThanInputForRfq ||
(isConnected && !hasValidQuote) ||
(isConnected && !hasSufficientBalance) ||
(isConnected && isQuoteStale) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

Adjust 'isButtonDisabled' logic to enable user confirmation

Including (isConnected && isQuoteStale) in isButtonDisabled disables the button when the quote is stale, even when user confirmation is required. Modify the condition to exclude cases where isPendingConfirmChange is true, allowing the user to confirm the new quote.

Apply this diff to adjust the disabling logic:

     const isButtonDisabled =
       isBridgePaused ||
       isTyping ||
       isLoading ||
       isWalletPending ||
       !hasValidInput ||
       !doesBridgeStateMatchQuote ||
       isBridgeQuoteAmountGreaterThanInputForRfq ||
       (isConnected && !hasValidQuote) ||
       (isConnected && !hasSufficientBalance) ||
-      (isConnected && isQuoteStale) ||
+      (isConnected && isQuoteStale && !isPendingConfirmChange) ||
       (destinationAddress && !isAddress(destinationAddress))

Committable suggestion was skipped due to low confidence.

(destinationAddress && !isAddress(destinationAddress))

let buttonProperties
Expand All @@ -97,6 +102,26 @@ export const BridgeTransactionButton = ({
label: t('Please select an Origin token'),
onClick: null,
}
} else if (isConnected && !hasSufficientBalance) {
buttonProperties = {
label: t('Insufficient balance'),
onClick: null,
}
} else if (isLoading && hasValidQuote) {
buttonProperties = {
label: isPendingConfirmChange
? t('Confirm new quote')
: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
pendingLabel: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
onClick: null,
className: `
${
isPendingConfirmChange
? '!outline !outline-1 !outline-synapsePurple !outline-offset-[-1px] !from-bgLight !to-bgLight'
: '!bg-gradient-to-r !from-fuchsia-500 !to-purple-500 dark:!to-purple-600'
}
!opacity-100`,
}
Comment on lines +110 to +124
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure 'Confirm new quote' button is actionable when user confirmation is required

When isPendingConfirmChange is true, the button displays "Confirm new quote" but onClick is set to null, preventing the user from confirming the new quote. To allow users to proceed, the onClick handler should invoke onUserAcceptChange.

Apply this diff to set onClick appropriately:

       buttonProperties = {
         label: isPendingConfirmChange
           ? t('Confirm new quote')
           : t('Bridge {symbol}', { symbol: fromToken?.symbol }),
         pendingLabel: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
-        onClick: null,
+        onClick: isPendingConfirmChange ? () => onUserAcceptChange() : null,
         className: `
           ${
             isPendingConfirmChange
               ? '!outline !outline-1 !outline-synapsePurple !outline-offset-[-1px] !from-bgLight !to-bgLight'
               : '!bg-gradient-to-r !from-fuchsia-500 !to-purple-500 dark:!to-purple-600'
           }
           !opacity-100`,
       }
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
} else if (isLoading && hasValidQuote) {
buttonProperties = {
label: isPendingConfirmChange
? t('Confirm new quote')
: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
pendingLabel: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
onClick: null,
className: `
${
isPendingConfirmChange
? '!outline !outline-1 !outline-synapsePurple !outline-offset-[-1px] !from-bgLight !to-bgLight'
: '!bg-gradient-to-r !from-fuchsia-500 !to-purple-500 dark:!to-purple-600'
}
!opacity-100`,
}
} else if (isLoading && hasValidQuote) {
buttonProperties = {
label: isPendingConfirmChange
? t('Confirm new quote')
: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
pendingLabel: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
onClick: isPendingConfirmChange ? () => onUserAcceptChange() : null,
className: `
${
isPendingConfirmChange
? '!outline !outline-1 !outline-synapsePurple !outline-offset-[-1px] !from-bgLight !to-bgLight'
: '!bg-gradient-to-r !from-fuchsia-500 !to-purple-500 dark:!to-purple-600'
}
!opacity-100`,
}

} else if (isLoading) {
buttonProperties = {
label: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
Expand Down Expand Up @@ -144,11 +169,6 @@ export const BridgeTransactionButton = ({
label: t('Invalid bridge quote'),
onClick: null,
}
} else if (!isLoading && isConnected && !hasSufficientBalance) {
buttonProperties = {
label: t('Insufficient balance'),
onClick: null,
}
} else if (destinationAddress && !isAddress(destinationAddress)) {
buttonProperties = {
label: t('Invalid Destination address'),
Expand All @@ -167,6 +187,13 @@ export const BridgeTransactionButton = ({
onClick: () => switchChain({ chainId: fromChainId }),
pendingLabel: t('Switching chains'),
}
} else if (isApproved && hasValidQuote && isPendingConfirmChange) {
buttonProperties = {
label: t('Confirm new quote'),
onClick: () => onUserAcceptChange(),
className:
'!outline !outline-1 !outline-synapsePurple !outline-offset-[-1px] !from-bgLight !to-bgLight',
}
Comment on lines +190 to +196
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid redundant condition by reusing existing logic

The condition starting at line 190 checks for isApproved && hasValidQuote && isPendingConfirmChange, which may overlap with earlier conditions. To simplify and prevent duplication, consider combining this logic with the adjustment made to isButtonDisabled.

} else if (!isApproved && hasValidInput && hasValidQuote) {
buttonProperties = {
onClick: approveTxn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
import { useBridgeValidations } from './hooks/useBridgeValidations'
import { useTranslations } from 'next-intl'

export const OutputContainer = () => {
interface OutputContainerProps {
isQuoteStale: boolean
}

export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => {
const { address } = useAccount()
const { bridgeQuote, isLoading } = useBridgeQuoteState()
const { showDestinationAddress } = useBridgeDisplayState()
Expand All @@ -33,6 +37,8 @@ export const OutputContainer = () => {
}
}, [bridgeQuote, hasValidInput, hasValidQuote])

const inputClassName = isQuoteStale ? 'opacity-50' : undefined

return (
<BridgeSectionContainer>
<div className="flex items-center justify-between">
Expand All @@ -48,6 +54,7 @@ export const OutputContainer = () => {
disabled={true}
showValue={showValue}
isLoading={isLoading}
className={inputClassName}
/>
</BridgeAmountContainer>
</BridgeSectionContainer>
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,125 @@
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 triggerQuoteRef = useRef<BridgeQuote | null>(null)
const bpsThreshold = 0.0001 // 1bps

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]
)

const isPendingConfirmChange =
hasQuoteOutputChanged &&
hasSameSelectionsAsPreviousQuote &&
!hasUserConfirmedChange

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

const hasBridgeModuleChanged =
bridgeQuote?.bridgeModuleName !==
(triggerQuoteRef.current?.bridgeModuleName ??
previousBridgeQuote?.bridgeModuleName)

const outputAmountDiffMoreThanThreshold = validQuotes
? calculateOutputRelativeDifference(
bridgeQuote,
triggerQuoteRef.current ?? previousBridgeQuote
) > bpsThreshold
: false

Comment on lines +64 to +70
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure safe comparison when 'calculateOutputRelativeDifference' may return null

The function calculateOutputRelativeDifference may return null if outputAmountString is missing or if there's a division by zero. Comparing null directly with a number using the > operator can lead to unintended behavior. It's important to check for null before performing the comparison to ensure the application logic is sound.

Apply the following changes to handle the null case safely:

 const diff = calculateOutputRelativeDifference(
   bridgeQuote,
   triggerQuoteRef.current ?? previousBridgeQuote
 )

 const outputAmountDiffMoreThanThreshold = validQuotes
-  ? diff > bpsThreshold
+  ? diff !== null && diff > bpsThreshold
   : false

Committable suggestion was skipped due to low confidence.

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

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

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

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

return {
isPendingConfirmChange,
onUserAcceptChange,
}
}

const calculateOutputRelativeDifference = (
currentQuote?: BridgeQuote,
previousQuote?: BridgeQuote
) => {
if (!currentQuote?.outputAmountString || !previousQuote?.outputAmountString) {
return null
}

const currentOutput = parseFloat(currentQuote.outputAmountString)
const previousOutput = parseFloat(previousQuote.outputAmountString)

if (previousOutput === 0) {
return null
}

return (previousOutput - currentOutput) / previousOutput
}
Loading
Loading