import { useMemo, useEffect, useContext, useCallback, useRef, useState, } from 'react' import { type BridgeableToken, type Chain, type CustomThemeVariables, } from 'types' import { ZeroAddress } from 'ethers' import { Web3Context } from '@/providers/Web3Provider' import { formatBigIntToString } from '@/utils/formatBigIntToString' import { stringToBigInt } from '@/utils/stringToBigInt' import { cleanNumberInput } from '@/utils/cleanNumberInput' import { Receipt } from '@/components/Receipt' import { ChainSelect } from '@/components/ui/ChainSelect' import { TokenSelect } from '@/components/ui/TokenSelect' import { useAppDispatch } from '@/state/hooks' import { setDestinationChainId, setOriginChainId, setOriginToken, setDestinationToken, setTargetTokens, setDebouncedInputAmount, setTargetChainIds, setProtocolName, } from '@/state/slices/bridge/reducer' import { useBridgeState } from '@/state/slices/bridge/hooks' import { setIsWalletPending } from '@/state/slices/wallet/reducer' import { fetchAndStoreAllowance, fetchAndStoreTokenBalances, useWalletState, } from '@/state/slices/wallet/hooks' import { BridgeButton } from '@/components/BridgeButton' import { AvailableBalance } from '@/components/AvailableBalance' import { useValidations } from '@/hooks/useValidations' import { fetchBridgeQuote, useBridgeQuoteState, } from '@/state/slices/bridgeQuote/hooks' import { type BridgeQuote, EMPTY_BRIDGE_QUOTE, resetQuote, } from '@/state/slices/bridgeQuote/reducer' import { executeBridgeTxn, useBridgeTransactionState, } from '@/state/slices/bridgeTransaction/hooks' import { BridgeTransactionStatus } from '@/state/slices/bridgeTransaction/reducer' import { executeApproveTxn, useApproveTransactionState, } from '@/state/slices/approveTransaction/hooks' import { ApproveTransactionStatus } from '@/state/slices/approveTransaction/reducer' import { useThemeVariables } from '@/hooks/useThemeVariables' import { Transactions } from '@/components/Transactions' import { CHAINS_BY_ID } from '@/constants/chains' import { useSynapseContext } from '@/providers/SynapseProvider' import { getFromTokens } from '@/utils/routeMaker/getFromTokens' import { getSymbol } from '@/utils/routeMaker/generateRoutePossibilities' import { findTokenByRouteSymbol } from '@/utils/findTokenByRouteSymbol' import { useMaintenance } from '@/components/Maintenance/Maintenance' import { getTimeMinutesFromNow } from '@/utils/getTimeMinutesFromNow' import { useBridgeQuoteUpdater } from '@/hooks/useBridgeQuoteUpdater' import { SwitchButton } from '@/components/ui/SwitchButton' import { useCurrentTokenBalance } from '@/hooks/useCurrentTokenBalance' import { useNativeSafeMax } from '@/hooks/useNativeSafeMax' interface WidgetProps { customTheme: CustomThemeVariables container?: Boolean targetTokens?: BridgeableToken[] targetChainIds?: number[] protocolName?: string } const formatReceiptAmount = ( amount: string | undefined, tokenSymbol?: string ): string | undefined => { if (!amount) { return amount } return tokenSymbol ? `${amount} ${tokenSymbol}` : amount } export const Widget = ({ customTheme, container = false, targetChainIds, targetTokens, protocolName, }: WidgetProps) => { const dispatch = useAppDispatch() const currentSDKRequestID = useRef(0) const receiptQuoteSelectionRef = useRef<{ quoteKey: string | BridgeQuote originChainId: number destinationChainId: number originTokenRouteSymbol: string | undefined destinationTokenRouteSymbol: string | undefined debouncedInputAmount: string } | null>(null) const { synapseSDK, synapseProviders } = useSynapseContext() const web3Context = useContext(Web3Context) const { connectedAddress, signer, provider, networkId } = web3Context.web3Provider const [inputAmount, setInputAmount] = useState('') const { debouncedInputAmount, originChainId, originToken, destinationChainId, destinationToken, } = useBridgeState() const { isBridgePaused, pausedModulesList, BridgeMaintenanceProgressBar, BridgeMaintenanceWarningMessage, } = useMaintenance() const pausedModulesKey = useMemo( () => JSON.stringify(pausedModulesList), [pausedModulesList] ) const allTokens = useMemo(() => { return getFromTokens({ fromChainId: originChainId, fromTokenRouteSymbol: null, toChainId: null, toTokenRouteSymbol: null, }) .map(getSymbol) .map(findTokenByRouteSymbol) }, [originChainId]) const { bridgeQuote, isLoading } = useBridgeQuoteState() const { isInputValid, hasValidSelections } = useValidations() const { isWalletPending } = useWalletState() const tokenBalance = useCurrentTokenBalance() const { bridgeTxnStatus } = useBridgeTransactionState() const { approveTxnStatus } = useApproveTransactionState() const themeVariables = useThemeVariables(customTheme) const originChainProvider = useMemo(() => { return synapseProviders.find( (p) => Number(p?._network?.chainId) === originChainId ) }, [originChainId, synapseProviders]) const originTokenRouteSymbol = originToken?.routeSymbol const destinationTokenRouteSymbol = destinationToken?.routeSymbol const receiptQuoteKey = bridgeQuote.id ?? bridgeQuote if (bridgeQuote === EMPTY_BRIDGE_QUOTE) { receiptQuoteSelectionRef.current = null } else if (receiptQuoteSelectionRef.current?.quoteKey !== receiptQuoteKey) { receiptQuoteSelectionRef.current = { quoteKey: receiptQuoteKey, originChainId, destinationChainId, originTokenRouteSymbol, destinationTokenRouteSymbol, debouncedInputAmount, } } const receiptQuoteSelection = receiptQuoteSelectionRef.current?.quoteKey === receiptQuoteKey ? receiptQuoteSelectionRef.current : null const hasMatchingReceiptQuoteSelection = receiptQuoteSelection?.originChainId === originChainId && receiptQuoteSelection?.destinationChainId === destinationChainId && receiptQuoteSelection?.originTokenRouteSymbol === originTokenRouteSymbol && receiptQuoteSelection?.destinationTokenRouteSymbol === destinationTokenRouteSymbol && receiptQuoteSelection?.debouncedInputAmount === debouncedInputAmount const receiptQuote = hasMatchingReceiptQuoteSelection ? bridgeQuote : EMPTY_BRIDGE_QUOTE useEffect(() => { dispatch(setOriginChainId(networkId)) }, [networkId]) useEffect(() => { dispatch(setTargetTokens(targetTokens)) dispatch(setTargetChainIds(targetChainIds)) if (targetChainIds && targetChainIds.length > 0) { dispatch(setDestinationChainId(targetChainIds[0])) } dispatch(setProtocolName(protocolName)) }, [targetTokens, targetChainIds, targetTokens, protocolName]) /** Debounce user input to fetch bridge quote (in ms) */ useEffect(() => { const DEBOUNCE_DELAY = 300 const debounceTimer = setTimeout(() => { dispatch(setDebouncedInputAmount(inputAmount)) }, DEBOUNCE_DELAY) return () => { clearTimeout(debounceTimer) } }, [dispatch, inputAmount]) /** Fetch token balances when signer/address connected */ useEffect(() => { if (!signer && !originChainProvider) return if (originChainId && allTokens && connectedAddress) { dispatch( fetchAndStoreTokenBalances({ address: connectedAddress, chainId: originChainId, tokens: allTokens, signerOrProvider: originChainProvider ?? signer, }) ) } }, [originChainId, allTokens, connectedAddress, signer, originChainProvider]) /** Fetch and store token allowance */ useEffect(() => { if ( connectedAddress && originToken?.addresses[originChainId] !== ZeroAddress && bridgeQuote?.routerAddress ) { dispatch( fetchAndStoreAllowance({ spenderAddress: bridgeQuote?.routerAddress, ownerAddress: connectedAddress, chainId: originChainId, token: originToken, provider: originChainProvider ?? provider, }) ) } }, [ originToken?.routeSymbol, originChainId, connectedAddress, bridgeQuote?.routerAddress, ]) const fetchAndStoreBridgeQuote = useCallback(async () => { currentSDKRequestID.current += 1 const thisRequestId = currentSDKRequestID.current dispatch(resetQuote()) const currentTimestamp: number = getTimeMinutesFromNow(0) if (thisRequestId === currentSDKRequestID.current) { dispatch( fetchBridgeQuote({ originChainId, destinationChainId, originToken, destinationToken, amount: stringToBigInt( debouncedInputAmount, originToken.decimals[originChainId] ), debouncedInputAmount, synapseSDK, requestId: thisRequestId, pausedModules: pausedModulesList, timestamp: currentTimestamp, connectedAddress: connectedAddress || undefined, }) ) } }, [ connectedAddress, debouncedInputAmount, destinationChainId, destinationToken, dispatch, originChainId, originToken, pausedModulesKey, synapseSDK, ]) /** Handle refreshing quotes */ useEffect(() => { if (isInputValid && hasValidSelections) { fetchAndStoreBridgeQuote() } else { dispatch(resetQuote()) } }, [dispatch, fetchAndStoreBridgeQuote, hasValidSelections, isInputValid]) useBridgeQuoteUpdater( bridgeQuote, fetchAndStoreBridgeQuote, isLoading, isWalletPending ) const handleUserInput = useCallback( (event: React.ChangeEvent) => { const value = cleanNumberInput(event.target.value) setInputAmount(value) }, [] ) const handleOriginChainSelection = useCallback( (newOriginChain: Chain) => { dispatch(setOriginChainId(newOriginChain.id)) }, [dispatch, provider] ) const handleDestinationChainSelection = useCallback( (newDestinationChain: Chain) => { dispatch(setDestinationChainId(newDestinationChain.id)) }, [dispatch] ) const handleOriginTokenSelection = useCallback( (newOriginToken: BridgeableToken) => { dispatch(setOriginToken(newOriginToken)) }, [dispatch] ) const handleDestinationTokenSelection = useCallback( (newDestinationToken: BridgeableToken) => { dispatch(setDestinationToken(newDestinationToken)) }, [dispatch] ) const executeApproval = async () => { try { dispatch(setIsWalletPending(true)) const tx = await dispatch( executeApproveTxn({ spenderAddress: bridgeQuote?.routerAddress, tokenAddress: originToken?.addresses[originChainId], amount: stringToBigInt( debouncedInputAmount, originToken?.decimals[originChainId] ), signer, }) ) /** Fetch allowance on successful approval tx */ if (tx?.payload?.hash || tx?.payload?.transactionHash) { dispatch( fetchAndStoreAllowance({ spenderAddress: bridgeQuote?.routerAddress, ownerAddress: connectedAddress, chainId: originChainId, token: originToken, provider: originChainProvider ?? provider, }) ) } } catch (error) { console.error(`[Synapse Widget] Error while approving token: `, error) } finally { dispatch(setIsWalletPending(false)) } } const executeBridge = async () => { const quoteTx = bridgeQuote?.tx const canExecuteConnectedQuote = Boolean(connectedAddress && signer) && bridgeQuote?.quoteAddress === connectedAddress && Boolean(quoteTx?.to && quoteTx?.data) if (!canExecuteConnectedQuote || !quoteTx) { return } try { dispatch(setIsWalletPending(true)) const action = await dispatch( executeBridgeTxn({ originChainId, destinationChainId, parsedOriginAmount: debouncedInputAmount, originTokenSymbol: originToken?.symbol, bridgeModuleName: bridgeQuote?.bridgeModuleName, estimatedTime: bridgeQuote?.estimatedTime, quoteTx, signer, }) ) /** Check thunk action is fulfilled */ if (executeBridgeTxn.fulfilled.match(action)) { const tx = action.payload /** Fetch balance/allowance on successful bridge tx */ if (tx?.txHash) { dispatch( fetchAndStoreTokenBalances({ address: connectedAddress, chainId: originChainId, tokens: allTokens, signerOrProvider: originChainProvider ?? signer, }) ) dispatch( fetchAndStoreAllowance({ spenderAddress: bridgeQuote?.routerAddress, ownerAddress: connectedAddress, chainId: originChainId, token: originToken, provider: originChainProvider ?? provider, }) ) } } } catch (error) { console.error('[Synapse Widget] Error bridging: ', error) } finally { dispatch(setIsWalletPending(false)) } } const containerStyle = ` ${container === false ? 'p-2 rounded-[inherit]' : 'p-2 rounded-lg'}` const cardStyle = ` grid grid-cols-[1fr_auto] rounded-md p-2 gap-1 border border-solid border-[--synapse-border] ` const inputStyle = ` text-3xl w-full font-regular bg-transparent border-none block text-[--synapse-text] placeholder:text-[--synapse-secondary] focus:outline-none disabled:cursor-not-allowed font-sans ` const isCurrentRequestedQuote = bridgeQuote?.requestId === currentSDKRequestID.current const hasMatchingConnectedQuote = !connectedAddress || (bridgeQuote?.quoteAddress === connectedAddress && Boolean(bridgeQuote?.tx?.to && bridgeQuote?.tx?.data)) const isValidBridgeQuote = Boolean(bridgeQuote) && bridgeQuote !== EMPTY_BRIDGE_QUOTE && isCurrentRequestedQuote && hasMatchingConnectedQuote const destinationValue = useMemo(() => { if (isLoading) { return '...' } else if (!hasValidSelections) { return '' } else if (!bridgeQuote) { return '' } else if (bridgeQuote.outputAmountString === '') { return '0' } else if (!isCurrentRequestedQuote) { return '...' } return bridgeQuote.outputAmountString }, [isLoading, bridgeQuote, isCurrentRequestedQuote, hasValidSelections]) const nativeSafeMax = useNativeSafeMax({ amountKey: inputAmount, connectedAddress: connectedAddress || undefined, destinationChainId, destinationToken, isWalletPending, originChainId, originChainProvider, originToken, pausedModules: pausedModulesList, rawBalance: tokenBalance.rawBalance, synapseSDK, }) return (
{ dispatch(setDestinationChainId(originChainId)) dispatch(setDestinationToken(originToken)) dispatch(setOriginChainId(destinationChainId)) dispatch(setOriginToken(destinationToken)) }} />
) }