diff --git a/README.md b/README.md index ca9df5e7..a8582b7c 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,11 @@ cp .env.production.example .env You have successfully set up Soroswap on your local machine! Start swapping, pooling, and exploring the possibilities of decentralized finance (DeFi) on the Soroban network. +If you want to add or remove supported protocols, you can do so by editing the `functions/generateRoute.ts:79-97` file and adding or removing the protocols you want to support on swap. + +> [!HINT] +> You can found the list of supported protocols in the `soroswap-router-sdk` repository. + ## 🧪🔨 Testing 🧪🔨 To execute the tests, you must first start the development container. To do this, run the following command from your host machine: diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index fd9aa524..1669b353 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -1,8 +1,6 @@ import { Analytics } from '@vercel/analytics/react'; -import { AppContext, AppContextType, ColorModeContext, SnackbarIconType } from 'contexts'; -import { CssBaseline, ThemeProvider } from 'soroswap-ui'; +import { AppContext, AppContextType, ColorModeContext, SnackbarIconType, ProtocolsStatus } from 'contexts'; import { Provider } from 'react-redux'; -import { theme } from 'soroswap-ui'; import { useMemo, useState } from 'react'; import InkathonProvider from 'inkathon/InkathonProvider'; import MainLayout from './Layout/MainLayout'; @@ -10,6 +8,7 @@ import MySorobanReactProvider from 'soroban/MySorobanReactProvider'; import store from 'state'; import { SorobanContextType } from '@soroban-react/core'; import { SoroswapThemeProvider } from 'soroswap-ui'; + export default function Providers({ children, sorobanReactProviderProps, @@ -20,7 +19,7 @@ export default function Providers({ const [isConnectWalletModal, setConnectWalletModal] = useState(false); const [maxHops, setMaxHops] = useState(2); - + const [protocolsStatus, setProtocolsStatus] = useState([]); const [openSnackbar, setOpenSnackbar] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(''); const [snackbarTitle, setSnackbarTitle] = useState('Swapped'); @@ -54,6 +53,8 @@ export default function Providers({ Settings: { maxHops, setMaxHops, + protocolsStatus, + setProtocolsStatus, }, }; diff --git a/src/components/Settings/ProtocolsSettings/index.tsx b/src/components/Settings/ProtocolsSettings/index.tsx new file mode 100644 index 00000000..12276c4f --- /dev/null +++ b/src/components/Settings/ProtocolsSettings/index.tsx @@ -0,0 +1,133 @@ +import Expand from 'components/Expand'; +import QuestionHelper from 'components/QuestionHelper'; +import Row, { RowBetween } from 'components/Row'; +import { BodySmall } from 'components/Text'; +import { AppContext } from 'contexts'; +import { useRouterSDK } from 'functions/generateRoute'; +import React, { useContext, useEffect, useState } from 'react' +import { Box, styled, Switch, SwitchProps, Typography, useTheme } from 'soroswap-ui'; +import { useSWRConfig } from 'swr'; + + +export const CustomSwitch = styled((props: SwitchProps) => ( + +))(({ theme }) => ({ + width: 42, + height: 26, + padding: 0, + alignContent: 'center', + alignItems: 'center', + '& .MuiSwitch-switchBase': { + padding: 0, + margin: 3, + '&.Mui-checked': { + transform: 'translateX(16px)', + color: '#8866DD', + '& .MuiSwitch-thumb:before': { + backgroundColor: '#8866DD', + borderRadius: 32, + }, + '& + .MuiSwitch-track': { + backgroundColor: theme.palette.background.paper, + opacity: 1, + border: 0, + }, + }, + }, + '& .MuiSwitch-thumb': { + backgroundColor: 'rgba(136, 102, 221, 0.25)', + width: 20, + height: 20, + '&:before': { + content: "''", + position: 'absolute', + width: '100%', + height: '100%', + left: 0, + top: 0, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + }, + }, + '& .MuiSwitch-track': { + borderRadius: 32, + backgroundColor: theme.palette.background.paper, + opacity: 1, + }, +})); + + +const firstLetterUppercase = (string: string) => { + return string.charAt(0).toUpperCase() + string.slice(1); +} +const ProtocolsSettings = () => { + const { resetRouterSdkCache } = useRouterSDK(); + const theme = useTheme(); + const [isOpen, setIsOpen] = useState(false); + const { protocolsStatus, setProtocolsStatus } = useContext(AppContext).Settings; + const { mutate } = useSWRConfig(); + + const switchProtocolValue = (key: string) => { + const newProtocolsStatus = protocolsStatus.map((protocol) => { + if (protocol.key === key) { + return { + key: protocol.key, + value: !protocol.value, + }; + } + return protocol; + }); + const hasTrueValue = newProtocolsStatus.some((protocol) => protocol.value); + if (hasTrueValue) { + resetRouterSdkCache(); + setProtocolsStatus(newProtocolsStatus); + mutate( + (key: any) => { + return true; + }, + undefined, + { revalidate: true }, + ); + } + else return; + } + + return ( + + setIsOpen(!isOpen)} + header={ + + Protocols + + The protocols Soroswap.finance will use to calculate the most efficient path for your transaction. + + } + /> + + } + button={<>} + > + + + {protocolsStatus.map((option, index) => { + return ( + + {firstLetterUppercase(option.key)} + { switchProtocolValue(option.key) }} color="secondary" /> + + ) + })} + + + + + + ) +} + +export default ProtocolsSettings diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx index b620036a..c6665f5e 100644 --- a/src/components/Settings/index.tsx +++ b/src/components/Settings/index.tsx @@ -8,6 +8,7 @@ import { useRef, useState } from 'react'; import MaxSlippageSettings from './MaxSlippageSettings'; import MenuButton from './MenuButton'; import MaxHopsSettings from './MaxHopsSettings'; +import ProtocolsSettings from './ProtocolsSettings'; const Menu = styled('div')` position: relative; @@ -74,6 +75,7 @@ export default function SettingsTab({ + diff --git a/src/components/Swap/SwapPathComponent.tsx b/src/components/Swap/SwapPathComponent.tsx index d9e41bb9..10715c5f 100644 --- a/src/components/Swap/SwapPathComponent.tsx +++ b/src/components/Swap/SwapPathComponent.tsx @@ -106,8 +106,8 @@ function SwapPathComponent({ trade }: { trade: InterfaceTrade | undefined }) { tempDistributionArray.push({ path: fulfilledValues, parts: distribution.parts, protocol: distribution.protocol_id }); setDistributionArray(tempDistributionArray); setTotalParts(tempDistributionArray.reduce((acc, curr) => acc + curr.parts, 0)); - setPathTokensIsLoading(false); } + setPathTokensIsLoading(false); } })(); }, [trade?.path, isLoading, sorobanContext]); diff --git a/src/contexts/index.ts b/src/contexts/index.ts index f0f8a446..f3a45c7d 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,4 +1,6 @@ import React from 'react'; +import { Protocols } from 'soroswap-router-sdk'; +import { PlatformType } from 'state/routing/types'; type ConnectWalletModalType = { isConnectWalletModalOpen: boolean; @@ -13,6 +15,11 @@ export enum SnackbarIconType { ERROR, } +export interface ProtocolsStatus { + key: Protocols | PlatformType; + value: boolean; +} + export type SnackbarContextType = { openSnackbar: boolean; snackbarMessage: string; @@ -27,6 +34,8 @@ export type SnackbarContextType = { export type Settings = { maxHops: number; setMaxHops: React.Dispatch>; + protocolsStatus: ProtocolsStatus[]; + setProtocolsStatus: React.Dispatch>; }; export type AppContextType = { @@ -57,5 +66,7 @@ export const AppContext = React.createContext({ Settings: { maxHops: 2, setMaxHops: () => {}, + protocolsStatus: [], + setProtocolsStatus: () => {}, }, }); diff --git a/src/functions/generateRoute.ts b/src/functions/generateRoute.ts index b44f6479..6508da6c 100644 --- a/src/functions/generateRoute.ts +++ b/src/functions/generateRoute.ts @@ -1,8 +1,8 @@ -import { SorobanContextType, useSorobanReact } from '@soroban-react/core'; +import { useSorobanReact } from '@soroban-react/core'; import { AppContext } from 'contexts'; import { useFactory } from 'hooks'; import { useAggregator } from 'hooks/useAggregator'; -import { useContext, useMemo } from 'react'; +import { useContext, useEffect, useMemo } from 'react'; import { fetchAllPhoenixPairs, fetchAllSoroswapPairs } from 'services/pairs'; import { Currency, @@ -57,34 +57,92 @@ export const useRouterSDK = () => { const { isEnabled: isAggregator } = useAggregator(); const { Settings } = useContext(AppContext); - const { maxHops } = Settings; + const { maxHops, protocolsStatus, setProtocolsStatus } = Settings; const network = sorobanContext.activeChain?.networkPassphrase as Networks; - const router = useMemo(() => { - const protocols = [Protocols.SOROSWAP]; + const getValuebyKey = (key: string) => { + let value = protocolsStatus.find((p) => p.key === key)?.value; + if (value === undefined && key === Protocols.SOROSWAP) { + return true; + } + if (typeof value === 'undefined') { + return false; + } + if (value === true || value === false) { + return value; + } + return value; + } + + const getDefaultProtocolsStatus = async (network: Networks) => { + switch (network) { + case Networks.PUBLIC: + // here you should add your new supported protocols + return [ + { key: Protocols.SOROSWAP , value: getValuebyKey(Protocols.SOROSWAP) }, + { key: PlatformType.STELLAR_CLASSIC, value: getValuebyKey(PlatformType.STELLAR_CLASSIC) }, + ]; + case Networks.TESTNET: + return [ + { key: Protocols.SOROSWAP, value: getValuebyKey(Protocols.SOROSWAP) }, + { key: Protocols.PHOENIX, value: getValuebyKey(Protocols.PHOENIX) }, + ]; + default: + return [ + { key: Protocols.SOROSWAP, value: true }, + { key: Protocols.PHOENIX, value: false }, + { key: PlatformType.STELLAR_CLASSIC, value: false }, + ]; + } + } - // if (isAggregator) protocols.push(Protocols.PHOENIX); + useEffect(() => { + const fetchProtocolsStatus = async () => { + const defaultProtocols = await getDefaultProtocolsStatus(network); + setProtocolsStatus(defaultProtocols); + }; + fetchProtocolsStatus(); + }, [network]); + + const getPairsFns = useMemo(() => { + const routerProtocols = [] + if(!shouldUseBackend) return undefined +// here you should add your new supported aggregator protocols + for(let protocol of protocolsStatus){ + if(protocol.key === Protocols.SOROSWAP && protocol.value === true){ + routerProtocols.push({protocol: Protocols.SOROSWAP, fn: async () => fetchAllSoroswapPairs(network)}); + } + if(protocol.key === Protocols.PHOENIX && protocol.value === true){ + routerProtocols.push({protocol: Protocols.PHOENIX, fn: async () => fetchAllPhoenixPairs(network)}); + } + } + return routerProtocols; + }, [network, protocolsStatus]); + + const getProtocols = useMemo(() => { + const newProtocols = []; + for(let protocol of protocolsStatus){ + if(protocol.key != PlatformType.STELLAR_CLASSIC && protocol.value === true){ + newProtocols.push(protocol.key); + } + } + return newProtocols as Protocols[]; + },[protocolsStatus]); + const router = useMemo(() => { return new Router({ - getPairsFns: shouldUseBackend - ? [ - { - protocol: Protocols.SOROSWAP, - fn: async () => fetchAllSoroswapPairs(network), - }, - { - protocol: Protocols.PHOENIX, - fn: async () => fetchAllPhoenixPairs(network), - }, - ] - : undefined, - pairsCacheInSeconds: 60, - protocols: protocols, + getPairsFns: getPairsFns, + pairsCacheInSeconds: 5, + protocols: getProtocols, network, maxHops, }); - }, [network, maxHops, isAggregator]); + }, [network, maxHops, isAggregator, protocolsStatus]); + + const isProtocolEnabled = (protocol: any) => { + return protocolsStatus.find((p) => p.key === protocol)?.value; + } const fromAddressToToken = (address: string) => { return new Token(network, address, 18); @@ -112,51 +170,58 @@ export const useRouterSDK = () => { ); const quoteCurrency = fromAddressToToken(quoteAsset.contract); + const isHorizonEnabled = isProtocolEnabled(PlatformType.STELLAR_CLASSIC); + const isSoroswapEnabled = isProtocolEnabled(Protocols.SOROSWAP); + const horizonProps = { assetFrom: amountAsset.currency, assetTo: quoteAsset, amount, tradeType, }; - let horizonPath: BuildTradeRoute; - try { + + let horizonPath: BuildTradeRoute | undefined; + if(isHorizonEnabled){ horizonPath = (await getHorizonBestPath(horizonProps, sorobanContext)) as BuildTradeRoute; - } catch (error) { - console.error('Error getting horizon path'); } - let sorobanPath: BuildTradeRoute; - try{ - if (isAggregator) { - sorobanPath = (await router - .routeSplit(currencyAmount, quoteCurrency, tradeType) - .then((response) => { - if (!response) return undefined; - const result = { - ...response, - platform: PlatformType.AGGREGATOR, - }; - return result; - })) as BuildTradeRoute; - } else { - sorobanPath = (await router - .route(currencyAmount, quoteCurrency, tradeType, factory, sorobanContext as any) - .then((response) => { - if (!response) return undefined; - const result = { - ...response, - platform: PlatformType.ROUTER, - }; - return result; - })) as BuildTradeRoute; - } - } catch (error) { - console.error('Error getting soroban path', error); + + let sorobanPath: BuildTradeRoute | undefined; + if (isAggregator) { + sorobanPath = (await router + .routeSplit(currencyAmount, quoteCurrency, tradeType) + .then((response) => { + if (!response) return undefined; + const result = { + ...response, + platform: PlatformType.AGGREGATOR, + }; + return result; + })) as BuildTradeRoute; + } else if(isSoroswapEnabled){ + sorobanPath = (await router + .route(currencyAmount, quoteCurrency, tradeType, factory, sorobanContext as any) + .then((response) => { + if (!response) return undefined; + const result = { + ...response, + platform: PlatformType.ROUTER, + }; + return result; + })) as BuildTradeRoute; } - const bestPath = getBestPath(horizonPath!, sorobanPath!, tradeType); + const bestPath = getBestPath(horizonPath, sorobanPath, tradeType); return bestPath; }; return { generateRoute, resetRouterSdkCache, maxHops }; }; +// .then((res) => { +// if (!res) return; +// const response = { +// ...res, +// platform: PlatformType.ROUTER, +// }; +// return response; +// }); diff --git a/src/hooks/useBestTrade.ts b/src/hooks/useBestTrade.ts index d59d71f3..63c37c88 100644 --- a/src/hooks/useBestTrade.ts +++ b/src/hooks/useBestTrade.ts @@ -1,7 +1,8 @@ +import { AppContext } from 'contexts'; import { useRouterSDK } from 'functions/generateRoute'; import { hasDistribution } from 'helpers/aggregator'; import { CurrencyAmount, TokenType } from 'interfaces'; -import { useEffect, useMemo, useState } from 'react'; +import { useContext, useEffect, useMemo, useState } from 'react'; import { TradeType as SdkTradeType } from 'soroswap-router-sdk'; import { InterfaceTrade, QuoteState, TradeState, TradeType } from 'state/routing/types'; import useSWR from 'swr'; @@ -29,6 +30,7 @@ export function useBestTrade( resetRouterSdkCache: () => void; } { const { generateRoute, resetRouterSdkCache, maxHops } = useRouterSDK(); + const {protocolsStatus} = useContext(AppContext).Settings; /** * Custom hook that fetches the best trade based on the specified amount and trade type. * @@ -143,7 +145,7 @@ export function useBestTrade( } return baseTrade; - }, [expectedAmount, inputAmount, outputAmount, tradeType, data]); + }, [expectedAmount, inputAmount, outputAmount, tradeType, data, protocolsStatus]); /* If the pairAddress or the trades chenges, we upgrade the tradeResult @@ -155,7 +157,7 @@ export function useBestTrade( const myTradeResult = { state: state, trade: trade }; return myTradeResult; - }, [data, trade]); //should get the pair address and quotes + }, [data, trade, protocolsStatus]); //should get the pair address and quotes const skipFetch: boolean = false; @@ -196,6 +198,7 @@ export function useBestTrade( trade, isLoading, resetRouterSdkCache, + protocolsStatus ]); return bestTrade;