From 20a4f7e2e71698b1ca276108047b29c0c5050be5 Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Tue, 13 Aug 2024 17:11:04 -0400 Subject: [PATCH 01/11] Extracts bridgeQuote into own state --- .../BridgeExchangeRateInfo.tsx | 15 ++++---- .../BridgeTransactionButton.tsx | 5 +-- .../StateManagedBridge/OutputContainer.tsx | 3 +- .../pages/state-managed-bridge/index.tsx | 8 ++--- .../slices/bridge/reducer.ts | 16 --------- .../slices/bridgeQuote/hooks.ts | 6 ++++ .../slices/bridgeQuote/reducer.ts | 35 +++++++++++++++++++ packages/synapse-interface/store/reducer.ts | 2 ++ packages/synapse-interface/store/store.ts | 12 ++++--- .../utils/hooks/useBridgeListener.ts | 2 +- 10 files changed, 69 insertions(+), 35 deletions(-) create mode 100644 packages/synapse-interface/slices/bridgeQuote/hooks.ts create mode 100644 packages/synapse-interface/slices/bridgeQuote/reducer.ts diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeExchangeRateInfo.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeExchangeRateInfo.tsx index 11283e8b62..4af36e5a57 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeExchangeRateInfo.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeExchangeRateInfo.tsx @@ -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: @@ -55,10 +56,11 @@ const DestinationAddress = () => { } const Slippage = () => { + const { fromValue } = useBridgeState() + const { - fromValue, bridgeQuote: { exchangeRate }, - } = useBridgeState() + } = useBridgeQuoteState() const { formattedPercentSlippage, safeFromAmount, underFee, textColor } = useExchangeRateInfo(fromValue, exchangeRate) @@ -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)) { diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx index 9e76fc286b..6e63de8ab5 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx @@ -12,6 +12,7 @@ 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' export const BridgeTransactionButton = ({ approveTxn, @@ -43,10 +44,10 @@ export const BridgeTransactionButton = ({ toToken, fromChainId, toChainId, - isLoading, - bridgeQuote, } = useBridgeState() + const { isLoading, bridgeQuote } = useBridgeQuoteState() + const { isWalletPending } = useWalletState() const { showDestinationWarning, isDestinationWarningAccepted } = useBridgeDisplayState() diff --git a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx index 9d3459e551..c7e8d05ade 100644 --- a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx @@ -12,10 +12,11 @@ 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' export const OutputContainer = () => { const { address } = useAccount() - const { bridgeQuote, isLoading } = useBridgeState() + const { bridgeQuote, isLoading } = useBridgeQuoteState() const { showDestinationAddress } = useBridgeDisplayState() const showValue = diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index 5cb882de61..de337c73ee 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -36,8 +36,6 @@ import { setToChainId, setToToken, updateFromValue, - setBridgeQuote, - setIsLoading, setDestinationAddress, } from '@/slices/bridge/reducer' import { setIsWalletPending } from '@/slices/wallet/reducer' @@ -70,6 +68,8 @@ 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 { setBridgeQuote, setIsLoading } from '@/slices/bridgeQuote/reducer' const StateManagedBridge = () => { const { address } = useAccount() @@ -86,12 +86,12 @@ const StateManagedBridge = () => { toChainId, fromToken, toToken, - bridgeQuote, debouncedFromValue, destinationAddress, - isLoading: isQuoteLoading, }: BridgeState = useBridgeState() + const { bridgeQuote, isLoading: isQuoteLoading } = useBridgeQuoteState() + const { isWalletPending } = useWalletState() const { showSettingsSlideOver, showDestinationAddress } = useSelector( diff --git a/packages/synapse-interface/slices/bridge/reducer.ts b/packages/synapse-interface/slices/bridge/reducer.ts index 2891b5d92f..6a27ef3c64 100644 --- a/packages/synapse-interface/slices/bridge/reducer.ts +++ b/packages/synapse-interface/slices/bridge/reducer.ts @@ -27,8 +27,6 @@ export interface BridgeState { fromValue: string debouncedFromValue: string debouncedToTokensFromValue: string - bridgeQuote: BridgeQuote - isLoading: boolean deadlineMinutes: number | null destinationAddress: Address | null } @@ -62,8 +60,6 @@ export const initialState: BridgeState = { fromValue: '', debouncedFromValue: '', debouncedToTokensFromValue: '', - bridgeQuote: EMPTY_BRIDGE_QUOTE, - isLoading: false, deadlineMinutes: null, destinationAddress: null, } @@ -72,9 +68,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,9 +424,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 }, @@ -455,9 +445,6 @@ 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 @@ -472,8 +459,6 @@ export const bridgeSlice = createSlice({ export const { updateDebouncedFromValue, updateDebouncedToTokensFromValue, - setBridgeQuote, - resetBridgeQuote, setFromChainId, setToChainId, setFromToken, @@ -481,7 +466,6 @@ export const { 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..abfb32935f --- /dev/null +++ b/packages/synapse-interface/slices/bridgeQuote/reducer.ts @@ -0,0 +1,35 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +import { EMPTY_BRIDGE_QUOTE } from '@/constants/bridge' +import { type BridgeQuote } from '@/utils/types' + +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 + }, + setBridgeQuote: (state, action: PayloadAction) => { + state.bridgeQuote = action.payload + }, + resetBridgeQuote: (state) => { + state.bridgeQuote = initialState.bridgeQuote + }, + }, +}) + +export const { setBridgeQuote, resetBridgeQuote, setIsLoading } = + bridgeQuoteSlice.actions + +export default bridgeQuoteSlice.reducer 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 index e19fd6f2b9..0855ce2d1c 100644 --- a/packages/synapse-interface/utils/hooks/useBridgeListener.ts +++ b/packages/synapse-interface/utils/hooks/useBridgeListener.ts @@ -4,11 +4,11 @@ import { useAppDispatch } from '@/store/hooks' import { useBridgeState } from '@/slices/bridge/hooks' import { BridgeState, - setIsLoading, initialState, updateDebouncedFromValue, updateDebouncedToTokensFromValue, } from '@/slices/bridge/reducer' +import { setIsLoading } from '@/slices/bridgeQuote/reducer' export const useBridgeListener = () => { const dispatch = useAppDispatch() From 4f2cb18b68647587c819451b850743ed6b5d7740 Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Tue, 13 Aug 2024 17:11:20 -0400 Subject: [PATCH 02/11] Removes teaser --- .../synapse-interface/pages/teaser/#index.tsx | 30 --- .../pages/teaser/FauxBridge.tsx | 248 ------------------ .../synapse-interface/pages/teaser/Hero.tsx | 153 ----------- .../pages/teaser/ValueProps.tsx | 231 ---------------- 4 files changed, 662 deletions(-) delete mode 100644 packages/synapse-interface/pages/teaser/#index.tsx delete mode 100644 packages/synapse-interface/pages/teaser/FauxBridge.tsx delete mode 100644 packages/synapse-interface/pages/teaser/Hero.tsx delete mode 100644 packages/synapse-interface/pages/teaser/ValueProps.tsx diff --git a/packages/synapse-interface/pages/teaser/#index.tsx b/packages/synapse-interface/pages/teaser/#index.tsx deleted file mode 100644 index 635b626b15..0000000000 --- a/packages/synapse-interface/pages/teaser/#index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useRouter } from 'next/router' -import { useEffect } from 'react' -import { useAccount } from 'wagmi' - -import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' - -import Hero from './Hero' -import ValueProps from './ValueProps' - -import Wrapper from '@/components/WipWrapperComponents/Wrapper' - -const LandingPage = () => { - 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 ( -
- -
- - - - - - - - - {arr.map((item, i) => { - reduce() - return ( - - - - - ) - })} - - -
- {type} - - {header} -
- - {item[name]} - {format()}
-
-
- ) -} - -const SupportedWallets = () => ( -
-
-
Supported wallets
-
    - {Object.values(WALLET_ICONS).map((icon, i) => ( -
  • {icon({ width: 24, height: 24 })}
  • - ))} -
-
- -
-) - -const HistoricMax = () => ( -
-
- -
    -
  • 40,668 ETH
  • -
  • Fantom
  • -
- -
    -
  • 40,668 ETH
  • -
  • Ethereum
  • -
-
Jan 29, 2022 – #1
-
-
- -
-) - -const RightAngle = ({ height }) => { - const width = height / 2 - return ( - - - - ) -} - -const BridgeButton = () => ( -
-
-
- Visit Bridge -
-
- { - const target = e.target as HTMLAnchorElement - target.querySelector('animate')?.beginElement() - }} - > - Bridge - - - - - -
-) \ 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()} - > -

- {url ? ( - { - const target = e.target as HTMLAnchorElement - target.querySelector('animate')?.beginElement() - }} - className="p-4 hover:text-black hover:dark:text-white" - > - - {index === tag.length && } - - ) : ( - - )} -

-
- - Bridge - - - Build - -
-
-

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

    -
  • -
-
*/} - {/*
-
-

Widely Integrated

-

- Synapse is widely integrated across the most-used Layer 1 & 2 - networks for a seamless cross-chain experience. -

-
-
    - {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' - }` - } - */ From e577fe35cb51457ccad8b6f8447afde66a2389bd Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Tue, 13 Aug 2024 19:13:40 -0400 Subject: [PATCH 03/11] Moves quote fetching to async thunk --- .../BridgeTransactionButton.tsx | 3 +- .../synapse-interface/constants/bridge.ts | 18 +- .../pages/state-managed-bridge/index.tsx | 206 +++--------------- .../slices/bridge/reducer.ts | 3 +- .../slices/bridgeQuote/reducer.ts | 30 ++- .../slices/bridgeQuote/thunks.ts | 185 ++++++++++++++++ .../synapse-interface/utils/types/index.tsx | 1 + 7 files changed, 244 insertions(+), 202 deletions(-) create mode 100644 packages/synapse-interface/slices/bridgeQuote/thunks.ts diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx index 6e63de8ab5..2b070970bd 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { TransactionButton } from '@/components/buttons/TransactionButton' -import { EMPTY_BRIDGE_QUOTE, EMPTY_BRIDGE_QUOTE_ZERO } from '@/constants/bridge' +import { EMPTY_BRIDGE_QUOTE } from '@/constants/bridge' import { useAccount, useAccountEffect, useSwitchChain } from 'wagmi' import { useEffect, useState } from 'react' import { isAddress } from 'viem' @@ -90,7 +90,6 @@ export const BridgeTransactionButton = ({ const isButtonDisabled = isLoading || isWalletPending || - bridgeQuote === EMPTY_BRIDGE_QUOTE_ZERO || bridgeQuote === EMPTY_BRIDGE_QUOTE || (destinationAddress && !isAddress(destinationAddress)) || (isConnected && !sufficientBalance) || diff --git a/packages/synapse-interface/constants/bridge.ts b/packages/synapse-interface/constants/bridge.ts index 40708c1f1a..5c40ff4c88 100644 --- a/packages/synapse-interface/constants/bridge.ts +++ b/packages/synapse-interface/constants/bridge.ts @@ -19,25 +19,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/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index de337c73ee..488c4cb498 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -25,8 +25,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 { @@ -44,9 +43,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' @@ -61,15 +57,14 @@ 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 { setBridgeQuote, setIsLoading } from '@/slices/bridgeQuote/reducer' +import { resetBridgeQuote, setIsLoading } from '@/slices/bridgeQuote/reducer' +import { fetchBridgeQuote } from '@/slices/bridgeQuote/thunks' const StateManagedBridge = () => { const { address } = useAccount() @@ -127,7 +122,7 @@ const StateManagedBridge = () => { console.log('trying to set bridge quote') getAndSetBridgeQuote() } else { - dispatch(setBridgeQuote(EMPTY_BRIDGE_QUOTE_ZERO)) + dispatch(resetBridgeQuote()) dispatch(setIsLoading(false)) } }, [fromChainId, toChainId, fromToken, toToken, debouncedFromValue]) @@ -156,176 +151,39 @@ const StateManagedBridge = () => { // will have to handle deadlineMinutes here at later time, gets passed as optional last arg in .bridgeQuote() /* clear stored bridge quote before requesting new bridge quote */ - dispatch(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)) { + quoteToastRef.current.id = toast(result.payload as string, { + duration: 3000, + }) + } } } catch (err) { console.log(err) @@ -347,14 +205,10 @@ 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)) - } } } @@ -509,7 +363,7 @@ const StateManagedBridge = () => { isSubmitted: false, }) ) - dispatch(setBridgeQuote(EMPTY_BRIDGE_QUOTE_ZERO)) + dispatch(resetBridgeQuote()) dispatch(setDestinationAddress(null)) dispatch(setShowDestinationAddress(false)) dispatch(updateFromValue('')) diff --git a/packages/synapse-interface/slices/bridge/reducer.ts b/packages/synapse-interface/slices/bridge/reducer.ts index 6a27ef3c64..602d6d873e 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, diff --git a/packages/synapse-interface/slices/bridgeQuote/reducer.ts b/packages/synapse-interface/slices/bridgeQuote/reducer.ts index abfb32935f..5c7575f91a 100644 --- a/packages/synapse-interface/slices/bridgeQuote/reducer.ts +++ b/packages/synapse-interface/slices/bridgeQuote/reducer.ts @@ -2,15 +2,18 @@ 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 + error: any } export const initialState: BridgeQuoteState = { bridgeQuote: EMPTY_BRIDGE_QUOTE, isLoading: false, + error: null, } export const bridgeQuoteSlice = createSlice({ @@ -20,16 +23,33 @@ export const bridgeQuoteSlice = createSlice({ setIsLoading: (state, action: PayloadAction) => { state.isLoading = action.payload }, - setBridgeQuote: (state, action: PayloadAction) => { - state.bridgeQuote = action.payload - }, resetBridgeQuote: (state) => { state.bridgeQuote = initialState.bridgeQuote }, }, + extraReducers: (builder) => { + builder + .addCase(fetchBridgeQuote.pending, (state) => { + // state.status = FetchState.LOADING + state.isLoading = true + }) + .addCase( + fetchBridgeQuote.fulfilled, + (state, action: PayloadAction) => { + state.bridgeQuote = action.payload + // state.status = FetchState.VALID + state.isLoading = false + } + ) + .addCase(fetchBridgeQuote.rejected, (state, action) => { + // state.error = action.payload + state.bridgeQuote = EMPTY_BRIDGE_QUOTE + // state.status = FetchState.INVALID + state.isLoading = false + }) + }, }) -export const { setBridgeQuote, resetBridgeQuote, setIsLoading } = - bridgeQuoteSlice.actions +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..d984a15dee --- /dev/null +++ b/packages/synapse-interface/slices/bridgeQuote/thunks.ts @@ -0,0 +1,185 @@ +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' + +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: any + }, + { 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 { + 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/utils/types/index.tsx b/packages/synapse-interface/utils/types/index.tsx index e44e870f35..8454ba91e0 100644 --- a/packages/synapse-interface/utils/types/index.tsx +++ b/packages/synapse-interface/utils/types/index.tsx @@ -85,6 +85,7 @@ export type BridgeQuote = { timestamp: number originChainId: number destChainId: number + requestId: number } interface TokensByChain { From d23cb0aecc862324a01c56580a9aeefa65cf536d Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Tue, 13 Aug 2024 19:38:25 -0400 Subject: [PATCH 04/11] Lints --- .../pages/state-managed-bridge/index.tsx | 4 +--- packages/synapse-interface/slices/bridgeQuote/reducer.ts | 8 +------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index 488c4cb498..f2b04e8d5a 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' @@ -63,7 +62,7 @@ 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, setIsLoading } from '@/slices/bridgeQuote/reducer' +import { resetBridgeQuote } from '@/slices/bridgeQuote/reducer' import { fetchBridgeQuote } from '@/slices/bridgeQuote/thunks' const StateManagedBridge = () => { @@ -123,7 +122,6 @@ const StateManagedBridge = () => { getAndSetBridgeQuote() } else { dispatch(resetBridgeQuote()) - dispatch(setIsLoading(false)) } }, [fromChainId, toChainId, fromToken, toToken, debouncedFromValue]) diff --git a/packages/synapse-interface/slices/bridgeQuote/reducer.ts b/packages/synapse-interface/slices/bridgeQuote/reducer.ts index 5c7575f91a..8407876035 100644 --- a/packages/synapse-interface/slices/bridgeQuote/reducer.ts +++ b/packages/synapse-interface/slices/bridgeQuote/reducer.ts @@ -7,13 +7,11 @@ import { fetchBridgeQuote } from './thunks' export interface BridgeQuoteState { bridgeQuote: BridgeQuote isLoading: boolean - error: any } export const initialState: BridgeQuoteState = { bridgeQuote: EMPTY_BRIDGE_QUOTE, isLoading: false, - error: null, } export const bridgeQuoteSlice = createSlice({ @@ -30,21 +28,17 @@ export const bridgeQuoteSlice = createSlice({ extraReducers: (builder) => { builder .addCase(fetchBridgeQuote.pending, (state) => { - // state.status = FetchState.LOADING state.isLoading = true }) .addCase( fetchBridgeQuote.fulfilled, (state, action: PayloadAction) => { state.bridgeQuote = action.payload - // state.status = FetchState.VALID state.isLoading = false } ) - .addCase(fetchBridgeQuote.rejected, (state, action) => { - // state.error = action.payload + .addCase(fetchBridgeQuote.rejected, (state) => { state.bridgeQuote = EMPTY_BRIDGE_QUOTE - // state.status = FetchState.INVALID state.isLoading = false }) }, From 82519892733108dd3f4ada08f448f22777dd5746 Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Wed, 14 Aug 2024 13:01:46 -0400 Subject: [PATCH 05/11] Bridge approved check hook --- .../pages/state-managed-bridge/index.tsx | 38 +++++++------------ .../utils/hooks/useIsBridgeApproved.ts | 31 +++++++++++++++ 2 files changed, 44 insertions(+), 25 deletions(-) create mode 100644 packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index 3c990d9a3e..f955de9c00 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -64,6 +64,7 @@ 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() @@ -84,7 +85,14 @@ const StateManagedBridge = () => { destinationAddress, }: BridgeState = useBridgeState() - const { bridgeQuote, isLoading: isQuoteLoading } = useBridgeQuoteState() + const { bridgeQuote, isLoading } = useBridgeQuoteState() + + const isApproved = useIsBridgeApproved( + fromToken, + fromChainId, + bridgeQuote, + debouncedFromValue + ) const { isWalletPending } = useWalletState() @@ -99,8 +107,6 @@ const StateManagedBridge = () => { BridgeMaintenanceWarningMessage, } = useMaintenance() - const [isApproved, setIsApproved] = useState(false) - const dispatch = useAppDispatch() useEffect(() => { @@ -125,24 +131,6 @@ const StateManagedBridge = () => { } }, [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 @@ -178,9 +166,9 @@ const StateManagedBridge = () => { } if (fetchBridgeQuote.rejected.match(result)) { - quoteToastRef.current.id = toast(result.payload as string, { - duration: 3000, - }) + const message = result.payload as string + + quoteToastRef.current.id = toast(message, { duration: 3000 }) } } } catch (err) { @@ -213,7 +201,7 @@ const StateManagedBridge = () => { useStaleQuoteUpdater( bridgeQuote, getAndSetBridgeQuote, - isQuoteLoading, + isLoading, isWalletPending ) diff --git a/packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts b/packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts new file mode 100644 index 0000000000..fbdca7c415 --- /dev/null +++ b/packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react' +import { zeroAddress } from 'viem' + +import { stringToBigInt } from '@/utils/bigint/format' +import { BridgeQuote, Token } from '@/utils/types' + +export const useIsBridgeApproved = ( + fromToken: Token | null, + fromChainId: number, + bridgeQuote: BridgeQuote | null, + debouncedFromValue: string +) => { + 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 +} From 90316a2de6c105337fc18abb11ec33c0e744a1c6 Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Thu, 15 Aug 2024 11:40:52 -0400 Subject: [PATCH 06/11] Deprecates redux fromValue in favor of local input component behavior --- .../BridgeExchangeRateInfo.tsx | 8 +- .../BridgeTransactionButton.tsx | 34 ++-- .../StateManagedBridge/FromChainSelector.tsx | 24 +++ .../StateManagedBridge/FromTokenSelector.tsx | 23 +++ .../StateManagedBridge/InputContainer.tsx | 152 +++++++----------- .../contexts/BackgroundListenerProvider.tsx | 2 - .../pages/state-managed-bridge/index.tsx | 13 +- .../slices/bridge/reducer.ts | 7 - .../utils/hooks/useBridgeListener.ts | 56 ------- .../utils/hooks/useIsBridgeApproved.ts | 13 +- 10 files changed, 140 insertions(+), 192 deletions(-) create mode 100644 packages/synapse-interface/components/StateManagedBridge/FromChainSelector.tsx create mode 100644 packages/synapse-interface/components/StateManagedBridge/FromTokenSelector.tsx delete mode 100644 packages/synapse-interface/utils/hooks/useBridgeListener.ts diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeExchangeRateInfo.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeExchangeRateInfo.tsx index 4af36e5a57..9524d59225 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeExchangeRateInfo.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeExchangeRateInfo.tsx @@ -56,14 +56,14 @@ const DestinationAddress = () => { } const Slippage = () => { - const { fromValue } = useBridgeState() + const { debouncedFromValue } = useBridgeState() const { bridgeQuote: { exchangeRate }, } = useBridgeQuoteState() const { formattedPercentSlippage, safeFromAmount, underFee, textColor } = - useExchangeRateInfo(fromValue, exchangeRate) + useExchangeRateInfo(debouncedFromValue, exchangeRate) return (
Slippage @@ -169,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 2b070970bd..c5c9c1ea34 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx @@ -40,7 +40,7 @@ export const BridgeTransactionButton = ({ const { destinationAddress, fromToken, - fromValue, + debouncedFromValue, toToken, fromChainId, toChainId, @@ -61,24 +61,26 @@ export const BridgeTransactionButton = ({ const sufficientBalance = useMemo(() => { if (!fromChainId || !fromToken || !toChainId || !toToken) return false return ( - stringToBigInt(fromValue, fromToken?.decimals[fromChainId]) <= + stringToBigInt(debouncedFromValue, fromToken?.decimals[fromChainId]) <= balanceForToken ) - }, [balanceForToken, fromValue, fromChainId, toChainId, toToken]) + }, [balanceForToken, debouncedFromValue, fromChainId, toChainId, toToken]) const fromTokenDecimals: number | undefined = fromToken && fromToken?.decimals[fromChainId] - const fromValueBigInt = useMemo(() => { - return fromTokenDecimals ? stringToBigInt(fromValue, fromTokenDecimals) : 0 - }, [fromValue, fromTokenDecimals]) + const debouncedFromValueBigInt = useMemo(() => { + return fromTokenDecimals + ? stringToBigInt(debouncedFromValue, fromTokenDecimals) + : 0 + }, [debouncedFromValue, fromTokenDecimals]) const bridgeQuoteAmountGreaterThanInputForRfq = useMemo(() => { return ( bridgeQuote.bridgeModuleName === 'SynapseRFQ' && - bridgeQuote.outputAmount > fromValueBigInt + bridgeQuote.outputAmount > debouncedFromValueBigInt ) - }, [bridgeQuote.outputAmount, fromValueBigInt]) + }, [bridgeQuote.outputAmount, debouncedFromValueBigInt]) const chainSelectionsMatchBridgeQuote = useMemo(() => { return ( @@ -127,7 +129,7 @@ export const BridgeTransactionButton = ({ } else if ( !isLoading && bridgeQuote?.feeAmount === 0n && - fromValueBigInt > 0 + debouncedFromValueBigInt > 0 ) { buttonProperties = { label: `Amount must be greater than fee`, @@ -136,7 +138,7 @@ export const BridgeTransactionButton = ({ } else if ( !isLoading && !chainSelectionsMatchBridgeQuote && - fromValueBigInt > 0 + debouncedFromValueBigInt > 0 ) { buttonProperties = { label: 'Please reset chain selection', @@ -145,13 +147,13 @@ export const BridgeTransactionButton = ({ } else if ( !isLoading && bridgeQuoteAmountGreaterThanInputForRfq && - fromValueBigInt > 0 + debouncedFromValueBigInt > 0 ) { buttonProperties = { label: 'Invalid bridge quote', onClick: null, } - } else if (!isConnected && fromValueBigInt > 0) { + } else if (!isConnected && debouncedFromValueBigInt > 0) { buttonProperties = { label: `Connect Wallet to Bridge`, onClick: openConnectModal, @@ -171,13 +173,17 @@ export const BridgeTransactionButton = ({ onClick: () => dispatch(setIsDestinationWarningAccepted(true)), className: '!from-bgLight !to-bgLight', } - } else if (chain?.id != fromChainId && fromValueBigInt > 0) { + } else if (chain?.id != fromChainId && debouncedFromValueBigInt > 0) { 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 && + debouncedFromValueBigInt > 0 && + bridgeQuote?.destQuery + ) { 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..ab21d46c55 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, isNumber } 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,19 +11,18 @@ 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' export const inputRef = React.createRef() @@ -38,11 +30,10 @@ export const InputContainer = () => { const dispatch = useAppDispatch() const { chain, isConnected } = useAccount() const { balances } = usePortfolioState() - const { fromChainId, toChainId, fromToken, toToken, fromValue } = + const { fromChainId, toChainId, fromToken, toToken, debouncedFromValue } = useBridgeState() const { isWalletPending } = useWalletState() - const [showValue, setShowValue] = useState('') - const [hasMounted, setHasMounted] = useState(false) + const [localInputValue, setLocalInputValue] = useState(debouncedFromValue) const { addresses, decimals } = fromToken || {} const tokenDecimals = isNumber(decimals) ? decimals : decimals?.[fromChainId] @@ -70,27 +61,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)), + 300 + ), + [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 +132,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 && fromChainId === chain?.id) { return - } else if (hasMounted && isConnected && fromChainId !== chain?.id) { + } else if (isConnected && fromChainId !== chain?.id) { 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 ( @@ -152,7 +156,7 @@ export const InputContainer = () => {
@@ -179,39 +183,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/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 f955de9c00..b4dfed48a6 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -1,5 +1,5 @@ import toast from 'react-hot-toast' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef } from 'react' import { Address, zeroAddress, isAddress } from 'viem' import { polygon } from 'viem/chains' import { useAccount } from 'wagmi' @@ -33,7 +33,6 @@ import { setFromToken, setToChainId, setToToken, - updateFromValue, setDestinationAddress, } from '@/slices/bridge/reducer' import { setIsWalletPending } from '@/slices/wallet/reducer' @@ -87,16 +86,11 @@ const StateManagedBridge = () => { const { bridgeQuote, isLoading } = useBridgeQuoteState() - const isApproved = useIsBridgeApproved( - fromToken, - fromChainId, - bridgeQuote, - debouncedFromValue - ) + const isApproved = useIsBridgeApproved() const { isWalletPending } = useWalletState() - const { showSettingsSlideOver, showDestinationAddress } = useSelector( + const { showSettingsSlideOver } = useSelector( (state: RootState) => state.bridgeDisplay ) @@ -338,7 +332,6 @@ const StateManagedBridge = () => { dispatch(resetBridgeQuote()) dispatch(setDestinationAddress(null)) dispatch(setShowDestinationAddress(false)) - dispatch(updateFromValue('')) const successToastContent = (
diff --git a/packages/synapse-interface/slices/bridge/reducer.ts b/packages/synapse-interface/slices/bridge/reducer.ts index 602d6d873e..fa48546045 100644 --- a/packages/synapse-interface/slices/bridge/reducer.ts +++ b/packages/synapse-interface/slices/bridge/reducer.ts @@ -23,7 +23,6 @@ export interface BridgeState { fromTokens: Token[] toTokens: Token[] - fromValue: string debouncedFromValue: string debouncedToTokensFromValue: string deadlineMinutes: number | null @@ -56,7 +55,6 @@ export const initialState: BridgeState = { fromTokens, toTokens, - fromValue: '', debouncedFromValue: '', debouncedToTokensFromValue: '', deadlineMinutes: null, @@ -423,9 +421,6 @@ export const bridgeSlice = createSlice({ state.toChainIds = toChainIds state.toTokens = toTokens }, - updateFromValue: (state, action: PayloadAction) => { - state.fromValue = action.payload - }, updateDebouncedFromValue: (state, action: PayloadAction) => { state.debouncedFromValue = action.payload }, @@ -449,7 +444,6 @@ export const bridgeSlice = createSlice({ state.fromToken = initialState.fromToken state.toChainId = initialState.toChainId state.toToken = initialState.toToken - state.fromValue = initialState.fromValue state.debouncedFromValue = initialState.debouncedFromValue }, }, @@ -462,7 +456,6 @@ export const { setToChainId, setFromToken, setToToken, - updateFromValue, setDeadlineMinutes, setDestinationAddress, resetBridgeInputs, diff --git a/packages/synapse-interface/utils/hooks/useBridgeListener.ts b/packages/synapse-interface/utils/hooks/useBridgeListener.ts deleted file mode 100644 index 0855ce2d1c..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, - initialState, - updateDebouncedFromValue, - updateDebouncedToTokensFromValue, -} from '@/slices/bridge/reducer' -import { setIsLoading } from '@/slices/bridgeQuote/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 index fbdca7c415..e5124d17b0 100644 --- a/packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts +++ b/packages/synapse-interface/utils/hooks/useIsBridgeApproved.ts @@ -2,14 +2,13 @@ import { useEffect, useState } from 'react' import { zeroAddress } from 'viem' import { stringToBigInt } from '@/utils/bigint/format' -import { BridgeQuote, Token } from '@/utils/types' +import { useBridgeState } from '@/slices/bridge/hooks' +import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks' + +export const useIsBridgeApproved = () => { + const { debouncedFromValue, fromChainId, fromToken } = useBridgeState() + const { bridgeQuote } = useBridgeQuoteState() -export const useIsBridgeApproved = ( - fromToken: Token | null, - fromChainId: number, - bridgeQuote: BridgeQuote | null, - debouncedFromValue: string -) => { const [isApproved, setIsApproved] = useState(false) useEffect(() => { From d6a00f18d31d864d8df8be3aafb0fedeb71f870a Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Fri, 16 Aug 2024 15:10:40 -0400 Subject: [PATCH 07/11] Types pausedModulesList --- .../synapse-interface/components/Maintenance/Maintenance.tsx | 2 +- packages/synapse-interface/slices/bridgeQuote/thunks.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/synapse-interface/components/Maintenance/Maintenance.tsx b/packages/synapse-interface/components/Maintenance/Maintenance.tsx index 5b64ed4e7a..d6e2858089 100644 --- a/packages/synapse-interface/components/Maintenance/Maintenance.tsx +++ b/packages/synapse-interface/components/Maintenance/Maintenance.tsx @@ -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' } diff --git a/packages/synapse-interface/slices/bridgeQuote/thunks.ts b/packages/synapse-interface/slices/bridgeQuote/thunks.ts index d984a15dee..caad84d93e 100644 --- a/packages/synapse-interface/slices/bridgeQuote/thunks.ts +++ b/packages/synapse-interface/slices/bridgeQuote/thunks.ts @@ -9,6 +9,7 @@ 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', @@ -34,7 +35,7 @@ export const fetchBridgeQuote = createAsyncThunk( requestId: number currentTimestamp: number address: Address - pausedModulesList: any + pausedModulesList: BridgeModulePause[] }, { rejectWithValue } ) => { From e27f2e072fd19be62fd8b8adaf20a09b53294de2 Mon Sep 17 00:00:00 2001 From: bigboydiamonds <57741810+bigboydiamonds@users.noreply.github.com> Date: Fri, 16 Aug 2024 12:58:15 -0700 Subject: [PATCH 08/11] feat(synapse-interface): bridge state validations (#3021) * useBridgeSelections * useBridgeValidations * fix: stale output, infinite loader * fix: track when input less than fees * Remove finally * apply validation + selection hook to input/output containes * use bridge state for bridge write actions * Fix chain comparison * [wip] feat(synapse-interface): track quoted input (#3011) * 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 * hasValidFromSelections --- .../BridgeTransactionButton.tsx | 136 +++++++----------- .../StateManagedBridge/InputContainer.tsx | 34 ++--- .../StateManagedBridge/OutputContainer.tsx | 16 ++- .../hooks/useBridgeSelections.ts | 30 ++++ .../hooks/useBridgeValidations.ts | 125 ++++++++++++++++ .../synapse-interface/constants/bridge.ts | 3 + .../slices/bridgeQuote/thunks.ts | 3 + .../synapse-interface/utils/types/index.tsx | 3 + 8 files changed, 241 insertions(+), 109 deletions(-) create mode 100644 packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeSelections.ts create mode 100644 packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx index c5c9c1ea34..e972f72bb8 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx @@ -1,18 +1,16 @@ -import { useMemo } from 'react' -import { TransactionButton } from '@/components/buttons/TransactionButton' -import { EMPTY_BRIDGE_QUOTE } 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, @@ -21,10 +19,10 @@ export const BridgeTransactionButton = ({ isBridgePaused, }) => { 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({ @@ -40,64 +38,37 @@ export const BridgeTransactionButton = ({ const { destinationAddress, fromToken, - debouncedFromValue, - toToken, fromChainId, + toToken, toChainId, + debouncedFromValue, } = useBridgeState() - - const { isLoading, bridgeQuote } = useBridgeQuoteState() + 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(debouncedFromValue, fromToken?.decimals[fromChainId]) <= - balanceForToken - ) - }, [balanceForToken, debouncedFromValue, fromChainId, toChainId, toToken]) - - const fromTokenDecimals: number | undefined = - fromToken && fromToken?.decimals[fromChainId] - - const debouncedFromValueBigInt = useMemo(() => { - return fromTokenDecimals - ? stringToBigInt(debouncedFromValue, fromTokenDecimals) - : 0 - }, [debouncedFromValue, fromTokenDecimals]) - - const bridgeQuoteAmountGreaterThanInputForRfq = useMemo(() => { - return ( - bridgeQuote.bridgeModuleName === 'SynapseRFQ' && - bridgeQuote.outputAmount > debouncedFromValueBigInt - ) - }, [bridgeQuote.outputAmount, debouncedFromValueBigInt]) - - 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 || isLoading || isWalletPending || - bridgeQuote === EMPTY_BRIDGE_QUOTE || - (destinationAddress && !isAddress(destinationAddress)) || - (isConnected && !sufficientBalance) || - bridgeQuoteAmountGreaterThanInputForRfq || - !chainSelectionsMatchBridgeQuote || - isBridgePaused + !hasValidInput || + !doesBridgeStateMatchQuote || + isBridgeQuoteAmountGreaterThanInputForRfq || + (isConnected && !hasValidQuote) || + (isConnected && !hasSufficientBalance) || + (destinationAddress && !isAddress(destinationAddress)) let buttonProperties @@ -126,39 +97,42 @@ export const BridgeTransactionButton = ({ label: `Bridge ${fromToken?.symbol}`, onClick: null, } - } else if ( - !isLoading && - bridgeQuote?.feeAmount === 0n && - debouncedFromValueBigInt > 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 ( - !isLoading && - !chainSelectionsMatchBridgeQuote && - debouncedFromValueBigInt > 0 - ) { + } else if (!isLoading && !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 && - debouncedFromValueBigInt > 0 + isBridgeQuoteAmountGreaterThanInputForRfq && + hasValidInput ) { buttonProperties = { label: 'Invalid bridge quote', onClick: null, } - } else if (!isConnected && debouncedFromValueBigInt > 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, @@ -173,17 +147,13 @@ export const BridgeTransactionButton = ({ onClick: () => dispatch(setIsDestinationWarningAccepted(true)), className: '!from-bgLight !to-bgLight', } - } else if (chain?.id != fromChainId && debouncedFromValueBigInt > 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 && - debouncedFromValueBigInt > 0 && - bridgeQuote?.destQuery - ) { + } else if (!isApproved && hasValidInput && hasValidQuote) { buttonProperties = { onClick: approveTxn, label: `Approve ${fromToken?.symbol}`, diff --git a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx index ab21d46c55..f044933e03 100644 --- a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx @@ -1,4 +1,4 @@ -import { debounce, 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' @@ -12,7 +12,6 @@ import { ConnectedIndicator, } from '@/components/ConnectionIndicators' 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 { AvailableBalance } from './AvailableBalance' @@ -23,33 +22,24 @@ 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 = () => { const dispatch = useAppDispatch() const { chain, isConnected } = useAccount() - const { balances } = usePortfolioState() - const { fromChainId, toChainId, fromToken, toToken, debouncedFromValue } = - useBridgeState() const { isWalletPending } = useWalletState() + const { fromChainId, fromToken, debouncedFromValue } = useBridgeState() const [localInputValue, setLocalInputValue] = useState(debouncedFromValue) - 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 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, @@ -138,9 +128,9 @@ export const InputContainer = () => { const connectedStatus = useMemo(() => { if (!isConnected) { return - } else if (isConnected && fromChainId === chain?.id) { + } else if (isConnected && onSelectedChain) { return - } else if (isConnected && fromChainId !== chain?.id) { + } else if (isConnected && !onSelectedChain) { return } }, [chain, fromChainId, isConnected]) @@ -172,7 +162,7 @@ export const InputContainer = () => { onClick={onMaxBalance} isHidden={ !isConnected || - !hasValidInputSelections || + !hasValidSelections || isLoading || isInputMax || isWalletPending diff --git a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx index c7e8d05ade..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' @@ -13,16 +14,23 @@ 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 } = 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..cb69ec93f6 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts @@ -0,0 +1,125 @@ +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.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/constants/bridge.ts b/packages/synapse-interface/constants/bridge.ts index 5c40ff4c88..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: '', diff --git a/packages/synapse-interface/slices/bridgeQuote/thunks.ts b/packages/synapse-interface/slices/bridgeQuote/thunks.ts index caad84d93e..1705cd9892 100644 --- a/packages/synapse-interface/slices/bridgeQuote/thunks.ts +++ b/packages/synapse-interface/slices/bridgeQuote/thunks.ts @@ -157,6 +157,9 @@ export const fetchBridgeQuote = createAsyncThunk( } = synapseSDK.applyBridgeSlippage(bridgeModuleName, originQuery, destQuery) return { + inputAmountForQuote: debouncedFromValue, + originTokenForQuote: fromToken, + destTokenForQuote: toToken, outputAmount: toValueBigInt, outputAmountString: commify( formatBigIntToString(toValueBigInt, toToken.decimals[toChainId], 8) diff --git a/packages/synapse-interface/utils/types/index.tsx b/packages/synapse-interface/utils/types/index.tsx index 8454ba91e0..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 From ed1d896d46a7fcb080fcf54ee0795d9038245065 Mon Sep 17 00:00:00 2001 From: bigboydiamonds <57741810+bigboydiamonds@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:08:58 -0700 Subject: [PATCH 09/11] [do not merge] isTyping example with bridge quote state update (#3035) * Checks if user is typing * Optional param * remove local input state * optional setIsTyping callback * Adds swap behavior --------- Co-authored-by: abtestingalpha --- .../BridgeTransactionButton.tsx | 9 ++++++- .../StateManagedBridge/InputContainer.tsx | 11 ++++++-- .../StateManagedSwap/SwapInputContainer.tsx | 26 ++++++++++++++++--- .../SwapTransactionButton.tsx | 4 ++- .../components/ui/AmountInput.tsx | 18 +++++++++++-- .../pages/state-managed-bridge/index.tsx | 8 ++++-- .../synapse-interface/pages/swap/index.tsx | 5 +++- 7 files changed, 69 insertions(+), 12 deletions(-) diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx index e972f72bb8..56f9ddcf14 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx @@ -17,6 +17,7 @@ export const BridgeTransactionButton = ({ executeBridge, isApproved, isBridgePaused, + isTyping, }) => { const dispatch = useAppDispatch() const { openConnectModal } = useConnectModal() @@ -61,6 +62,7 @@ export const BridgeTransactionButton = ({ const isButtonDisabled = isBridgePaused || + isTyping || isLoading || isWalletPending || !hasValidInput || @@ -107,7 +109,12 @@ export const BridgeTransactionButton = ({ label: `Amount must be greater than fee`, onClick: null, } - } else if (!isLoading && !doesBridgeStateMatchQuote && hasValidInput) { + } else if ( + !isLoading && + !isTyping && + !doesBridgeStateMatchQuote && + hasValidInput + ) { buttonProperties = { label: 'Error in bridge quote', onClick: null, diff --git a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx index f044933e03..c6e70a5ae5 100644 --- a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx @@ -27,7 +27,13 @@ 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 { isWalletPending } = useWalletState() @@ -58,7 +64,7 @@ export const InputContainer = () => { () => debounce( (value: string) => dispatch(updateDebouncedFromValue(value)), - 300 + 400 ), [dispatch] ) @@ -145,6 +151,7 @@ export const InputContainer = () => {
{ +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/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index b4dfed48a6..03b4806f27 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -1,5 +1,5 @@ import toast from 'react-hot-toast' -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import { Address, zeroAddress, isAddress } from 'viem' import { polygon } from 'viem/chains' import { useAccount } from 'wagmi' @@ -75,6 +75,8 @@ const StateManagedBridge = () => { const currentSDKRequestID = useRef(0) const quoteToastRef = useRef({ id: '' }) + const [isTyping, setIsTyping] = useState(false) + const { fromChainId, toChainId, @@ -128,6 +130,7 @@ const StateManagedBridge = () => { 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 */ @@ -420,7 +423,7 @@ const StateManagedBridge = () => {
) : ( <> - + { dispatch(setFromChainId(toChainId)) @@ -436,6 +439,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} /> Date: Mon, 26 Aug 2024 11:27:43 -0400 Subject: [PATCH 10/11] Post submit --- .../StateManagedBridge/BridgeTransactionButton.tsx | 1 + .../StateManagedBridge/hooks/useBridgeValidations.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx index 56f9ddcf14..48c0aa19ec 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx @@ -110,6 +110,7 @@ export const BridgeTransactionButton = ({ onClick: null, } } else if ( + bridgeQuote.bridgeModuleName !== null && !isLoading && !isTyping && !doesBridgeStateMatchQuote && diff --git a/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts index cb69ec93f6..e64ac72587 100644 --- a/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts +++ b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts @@ -87,7 +87,11 @@ export const useBridgeValidations = () => { ]) const isBridgeFeeGreaterThanInput = useMemo(() => { - return bridgeQuote.feeAmount === 0n && debouncedFromValueBigInt > 0n + return ( + bridgeQuote.bridgeModuleName !== null && + bridgeQuote.feeAmount === 0n && + debouncedFromValueBigInt > 0n + ) }, [bridgeQuote.feeAmount, debouncedFromValueBigInt]) const onSelectedChain: boolean = useMemo(() => { From e90f6a1567841ef6f887b19a55d85a686fbbbeca Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Mon, 26 Aug 2024 11:45:29 -0400 Subject: [PATCH 11/11] Clears input value post submit --- packages/synapse-interface/pages/state-managed-bridge/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index 03b4806f27..63b7b71e42 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -34,6 +34,7 @@ import { setToChainId, setToToken, setDestinationAddress, + updateDebouncedFromValue, } from '@/slices/bridge/reducer' import { setIsWalletPending } from '@/slices/wallet/reducer' import { @@ -335,6 +336,7 @@ const StateManagedBridge = () => { dispatch(resetBridgeQuote()) dispatch(setDestinationAddress(null)) dispatch(setShowDestinationAddress(false)) + dispatch(updateDebouncedFromValue('')) const successToastContent = (