Skip to content

Commit

Permalink
feat(synapse-interface): bridge quote state validations (#3019)
Browse files Browse the repository at this point in the history
* Extracts bridgeQuote into own state

* Moves quote fetching to async thunk

* Bridge approved check hook

* Deprecates redux fromValue in favor of local input component behavior

* useBridgeSelections

* useBridgeValidations

* fix: stale output, infinite loader

* fix: track when input less than fees

* apply validation + selection hook to input/output containes

* use bridge state for bridge write actions

* Fix chain comparison

* feat: compare input amount vs tracked quoted input

* Compare stringified bridge quote v bridge state for validation

* feat: track quoted tokens <> bridge quote (#3018)

* replace validation in callback with button check

* memoize bridge selection comparisons

* segment tracking state <> quote mismatch error

* simplify segment

* switch order

* Add back doesBridgeStateMatchQuote

* add back additional bridge quote fields

* remove unused bridge selection vars

* fix: conditions for active button for connect wallet

* fix: condition for showing amount must be greater than fee

* Checks if user is typing

* Adds swap behavior

---------

Co-authored-by: abtestingalpha <[email protected]>

* Post submit

* Clears input value post submit

---------

Co-authored-by: bigboydiamonds <[email protected]>
  • Loading branch information
abtestingalpha and bigboydiamonds authored Aug 26, 2024
1 parent 013d4b9 commit 649b3f5
Show file tree
Hide file tree
Showing 29 changed files with 751 additions and 1,181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface ChainPause {
disableCountdown: boolean
}

interface BridgeModulePause {
export interface BridgeModulePause {
chainId?: number // If undefined, pause bridge module for all chains.
bridgeModuleName: 'SynapseBridge' | 'SynapseRFQ' | 'SynapseCCTP' | 'ALL'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getValidAddress, isValidAddress } from '@/utils/isValidAddress'
import { EMPTY_BRIDGE_QUOTE } from '@/constants/bridge'
import { CHAINS_BY_ID } from '@constants/chains'
import * as CHAINS from '@constants/chains/master'
import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'

export const BridgeExchangeRateInfo = () => {
/* TODO:
Expand Down Expand Up @@ -55,13 +56,14 @@ const DestinationAddress = () => {
}

const Slippage = () => {
const { debouncedFromValue } = useBridgeState()

const {
fromValue,
bridgeQuote: { exchangeRate },
} = useBridgeState()
} = useBridgeQuoteState()

const { formattedPercentSlippage, safeFromAmount, underFee, textColor } =
useExchangeRateInfo(fromValue, exchangeRate)
useExchangeRateInfo(debouncedFromValue, exchangeRate)
return (
<div className="flex justify-between">
<span className="text-zinc-500 dark:text-zinc-400">Slippage</span>
Expand All @@ -77,7 +79,7 @@ const Slippage = () => {
const Router = () => {
const {
bridgeQuote: { bridgeModuleName },
} = useBridgeState()
} = useBridgeQuoteState()
return (
<div className="flex justify-between">
<span className="text-zinc-500 dark:text-zinc-400">Router</span>
Expand All @@ -87,7 +89,8 @@ const Router = () => {
}

const TimeEstimate = () => {
const { fromToken, bridgeQuote } = useBridgeState()
const { fromToken } = useBridgeState()
const { bridgeQuote } = useBridgeQuoteState()

let showText
let showTime
Expand Down Expand Up @@ -125,10 +128,10 @@ const TimeEstimate = () => {

const GasDropLabel = () => {
let decimalsToDisplay
const { toChainId } = useBridgeState()
const {
bridgeQuote: { gasDropAmount },
toChainId,
} = useBridgeState()
} = useBridgeQuoteState()
const symbol = CHAINS_BY_ID[toChainId]?.nativeCurrency.symbol

if ([CHAINS.FANTOM.id].includes(toChainId)) {
Expand Down Expand Up @@ -166,9 +169,9 @@ const GasDropLabel = () => {
)
}

const useExchangeRateInfo = (fromValue, exchangeRate) => {
const useExchangeRateInfo = (value, exchangeRate) => {
const safeExchangeRate = typeof exchangeRate === 'bigint' ? exchangeRate : 0n
const safeFromAmount = fromValue ?? '0'
const safeFromAmount = value ?? '0'

const formattedExchangeRate = formatBigIntToString(safeExchangeRate, 18, 4)
const numExchangeRate = Number(formattedExchangeRate)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import { useMemo } from 'react'
import { TransactionButton } from '@/components/buttons/TransactionButton'
import { EMPTY_BRIDGE_QUOTE, EMPTY_BRIDGE_QUOTE_ZERO } from '@/constants/bridge'
import { useAccount, useAccountEffect, useSwitchChain } from 'wagmi'
import { useEffect, useState } from 'react'
import { isAddress } from 'viem'

import { useEffect, useState } from 'react'
import { useAccount, useAccountEffect, useSwitchChain } from 'wagmi'
import { useConnectModal } from '@rainbow-me/rainbowkit'
import { stringToBigInt } from '@/utils/bigint/format'
import { useBridgeDisplayState, useBridgeState } from '@/slices/bridge/hooks'
import { usePortfolioBalances } from '@/slices/portfolio/hooks'

import { useAppDispatch } from '@/store/hooks'
import { setIsDestinationWarningAccepted } from '@/slices/bridgeDisplaySlice'
import { useWalletState } from '@/slices/wallet/hooks'
import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
import { setIsDestinationWarningAccepted } from '@/slices/bridgeDisplaySlice'
import { useBridgeDisplayState, useBridgeState } from '@/slices/bridge/hooks'
import { TransactionButton } from '@/components/buttons/TransactionButton'
import { useBridgeValidations } from './hooks/useBridgeValidations'
import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider'

export const BridgeTransactionButton = ({
approveTxn,
executeBridge,
isApproved,
isBridgePaused,
isTyping,
}) => {
const dispatch = useAppDispatch()
const [isConnected, setIsConnected] = useState(false)
const { openConnectModal } = useConnectModal()
const [isConnected, setIsConnected] = useState(false)

const { chain, isConnected: isConnectedInit } = useAccount()
const { isConnected: isConnectedInit } = useAccount()
const { chains, switchChain } = useSwitchChain()

useAccountEffect({
Expand All @@ -39,63 +39,38 @@ export const BridgeTransactionButton = ({
const {
destinationAddress,
fromToken,
fromValue,
toToken,
fromChainId,
toToken,
toChainId,
isLoading,
bridgeQuote,
debouncedFromValue,
} = useBridgeState()
const { bridgeQuote, isLoading } = useBridgeQuoteState()

const { isWalletPending } = useWalletState()
const { showDestinationWarning, isDestinationWarningAccepted } =
useBridgeDisplayState()

const balances = usePortfolioBalances()
const balancesForChain = balances[fromChainId]
const balanceForToken = balancesForChain?.find(
(t) => t.tokenAddress === fromToken?.addresses[fromChainId]
)?.balance

const sufficientBalance = useMemo(() => {
if (!fromChainId || !fromToken || !toChainId || !toToken) return false
return (
stringToBigInt(fromValue, fromToken?.decimals[fromChainId]) <=
balanceForToken
)
}, [balanceForToken, fromValue, fromChainId, toChainId, toToken])

const fromTokenDecimals: number | undefined =
fromToken && fromToken?.decimals[fromChainId]

const fromValueBigInt = useMemo(() => {
return fromTokenDecimals ? stringToBigInt(fromValue, fromTokenDecimals) : 0
}, [fromValue, fromTokenDecimals])

const bridgeQuoteAmountGreaterThanInputForRfq = useMemo(() => {
return (
bridgeQuote.bridgeModuleName === 'SynapseRFQ' &&
bridgeQuote.outputAmount > fromValueBigInt
)
}, [bridgeQuote.outputAmount, fromValueBigInt])

const chainSelectionsMatchBridgeQuote = useMemo(() => {
return (
fromChainId === bridgeQuote.originChainId &&
toChainId === bridgeQuote.destChainId
)
}, [fromChainId, toChainId, bridgeQuote])
const {
hasValidInput,
hasValidQuote,
hasSufficientBalance,
doesBridgeStateMatchQuote,
isBridgeFeeGreaterThanInput,
isBridgeQuoteAmountGreaterThanInputForRfq,
onSelectedChain,
} = useBridgeValidations()

const isButtonDisabled =
isBridgePaused ||
isTyping ||
isLoading ||
isWalletPending ||
bridgeQuote === EMPTY_BRIDGE_QUOTE_ZERO ||
bridgeQuote === EMPTY_BRIDGE_QUOTE ||
(destinationAddress && !isAddress(destinationAddress)) ||
(isConnected && !sufficientBalance) ||
bridgeQuoteAmountGreaterThanInputForRfq ||
!chainSelectionsMatchBridgeQuote ||
isBridgePaused
!hasValidInput ||
!doesBridgeStateMatchQuote ||
isBridgeQuoteAmountGreaterThanInputForRfq ||
(isConnected && !hasValidQuote) ||
(isConnected && !hasSufficientBalance) ||
(destinationAddress && !isAddress(destinationAddress))

let buttonProperties

Expand Down Expand Up @@ -124,39 +99,48 @@ export const BridgeTransactionButton = ({
label: `Bridge ${fromToken?.symbol}`,
onClick: null,
}
} else if (
!isLoading &&
bridgeQuote?.feeAmount === 0n &&
fromValueBigInt > 0
) {
} else if (!isConnected && hasValidInput) {
buttonProperties = {
label: `Connect Wallet to Bridge`,
onClick: openConnectModal,
}
} else if (!isLoading && isBridgeFeeGreaterThanInput && hasValidInput) {
buttonProperties = {
label: `Amount must be greater than fee`,
onClick: null,
}
} else if (
bridgeQuote.bridgeModuleName !== null &&
!isLoading &&
!chainSelectionsMatchBridgeQuote &&
fromValueBigInt > 0
!isTyping &&
!doesBridgeStateMatchQuote &&
hasValidInput
) {
buttonProperties = {
label: 'Please reset chain selection',
label: 'Error in bridge quote',
onClick: null,
}

segmentAnalyticsEvent(`[Bridge] error: state out of sync with quote`, {
inputAmountForState: debouncedFromValue,
originChainIdForState: fromChainId,
originTokenForState: fromToken.symbol,
originTokenAddressForState: fromToken.addresses[fromChainId],
destinationChainIdForState: toChainId,
destinationTokenForState: toToken.symbol,
destinationTokenAddressForState: toToken.addresses[toChainId],
bridgeQuote,
})
} else if (
!isLoading &&
bridgeQuoteAmountGreaterThanInputForRfq &&
fromValueBigInt > 0
isBridgeQuoteAmountGreaterThanInputForRfq &&
hasValidInput
) {
buttonProperties = {
label: 'Invalid bridge quote',
onClick: null,
}
} else if (!isConnected && fromValueBigInt > 0) {
buttonProperties = {
label: `Connect Wallet to Bridge`,
onClick: openConnectModal,
}
} else if (!isLoading && isConnected && !sufficientBalance) {
} else if (!isLoading && isConnected && !hasSufficientBalance) {
buttonProperties = {
label: 'Insufficient balance',
onClick: null,
Expand All @@ -171,13 +155,13 @@ export const BridgeTransactionButton = ({
onClick: () => dispatch(setIsDestinationWarningAccepted(true)),
className: '!from-bgLight !to-bgLight',
}
} else if (chain?.id != fromChainId && fromValueBigInt > 0) {
} else if (!onSelectedChain && hasValidInput) {
buttonProperties = {
label: `Switch to ${chains.find((c) => c.id === fromChainId)?.name}`,
onClick: () => switchChain({ chainId: fromChainId }),
pendingLabel: 'Switching chains',
}
} else if (!isApproved && fromValueBigInt > 0 && bridgeQuote?.destQuery) {
} else if (!isApproved && hasValidInput && hasValidQuote) {
buttonProperties = {
onClick: approveTxn,
label: `Approve ${fromToken?.symbol}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { setFromChainId } from '@/slices/bridge/reducer'
import { ChainSelector } from '@/components/ui/ChainSelector'
import { CHAINS_BY_ID } from '@/constants/chains'
import { useFromChainListArray } from './hooks/useFromChainListArray'
import { useBridgeState } from '@/slices/bridge/hooks'
import { useWalletState } from '@/slices/wallet/hooks'

export const FromChainSelector = () => {
const { fromChainId } = useBridgeState()
const { isWalletPending } = useWalletState()

return (
<ChainSelector
dataTestId="bridge-origin-chain"
selectedItem={CHAINS_BY_ID[fromChainId]}
isOrigin={true}
label="From"
itemListFunction={useFromChainListArray}
setFunction={setFromChainId}
action="Bridge"
disabled={isWalletPending}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { setFromToken } from '@/slices/bridge/reducer'
import { TokenSelector } from '@/components/ui/TokenSelector'
import { useBridgeState } from '@/slices/bridge/hooks'
import { useFromTokenListArray } from './hooks/useFromTokenListArray'
import { useWalletState } from '@/slices/wallet/hooks'

export const FromTokenSelector = () => {
const { fromToken } = useBridgeState()
const { isWalletPending } = useWalletState()

return (
<TokenSelector
dataTestId="bridge-origin-token"
selectedItem={fromToken}
isOrigin={true}
placeholder="Out"
itemListFunction={useFromTokenListArray}
setFunction={setFromToken}
action="Bridge"
disabled={isWalletPending}
/>
)
}
Loading

0 comments on commit 649b3f5

Please sign in to comment.