@@ -77,7 +79,7 @@ const Slippage = () => {
const Router = () => {
const {
bridgeQuote: { bridgeModuleName },
- } = useBridgeState()
+ } = useBridgeQuoteState()
return (
Router
@@ -87,7 +89,8 @@ const Router = () => {
}
const TimeEstimate = () => {
- const { fromToken, bridgeQuote } = useBridgeState()
+ const { fromToken } = useBridgeState()
+ const { bridgeQuote } = useBridgeQuoteState()
let showText
let showTime
@@ -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)) {
@@ -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)
diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx
index 9e76fc286b..48c0aa19ec 100644
--- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx
+++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx
@@ -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({
@@ -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
@@ -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,
@@ -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}`,
diff --git a/packages/synapse-interface/components/StateManagedBridge/FromChainSelector.tsx b/packages/synapse-interface/components/StateManagedBridge/FromChainSelector.tsx
new file mode 100644
index 0000000000..ecde8d2816
--- /dev/null
+++ b/packages/synapse-interface/components/StateManagedBridge/FromChainSelector.tsx
@@ -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 (
+
+ )
+}
diff --git a/packages/synapse-interface/components/StateManagedBridge/FromTokenSelector.tsx b/packages/synapse-interface/components/StateManagedBridge/FromTokenSelector.tsx
new file mode 100644
index 0000000000..cdf74c59f7
--- /dev/null
+++ b/packages/synapse-interface/components/StateManagedBridge/FromTokenSelector.tsx
@@ -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 (
+
+ )
+}
diff --git a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx
index 564efeea94..c6e70a5ae5 100644
--- a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx
+++ b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx
@@ -1,16 +1,9 @@
-import { isNull, isNumber } from 'lodash'
+import { debounce, isNull } from 'lodash'
import toast from 'react-hot-toast'
import React, { useEffect, useState, useCallback, useMemo } from 'react'
import { useAccount } from 'wagmi'
import { useAppDispatch } from '@/store/hooks'
-import {
- initialState,
- updateFromValue,
- setFromChainId,
- setFromToken,
-} from '@/slices/bridge/reducer'
-import { ChainSelector } from '@/components/ui/ChainSelector'
-import { TokenSelector } from '@/components/ui/TokenSelector'
+import { updateDebouncedFromValue } from '@/slices/bridge/reducer'
import { AmountInput } from '@/components/ui/AmountInput'
import { cleanNumberInput } from '@/utils/cleanNumberInput'
import {
@@ -18,47 +11,41 @@ import {
ConnectWalletButton,
ConnectedIndicator,
} from '@/components/ConnectionIndicators'
-import { CHAINS_BY_ID } from '@/constants/chains'
-import { useFromChainListArray } from './hooks/useFromChainListArray'
import { useBridgeState } from '@/slices/bridge/hooks'
-import { usePortfolioState } from '@/slices/portfolio/hooks'
import { BridgeSectionContainer } from '@/components/ui/BridgeSectionContainer'
import { BridgeAmountContainer } from '@/components/ui/BridgeAmountContainer'
-import { useFromTokenListArray } from './hooks/useFromTokenListArray'
import { AvailableBalance } from './AvailableBalance'
import { useGasEstimator } from '../../utils/hooks/useGasEstimator'
import { getParsedBalance } from '@/utils/getParsedBalance'
import { MaxButton } from './MaxButton'
import { formatAmount } from '../../utils/formatAmount'
import { useWalletState } from '@/slices/wallet/hooks'
+import { FromChainSelector } from '@/components/StateManagedBridge/FromChainSelector'
+import { FromTokenSelector } from '@/components/StateManagedBridge/FromTokenSelector'
+import { useBridgeSelections } from './hooks/useBridgeSelections'
+import { useBridgeValidations } from './hooks/useBridgeValidations'
export const inputRef = React.createRef
()
-export const InputContainer = () => {
+interface InputContainerProps {
+ setIsTyping: React.Dispatch>
+}
+
+export const InputContainer: React.FC = ({
+ setIsTyping,
+}) => {
const dispatch = useAppDispatch()
const { chain, isConnected } = useAccount()
- const { balances } = usePortfolioState()
- const { fromChainId, toChainId, fromToken, toToken, fromValue } =
- useBridgeState()
const { isWalletPending } = useWalletState()
- const [showValue, setShowValue] = useState('')
- const [hasMounted, setHasMounted] = useState(false)
-
- const { addresses, decimals } = fromToken || {}
- const tokenDecimals = isNumber(decimals) ? decimals : decimals?.[fromChainId]
- const balance: bigint = balances[fromChainId]?.find(
- (token) => token.tokenAddress === addresses?.[fromChainId]
- )?.balance
- const parsedBalance = getParsedBalance(balance, tokenDecimals)
- const formattedBalance = formatAmount(parsedBalance)
+ const { fromChainId, fromToken, debouncedFromValue } = useBridgeState()
+ const [localInputValue, setLocalInputValue] = useState(debouncedFromValue)
- const hasValidFromSelections: boolean = useMemo(() => {
- return Boolean(fromChainId && fromToken)
- }, [fromChainId, fromToken])
+ const { hasValidFromSelections, hasValidSelections, onSelectedChain } =
+ useBridgeValidations()
+ const { fromTokenBalance, fromTokenDecimals } = useBridgeSelections()
- const hasValidInputSelections: boolean = useMemo(() => {
- return Boolean(fromChainId && fromToken && toChainId && toToken)
- }, [fromChainId, toChainId, fromToken, toToken])
+ const parsedBalance = getParsedBalance(fromTokenBalance, fromTokenDecimals)
+ const formattedBalance = formatAmount(parsedBalance)
const {
isLoading,
@@ -70,27 +57,69 @@ export const InputContainer = () => {
} = useGasEstimator()
const isInputMax =
- maxBridgeableGas?.toString() === fromValue || parsedBalance === fromValue
+ maxBridgeableGas?.toString() === debouncedFromValue ||
+ parsedBalance === debouncedFromValue
+
+ const debouncedUpdateFromValue = useMemo(
+ () =>
+ debounce(
+ (value: string) => dispatch(updateDebouncedFromValue(value)),
+ 400
+ ),
+ [dispatch]
+ )
+
+ useEffect(() => {
+ return () => {
+ debouncedUpdateFromValue.cancel()
+ }
+ }, [debouncedUpdateFromValue])
+
+ const handleFromValueChange = useCallback(
+ (event: React.ChangeEvent) => {
+ const cleanedValue = cleanNumberInput(event.target.value)
+ try {
+ setLocalInputValue(cleanedValue)
+ debouncedUpdateFromValue(cleanedValue)
+ } catch (error) {
+ console.log('Invalid value for conversion to BigInteger')
+ const inputValue = event.target.value
+ const regex = /^[0-9]*[.,]?[0-9]*$/
+
+ if (regex.test(inputValue) || inputValue === '') {
+ setLocalInputValue(cleanedValue)
+ debouncedUpdateFromValue(cleanedValue)
+ }
+ }
+ },
+ [debouncedUpdateFromValue]
+ )
const onMaxBalance = useCallback(async () => {
if (hasValidGasEstimateInputs()) {
const bridgeableBalance = await estimateBridgeableBalanceCallback()
if (isNull(bridgeableBalance)) {
- dispatch(updateFromValue(parsedBalance))
+ setLocalInputValue(parsedBalance)
+ dispatch(updateDebouncedFromValue(parsedBalance))
} else if (bridgeableBalance > 0) {
- dispatch(updateFromValue(bridgeableBalance?.toString()))
+ const bridgeableBalanceString = bridgeableBalance.toString()
+ setLocalInputValue(bridgeableBalanceString)
+ dispatch(updateDebouncedFromValue(bridgeableBalanceString))
} else {
- dispatch(updateFromValue('0.0'))
+ setLocalInputValue('0.0')
+ dispatch(updateDebouncedFromValue('0.0'))
toast.error('Gas fees likely exceeds your balance.', {
id: 'toast-error-not-enough-gas',
duration: 10000,
})
}
} else {
- dispatch(updateFromValue(parsedBalance))
+ setLocalInputValue(parsedBalance)
+ dispatch(updateDebouncedFromValue(parsedBalance))
}
}, [
+ dispatch,
fromChainId,
fromToken,
parsedBalance,
@@ -99,47 +128,18 @@ export const InputContainer = () => {
])
useEffect(() => {
- setHasMounted(true)
- }, [])
+ setLocalInputValue(debouncedFromValue)
+ }, [debouncedFromValue])
const connectedStatus = useMemo(() => {
- if (hasMounted && !isConnected) {
+ if (!isConnected) {
return
- } else if (hasMounted && isConnected && fromChainId === chain?.id) {
+ } else if (isConnected && onSelectedChain) {
return
- } else if (hasMounted && isConnected && fromChainId !== chain?.id) {
+ } else if (isConnected && !onSelectedChain) {
return
}
- }, [chain, fromChainId, isConnected, hasMounted])
-
- useEffect(() => {
- if (fromToken && tokenDecimals) {
- setShowValue(fromValue)
- }
-
- if (fromValue === initialState.fromValue) {
- setShowValue(initialState.fromValue)
- }
- }, [fromValue, inputRef, fromChainId, fromToken])
-
- const handleFromValueChange = (
- event: React.ChangeEvent
- ) => {
- const fromValueString: string = cleanNumberInput(event.target.value)
- try {
- dispatch(updateFromValue(fromValueString))
- setShowValue(fromValueString)
- } catch (error) {
- console.error('Invalid value for conversion to BigInteger')
- const inputValue = event.target.value
- const regex = /^[0-9]*[.,]?[0-9]*$/
-
- if (regex.test(inputValue) || inputValue === '') {
- dispatch(updateFromValue(inputValue))
- setShowValue(inputValue)
- }
- }
- }
+ }, [chain, fromChainId, isConnected])
return (
@@ -151,8 +151,9 @@ export const InputContainer = () => {
@@ -168,7 +169,7 @@ export const InputContainer = () => {
onClick={onMaxBalance}
isHidden={
!isConnected ||
- !hasValidInputSelections ||
+ !hasValidSelections ||
isLoading ||
isInputMax ||
isWalletPending
@@ -179,39 +180,3 @@ export const InputContainer = () => {
)
}
-
-const FromChainSelector = () => {
- const { fromChainId } = useBridgeState()
- const { isWalletPending } = useWalletState()
-
- return (
-
- )
-}
-
-const FromTokenSelector = () => {
- const { fromToken } = useBridgeState()
- const { isWalletPending } = useWalletState()
-
- return (
-
- )
-}
diff --git a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx
index 9d3459e551..9f06ed4bd2 100644
--- a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx
+++ b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx
@@ -1,4 +1,5 @@
import { useAccount } from 'wagmi'
+import { useMemo } from 'react'
import { ChainSelector } from '@/components/ui/ChainSelector'
import { TokenSelector } from '@/components/ui/TokenSelector'
@@ -12,16 +13,24 @@ import { CHAINS_BY_ID } from '@/constants/chains'
import { setToChainId, setToToken } from '@/slices/bridge/reducer'
import { useBridgeDisplayState, useBridgeState } from '@/slices/bridge/hooks'
import { useWalletState } from '@/slices/wallet/hooks'
+import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
+import { useBridgeValidations } from './hooks/useBridgeValidations'
export const OutputContainer = () => {
const { address } = useAccount()
- const { bridgeQuote, isLoading } = useBridgeState()
+ const { bridgeQuote, isLoading } = useBridgeQuoteState()
const { showDestinationAddress } = useBridgeDisplayState()
+ const { hasValidInput, hasValidQuote } = useBridgeValidations()
- const showValue =
- bridgeQuote?.outputAmountString === '0'
- ? ''
- : bridgeQuote?.outputAmountString
+ const showValue = useMemo(() => {
+ if (!hasValidInput) {
+ return ''
+ } else if (hasValidQuote) {
+ return bridgeQuote?.outputAmountString
+ } else {
+ return ''
+ }
+ }, [bridgeQuote, hasValidInput, hasValidQuote])
return (
diff --git a/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeSelections.ts b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeSelections.ts
new file mode 100644
index 0000000000..6a6af1dbd2
--- /dev/null
+++ b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeSelections.ts
@@ -0,0 +1,30 @@
+import { useBridgeState } from '@/slices/bridge/hooks'
+import { BridgeState } from '@/slices/bridge/reducer'
+import { usePortfolioBalances } from '@/slices/portfolio/hooks'
+import { stringToBigInt } from '@/utils/bigint/format'
+
+export const useBridgeSelections = () => {
+ const { fromChainId, fromToken, debouncedFromValue }: BridgeState =
+ useBridgeState()
+ const balances = usePortfolioBalances()
+
+ const fromTokenDecimals = fromToken?.decimals[fromChainId]
+ const fromTokenAddress = fromToken?.addresses[fromChainId]
+
+ const fromChainBalances = balances[fromChainId]
+ const fromTokenBalance = fromChainBalances?.find(
+ (t) => t.tokenAddress === fromTokenAddress
+ )?.balance
+
+ const debouncedFromValueBigInt = stringToBigInt(
+ debouncedFromValue,
+ fromTokenDecimals
+ )
+
+ return {
+ fromTokenBalance,
+ fromTokenDecimals,
+ fromTokenAddress,
+ debouncedFromValueBigInt,
+ }
+}
diff --git a/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts
new file mode 100644
index 0000000000..e64ac72587
--- /dev/null
+++ b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts
@@ -0,0 +1,129 @@
+import { useMemo } from 'react'
+import { useAccount } from 'wagmi'
+
+import { useBridgeState } from '@/slices/bridge/hooks'
+import { BridgeState } from '@/slices/bridge/reducer'
+import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
+import { BridgeQuoteState } from '@/slices/bridgeQuote/reducer'
+import { EMPTY_BRIDGE_QUOTE } from '@/constants/bridge'
+import { hasOnlyZeroes } from '@/utils/hasOnlyZeroes'
+import { useBridgeSelections } from './useBridgeSelections'
+
+export const useBridgeValidations = () => {
+ const { chainId } = useAccount()
+ const {
+ fromChainId,
+ toChainId,
+ fromToken,
+ toToken,
+ debouncedFromValue,
+ }: BridgeState = useBridgeState()
+ const { bridgeQuote }: BridgeQuoteState = useBridgeQuoteState()
+ const { fromTokenBalance, debouncedFromValueBigInt } = useBridgeSelections()
+
+ const hasValidInput: boolean = useMemo(() => {
+ if (debouncedFromValue === '') return false
+ if (hasOnlyZeroes(debouncedFromValue)) return false
+ return debouncedFromValueBigInt > 0n
+ }, [debouncedFromValue, debouncedFromValueBigInt])
+
+ const hasValidFromSelections = useMemo(() => {
+ return Boolean(fromChainId && fromToken)
+ }, [fromChainId, fromToken])
+
+ const hasValidSelections = useMemo(() => {
+ return Boolean(fromChainId && fromToken && toChainId && toToken)
+ }, [fromChainId, fromToken, toChainId, toToken])
+
+ const hasValidQuote: boolean = useMemo(() => {
+ return bridgeQuote !== EMPTY_BRIDGE_QUOTE
+ }, [bridgeQuote])
+
+ const hasSufficientBalance: boolean = useMemo(() => {
+ return hasValidSelections
+ ? debouncedFromValueBigInt <= fromTokenBalance
+ : false
+ }, [hasValidSelections, debouncedFromValueBigInt, fromTokenBalance])
+
+ const stringifiedBridgeQuote = useMemo(() => {
+ return constructStringifiedBridgeSelections(
+ bridgeQuote.inputAmountForQuote,
+ bridgeQuote.originChainId,
+ bridgeQuote.originTokenForQuote,
+ bridgeQuote.destChainId,
+ bridgeQuote.destTokenForQuote
+ )
+ }, [
+ bridgeQuote.inputAmountForQuote,
+ bridgeQuote.originChainId,
+ bridgeQuote.originTokenForQuote,
+ bridgeQuote.destChainId,
+ bridgeQuote.destTokenForQuote,
+ ])
+
+ const stringifiedBridgeState = useMemo(() => {
+ return constructStringifiedBridgeSelections(
+ debouncedFromValue,
+ fromChainId,
+ fromToken,
+ toChainId,
+ toToken
+ )
+ }, [debouncedFromValue, fromChainId, fromToken, toChainId, toToken])
+
+ const doesBridgeStateMatchQuote = useMemo(() => {
+ return stringifiedBridgeQuote === stringifiedBridgeState
+ }, [stringifiedBridgeQuote, stringifiedBridgeState])
+
+ const isBridgeQuoteAmountGreaterThanInputForRfq = useMemo(() => {
+ return (
+ bridgeQuote.bridgeModuleName === 'SynapseRFQ' &&
+ bridgeQuote.outputAmount > debouncedFromValueBigInt
+ )
+ }, [
+ bridgeQuote.outputAmount,
+ bridgeQuote.bridgeModuleName,
+ debouncedFromValueBigInt,
+ ])
+
+ const isBridgeFeeGreaterThanInput = useMemo(() => {
+ return (
+ bridgeQuote.bridgeModuleName !== null &&
+ bridgeQuote.feeAmount === 0n &&
+ debouncedFromValueBigInt > 0n
+ )
+ }, [bridgeQuote.feeAmount, debouncedFromValueBigInt])
+
+ const onSelectedChain: boolean = useMemo(() => {
+ return chainId === fromChainId
+ }, [fromChainId, chainId])
+
+ return {
+ hasValidInput,
+ hasValidFromSelections,
+ hasValidSelections,
+ hasValidQuote,
+ hasSufficientBalance,
+ doesBridgeStateMatchQuote,
+ isBridgeFeeGreaterThanInput,
+ isBridgeQuoteAmountGreaterThanInputForRfq,
+ onSelectedChain,
+ }
+}
+
+const constructStringifiedBridgeSelections = (
+ originAmount,
+ originChainId,
+ originToken,
+ destChainId,
+ destToken
+) => {
+ const state = {
+ originAmount,
+ originChainId,
+ originToken,
+ destChainId,
+ destToken,
+ }
+ return JSON.stringify(state)
+}
diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx
index 571e225900..135b672dc6 100644
--- a/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx
+++ b/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx
@@ -30,8 +30,15 @@ import { trimTrailingZeroesAfterDecimal } from '@/utils/trimTrailingZeroesAfterD
import { formatAmount } from '@/utils/formatAmount'
import { getParsedBalance } from '@/utils/getParsedBalance'
import { useWalletState } from '@/slices/wallet/hooks'
+import { debounce } from 'lodash'
-export const SwapInputContainer = () => {
+interface InputContainerProps {
+ setIsTyping: React.Dispatch>
+}
+
+export const SwapInputContainer: React.FC = ({
+ setIsTyping,
+}) => {
const inputRef = useRef(null)
const { swapChainId, swapFromToken, swapToToken, swapFromValue } =
useSwapState()
@@ -77,20 +84,32 @@ export const SwapInputContainer = () => {
}
}, [swapFromValue, swapChainId, swapFromToken])
+ const debouncedUpdateSwapFromValue = useMemo(
+ () =>
+ debounce((value: string) => dispatch(updateSwapFromValue(value)), 400),
+ [dispatch]
+ )
+
+ useEffect(() => {
+ return () => {
+ debouncedUpdateSwapFromValue.cancel()
+ }
+ }, [debouncedUpdateSwapFromValue])
+
const handleFromValueChange = (
event: React.ChangeEvent
) => {
const swapFromValueString: string = cleanNumberInput(event.target.value)
try {
- dispatch(updateSwapFromValue(swapFromValueString))
setShowValue(swapFromValueString)
+ debouncedUpdateSwapFromValue(swapFromValueString)
} catch (error) {
console.error('Invalid value for conversion to BigInteger')
const inputValue = event.target.value
const regex = /^[0-9]*[.,]?[0-9]*$/
if (regex.test(inputValue) || inputValue === '') {
- dispatch(updateSwapFromValue(''))
+ debouncedUpdateSwapFromValue(inputValue)
setShowValue(inputValue)
}
}
@@ -134,6 +153,7 @@ export const SwapInputContainer = () => {
) => void
+ setIsTyping?: (isTyping: boolean) => void
}
export function AmountInput({
@@ -16,7 +18,19 @@ export function AmountInput({
isLoading = false,
showValue,
handleFromValueChange,
+ setIsTyping,
}: AmountInputTypes) {
+ const debouncedSetIsTyping = useCallback(
+ debounce((value: boolean) => setIsTyping?.(value), 600),
+ [setIsTyping]
+ )
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ setIsTyping?.(true)
+ debouncedSetIsTyping(false)
+ handleFromValueChange?.(event)
+ }
+
const inputClassName = joinClassNames({
unset: 'bg-transparent border-none p-0',
layout: 'w-full',
@@ -37,7 +51,7 @@ export function AmountInput({
readOnly={disabled}
className={inputClassName}
placeholder="0.0000"
- onChange={handleFromValueChange}
+ onChange={handleInputChange}
value={showValue}
name="inputRow"
autoComplete="off"
diff --git a/packages/synapse-interface/constants/bridge.ts b/packages/synapse-interface/constants/bridge.ts
index 40708c1f1a..9b94e48f62 100644
--- a/packages/synapse-interface/constants/bridge.ts
+++ b/packages/synapse-interface/constants/bridge.ts
@@ -4,6 +4,9 @@ import * as CHAINS from '@constants/chains/master'
export const QUOTE_POLLING_INTERVAL = 10000
export const EMPTY_BRIDGE_QUOTE = {
+ inputAmountForQuote: '',
+ originTokenForQuote: null,
+ destTokenForQuote: null,
outputAmount: 0n,
outputAmountString: '',
routerAddress: '',
@@ -19,25 +22,9 @@ export const EMPTY_BRIDGE_QUOTE = {
timestamp: null,
originChainId: null,
destChainId: null,
+ requestId: null,
}
-export const EMPTY_BRIDGE_QUOTE_ZERO = {
- outputAmount: 0n,
- outputAmountString: '0',
- routerAddress: '',
- allowance: 0n,
- exchangeRate: 0n,
- feeAmount: 0n,
- delta: 0n,
- originQuery: null,
- destQuery: null,
- estimatedTime: null,
- bridgeModuleName: null,
- gasDropAmount: 0n,
- timestamp: null,
- originChainId: null,
- destChainId: null,
-}
/**
* ETH Only Bridge Config used to calculate swap fees
*
diff --git a/packages/synapse-interface/contexts/BackgroundListenerProvider.tsx b/packages/synapse-interface/contexts/BackgroundListenerProvider.tsx
index 5f3c5b1b70..70f5605d4a 100644
--- a/packages/synapse-interface/contexts/BackgroundListenerProvider.tsx
+++ b/packages/synapse-interface/contexts/BackgroundListenerProvider.tsx
@@ -1,7 +1,6 @@
import React, { createContext } from 'react'
import { useApplicationListener } from '@/utils/hooks/useApplicationListener'
-import { useBridgeListener } from '@/utils/hooks/useBridgeListener'
import { usePortfolioListener } from '@/utils/hooks/usePortfolioListener'
import { useRiskEvent } from '@/utils/hooks/useRiskEvent'
import { useTransactionListener } from '@/utils/hooks/useTransactionListener'
@@ -18,7 +17,6 @@ export const BackgroundListenerProvider = ({ children }) => {
usePortfolioListener()
useTransactionListener()
use_TransactionsListener()
- useBridgeListener()
useRiskEvent()
useFetchPricesOnInterval()
useFetchGasDataOnInterval()
diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx
index fc86e9456e..63b7b71e42 100644
--- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx
+++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx
@@ -1,6 +1,5 @@
import toast from 'react-hot-toast'
import { useEffect, useRef, useState } from 'react'
-import { commify } from '@ethersproject/units'
import { Address, zeroAddress, isAddress } from 'viem'
import { polygon } from 'viem/chains'
import { useAccount } from 'wagmi'
@@ -25,8 +24,7 @@ import Button from '@/components/ui/tailwind/Button'
import { SettingsToggle } from '@/components/StateManagedBridge/SettingsToggle'
import { BridgeCard } from '@/components/ui/BridgeCard'
import { ConfirmDestinationAddressWarning } from '@/components/StateManagedBridge/BridgeWarnings'
-import { EMPTY_BRIDGE_QUOTE_ZERO } from '@/constants/bridge'
-import { AcceptedChainId, CHAINS_BY_ID } from '@/constants/chains'
+import { CHAINS_BY_ID } from '@/constants/chains'
import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider'
import { useBridgeState } from '@/slices/bridge/hooks'
import {
@@ -35,10 +33,8 @@ import {
setFromToken,
setToChainId,
setToToken,
- updateFromValue,
- setBridgeQuote,
- setIsLoading,
setDestinationAddress,
+ updateDebouncedFromValue,
} from '@/slices/bridge/reducer'
import { setIsWalletPending } from '@/slices/wallet/reducer'
import {
@@ -46,9 +42,6 @@ import {
setShowSettingsSlideOver,
} from '@/slices/bridgeDisplaySlice'
import { useSynapseContext } from '@/utils/providers/SynapseProvider'
-import { getErc20TokenAllowance } from '@/actions/getErc20TokenAllowance'
-import { formatBigIntToString } from '@/utils/bigint/format'
-import { calculateExchangeRate } from '@/utils/calculateExchangeRate'
import { Token } from '@/utils/types'
import { txErrorHandler } from '@/utils/txErrorHandler'
import { approveToken } from '@/utils/approveToken'
@@ -63,13 +56,15 @@ import { useAppDispatch } from '@/store/hooks'
import { RootState } from '@/store/store'
import { getTimeMinutesFromNow } from '@/utils/time'
import { isTransactionReceiptError } from '@/utils/isTransactionReceiptError'
-import { isTransactionUserRejectedError } from '@/utils/isTransactionUserRejectedError'
import { useMaintenance } from '@/components/Maintenance/Maintenance'
-import { getBridgeModuleNames } from '@/utils/getBridgeModuleNames'
import { wagmiConfig } from '@/wagmiConfig'
import { useStaleQuoteUpdater } from '@/utils/hooks/useStaleQuoteUpdater'
import { screenAddress } from '@/utils/screenAddress'
import { useWalletState } from '@/slices/wallet/hooks'
+import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
+import { resetBridgeQuote } from '@/slices/bridgeQuote/reducer'
+import { fetchBridgeQuote } from '@/slices/bridgeQuote/thunks'
+import { useIsBridgeApproved } from '@/utils/hooks/useIsBridgeApproved'
const StateManagedBridge = () => {
const { address } = useAccount()
@@ -81,20 +76,24 @@ const StateManagedBridge = () => {
const currentSDKRequestID = useRef(0)
const quoteToastRef = useRef({ id: '' })
+ const [isTyping, setIsTyping] = useState(false)
+
const {
fromChainId,
toChainId,
fromToken,
toToken,
- bridgeQuote,
debouncedFromValue,
destinationAddress,
- isLoading: isQuoteLoading,
}: BridgeState = useBridgeState()
+ const { bridgeQuote, isLoading } = useBridgeQuoteState()
+
+ const isApproved = useIsBridgeApproved()
+
const { isWalletPending } = useWalletState()
- const { showSettingsSlideOver, showDestinationAddress } = useSelector(
+ const { showSettingsSlideOver } = useSelector(
(state: RootState) => state.bridgeDisplay
)
@@ -105,8 +104,6 @@ const StateManagedBridge = () => {
BridgeMaintenanceWarningMessage,
} = useMaintenance()
- const [isApproved, setIsApproved] = useState(false)
-
const dispatch = useAppDispatch()
useEffect(() => {
@@ -127,205 +124,50 @@ const StateManagedBridge = () => {
console.log('trying to set bridge quote')
getAndSetBridgeQuote()
} else {
- dispatch(setBridgeQuote(EMPTY_BRIDGE_QUOTE_ZERO))
- dispatch(setIsLoading(false))
+ dispatch(resetBridgeQuote())
}
}, [fromChainId, toChainId, fromToken, toToken, debouncedFromValue])
- // don't like this, rewrite: could be custom hook
- useEffect(() => {
- if (fromToken && fromToken?.addresses[fromChainId] === zeroAddress) {
- setIsApproved(true)
- } else {
- if (
- fromToken &&
- bridgeQuote?.allowance &&
- stringToBigInt(debouncedFromValue, fromToken.decimals[fromChainId]) <=
- bridgeQuote.allowance
- ) {
- setIsApproved(true)
- } else {
- setIsApproved(false)
- }
- }
- }, [bridgeQuote, fromToken, debouncedFromValue, fromChainId, toChainId])
-
const getAndSetBridgeQuote = async () => {
currentSDKRequestID.current += 1
const thisRequestId = currentSDKRequestID.current
+
// 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(setBridgeQuote(EMPTY_BRIDGE_QUOTE_ZERO))
+ dispatch(resetBridgeQuote())
+ const currentTimestamp: number = getTimeMinutesFromNow(0)
try {
- dispatch(setIsLoading(true))
- const currentTimestamp: number = getTimeMinutesFromNow(0)
-
- const allQuotes = await synapseSDK.allBridgeQuotes(
- fromChainId,
- toChainId,
- fromToken.addresses[fromChainId],
- toToken.addresses[toChainId],
- stringToBigInt(debouncedFromValue, fromToken?.decimals[fromChainId]),
- {
- originUserAddress: address,
- }
- )
-
- const pausedBridgeModules = new Set(
- pausedModulesList
- .filter((module) =>
- module.chainId ? module.chainId === fromChainId : true
- )
- .flatMap(getBridgeModuleNames)
- )
-
- const activeQuotes = allQuotes.filter(
- (quote) => !pausedBridgeModules.has(quote.bridgeModuleName)
- )
-
- if (activeQuotes.length === 0) {
- const msg = `No route found for bridging ${debouncedFromValue} ${fromToken?.symbol} on ${CHAINS_BY_ID[fromChainId]?.name} to ${toToken?.symbol} on ${CHAINS_BY_ID[toChainId]?.name}`
- throw new Error(msg)
- }
-
- const rfqQuote = activeQuotes.find(
- (quote) => quote.bridgeModuleName === 'SynapseRFQ'
- )
-
- const nonRfqQuote = activeQuotes.find(
- (quote) => quote.bridgeModuleName !== 'SynapseRFQ'
- )
-
- let quote
-
- if (rfqQuote && nonRfqQuote) {
- const rfqMaxAmountOut = BigInt(rfqQuote.maxAmountOut.toString())
- const nonRfqMaxAmountOut = BigInt(nonRfqQuote.maxAmountOut.toString())
-
- const allowedPercentileDifference = 30n
- const maxDifference =
- (nonRfqMaxAmountOut * allowedPercentileDifference) / 100n
-
- if (rfqMaxAmountOut > nonRfqMaxAmountOut - maxDifference) {
- quote = rfqQuote
- } else {
- quote = nonRfqQuote
-
- segmentAnalyticsEvent(`[Bridge] use non-RFQ quote over RFQ`, {
- bridgeModuleName: nonRfqQuote.bridgeModuleName,
- originChainId: fromChainId,
- originToken: fromToken.symbol,
- originTokenAddress: fromToken.addresses[fromChainId],
- destinationChainId: toChainId,
- destinationToken: toToken.symbol,
- destinationTokenAddress: toToken.addresses[toChainId],
- rfqQuoteAmountOut: rfqQuote.maxAmountOut.toString(),
- nonRfqMaxAmountOut: nonRfqQuote.maxAmountOut.toString(),
- })
- }
- } else {
- quote = rfqQuote ?? nonRfqQuote
- }
-
- const {
- feeAmount,
- routerAddress,
- maxAmountOut,
- originQuery,
- destQuery,
- estimatedTime,
- bridgeModuleName,
- gasDropAmount,
- originChainId,
- destChainId,
- } = quote
-
- if (!(originQuery && maxAmountOut && destQuery && feeAmount)) {
- dispatch(setBridgeQuote(EMPTY_BRIDGE_QUOTE_ZERO))
- dispatch(setIsLoading(false))
- return
- }
-
- const toValueBigInt = BigInt(maxAmountOut.toString()) ?? 0n
-
- // Bridge Lifecycle: originToken -> bridgeToken -> destToken
- // debouncedFromValue is in originToken decimals
- // originQuery.minAmountOut and feeAmount is in bridgeToken decimals
- // Adjust feeAmount to be in originToken decimals
- const adjustedFeeAmount =
- (BigInt(feeAmount) *
- stringToBigInt(
- `${debouncedFromValue}`,
- fromToken?.decimals[fromChainId]
- )) /
- BigInt(originQuery.minAmountOut)
-
- const isUnsupported = AcceptedChainId[fromChainId] ? false : true
-
- const allowance =
- fromToken?.addresses[fromChainId] === zeroAddress ||
- address === undefined ||
- isUnsupported
- ? 0n
- : await getErc20TokenAllowance({
- address,
- chainId: fromChainId,
- tokenAddress: fromToken?.addresses[fromChainId] as Address,
- spender: routerAddress,
- })
-
- const {
- originQuery: originQueryWithSlippage,
- destQuery: destQueryWithSlippage,
- } = synapseSDK.applyBridgeSlippage(
- bridgeModuleName,
- originQuery,
- destQuery
- )
-
if (thisRequestId === currentSDKRequestID.current) {
- dispatch(
- setBridgeQuote({
- outputAmount: toValueBigInt,
- outputAmountString: commify(
- formatBigIntToString(
- toValueBigInt,
- toToken.decimals[toChainId],
- 8
- )
- ),
- routerAddress,
- allowance,
- exchangeRate: calculateExchangeRate(
- stringToBigInt(
- debouncedFromValue,
- fromToken?.decimals[fromChainId]
- ) - BigInt(adjustedFeeAmount),
- fromToken?.decimals[fromChainId],
- toValueBigInt,
- toToken.decimals[toChainId]
- ),
- feeAmount,
- delta: BigInt(maxAmountOut.toString()),
- originQuery: originQueryWithSlippage,
- destQuery: destQueryWithSlippage,
- estimatedTime: estimatedTime,
- bridgeModuleName: bridgeModuleName,
- gasDropAmount: BigInt(gasDropAmount.toString()),
- timestamp: currentTimestamp,
- originChainId,
- destChainId,
+ const result = await dispatch(
+ fetchBridgeQuote({
+ synapseSDK,
+ fromChainId,
+ toChainId,
+ fromToken,
+ toToken,
+ debouncedFromValue,
+ requestId: thisRequestId,
+ currentTimestamp,
+ address,
+ pausedModulesList,
})
)
toast.dismiss(quoteToastRef.current.id)
- const message = `Route found for bridging ${debouncedFromValue} ${fromToken?.symbol} on ${CHAINS_BY_ID[fromChainId]?.name} to ${toToken.symbol} on ${CHAINS_BY_ID[toChainId]?.name}`
- console.log(message)
+ if (fetchBridgeQuote.fulfilled.match(result)) {
+ const message = `Route found for bridging ${debouncedFromValue} ${fromToken?.symbol} on ${CHAINS_BY_ID[fromChainId]?.name} to ${toToken.symbol} on ${CHAINS_BY_ID[toChainId]?.name}`
- quoteToastRef.current.id = toast(message, { duration: 3000 })
+ quoteToastRef.current.id = toast(message, { duration: 3000 })
+ }
+
+ if (fetchBridgeQuote.rejected.match(result)) {
+ const message = result.payload as string
+
+ quoteToastRef.current.id = toast(message, { duration: 3000 })
+ }
}
} catch (err) {
console.log(err)
@@ -347,21 +189,17 @@ const StateManagedBridge = () => {
console.log(message)
quoteToastRef.current.id = toast(message, { duration: 3000 })
- dispatch(setBridgeQuote(EMPTY_BRIDGE_QUOTE_ZERO))
+ dispatch(resetBridgeQuote())
return
}
- } finally {
- if (thisRequestId === currentSDKRequestID.current) {
- dispatch(setIsLoading(false))
- }
}
}
useStaleQuoteUpdater(
bridgeQuote,
getAndSetBridgeQuote,
- isQuoteLoading,
+ isLoading,
isWalletPending
)
@@ -495,10 +333,10 @@ const StateManagedBridge = () => {
isSubmitted: false,
})
)
- dispatch(setBridgeQuote(EMPTY_BRIDGE_QUOTE_ZERO))
+ dispatch(resetBridgeQuote())
dispatch(setDestinationAddress(null))
dispatch(setShowDestinationAddress(false))
- dispatch(updateFromValue(''))
+ dispatch(updateDebouncedFromValue(''))
const successToastContent = (
@@ -587,7 +425,7 @@ const StateManagedBridge = () => {
) : (
<>
-
+
{
dispatch(setFromChainId(toChainId))
@@ -603,6 +441,7 @@ const StateManagedBridge = () => {
{
const router = useRouter()
const { query, pathname } = router
+ const [isTyping, setIsTyping] = useState(false)
+
useSyncQueryParamsWithSwapState()
const { balances: portfolioBalances } = useFetchPortfolioBalances()
@@ -354,7 +356,7 @@ const StateManagedSwap = () => {
-
+
{
dispatch(setSwapFromToken(swapToToken))
@@ -378,6 +380,7 @@ const StateManagedSwap = () => {
toChainId={swapChainId}
/>
{
- const router = useRouter()
-
- useEffect(() => {
- segmentAnalyticsEvent(`[Teaser] arrives`, {
- query: router.query,
- pathname: router.pathname,
- })
- }, [])
-
- return (
-
-
-
-
- )
-}
-
-export default LandingPage
diff --git a/packages/synapse-interface/pages/teaser/FauxBridge.tsx b/packages/synapse-interface/pages/teaser/FauxBridge.tsx
deleted file mode 100644
index 2e7390cb8d..0000000000
--- a/packages/synapse-interface/pages/teaser/FauxBridge.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-import PulseDot from '@/components/icons/PulseDot'
-import { CHAINS_ARR } from '@/constants/chains'
-import * as BRIDGEABLE from '@constants/tokens/bridgeable'
-import { TOKENS_SORTED_BY_SWAPABLETYPE } from '@/constants/tokens'
-import * as WALLET_ICONS from '@components/WalletIcons'
-
-const cardStyle =
- 'text-black dark:text-white bg-zinc-100 dark:bg-zinc-900/95 p-3 rounded-md border border-zinc-200 dark:border-zinc-800 shadow-xl grid gap-4 max-w-sm'
-const sectionStyle =
- 'relative bg-zinc-50 dark:bg-zinc-800 rounded-md px-2.5 py-3 grid gap-3 grid-cols-2 border border-zinc-300 dark:border-transparent'
-const buttonStyle =
- 'rounded px-4 py-1 bg-zinc-100 dark:bg-zinc-700 border border-zinc-200 dark:border-transparent hover:border-zinc-400 hover:dark:border-zinc-500 h-fit mr-1 cursor-pointer focus:border-zinc-400 focus:dark:borer-zinc-500'
-const buttonSelectStyle =
- 'flex gap-1.5 items-center rounded px-3 py-1.5 bg-inherit dark:bg-zinc-700 border border-zinc-200 dark:border-transparent hover:border-zinc-400 hover:dark:border-zinc-500 active:opacity-70 focus:ring-1 focus:ring-zinc-500 focus:border-transparent'
-const inputWrapperStyle =
- 'relative flex bg-white dark:bg-inherit border border-zinc-200 dark:border-zinc-700 rounded-md gap-0 p-1.5 col-span-2 gap-1.5 items-center'
-const inputStyle =
- 'bg-inherit border-none w-full p-1.5 text-xxl font-normal dark:font-light tracking-wide rounded'
-
-export default () => {
- return (
-
- )
-}
-
-const Select = ({
- type,
- data,
-}: {
- type: 'Chain' | 'Token'
- data: 'volume' | 'count'
-}) => {
- let button: string
- let header: string
- let value: number
- let reduce: Function
- let format: Function
- switch (data) {
- case 'volume':
- button = `Volume by ${type}`
- header = '$ vol.'
- value = 1000000000 + Math.random() * 100000000
- reduce = () => (value *= 0.85)
- format = () => {
- if (value >= 1000000) return '$' + (value / 1000000).toFixed(1) + 'M'
- let str = value.toFixed(0)
- if (value >= 1000) {
- for (let i = 3; i < str.length; i += 4)
- str = `${str.slice(0, str.length - i)},${str.slice(-i)}`
- return '$' + str
- }
- return '$' + value.toFixed(2)
- }
- break
- case 'count':
- button = `Txns by ${type}`
- header = 'Txns'
- value = 10000 + Math.random() * 1000
- reduce = () => (value *= 0.9)
- format = () => {
- let str = value.toFixed()
- for (let i = 3; i < str.length; i += 4)
- str = `${str.slice(0, str.length - i)},${str.slice(-i)}`
- return str
- }
- break
- }
-
- let arr
- let key: string
- let img: string
- let name: string
-
- switch (type) {
- case 'Chain':
- arr = CHAINS_ARR
- key = 'id'
- img = 'chainImg'
- name = 'name'
- break
- case 'Token':
- arr = Object.values(BRIDGEABLE)
- key = 'name'
- img = 'icon'
- name = 'symbol'
- break
- }
-
- return (
-
-
- {type} ▼
-
-
-
-
-
-
- {type}
-
-
- {header}
-
-
-
-
- {arr.map((item, i) => {
- reduce()
- return (
-
-
-
- {item[name]}
-
- {format()}
-
- )
- })}
-
-
-
-
-
- )
-}
-
-const SupportedWallets = () => (
-
-
-
-
- {Object.values(WALLET_ICONS).map((icon, i) => (
- {icon({ width: 24, height: 24 })}
- ))}
-
-
-
- Connected
-
-
-)
-
-const HistoricMax = () => (
-
-)
-
-const RightAngle = ({ height }) => {
- const width = height / 2
- return (
-
-
-
- )
-}
-
-const BridgeButton = () => (
-
-)
\ No newline at end of file
diff --git a/packages/synapse-interface/pages/teaser/Hero.tsx b/packages/synapse-interface/pages/teaser/Hero.tsx
deleted file mode 100644
index a92906294f..0000000000
--- a/packages/synapse-interface/pages/teaser/Hero.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import { useEffect, useRef, useState } from 'react'
-
-export default function Hero() {
- const [h1, setH1] = useState<[cta: string] | [cta: string, index: number]>([
- 'default',
- ])
-
- const bridgeRef = useRef(null)
- const buildRef = useRef(null)
-
- const [cta, index] = h1
-
- const ctas = {
- default: {
- tag: 'Synapse 2.0: The Modular Interchain Network',
- },
- bridge: {
- tag: 'Any asset to any chain',
- url: '#',
- },
- build: {
- tag: 'Custom everything',
- url: '#',
- },
- }
-
- const { tag, url } = ctas[cta]
-
- const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time))
-
- useEffect(() => {
- if (index < tag.length) {
- sleep((index / tag.length) * 5 + 5).then(() => setH1([cta, +index + 1]))
- } else {
- bridgeRef?.current?.addEventListener(
- 'mousemove',
- () => setH1(['bridge', 0]),
- { once: true }
- )
-
- buildRef?.current?.addEventListener(
- 'mousemove',
- () => setH1(['build', 0]),
- { once: true }
- )
- }
-
- if (cta !== 'default') {
- document.addEventListener('mousemove', () => setH1(['default', 0]), {
- once: true,
- })
- }
- })
-
- const Tagline = () => {
- return (
- <>
- {tag.slice(0, index)}
- {index < tag.length - 4 && (
-
- {String.fromCharCode(Math.random() * 61 + 65)}
-
- )}
- {index < tag.length - 5 && (
- _
- )}
- >
- )
- }
-
- const ctaButtonBaseStyle =
- 'px-5 pt-1.5 pb-2 text-lg m-2 border rounded inline-block'
-
- return (
-
-
- Modular Interchain Messages
-
- e.stopPropagation()}
- >
-
-
-
-
- Say goodbye to centralized resource pools for cross-chain communication.
- Synapse lets you customize literally every aspect of your interchain
- communications.
-
-
- )
-}
-
-const ArrowBounce = () => (
-
-
-
-
-
-)
diff --git a/packages/synapse-interface/pages/teaser/ValueProps.tsx b/packages/synapse-interface/pages/teaser/ValueProps.tsx
deleted file mode 100644
index 26323d5cd0..0000000000
--- a/packages/synapse-interface/pages/teaser/ValueProps.tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-import FauxBridge from './FauxBridge'
-
-export default function ValueProps() {
- return (
-
-
- {/*
-
- Bridge volume
-
-
- $
-
- 45.3
-
- B
-
-
- Transactions
-
-
-
- 10.6
-
- M
-
-
- Total value locked
-
-
- $
-
- 116.7
-
- M
-
- */}
-
-
- $45.3B Bridge volume
-
-
- 10.6M transactions
-
-
- $116.7M Total value locked
-
-
- {/*
- 50 blockchains
- 50,000 validators
- 10.2B messages
- $1.2B transferred
- */}
-
-
-
-
-
Reach every user
-
- Synapse enables developers to build truly native cross-chain
- applications with an economically secure method to reach consensus
- on interchain transactions,
-
-
-
- {/*
-
-
- Extensible
-
- Synapse’s cross-chain messaging contracts can be deployed across
- any blockchain
-
-
-
- Secure
-
- Synapse employs an Optimistic security model to ensure integrity
- of cross-chain messages
-
-
-
- Generalized
-
- Any arbitrary data can be sent across chains including contract
- calls, NFTs, snapshots, and more
-
-
-
- */}
-
-
-
- Build powerful decentralized apps
-
-
- Synapse Bridge is built on top of the cross-chain infrastructure
- enabling users to seamlessly transfer assets across all blockchains.
- The Bridge has become the most widely-used method to move assets
- cross-chain, offering low cost, fast, and secure bridging.
-
-
-
-
-
-
- {/*
-
-
- Deep Liquidity
-
- Swap native assets using our cross-chain AMM liquidity pools
-
-
-
- Wide Support
-
- Access over 16 different EVM and non-EVM blockchains with more
- integrations coming soon
-
-
-
- Developer Friendly
-
- Easily integrate cross-chain token bridging natively into your
- decentralized application
-
-
-
- */}
- {/*
-
-
- {ChainList().map((a) => {
- return (
-
- {a}
-
- )
- })}
-
- */}
-
-
-
-
-
-
-
-
Secure your routes
-
- Synapse has processed millions of transactions and tens of billions
- in bridged assets.
-
-
-
-
- )
-}
-
-/*
-if (theme) {
- formStyle = `p-3 rounded-md border shadow-sm grid gap-4 absolute w-96 ${
- theme === 'dark'
- ? 'text-white bg-zinc-900 border-zinc-800 mr-8 mt-8'
- : 'text-black bg-neutral-100 border-zinc-300 ml-8 mb-8'
- }`
- sectionStyle = `rounded px-2.5 py-3 grid gap-3 grid-cols-2 ${
- theme === 'dark' ? 'bg-zinc-800' : 'bg-zinc-50 border border-zinc-200'
- }`
- selectStyle = `rounded w-fit cursor-pointer border ${
- theme === 'dark'
- ? 'bg-zinc-700 border-transparent'
- : 'bg-inherit border-zinc-300'
- }`
- inputWrapperStyle = `flex border rounded-md p-1.5 col-span-2 gap-1.5 ${
- theme === 'dark'
- ? 'bg-inherit border-zinc-700'
- : 'bg-white border-zinc-200 '
- }`
- inputStyle = `bg-inherit border-none w-full p-1.5 text-xxl font-normal tracking-wide rounded ${
- theme === 'dark' ? 'font-light' : 'font-normal'
- }`
- }
- */
diff --git a/packages/synapse-interface/slices/bridge/reducer.ts b/packages/synapse-interface/slices/bridge/reducer.ts
index 2891b5d92f..fa48546045 100644
--- a/packages/synapse-interface/slices/bridge/reducer.ts
+++ b/packages/synapse-interface/slices/bridge/reducer.ts
@@ -1,8 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { type Address } from 'viem'
-import { EMPTY_BRIDGE_QUOTE } from '@/constants/bridge'
-import { type BridgeQuote, type Token } from '@/utils/types'
+import { type Token } from '@/utils/types'
import {
getRoutePossibilities,
getSymbol,
@@ -24,11 +23,8 @@ export interface BridgeState {
fromTokens: Token[]
toTokens: Token[]
- fromValue: string
debouncedFromValue: string
debouncedToTokensFromValue: string
- bridgeQuote: BridgeQuote
- isLoading: boolean
deadlineMinutes: number | null
destinationAddress: Address | null
}
@@ -59,11 +55,8 @@ export const initialState: BridgeState = {
fromTokens,
toTokens,
- fromValue: '',
debouncedFromValue: '',
debouncedToTokensFromValue: '',
- bridgeQuote: EMPTY_BRIDGE_QUOTE,
- isLoading: false,
deadlineMinutes: null,
destinationAddress: null,
}
@@ -72,9 +65,6 @@ export const bridgeSlice = createSlice({
name: 'bridge',
initialState,
reducers: {
- setIsLoading: (state, action: PayloadAction) => {
- state.isLoading = action.payload
- },
setFromChainId: (state, action: PayloadAction) => {
const incomingFromChainId = action.payload
@@ -431,12 +421,6 @@ export const bridgeSlice = createSlice({
state.toChainIds = toChainIds
state.toTokens = toTokens
},
- setBridgeQuote: (state, action: PayloadAction) => {
- state.bridgeQuote = action.payload
- },
- updateFromValue: (state, action: PayloadAction) => {
- state.fromValue = action.payload
- },
updateDebouncedFromValue: (state, action: PayloadAction) => {
state.debouncedFromValue = action.payload
},
@@ -455,15 +439,11 @@ export const bridgeSlice = createSlice({
clearDestinationAddress: (state) => {
state.destinationAddress = initialState.destinationAddress
},
- resetBridgeQuote: (state) => {
- state.bridgeQuote = initialState.bridgeQuote
- },
resetBridgeInputs: (state) => {
state.fromChainId = initialState.fromChainId
state.fromToken = initialState.fromToken
state.toChainId = initialState.toChainId
state.toToken = initialState.toToken
- state.fromValue = initialState.fromValue
state.debouncedFromValue = initialState.debouncedFromValue
},
},
@@ -472,16 +452,12 @@ export const bridgeSlice = createSlice({
export const {
updateDebouncedFromValue,
updateDebouncedToTokensFromValue,
- setBridgeQuote,
- resetBridgeQuote,
setFromChainId,
setToChainId,
setFromToken,
setToToken,
- updateFromValue,
setDeadlineMinutes,
setDestinationAddress,
- setIsLoading,
resetBridgeInputs,
clearDestinationAddress,
} = bridgeSlice.actions
diff --git a/packages/synapse-interface/slices/bridgeQuote/hooks.ts b/packages/synapse-interface/slices/bridgeQuote/hooks.ts
new file mode 100644
index 0000000000..ecc7e9c76e
--- /dev/null
+++ b/packages/synapse-interface/slices/bridgeQuote/hooks.ts
@@ -0,0 +1,6 @@
+import { RootState } from '@/store/store'
+import { useAppSelector } from '@/store/hooks'
+
+export const useBridgeQuoteState = (): RootState['bridgeQuote'] => {
+ return useAppSelector((state) => state.bridgeQuote)
+}
diff --git a/packages/synapse-interface/slices/bridgeQuote/reducer.ts b/packages/synapse-interface/slices/bridgeQuote/reducer.ts
new file mode 100644
index 0000000000..8407876035
--- /dev/null
+++ b/packages/synapse-interface/slices/bridgeQuote/reducer.ts
@@ -0,0 +1,49 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+
+import { EMPTY_BRIDGE_QUOTE } from '@/constants/bridge'
+import { type BridgeQuote } from '@/utils/types'
+import { fetchBridgeQuote } from './thunks'
+
+export interface BridgeQuoteState {
+ bridgeQuote: BridgeQuote
+ isLoading: boolean
+}
+
+export const initialState: BridgeQuoteState = {
+ bridgeQuote: EMPTY_BRIDGE_QUOTE,
+ isLoading: false,
+}
+
+export const bridgeQuoteSlice = createSlice({
+ name: 'bridgeQuote',
+ initialState,
+ reducers: {
+ setIsLoading: (state, action: PayloadAction) => {
+ state.isLoading = action.payload
+ },
+ resetBridgeQuote: (state) => {
+ state.bridgeQuote = initialState.bridgeQuote
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchBridgeQuote.pending, (state) => {
+ state.isLoading = true
+ })
+ .addCase(
+ fetchBridgeQuote.fulfilled,
+ (state, action: PayloadAction) => {
+ state.bridgeQuote = action.payload
+ state.isLoading = false
+ }
+ )
+ .addCase(fetchBridgeQuote.rejected, (state) => {
+ state.bridgeQuote = EMPTY_BRIDGE_QUOTE
+ state.isLoading = false
+ })
+ },
+})
+
+export const { resetBridgeQuote, setIsLoading } = bridgeQuoteSlice.actions
+
+export default bridgeQuoteSlice.reducer
diff --git a/packages/synapse-interface/slices/bridgeQuote/thunks.ts b/packages/synapse-interface/slices/bridgeQuote/thunks.ts
new file mode 100644
index 0000000000..1705cd9892
--- /dev/null
+++ b/packages/synapse-interface/slices/bridgeQuote/thunks.ts
@@ -0,0 +1,189 @@
+import { createAsyncThunk } from '@reduxjs/toolkit'
+import { commify } from '@ethersproject/units'
+import { Address, zeroAddress } from 'viem'
+
+import { getErc20TokenAllowance } from '@/actions/getErc20TokenAllowance'
+import { AcceptedChainId, CHAINS_BY_ID } from '@/constants/chains'
+import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider'
+import { stringToBigInt, formatBigIntToString } from '@/utils/bigint/format'
+import { calculateExchangeRate } from '@/utils/calculateExchangeRate'
+import { getBridgeModuleNames } from '@/utils/getBridgeModuleNames'
+import { Token } from '@/utils/types'
+import { BridgeModulePause } from '@/components/Maintenance/Maintenance'
+
+export const fetchBridgeQuote = createAsyncThunk(
+ 'bridgeQuote/fetchBridgeQuote',
+ async (
+ {
+ synapseSDK,
+ fromChainId,
+ toChainId,
+ fromToken,
+ toToken,
+ debouncedFromValue,
+ requestId,
+ currentTimestamp,
+ address,
+ pausedModulesList,
+ }: {
+ synapseSDK: any
+ fromChainId: number
+ toChainId: number
+ fromToken: Token
+ toToken: Token
+ debouncedFromValue: string
+ requestId: number
+ currentTimestamp: number
+ address: Address
+ pausedModulesList: BridgeModulePause[]
+ },
+ { rejectWithValue }
+ ) => {
+ const allQuotes = await synapseSDK.allBridgeQuotes(
+ fromChainId,
+ toChainId,
+ fromToken.addresses[fromChainId],
+ toToken.addresses[toChainId],
+ stringToBigInt(debouncedFromValue, fromToken?.decimals[fromChainId]),
+ {
+ originUserAddress: address,
+ }
+ )
+
+ const pausedBridgeModules = new Set(
+ pausedModulesList
+ .filter((module) =>
+ module.chainId ? module.chainId === fromChainId : true
+ )
+ .flatMap(getBridgeModuleNames)
+ )
+ const activeQuotes = allQuotes.filter(
+ (quote) => !pausedBridgeModules.has(quote.bridgeModuleName)
+ )
+
+ if (activeQuotes.length === 0) {
+ const msg = `No route found for bridging ${debouncedFromValue} ${fromToken?.symbol} on ${CHAINS_BY_ID[fromChainId]?.name} to ${toToken?.symbol} on ${CHAINS_BY_ID[toChainId]?.name}`
+ return rejectWithValue(msg)
+ }
+
+ const rfqQuote = activeQuotes.find(
+ (q) => q.bridgeModuleName === 'SynapseRFQ'
+ )
+
+ const nonRfqQuote = activeQuotes.find(
+ (quote) => quote.bridgeModuleName !== 'SynapseRFQ'
+ )
+
+ let quote
+
+ if (rfqQuote && nonRfqQuote) {
+ const rfqMaxAmountOut = BigInt(rfqQuote.maxAmountOut.toString())
+ const nonRfqMaxAmountOut = BigInt(nonRfqQuote.maxAmountOut.toString())
+
+ const allowedPercentileDifference = 30n
+ const maxDifference =
+ (nonRfqMaxAmountOut * allowedPercentileDifference) / 100n
+
+ if (rfqMaxAmountOut > nonRfqMaxAmountOut - maxDifference) {
+ quote = rfqQuote
+ } else {
+ quote = nonRfqQuote
+
+ segmentAnalyticsEvent(`[Bridge] use non-RFQ quote over RFQ`, {
+ bridgeModuleName: nonRfqQuote.bridgeModuleName,
+ originChainId: fromChainId,
+ originToken: fromToken.symbol,
+ originTokenAddress: fromToken.addresses[fromChainId],
+ destinationChainId: toChainId,
+ destinationToken: toToken.symbol,
+ destinationTokenAddress: toToken.addresses[toChainId],
+ rfqQuoteAmountOut: rfqQuote.maxAmountOut.toString(),
+ nonRfqMaxAmountOut: nonRfqQuote.maxAmountOut.toString(),
+ })
+ }
+ } else {
+ quote = rfqQuote ?? nonRfqQuote
+ }
+
+ const {
+ feeAmount,
+ routerAddress,
+ maxAmountOut,
+ originQuery,
+ destQuery,
+ estimatedTime,
+ bridgeModuleName,
+ gasDropAmount,
+ originChainId,
+ destChainId,
+ } = quote
+
+ if (!(originQuery && maxAmountOut && destQuery && feeAmount)) {
+ const msg = `No route found for bridging ${debouncedFromValue} ${fromToken?.symbol} on ${CHAINS_BY_ID[fromChainId]?.name} to ${toToken?.symbol} on ${CHAINS_BY_ID[toChainId]?.name}`
+ return rejectWithValue(msg)
+ }
+
+ const toValueBigInt = BigInt(maxAmountOut.toString()) ?? 0n
+
+ // Bridge Lifecycle: originToken -> bridgeToken -> destToken
+ // debouncedFromValue is in originToken decimals
+ // originQuery.minAmountOut and feeAmount is in bridgeToken decimals
+ // Adjust feeAmount to be in originToken decimals
+ const adjustedFeeAmount =
+ (BigInt(feeAmount) *
+ stringToBigInt(
+ `${debouncedFromValue}`,
+ fromToken?.decimals[fromChainId]
+ )) /
+ BigInt(originQuery.minAmountOut)
+
+ const isUnsupported = AcceptedChainId[fromChainId] ? false : true
+
+ const allowance =
+ fromToken?.addresses[fromChainId] === zeroAddress ||
+ address === undefined ||
+ isUnsupported
+ ? 0n
+ : await getErc20TokenAllowance({
+ address,
+ chainId: fromChainId,
+ tokenAddress: fromToken?.addresses[fromChainId] as Address,
+ spender: routerAddress,
+ })
+
+ const {
+ originQuery: originQueryWithSlippage,
+ destQuery: destQueryWithSlippage,
+ } = synapseSDK.applyBridgeSlippage(bridgeModuleName, originQuery, destQuery)
+
+ return {
+ inputAmountForQuote: debouncedFromValue,
+ originTokenForQuote: fromToken,
+ destTokenForQuote: toToken,
+ outputAmount: toValueBigInt,
+ outputAmountString: commify(
+ formatBigIntToString(toValueBigInt, toToken.decimals[toChainId], 8)
+ ),
+ routerAddress,
+ allowance,
+ exchangeRate: calculateExchangeRate(
+ stringToBigInt(debouncedFromValue, fromToken?.decimals[fromChainId]) -
+ BigInt(adjustedFeeAmount),
+ fromToken?.decimals[fromChainId],
+ toValueBigInt,
+ toToken.decimals[toChainId]
+ ),
+ feeAmount,
+ delta: BigInt(maxAmountOut.toString()),
+ originQuery: originQueryWithSlippage,
+ destQuery: destQueryWithSlippage,
+ estimatedTime,
+ bridgeModuleName,
+ gasDropAmount: BigInt(gasDropAmount.toString()),
+ timestamp: currentTimestamp,
+ originChainId,
+ destChainId,
+ requestId,
+ }
+ }
+)
diff --git a/packages/synapse-interface/store/reducer.ts b/packages/synapse-interface/store/reducer.ts
index f2d9c3a4e5..f6d940f210 100644
--- a/packages/synapse-interface/store/reducer.ts
+++ b/packages/synapse-interface/store/reducer.ts
@@ -17,6 +17,7 @@ import poolWithdraw from '@/slices/poolWithdrawSlice'
import priceData from '@/slices/priceDataSlice'
import gasData from '@/slices/gasDataSlice'
import wallet from '@/slices/wallet/reducer'
+import bridgeQuote from '@/slices/bridgeQuote/reducer'
import { api } from '@/slices/api/slice'
import { RootActions } from '@/slices/application/actions'
@@ -48,6 +49,7 @@ export const appReducer = combineReducers({
priceData,
gasData,
wallet,
+ bridgeQuote,
[api.reducerPath]: api.reducer,
...persistedReducers,
})
diff --git a/packages/synapse-interface/store/store.ts b/packages/synapse-interface/store/store.ts
index db96e735d7..70f5515a6f 100644
--- a/packages/synapse-interface/store/store.ts
+++ b/packages/synapse-interface/store/store.ts
@@ -45,6 +45,7 @@ let previousState = store.getState()
store.subscribe(() => {
const currentState = store.getState()
const bridgeState = currentState.bridge
+ const bridgeQuoteState = currentState.bridgeQuote
const address = currentState.application?.lastConnectedAddress
@@ -53,13 +54,14 @@ store.subscribe(() => {
if (
!_.isEqual(
- previousState.bridge.bridgeQuote,
- currentState.bridge.bridgeQuote
+ previousState.bridgeQuote.bridgeQuote,
+ currentState.bridgeQuote.bridgeQuote
) &&
- currentState.bridge.bridgeQuote.outputAmount !== 0n
+ currentState.bridgeQuote.bridgeQuote.outputAmount !== 0n
) {
const { outputAmountString, routerAddress, exchangeRate } =
- bridgeState.bridgeQuote
+ bridgeQuoteState.bridgeQuote
+
const { fromChainId, toChainId, fromToken, toToken, debouncedFromValue } =
bridgeState
@@ -74,7 +76,7 @@ store.subscribe(() => {
outputAmountString,
routerAddress,
exchangeRate: BigInt(exchangeRate.toString()),
- bridgeQuote: currentState.bridge.bridgeQuote,
+ bridgeQuote: currentState.bridgeQuote.bridgeQuote,
}
segmentAnalyticsEvent(eventTitle, eventData)
}
diff --git a/packages/synapse-interface/utils/hooks/useBridgeListener.ts b/packages/synapse-interface/utils/hooks/useBridgeListener.ts
deleted file mode 100644
index e19fd6f2b9..0000000000
--- a/packages/synapse-interface/utils/hooks/useBridgeListener.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useEffect } from 'react'
-
-import { useAppDispatch } from '@/store/hooks'
-import { useBridgeState } from '@/slices/bridge/hooks'
-import {
- BridgeState,
- setIsLoading,
- initialState,
- updateDebouncedFromValue,
- updateDebouncedToTokensFromValue,
-} from '@/slices/bridge/reducer'
-
-export const useBridgeListener = () => {
- const dispatch = useAppDispatch()
- const { fromValue, debouncedFromValue }: BridgeState = useBridgeState()
-
- /**
- * Debounce user input to fetch primary bridge quote (in ms)
- * Delay loading animation when user input updates
- */
- useEffect(() => {
- const DEBOUNCE_DELAY = 300
- const ANIMATION_DELAY = 200
-
- const animationTimer = setTimeout(() => {
- if (debouncedFromValue !== initialState.debouncedFromValue) {
- dispatch(setIsLoading(true))
- }
- }, ANIMATION_DELAY)
-
- const debounceTimer = setTimeout(() => {
- dispatch(updateDebouncedFromValue(fromValue))
- }, DEBOUNCE_DELAY)
-
- return () => {
- clearTimeout(debounceTimer)
- clearTimeout(animationTimer)
- dispatch(setIsLoading(false))
- }
- }, [fromValue])
-
- // Debounce alternative destination token bridge quotes
- useEffect(() => {
- const ALTERNATE_OPTIONS_DEBOUNCE_DELAY = 1000
-
- const alternativeOptionsDebounceTimer = setTimeout(() => {
- dispatch(updateDebouncedToTokensFromValue(debouncedFromValue))
- }, ALTERNATE_OPTIONS_DEBOUNCE_DELAY)
-
- return () => {
- clearTimeout(alternativeOptionsDebounceTimer)
- }
- }, [debouncedFromValue])
-
- return null
-}
diff --git a/packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts b/packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts
new file mode 100644
index 0000000000..e5124d17b0
--- /dev/null
+++ b/packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts
@@ -0,0 +1,30 @@
+import { useEffect, useState } from 'react'
+import { zeroAddress } from 'viem'
+
+import { stringToBigInt } from '@/utils/bigint/format'
+import { useBridgeState } from '@/slices/bridge/hooks'
+import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
+
+export const useIsBridgeApproved = () => {
+ const { debouncedFromValue, fromChainId, fromToken } = useBridgeState()
+ const { bridgeQuote } = useBridgeQuoteState()
+
+ const [isApproved, setIsApproved] = useState(false)
+
+ useEffect(() => {
+ if (fromToken && fromToken.addresses[fromChainId] === zeroAddress) {
+ setIsApproved(true)
+ } else if (
+ fromToken &&
+ bridgeQuote?.allowance &&
+ stringToBigInt(debouncedFromValue, fromToken.decimals[fromChainId]) <=
+ bridgeQuote.allowance
+ ) {
+ setIsApproved(true)
+ } else {
+ setIsApproved(false)
+ }
+ }, [bridgeQuote, fromToken, debouncedFromValue, fromChainId])
+
+ return isApproved
+}
diff --git a/packages/synapse-interface/utils/types/index.tsx b/packages/synapse-interface/utils/types/index.tsx
index e44e870f35..2497d9d7d2 100644
--- a/packages/synapse-interface/utils/types/index.tsx
+++ b/packages/synapse-interface/utils/types/index.tsx
@@ -70,6 +70,9 @@ type QuoteQuery = {
}
export type BridgeQuote = {
+ inputAmountForQuote: string
+ originTokenForQuote: Token
+ destTokenForQuote: Token
outputAmount: bigint
outputAmountString: string
routerAddress: string
@@ -85,6 +88,7 @@ export type BridgeQuote = {
timestamp: number
originChainId: number
destChainId: number
+ requestId: number
}
interface TokensByChain {