diff --git a/pages/index.tsx b/pages/index.tsx
index ebec30f2..82340071 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -21,7 +21,10 @@ export default function Home() {
[Field.OUTPUT]: { currencyId: null },
});
const activeChain = passphraseToBackendNetworkName[activeNetwork!].toLowerCase();
+
useEffect(() => {
+ if (!activeChain || !xlmTokenList) return undefined
+
if (prefilledState.INPUT?.currencyId == null) {
const newXlmToken =
xlmTokenList.find((tList) => tList.network === activeChain)?.assets[0].contract ?? null;
diff --git a/pages/swap/index.tsx b/pages/swap/index.tsx
index 6a37a537..8a040b41 100644
--- a/pages/swap/index.tsx
+++ b/pages/swap/index.tsx
@@ -17,6 +17,7 @@ export default function SwapPage() {
});
useEffect(() => {
+ if (!activeChain || !xlmTokenList) return undefined
const newXlmToken =
xlmTokenList.find((tList) => tList.network === activeChain)?.assets[0].contract ?? null;
setXlmToken(newXlmToken);
diff --git a/src/components/CurrencyInputPanel/CurrencyBalance.tsx b/src/components/CurrencyInputPanel/CurrencyBalance.tsx
index 2bc3bd76..6b8da5fc 100644
--- a/src/components/CurrencyInputPanel/CurrencyBalance.tsx
+++ b/src/components/CurrencyInputPanel/CurrencyBalance.tsx
@@ -1,14 +1,11 @@
-import { styled, useTheme } from 'soroswap-ui';
-import { RowFixed } from 'components/Row';
-import { BodySmall } from 'components/Text';
-import { MouseoverTooltip } from 'components/Tooltip';
-import useGetMyBalances from 'hooks/useGetMyBalances';
+import { styled } from 'soroswap-ui';;
+import XlmCurrencyBalance from './XlmCurrencyBalance';
+import TokenCurrencyBalance from './TokenCurrencyBalance';
import { useSorobanReact, WalletNetwork } from 'stellar-react';
import BigNumber from 'bignumber.js';
-import { TextWithLoadingPlaceholder } from 'components/Swap/AdvancedSwapDetails';
import { xlmTokenList } from 'constants/xlmToken';
-import { useEffect, useMemo, useState } from 'react';
+import { memo, useEffect, useMemo, useState } from 'react';
const StyledBalanceMax = styled('button')<{ disabled?: boolean }>`
background-color: transparent;
@@ -39,19 +36,14 @@ interface CurrencyBalanceProps {
isLoadingNetworkFees?: boolean;
}
-export default function CurrencyBalance({
+const CurrencyBalance = ({
contract,
onMax,
hideBalance,
showMaxButton,
networkFees,
isLoadingNetworkFees,
-}: CurrencyBalanceProps) {
- const {
- tokenBalancesResponse,
- availableNativeBalance,
- isLoading: isLoadingMyBalances,
- } = useGetMyBalances();
+}: CurrencyBalanceProps) => {
const { activeNetwork } = useSorobanReact();
const [activeChain, setActiveChain] = useState('');
useEffect(() => {
@@ -68,61 +60,38 @@ export default function CurrencyBalance({
}
}, [activeNetwork]);
const xlmTokenContract = useMemo(() => {
+
return xlmTokenList.find((tList) => tList.network === activeChain)?.assets[0].contract;
}, [activeChain]);
const isXLM = contract === xlmTokenContract;
- const balance =
- tokenBalancesResponse?.balances?.find((b) => b?.contract === contract)?.balance || '0';
- let availableBalance = balance;
+ if (isLoadingNetworkFees || !xlmTokenContract) return
+ /**
+ * NOTE: Decomposite Balance view for XLM/OTHER tokens and minifed requests
+ * */
if (isXLM) {
- availableBalance = availableNativeBalance(networkFees);
+ return (
+
+ );
+ } else {
+ return (
+
+ );
}
-
- const theme = useTheme();
-
- return (
-
-
- {isXLM ? (
-
- All Stellar accounts must maintain a minimum balance of lumens.{' '}
-
- Learn More
-
-
- }
- >
-
- {!hideBalance && contract ? `Available balance: ${Number(availableBalance)}` : null}
-
-
- ) : (
-
- {!hideBalance && contract ? `Balance: ${Number(availableBalance)}` : null}
-
- )}
-
- {showMaxButton && Number(availableBalance) > 0 ? (
- onMax(availableBalance.toString())}>
- Max
-
- ) : null}
-
-
- );
}
+
+export default memo(CurrencyBalance);
diff --git a/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx b/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx
index cdde1a4c..3dfcec16 100644
--- a/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx
+++ b/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx
@@ -2,7 +2,7 @@
import { ButtonBase, styled, useMediaQuery, useTheme } from 'soroswap-ui';
import { useSorobanReact } from 'stellar-react';
import { darken } from 'polished';
-import { ReactNode, useCallback, useState } from 'react';
+import { memo, ReactNode, useCallback, useState } from 'react';
import { ChevronDown } from 'react-feather';
import { TokenType } from '../../interfaces';
import { flexColumnNoWrap, flexRowNoWrap } from '../../themes/styles';
@@ -186,7 +186,7 @@ interface SwapCurrencyInputPanelProps {
isLoadingNetworkFees?: boolean;
}
-export default function SwapCurrencyInputPanel({
+const SwapCurrencyInputPanel = ({
value,
onUserInput,
onMax,
@@ -210,7 +210,7 @@ export default function SwapCurrencyInputPanel({
networkFees,
isLoadingNetworkFees,
...rest
-}: SwapCurrencyInputPanelProps) {
+}: SwapCurrencyInputPanelProps) => {
const [modalOpen, setModalOpen] = useState(false);
const { address } = useSorobanReact();
const theme = useTheme();
@@ -310,6 +310,10 @@ export default function SwapCurrencyInputPanel({
showCurrencyAmount={showCurrencyAmount}
disableNonToken={disableNonToken}
/>
+
);
}
+
+
+export default memo(SwapCurrencyInputPanel)
\ No newline at end of file
diff --git a/src/components/CurrencyInputPanel/TokenCurrencyBalance.tsx b/src/components/CurrencyInputPanel/TokenCurrencyBalance.tsx
new file mode 100644
index 00000000..7f1877d6
--- /dev/null
+++ b/src/components/CurrencyInputPanel/TokenCurrencyBalance.tsx
@@ -0,0 +1,75 @@
+import { styled, useTheme } from 'soroswap-ui';
+import { RowFixed } from 'components/Row';
+import { BodySmall } from 'components/Text';
+import useGetMyBalances from 'hooks/useGetMyBalances';
+import BigNumber from 'bignumber.js';
+import { TextWithLoadingPlaceholder } from 'components/Swap/AdvancedSwapDetails';
+import { memo } from 'react';
+
+const StyledBalanceMax = styled('button')<{ disabled?: boolean }>`
+ background-color: transparent;
+ border: none;
+ color: ${({ theme }) => theme.palette.custom.borderColor};
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
+ padding: 4px 6px;
+ pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')};
+
+ :hover {
+ opacity: ${({ disabled }) => (!disabled ? 0.8 : 0.4)};
+ }
+
+ :focus {
+ outline: none;
+ }
+`;
+
+interface TokenCurrencyBalanceProps {
+ contract: string;
+ onMax: (maxValue: string) => void;
+ hideBalance: any;
+ showMaxButton: any;
+}
+
+ const TokenCurrencyBalance = ({
+ contract,
+ onMax,
+ hideBalance,
+ showMaxButton,
+}: TokenCurrencyBalanceProps) => {
+
+ const {
+ tokenBalancesResponse,
+ isLoading: isLoadingMyBalances,
+ } = useGetMyBalances();
+
+ const balance =
+ tokenBalancesResponse?.balances?.find((b) => b?.contract === contract)?.balance || '0';
+
+ const availableBalance = balance;
+
+ const theme = useTheme();
+
+ return (
+
+
+
+ {!hideBalance && contract ? `Balance: ${Number(availableBalance)}` : null}
+
+
+ {showMaxButton && Number(availableBalance) > 0 ? (
+ onMax(availableBalance.toString())}>
+ Max
+
+ ) : null}
+
+
+ );
+}
+
+export default memo(TokenCurrencyBalance);
\ No newline at end of file
diff --git a/src/components/CurrencyInputPanel/XlmCurrencyBalance.tsx b/src/components/CurrencyInputPanel/XlmCurrencyBalance.tsx
new file mode 100644
index 00000000..69b48892
--- /dev/null
+++ b/src/components/CurrencyInputPanel/XlmCurrencyBalance.tsx
@@ -0,0 +1,91 @@
+import { styled, useTheme } from 'soroswap-ui';
+import { RowFixed } from 'components/Row';
+import { BodySmall } from 'components/Text';
+import { MouseoverTooltip } from 'components/Tooltip';
+import useGetMyBalances, { calculateAvailableBalance } from 'hooks/useGetMyBalances';
+import BigNumber from 'bignumber.js';
+import { TextWithLoadingPlaceholder } from 'components/Swap/AdvancedSwapDetails';
+import useGetSubentryCount from 'hooks/useGetSubentryCount';
+import { memo } from 'react';
+
+const StyledBalanceMax = styled('button')<{ disabled?: boolean }>`
+ background-color: transparent;
+ border: none;
+ color: ${({ theme }) => theme.palette.custom.borderColor};
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
+ padding: 4px 6px;
+ pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')};
+
+ :hover {
+ opacity: ${({ disabled }) => (!disabled ? 0.8 : 0.4)};
+ }
+
+ :focus {
+ outline: none;
+ }
+`;
+
+interface XlmCurrencyBalanceProps {
+ contract: string;
+ onMax: (maxValue: string) => void;
+ hideBalance: any;
+ showMaxButton: any;
+ networkFees?: string | number | BigNumber | null;
+ isLoadingNetworkFees?: boolean;
+}
+
+ const XlmCurrencyBalance = ({
+ contract,
+ onMax,
+ hideBalance,
+ showMaxButton,
+ networkFees,
+ isLoadingNetworkFees,
+}: XlmCurrencyBalanceProps) => {
+
+ const { subentryCount, nativeBalance, isLoading: isSubentryLoading } = useGetSubentryCount();
+
+ const availableBalance = calculateAvailableBalance(nativeBalance, networkFees, subentryCount)
+
+ const theme = useTheme();
+
+ return (
+
+
+
+ All Stellar accounts must maintain a minimum balance of lumens.{' '}
+
+ Learn More
+
+
+ }
+ >
+
+ {!hideBalance && contract ? `Available balance: ${Number(availableBalance)}` : null}
+
+
+
+ {showMaxButton && Number(availableBalance) > 0 ? (
+ onMax(availableBalance.toString())}>
+ Max
+
+ ) : null}
+
+
+ );
+}
+
+export default memo(XlmCurrencyBalance)
\ No newline at end of file
diff --git a/src/components/NumericalInput/index.tsx b/src/components/NumericalInput/index.tsx
index 79c57ae4..09ddbe1b 100644
--- a/src/components/NumericalInput/index.tsx
+++ b/src/components/NumericalInput/index.tsx
@@ -1,7 +1,17 @@
import { styled } from 'soroswap-ui';
-import React from 'react';
+import React, { useMemo } from 'react';
import { escapeRegExp } from '../../utils/escapeRegExp';
+interface InnerInputProps extends Omit, 'ref' | 'onChange' | 'as'> {
+ value: string | number;
+ onUserInput: (input: string) => void;
+ error ?: boolean;
+ fontSize ?: string;
+ align ?: 'right' | 'left';
+ prependSymbol ?: string;
+}
+
+
const StyledInput = styled('input')<{ error?: boolean; fontSize?: string; align?: string }>`
color: ${({ error, theme }) => (error ? theme.palette.error.main : theme.palette.primary.main)};
width: 0;
@@ -46,14 +56,9 @@ export const Input = React.memo(function InnerInput({
placeholder,
prependSymbol,
...rest
-}: {
- value: string | number;
- onUserInput: (input: string) => void;
- error?: boolean;
- fontSize?: string;
- align?: 'right' | 'left';
- prependSymbol?: string;
-} & Omit, 'ref' | 'onChange' | 'as'>) {
+}: InnerInputProps) {
+
+
const enforcer = (nextUserInput: string) => {
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
onUserInput(nextUserInput);
@@ -94,4 +99,5 @@ export const Input = React.memo(function InnerInput({
);
});
+
export default Input;
diff --git a/src/components/SearchModal/CurrencyList/index.tsx b/src/components/SearchModal/CurrencyList/index.tsx
index 5edd42cb..4ff52ea6 100644
--- a/src/components/SearchModal/CurrencyList/index.tsx
+++ b/src/components/SearchModal/CurrencyList/index.tsx
@@ -71,12 +71,13 @@ export const getCurrencyBalance = async (
sorobanContext: SorobanContextType,
account: AccountResponse,
) => {
- if (!sorobanContext.address) return '0';
+ if (!tokenBalancesResponse || !sorobanContext.address) return '0';
const findInBalances = tokenBalancesResponse?.balances.find(
(token) => token.contract === currency.contract,
);
+
//First find if balance is already in the "my balances" list
if (findInBalances) {
return findInBalances.balance as string;
diff --git a/src/components/SearchModal/CurrencySearch.tsx b/src/components/SearchModal/CurrencySearch.tsx
index ce6d0cc4..00a6fe53 100644
--- a/src/components/SearchModal/CurrencySearch.tsx
+++ b/src/components/SearchModal/CurrencySearch.tsx
@@ -79,6 +79,7 @@ export function CurrencySearch({
const sorobanContext = useSorobanReact();
const theme = useTheme();
+
const [tokenLoaderTimerElapsed, setTokenLoaderTimerElapsed] = useState(false);
// refs for fixed size lists
@@ -137,7 +138,7 @@ export function CurrencySearch({
}
const handleConfirmAddUserToken = () => {
- if (!searchToken) return;
+ if (!searchToken || !searchToken.code) return;
addUserToken(searchToken, sorobanContext);
onCurrencySelect(searchToken);
};
@@ -312,3 +313,4 @@ export function CurrencySearch({
>
);
}
+
diff --git a/src/components/Swap/AdvancedSwapDetails.tsx b/src/components/Swap/AdvancedSwapDetails.tsx
index 7428229a..47a2f215 100644
--- a/src/components/Swap/AdvancedSwapDetails.tsx
+++ b/src/components/Swap/AdvancedSwapDetails.tsx
@@ -67,7 +67,7 @@ export function AdvancedSwapDetails({
// const { chainId } = useWeb3React()
// const nativeCurrency = useNativeCurrency(chainId)
// const txCount = getTransactionCount(trade)
- const { tokensAsMap, isLoading } = useAllTokens();
+ const { isLoading } = useAllTokens();
return (
diff --git a/src/components/Swap/SwapComponent.tsx b/src/components/Swap/SwapComponent.tsx
index 63da9e10..62ca2371 100644
--- a/src/components/Swap/SwapComponent.tsx
+++ b/src/components/Swap/SwapComponent.tsx
@@ -12,13 +12,15 @@ import { sendNotification } from 'functions/sendNotification';
import { formatTokenAmount } from 'helpers/format';
import { requiresTrustline } from 'helpers/stellar';
import { relevantTokensType } from 'hooks';
-import { useToken } from 'hooks/tokens/useToken';
+import { findTokenService, getAllTokensService } from 'services/tokenService';
+import { getDerivedSwapInfoService, SwapInfoService } from 'services/swapService'; // Import the service and its type
import useGetNativeTokenBalance from 'hooks/useGetNativeTokenBalance';
import { useSwapCallback } from 'hooks/useSwapCallback';
import useSwapMainButton from 'hooks/useSwapMainButton';
import useSwapNetworkFees from 'hooks/useSwapNetworkFees';
import { TokenType } from 'interfaces';
import {
+ memo,
ReactNode,
SetStateAction,
useCallback,
@@ -31,13 +33,17 @@ import {
import { ArrowDown } from 'react-feather';
import { InterfaceTrade, TradeState } from 'state/routing/types';
import { Field } from 'state/swap/actions';
-import { useDerivedSwapInfo, useSwapActionHandlers } from 'state/swap/hooks';
+import { useSwapActionHandlers } from 'state/swap/hooks'; // Keep useSwapActionHandlers
import swapReducer, { SwapState, initialState as initialSwapState } from 'state/swap/reducer';
import { opacify } from 'themes/utils';
import SwapCurrencyInputPanel from '../CurrencyInputPanel/SwapCurrencyInputPanel';
import SwapHeader from './SwapHeader';
import { ArrowWrapper, SwapWrapper } from './styleds';
import useGetMyBalances from 'hooks/useGetMyBalances';
+import useHorizonLoadAccount from 'hooks/useHorizonLoadAccount'; // Import useHorizonLoadAccount
+import { useFactory } from 'hooks'; // Import useFactory
+import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'; // Import useUserSlippageToleranceWithDefault
+import { DEFAULT_SLIPPAGE_INPUT_VALUE } from 'components/Settings/MaxSlippageSettings'; // Import DEFAULT_SLIPPAGE_INPUT_VALUE
export const SwapSection = styled('div')(({ theme }) => ({
position: 'relative',
@@ -118,6 +124,7 @@ export function SwapComponent({
handleDoSwap?: (setSwapState: (value: SetStateAction) => void) => void;
}) {
const sorobanContext = useSorobanReact();
+ const { address } = sorobanContext;
const { refetch } = useGetMyBalances();
const { SnackbarContext } = useContext(AppContext);
const [showPriceImpactModal, setShowPriceImpactModal] = useState(false);
@@ -126,36 +133,133 @@ export function SwapComponent({
const [needTrustline, setNeedTrustline] = useState(true);
- const { token: prefilledToken } = useToken(prefilledState.INPUT?.currencyId!);
-
// modal and loading
const [{ showConfirm, tradeToConfirm, swapError, swapResult }, setSwapState] =
useState(INITIAL_SWAP_STATE);
const [state, dispatch] = useReducer(swapReducer, { ...initialSwapState, ...prefilledState });
+
const { typedValue, recipient, independentField } = state;
const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } =
useSwapActionHandlers(dispatch);
+
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT;
+ const [tokensAsMap, setTokensAsMap] = useState({});
+
useEffect(() => {
- if (prefilledToken) {
- onCurrencySelection(Field.INPUT, prefilledToken);
- }
- }, [onCurrencySelection, prefilledToken]);
+
+ const fetchTokens = async () => {
+ const { tokensAsMap } = await getAllTokensService(sorobanContext);
+ setTokensAsMap(tokensAsMap);
+ };
+
+ sorobanContext && fetchTokens();
+ }, [sorobanContext]);
+
+ useEffect(() => {
+ const fetchPrefilledToken = async () => {
+ if (prefilledState.INPUT?.currencyId && Object.keys(tokensAsMap).length > 0) {
+
+ const prefilledToken = await findTokenService(
+ prefilledState.INPUT.currencyId,
+ tokensAsMap,
+ sorobanContext
+ );
+ if (prefilledToken) {
+ onCurrencySelection(Field.INPUT, prefilledToken);
+ }
+ }
+ };
+
+ fetchPrefilledToken();
+ }, [prefilledState, sorobanContext, tokensAsMap]);
+
+ const { account: horizonAccount } = useHorizonLoadAccount(); // Fetch horizonAccount
+ const { factory } = useFactory(sorobanContext); // Fetch factory
+ const { Settings } = useContext(AppContext); // Access AppContext for Settings
+ // Need to check AppContext definition for how to access maxHops, protocolsStatus, and isAggregator
+ // For now, using placeholders or assuming direct access if no errors
+ const maxHops = Settings?.maxHops; // Assuming direct access
+ const protocolsStatus = Settings?.protocolsStatus; // Assuming direct access
+ const isAggregator = Settings?.isAggregatorState; // Use isAggregatorState
+
+ const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_SLIPPAGE_INPUT_VALUE); // Fetch allowedSlippage
+
+ const [swapInfoServiceResult, setSwapInfoServiceResult] = useState({
+ currencies: {},
+ currencyBalances: {},
+ parsedAmount: undefined,
+ inputError: undefined,
+ trade: {
+ trade: undefined,
+ state: TradeState.LOADING,
+ uniswapXGasUseEstimateUSD: undefined,
+ error: undefined
+ },
+ allowedSlippage: undefined,
+ autoSlippage: undefined,
+ });
+
+ const [isLoadingSwapInfo, setIsLoadingSwapInfo] = useState(false);
+
+
+ useEffect(() => {
+ const fetchSwapInfo = async () => {
+ // Ensure tokensAsMap and settings are loaded before fetching swap info
+ const dependenciesReady = Object.keys(tokensAsMap).length > 0 && protocolsStatus !== undefined && maxHops !== undefined && isAggregator !== undefined;
+
+ if (dependenciesReady) {
+ setIsLoadingSwapInfo(true); // Set loading to true before the async operation
+ try {
+ const result = await getDerivedSwapInfoService(
+ state,
+ sorobanContext,
+ address,
+ tokensAsMap,
+ horizonAccount,
+ allowedSlippage, // Pass allowedSlippage
+ factory, // Pass factory
+ maxHops, // Pass maxHops
+ protocolsStatus, // Pass protocolsStatus
+ isAggregator, // Pass isAggregator
+ );
+ setSwapInfoServiceResult(result);
+ } catch (error) {
+ console.error("Error fetching swap info:", error);
+ // Optionally set an error state here
+ } finally {
+ setIsLoadingSwapInfo(false); // Set loading to false after the operation
+ }
+ } else {
+ // If dependencies are not ready, keep loading state true
+ // The effect will re-run when dependencies change and the service will be called
+ setIsLoadingSwapInfo(true); // Keep loading true while waiting for dependencies
+ }
+ };
+
+ fetchSwapInfo();
+ }, [
+ state, sorobanContext, address, tokensAsMap, horizonAccount,
+ allowedSlippage, factory, maxHops, protocolsStatus, isAggregator
+ ]);
const {
- trade: { state: tradeState, trade, resetRouterSdkCache },
- allowedSlippage,
+ trade: { state: tradeState, trade },
currencyBalances,
parsedAmount,
currencies,
inputError: swapInputError,
- } = useDerivedSwapInfo(state);
+
+ } = useMemo(() => {
+ return swapInfoServiceResult
+ }, [swapInfoServiceResult, isLoadingSwapInfo])
+
useEffect(() => {
if (
- typeof currencyBalances[Field.OUTPUT] != 'string' &&
+ currencyBalances?.[Field.OUTPUT] &&
+ typeof currencyBalances[Field.OUTPUT] !== 'string' &&
!currencyBalances[Field.OUTPUT].balance
) {
setNeedTrustline(true);
@@ -169,21 +273,15 @@ export function SwapComponent({
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount,
}),
- [independentField, parsedAmount, trade],
+ [independentField, parsedAmount],
);
const decimals = useMemo(
() => ({
- [Field.INPUT]:
- independentField === Field.INPUT
- ? trade?.outputAmount?.currency.decimals ?? 7
- : trade?.inputAmount?.currency.decimals ?? 7,
- [Field.OUTPUT]:
- independentField === Field.OUTPUT
- ? trade?.inputAmount?.currency.decimals ?? 7
- : trade?.outputAmount?.currency.decimals ?? 7,
+ [Field.INPUT]: trade?.inputAmount?.currency.decimals ?? 7,
+ [Field.OUTPUT]: trade?.outputAmount?.currency.decimals ?? 7,
}),
- [independentField, trade],
+ [trade],
);
const userHasSpecifiedInputOutput = Boolean(
@@ -197,8 +295,8 @@ export function SwapComponent({
const showFiatValueInput = Boolean(parsedAmounts[Field.INPUT]);
const showFiatValueOutput = Boolean(parsedAmounts[Field.OUTPUT]);
- const maxInputAmount: relevantTokensType | string = useMemo(
- () => currencyBalances[Field.INPUT],
+ const maxInputAmount: relevantTokensType | string | undefined = useMemo(
+ () => currencyBalances?.[Field.INPUT],
// () => maxAmountSpend(currencyBalances[Field.INPUT]), TODO: Create maxAmountSpend if is native token (XLM) should count for the fees and minimum xlm for the account to have
[currencyBalances],
);
@@ -233,21 +331,19 @@ export function SwapComponent({
(value: string) => {
const currency = currencies[Field.INPUT];
const decimals = currency?.decimals ?? 7;
-
// Prevents user from inputting more decimals than the token supports
if (value.split('.').length > 1 && value.split('.')[1].length > decimals) {
return;
}
-
onUserInput(Field.INPUT, value);
},
[onUserInput, currencies],
);
+
const handleTypeOutput = useCallback(
(value: string) => {
const currency = currencies[Field.OUTPUT];
const decimals = currency?.decimals ?? 7;
-
// Prevents user from inputting more decimals than the token supports
if (value.split('.').length > 1 && value.split('.')[1].length > decimals) {
return;
@@ -260,22 +356,22 @@ export function SwapComponent({
const formattedAmounts = useMemo(
() => ({
- [independentField]: typedValue,
- [dependentField]: formatTokenAmount(trade?.expectedAmount, decimals[independentField]),
+ [Field.INPUT]: independentField === Field.INPUT ? typedValue : formatTokenAmount(trade?.inputAmount?.value, decimals[Field.INPUT]),
+ [Field.OUTPUT]: independentField === Field.OUTPUT ? typedValue : formatTokenAmount(trade?.outputAmount?.value, decimals[Field.OUTPUT]),
}),
- [decimals, dependentField, independentField, trade?.expectedAmount, typedValue],
+ [decimals, independentField, trade, typedValue],
);
const showMaxButton = Boolean((maxInputAmount as relevantTokensType)?.balance ?? 0 > 0);
- const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo(
- () => [
- tradeState === TradeState.NO_ROUTE_FOUND,
- tradeState === TradeState.LOADING,
- tradeState === TradeState.LOADING && Boolean(trade),
- ],
- [trade, tradeState],
- );
+ const {routeNotFound, routeIsLoading, routeIsSyncing} = useMemo(()=> {
+ return {
+ routeNotFound: tradeState === TradeState.NO_ROUTE_FOUND,
+ routeIsLoading: isLoadingSwapInfo || tradeState === TradeState.LOADING,
+ routeIsSyncing: isLoadingSwapInfo || (tradeState === TradeState.LOADING && Boolean(trade))
+ }
+ }, [isLoadingSwapInfo, tradeState])
+
const handleContinueToReview = () => {
setSwapState({
@@ -357,23 +453,29 @@ export function SwapComponent({
};
const showDetailsDropdown = Boolean(
- userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing),
+ userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing)
);
- const inputCurrency = currencies[Field.INPUT] ?? undefined;
+ const inputCurrency = currencies?.[Field.INPUT] ?? undefined;
const priceImpactSeverity = 2; //IF is < 2 it shows Swap anyway button in red
const showPriceImpactWarning = false;
+ //SECTION: Hook with effect
const nativeBalance = useGetNativeTokenBalance();
- const { networkFees, isLoading: isLoadingNetworkFees } = useSwapNetworkFees(trade, currencies);
+ //SECTION: Hook with effect
+ const { networkFees, isLoading: isLoadingNetworkFees } = useSwapNetworkFees(trade, currencies); // Keep useSwapNetworkFees
+
const { getMainButtonText, isMainButtonDisabled, handleMainButtonClick, getSwapValues } =
useSwapMainButton({
currencies,
- currencyBalances,
+ currencyBalances: { // Transform currencyBalances
+ [Field.INPUT]: currencyBalances?.[Field.INPUT] ?? '', // Provide a default empty string if undefined
+ [Field.OUTPUT]: currencyBalances?.[Field.OUTPUT] ?? '', // Provide a default empty string if undefined
+ },
formattedAmounts,
- routeNotFound,
+ routeNotFound, // Use routeNotFound variable
onSubmit: handleContinueToReview,
trade,
networkFees,
@@ -381,7 +483,7 @@ export function SwapComponent({
const useConfirmModal = useConfirmModalState({
trade: trade!,
- allowedSlippage,
+ allowedSlippage: allowedSlippage, // Use allowedSlippage variable
onSwap: handleSwap,
onSetTrustline: handleTrustline,
onCurrencySelection,
@@ -395,10 +497,10 @@ export function SwapComponent({
onUserInput(Field.INPUT, '');
}
- resetRouterSdkCache();
+ // resetRouterSdkCache(); // Commenting out as resetRouterSdkCache is an empty method
useConfirmModal.resetStates();
- }, [onUserInput, swapResult, useConfirmModal, resetRouterSdkCache]);
+ }, [onUserInput, swapResult, useConfirmModal]);
const handleErrorDismiss = () => {
setTxError(false);
@@ -406,6 +508,7 @@ export function SwapComponent({
handleConfirmDismiss();
};
+
return (
<>
@@ -465,13 +568,13 @@ export function SwapComponent({
onMax={(maxBalance) => handleTypeInput(maxBalance.toString())}
fiatValue={showFiatValueInput ? fiatValueInput : undefined}
onCurrencySelect={handleInputSelect}
- otherCurrency={currencies[Field.OUTPUT]}
+ otherCurrency={currencies?.[Field.OUTPUT]}
networkFees={networkFees}
isLoadingNetworkFees={isLoadingNetworkFees}
// showCommonBases
// id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
- loading={independentField === Field.OUTPUT && routeIsSyncing}
- currency={currencies[Field.INPUT] ?? null}
+ loading={routeIsLoading && independentField === Field.OUTPUT} // Use routeIsLoading variable
+ currency={currencies?.[Field.INPUT] ?? null}
id={'swap-input'}
disableInput={getSwapValues().noCurrencySelected}
/>
@@ -519,11 +622,12 @@ export function SwapComponent({
/>
+
{showDetailsDropdown && !getSwapValues().insufficientLiquidity && (
diff --git a/src/components/Swap/SwapDetailsDropdown.tsx b/src/components/Swap/SwapDetailsDropdown.tsx
index 63ddfa90..e5ee3581 100644
--- a/src/components/Swap/SwapDetailsDropdown.tsx
+++ b/src/components/Swap/SwapDetailsDropdown.tsx
@@ -4,7 +4,7 @@ import Column from 'components/Column';
import { LoadingOpacityContainer } from 'components/Loader/styled';
import { RowBetween, RowFixed } from 'components/Row';
import { SubHeaderSmall } from 'components/Text';
-import { useState } from 'react';
+import { memo, useMemo, useState } from 'react';
import { ChevronDown } from 'react-feather';
import { InterfaceTrade } from 'state/routing/types';
import { AdvancedSwapDetails } from './AdvancedSwapDetails';
@@ -88,13 +88,13 @@ interface SwapDetailsInlineProps {
networkFees: number | null;
}
-export default function SwapDetailsDropdown({
+ const SwapDetailsDropdown = ({
trade,
syncing,
loading,
allowedSlippage,
networkFees,
-}: SwapDetailsInlineProps) {
+}: SwapDetailsInlineProps) => {
const theme = useTheme();
const [showDetails, setShowDetails] = useState(false);
@@ -115,7 +115,7 @@ export default function SwapDetailsDropdown({
)}
<>
- {trade ? (
+ {!loading && trade ? (
@@ -132,7 +132,7 @@ export default function SwapDetailsDropdown({
/>
- {trade && (
+ {!loading && trade && (
);
}
+
+export default memo(SwapDetailsDropdown);
\ No newline at end of file
diff --git a/src/components/Swap/SwapPathComponent.tsx b/src/components/Swap/SwapPathComponent.tsx
index 54215846..921bb18a 100644
--- a/src/components/Swap/SwapPathComponent.tsx
+++ b/src/components/Swap/SwapPathComponent.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { ChevronRight } from 'react-feather';
import { Box, styled } from 'soroswap-ui';
import Row, { RowBetween, RowFixed } from 'components/Row';
@@ -58,50 +58,90 @@ function SwapPathComponent({ trade }: { trade: InterfaceTrade | undefined }) {
const sorobanContext = useSorobanReact();
const { tokensAsMap, isLoading } = useAllTokens();
+ const memoizedTradePath = useMemo(() => trade, [trade?.path, trade?.distribution]);
const [pathArray, setPathArray] = useState([]);
const [distributionArray, setDistributionArray] = useState([]);
const [totalParts, setTotalParts] = useState(0);
const [pathTokensIsLoading, setPathTokensIsLoading] = useState(false);
+ const getTradedCurrencies = async (pathes: string[]) => {
+ const promises = pathes.map(async (contract) => {
+ const asset = await findToken(
+ contract,
+ tokensAsMap,
+ sorobanContext
+ );
+ const code = asset?.code == 'native' ? 'XLM' : asset?.code;
+
+ return code;
+ });
+ const results = await Promise.allSettled(promises);
+
+ return results
+ .filter((result) => result.status === 'fulfilled' && result.value)
+ .map((result) => (result.status === 'fulfilled' && result.value ? result.value : ''));
+ };
+
+ const getAggregatorPathCurrencyCodes = async (
+ path: string[],
+ tokensAsMap: any,
+ sorobanContext: any,
+ ) => {
+ const promises = path.map(async (contract) => {
+ const asset = await findToken(
+ contract,
+ tokensAsMap,
+ sorobanContext
+ );
+ const code = asset?.code == 'native' ? 'XLM' : asset?.code;
+ return code;
+ });
+ const results = await Promise.allSettled(promises);
+ return results
+ .filter((result) => result.status === 'fulfilled' && result.value)
+ .map((result) => (result.status === 'fulfilled' && result.value ? result.value : ''));
+ };
+
useEffect(() => {
(async () => {
- if (!trade || isLoading) return;
- if (trade.platform == PlatformType.ROUTER && trade.path) {
- setPathTokensIsLoading(true);
- const promises = trade.path.map(async (contract) => {
- const asset = await findToken(contract, tokensAsMap, sorobanContext);
- const code = asset?.code == 'native' ? 'XLM' : asset?.code;
- return code;
- });
- const results = await Promise.allSettled(promises);
-
- const fulfilledValues = results
- .filter((result) => result.status === 'fulfilled' && result.value)
- .map((result) => (result.status === 'fulfilled' && result.value ? result.value : ''));
- setPathArray(fulfilledValues);
+ if (!memoizedTradePath || isLoading) return;
+ setPathTokensIsLoading(true);
+ if (memoizedTradePath.platform == PlatformType.ROUTER && memoizedTradePath.path) {
+ /**
+ * NOTE: memoizedTradePath.path.inputAmount && memoizedTradePath.path.output already have this - We don't need * external request
+ **/
+ let pathtokenNames = ['', ''];
+ if (
+ !memoizedTradePath.inputAmount ||
+ !memoizedTradePath.outputAmount ||
+ memoizedTradePath?.path[0] !== memoizedTradePath?.inputAmount?.currency?.contract ||
+ memoizedTradePath.path[1] !== memoizedTradePath?.outputAmount?.currency?.contract
+ ) {
+ pathtokenNames = await getTradedCurrencies(memoizedTradePath?.path);
+ } else {
+ pathtokenNames = [memoizedTradePath?.inputAmount?.currency?.code, memoizedTradePath.outputAmount?.currency?.code as string];
+ }
+
+ setPathArray(pathtokenNames);
setPathTokensIsLoading(false);
- } else if (trade.platform == PlatformType.STELLAR_CLASSIC && trade.path) {
- setPathTokensIsLoading(true);
- const codes = trade.path.map((address) => {
+ } else if (memoizedTradePath.platform == PlatformType.STELLAR_CLASSIC && memoizedTradePath.path) {
+ const codes = memoizedTradePath.path.map((address) => {
if (address == 'native') return 'XLM';
return address.split(':')[0];
});
setPathArray(codes);
setPathTokensIsLoading(false);
- } else if (trade.platform == PlatformType.AGGREGATOR) {
- if (!trade?.distribution) return;
+ } else if (memoizedTradePath.platform == PlatformType.AGGREGATOR) {
+ if (!memoizedTradePath?.distribution) return;
+
let tempDistributionArray: distributionData[] = [];
- setPathTokensIsLoading(!pathTokensIsLoading);
- for (let distribution of trade?.distribution) {
- const promises = distribution.path.map(async (contract) => {
- const asset = await findToken(contract, tokensAsMap, sorobanContext);
- const code = asset?.code == 'native' ? 'XLM' : asset?.code;
- return code;
- });
- const results = await Promise.allSettled(promises);
- const fulfilledValues = results
- .filter((result) => result.status === 'fulfilled' && result.value)
- .map((result) => (result.status === 'fulfilled' && result.value ? result.value : ''));
+ for (let distribution of memoizedTradePath?.distribution) {
+ const fulfilledValues = await getAggregatorPathCurrencyCodes(
+ distribution.path,
+ tokensAsMap,
+ sorobanContext,
+ );
+
tempDistributionArray.push({
path: fulfilledValues,
parts: distribution.parts,
@@ -111,9 +151,11 @@ function SwapPathComponent({ trade }: { trade: InterfaceTrade | undefined }) {
setTotalParts(tempDistributionArray.reduce((acc, curr) => acc + curr.parts, 0));
}
setPathTokensIsLoading(false);
+ } else {
+ setPathTokensIsLoading(false);
}
})();
- }, [trade?.path, isLoading, sorobanContext]);
+ }, [trade?.path, memoizedTradePath, isLoading, sorobanContext]);
return (
@@ -124,15 +166,15 @@ function SwapPathComponent({ trade }: { trade: InterfaceTrade | undefined }) {
`}
>
- {trade?.platform == PlatformType.AGGREGATOR && distributionArray.length > 1
+ {memoizedTradePath?.platform == PlatformType.AGGREGATOR && distributionArray.length > 1
? 'Paths:'
: 'Path'}
- {(trade?.platform == PlatformType.ROUTER ||
- trade?.platform == PlatformType.STELLAR_CLASSIC) && (
+ {(memoizedTradePath?.platform == PlatformType.ROUTER ||
+ memoizedTradePath?.platform == PlatformType.STELLAR_CLASSIC) && (
{pathArray?.map((contract, index) => (
@@ -144,7 +186,7 @@ function SwapPathComponent({ trade }: { trade: InterfaceTrade | undefined }) {
)}
- {trade?.platform == PlatformType.AGGREGATOR && (
+ {memoizedTradePath?.platform == PlatformType.AGGREGATOR && (
{distributionArray.map((distribution, index) => (
diff --git a/src/functions/getPairAddress.tsx b/src/functions/getPairAddress.tsx
index 0da2943f..b68dd119 100644
--- a/src/functions/getPairAddress.tsx
+++ b/src/functions/getPairAddress.tsx
@@ -10,13 +10,13 @@ export async function getPairAddress(
address_1: string | undefined,
sorobanContext: SorobanContextType,
) {
+
if (!address_0 || !address_1) return '';
const { activeNetwork } = sorobanContext;
const activeChain = passphraseToBackendNetworkName[activeNetwork!].toLowerCase();
const factory = await fetchFactory(activeChain);
-
const response = await contractInvoke({
contractAddress: factory.address,
method: 'get_pair',
diff --git a/src/hooks/tokens/useApiTokens.ts b/src/hooks/tokens/useApiTokens.ts
index 7d1034b8..76ec143e 100644
--- a/src/hooks/tokens/useApiTokens.ts
+++ b/src/hooks/tokens/useApiTokens.ts
@@ -1,6 +1,6 @@
import { useSorobanReact } from 'stellar-react';
import { TokenMapType, TokenType, tokensResponse } from 'interfaces';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useMemo } from 'react';
import { fetchTokens } from 'services/tokens';
import useSWRImmutable from 'swr/immutable';
import { tokensToMap } from './utils';
@@ -17,6 +17,7 @@ export const useApiTokens = () => {
setActiveChain(activeNetworkId);
}
}, [activeNetwork]);
+
const { data, mutate, isLoading, error } = useSWRImmutable(
['tokens', activeChain],
() => fetchTokens(activeChain),
diff --git a/src/hooks/tokens/useToken.ts b/src/hooks/tokens/useToken.ts
index f0d74eb5..1f6f610e 100644
--- a/src/hooks/tokens/useToken.ts
+++ b/src/hooks/tokens/useToken.ts
@@ -12,7 +12,7 @@ import { Asset } from '@stellar/stellar-sdk';
export const findToken = async (
tokenAddress: string | undefined,
tokensAsMap: TokenMapType,
- sorobanContext: SorobanContextType,
+ sorobanContext: SorobanContextType
) => {
if (!tokenAddress || tokenAddress === '') return undefined;
@@ -22,10 +22,11 @@ export const findToken = async (
if (!formattedAddress) return undefined;
const fromMap = tokensAsMap[formattedAddress];
-
+
if (fromMap) return fromMap;
const token = await getToken(sorobanContext, formattedAddress);
+
// const token: TokenType = {
// contract: formattedAddress,
// name: name as string,
@@ -74,6 +75,7 @@ export function useToken(tokenAddress: string | undefined) {
([key, tokenAddress, sorobanContext]) => getTokenName(tokenAddress!, sorobanContext),
);
+
const { mutate } = useSWRConfig();
const { data, isLoading, error } = useSWRImmutable(
['token', tokenAddress, tokensAsMap, sorobanContext],
@@ -81,6 +83,7 @@ export function useToken(tokenAddress: string | undefined) {
findToken(tokenAddress, tokensAsMap, sorobanContext),
);
+
const handleTokenRefresh = () => {
mutate((key: any) => revalidateKeysCondition(key), undefined, { revalidate: true });
};
diff --git a/src/hooks/tokens/useUserAddedTokens.ts b/src/hooks/tokens/useUserAddedTokens.ts
index ee73cbee..f8aa59cf 100644
--- a/src/hooks/tokens/useUserAddedTokens.ts
+++ b/src/hooks/tokens/useUserAddedTokens.ts
@@ -1,6 +1,6 @@
import { useSorobanReact } from 'stellar-react';
import { TokenMapType, TokenType, tokensResponse } from 'interfaces';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useMemo } from 'react';
import { tokensToMap } from './utils';
import { passphraseToBackendNetworkName } from 'services/pairs';
@@ -8,26 +8,25 @@ import { passphraseToBackendNetworkName } from 'services/pairs';
export const useUserAddedTokens = () => {
const sorobanContext = useSorobanReact();
+
const [userAddedTokens, setUserAddedTokens] = useState([]);
- const [tokensFromActiveChain, setTokensFromActiveChain] = useState([]);
- const [tokensAsMap, setTokensAsMap] = useState({});
const userAddedTokensLocalStorage = localStorage.getItem(`userAddedTokens`);
useEffect(() => {
const userAddedTokensStr = localStorage.getItem(`userAddedTokens`) || '[]';
const userAddedTokens = (JSON.parse(userAddedTokensStr) ?? []) as tokensResponse[];
- const activeNetwork = sorobanContext.activeNetwork;
- const activeChain = passphraseToBackendNetworkName[activeNetwork!].toLowerCase();
- const tokensFromActiveChain =
- userAddedTokens?.find((item: tokensResponse) => item?.network === activeChain)?.tokens ?? [];
- const tokensAsMap = tokensToMap(tokensFromActiveChain);
-
setUserAddedTokens(userAddedTokens);
+ }, [userAddedTokensLocalStorage]); // Depend only on localStorage string
+
+ const tokensFromActiveChain = useMemo(() => {
+ const activeNetwork = sorobanContext.activeNetwork;
+ if (!activeNetwork) return [];
+ const activeChain = passphraseToBackendNetworkName[activeNetwork].toLowerCase();
+ return userAddedTokens?.find((item: tokensResponse) => item?.network === activeChain)?.tokens ?? [];
+ }, [userAddedTokens, sorobanContext]); // Depend on userAddedTokens and sorobanContext
- setTokensFromActiveChain(tokensFromActiveChain);
- setTokensAsMap(tokensAsMap);
- }, [sorobanContext, userAddedTokensLocalStorage]);
+ const tokensAsMap = useMemo(() => tokensToMap(tokensFromActiveChain), [tokensFromActiveChain]); // Depend on tokensFromActiveChain
return {
allUserAddedTokens: userAddedTokens,
diff --git a/src/hooks/tokens/utils.ts b/src/hooks/tokens/utils.ts
index d07c899a..53124e74 100644
--- a/src/hooks/tokens/utils.ts
+++ b/src/hooks/tokens/utils.ts
@@ -54,7 +54,7 @@ export const addUserToken = (token: TokenType, sorobanContext: SorobanContextTyp
export async function getToken(
sorobanContext: SorobanContextType,
- tokenAddress?: string | undefined,
+ tokenAddress?: string | undefined
): Promise {
if (!tokenAddress || tokenAddress === '' || !sorobanContext.activeNetwork) return undefined;
diff --git a/src/hooks/useBalances.tsx b/src/hooks/useBalances.tsx
index 3dfb16f6..a3732191 100644
--- a/src/hooks/useBalances.tsx
+++ b/src/hooks/useBalances.tsx
@@ -30,7 +30,7 @@ export interface tokenBalancesType {
export async function tokenBalance(
tokenAddress: string,
userAddress: string,
- sorobanContext: SorobanContextType,
+ sorobanContext: SorobanContextType
) {
const user = accountToScVal(userAddress);
diff --git a/src/hooks/useBestTrade.ts b/src/hooks/useBestTrade.ts
index 42b7e8dd..0b6cdff7 100644
--- a/src/hooks/useBestTrade.ts
+++ b/src/hooks/useBestTrade.ts
@@ -56,6 +56,7 @@ export function useBestTrade(
isLoading: isLoadingSWR,
isValidating,
} = useSWR(
+
amountSpecified && otherCurrency
? [amountSpecified, otherCurrency, tradeType, amountSpecified.value, maxHops, protocolsStatus]
: null,
@@ -69,10 +70,11 @@ export function useBestTrade(
}),
{
revalidateIfStale: true,
- revalidateOnFocus: true,
+ revalidateOnFocus: false,
refreshInterval: 0,
},
);
+
const isLoading = isLoadingSWR || isValidating;
//Define the input and output currency based on the trade type
diff --git a/src/hooks/useGetMyBalances.ts b/src/hooks/useGetMyBalances.ts
index 1aa4d950..77d8531c 100644
--- a/src/hooks/useGetMyBalances.ts
+++ b/src/hooks/useGetMyBalances.ts
@@ -17,15 +17,14 @@ interface FetchBalancesProps {
account: AccountResponse | undefined;
}
-const fetchBalances = async ({ address, tokens, sorobanContext, account }: FetchBalancesProps) => {
+const fetchBalances = async ({ address, tokens, sorobanContext, account }: FetchBalancesProps, ) => {
if (!address || !tokens || !sorobanContext || !account) return null;
-
+
const response = await tokenBalances(address, tokens, sorobanContext, account, true);
-
return response;
};
-function calculateAvailableBalance(
+export function calculateAvailableBalance(
balance?: string | number | BigNumber | null,
networkFees?: string | number | BigNumber | null,
subentryCount?: number,
diff --git a/src/hooks/useRouterCallback.tsx b/src/hooks/useRouterCallback.tsx
index 9e953265..3c10253a 100644
--- a/src/hooks/useRouterCallback.tsx
+++ b/src/hooks/useRouterCallback.tsx
@@ -48,6 +48,8 @@ export function useRouterCallback() {
return useCallback(
async (method: RouterMethod, args?: StellarSdk.xdr.ScVal[], signAndSend?: boolean) => {
+
+
let result = (await contractInvoke({
contractAddress: router_address as string,
method: method,
diff --git a/src/hooks/useSwapNetworkFees.ts b/src/hooks/useSwapNetworkFees.ts
index 1e260f27..8512f0c6 100644
--- a/src/hooks/useSwapNetworkFees.ts
+++ b/src/hooks/useSwapNetworkFees.ts
@@ -18,7 +18,7 @@ const fetchNetworkFees = async (
trade: InterfaceTrade | undefined,
sorobanContext: SorobanContextType,
) => {
- if (!trade || !sorobanContext) return 0;
+ if (!trade || !sorobanContext) return 0;
const fees = await calculateSwapFees(sorobanContext, trade);
return fees ? Number(fees) / 10 ** 7 : 0;
};
diff --git a/src/services/swapService.ts b/src/services/swapService.ts
new file mode 100644
index 00000000..1b85b9ac
--- /dev/null
+++ b/src/services/swapService.ts
@@ -0,0 +1,390 @@
+import { SorobanContextType } from 'stellar-react';
+import { SwapState } from 'state/swap/reducer';
+import { Field } from 'state/swap/actions';
+import { TokenType, TokenMapType, CurrencyAmount } from 'interfaces'; // Import CurrencyAmount
+import { relevantTokensType, tokenBalancesType } from 'hooks'; // Import tokenBalancesType
+import { InterfaceTrade, TradeState, TradeType } from 'state/routing/types'; // Import TradeType
+import { ReactNode } from 'react';
+import { findTokenService } from './tokenService'; // Import findTokenService
+import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'; // Import tryParseCurrencyAmount
+import { tokenBalances } from 'hooks'; // Import tokenBalances
+import { useBestTrade } from 'hooks/useBestTrade'; // Import useBestTrade
+import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'; // Import useUserSlippageToleranceWithDefault
+import { DEFAULT_SLIPPAGE_INPUT_VALUE } from 'components/Settings/MaxSlippageSettings'; // Import DEFAULT_SLIPPAGE_INPUT_VALUE
+import { isAddress } from 'helpers/address'; // Import isAddress
+import { AccountResponse } from '@stellar/stellar-sdk/lib/horizon'; // Import AccountResponse
+import BigNumber from 'bignumber.js';
+
+
+export type SwapInfoService = {
+ currencies: { [field in Field]?: TokenType | null };
+ currencyBalances: { [field in Field]?: relevantTokensType | string | undefined }; //{ [field in Field]?: CurrencyAmount }
+ parsedAmount?: any; //CurrencyAmount
+ inputError?: ReactNode;
+ trade: {
+ trade?: any; //InterfaceTrade
+ state: any; //TradeState
+ uniswapXGasUseEstimateUSD?: number;
+ error?: any;
+ };
+ allowedSlippage: any; //Percent
+ autoSlippage: any; //Percent
+};
+
+// Import necessary types and functions for calculateBestRouteService
+import {
+ BuildSplitTradeReturn,
+ BuildTradeReturn,
+ PlatformType,
+ Protocol,
+ SwapRouteRequest,
+ SwapRouteSplitRequest,
+ ExactInBuildTradeReturn,
+ ExactInSplitBuildTradeReturn,
+ ExactOutBuildTradeReturn,
+ ExactOutSplitBuildTradeReturn,
+} from 'state/routing/types';
+import { CurrencyAmount as AmountAsset } from 'interfaces';
+import { getPairAddress } from 'functions/getPairAddress';
+import { reservesBNWithTokens } from 'hooks/useReserves'; //NOTE: This is a hook, need to check if it can be used or replaced
+import { getExpectedAmount } from 'functions/getExpectedAmount';
+import { Networks } from '@stellar/stellar-sdk';
+import { getSwapRoute, getSwapSplitRoute } from 'services/soroswapApi';
+import { passphraseToBackendNetworkName } from 'services/pairs';
+import { getBestPath, getHorizonBestPath } from 'helpers/horizon/getHorizonPath';
+import { ProtocolsStatus } from 'contexts';
+
+export interface CalculateRouteServiceProps {
+ amountAsset: AmountAsset;
+ quoteAsset: TokenType;
+ amount: string;
+ tradeType: TradeType;
+ currentProtocolsStatus: ProtocolsStatus[];
+ sorobanContext: SorobanContextType;
+ factory: string | undefined; // Assuming factory address is a string
+ maxHops: number;
+ protocolsStatus: ProtocolsStatus[];
+ isAggregator: boolean;
+ isExactIn: boolean;
+ allowedSlippage: number
+}
+
+const shouldUseDirectPath = process.env.NEXT_PUBLIC_DIRECT_PATH_ENABLED === 'true';
+
+export const calculateBestRouteService = async ({
+ amountAsset,
+ quoteAsset,
+ amount,
+ tradeType,
+ currentProtocolsStatus,
+ sorobanContext,
+ factory,
+ maxHops,
+ protocolsStatus,
+ isAggregator,
+ isExactIn,
+ allowedSlippage
+}: CalculateRouteServiceProps): Promise => {
+
+ if (shouldUseDirectPath) {
+ try {
+ // get pair address from factory
+ const pairAddress = await getPairAddress(
+ isExactIn ? amountAsset.currency.contract : quoteAsset.contract,
+ isExactIn ? quoteAsset.contract : amountAsset.currency.contract,
+ sorobanContext,
+ );
+
+ // Get reserves from pair
+ // reservesBNWithTokens is a hook, need to find an alternative
+ const reserves = await reservesBNWithTokens(pairAddress, sorobanContext);
+ if (!reserves?.reserve0 || !reserves?.reserve1) {
+ throw new Error('Reserves not found');
+ }
+
+
+ // Get amountOut or amountIn from reserves and tradeType
+ // getExpectedAmount is a function, can be used directly
+ let quoteAmount = await getExpectedAmount(
+ isExactIn ? amountAsset.currency : quoteAsset,
+ isExactIn ? quoteAsset : amountAsset.currency,
+ new BigNumber(amount),
+ sorobanContext,
+ tradeType,
+ );
+
+ // Convert from lumens to stroops (multiply by 10^7)
+ const quoteAmountInteger = quoteAmount.integerValue();
+
+ const toReturn: BuildTradeReturn =
+ tradeType === TradeType.EXACT_INPUT
+ ? {
+ assetIn: amountAsset.currency.contract,
+ assetOut: quoteAsset.contract,
+ priceImpact: {
+ numerator: 0,
+ denominator: 1,
+ },
+ platform: PlatformType.ROUTER,
+ tradeType: TradeType.EXACT_INPUT,
+ trade: {
+ amountIn: BigInt(amountAsset.value),
+ amountOutMin: BigInt(quoteAmountInteger.toString()),
+ path: [amountAsset.currency.contract, quoteAsset.contract],
+ }
+ }
+ : {
+ assetIn: amountAsset.currency.contract,
+ assetOut: quoteAsset.contract,
+ priceImpact: {
+ numerator: 0,
+ denominator: 1,
+ },
+ platform: PlatformType.ROUTER,
+ tradeType: TradeType.EXACT_OUTPUT,
+ trade: {
+ amountOut: BigInt(amount),
+ amountInMax: BigInt(quoteAmountInteger.toString()),
+ path: [amountAsset.currency.contract, quoteAsset.contract ],
+ },
+ };
+
+ return toReturn;
+ } catch (error) {
+ console.error('Error generating direct path:', error);
+ throw error;
+ }
+ }
+ if (!factory) throw new Error('Factory address not found');
+
+ const isHorizonEnabled = currentProtocolsStatus.find(
+ (p) => p.key === PlatformType.STELLAR_CLASSIC,
+ )?.value;
+
+ const isSoroswapEnabled = currentProtocolsStatus.find((p) => p.key === Protocol.SOROSWAP)
+ ?.value;
+
+ const horizonProps = {
+ assetFrom: isExactIn ? amountAsset.currency : quoteAsset,
+ assetTo: isExactIn ? quoteAsset : amountAsset.currency,
+ amount,
+ tradeType,
+ };
+
+ let horizonPath: BuildTradeReturn | undefined;
+ if (isHorizonEnabled) {
+ horizonPath = (await getHorizonBestPath(horizonProps, sorobanContext)) as BuildTradeReturn;
+ }
+
+ let sorobanPath: BuildTradeReturn | BuildSplitTradeReturn | undefined;
+ if (isAggregator) {
+
+ const swapSplitRequest: SwapRouteSplitRequest = {
+ assetIn: amountAsset.currency.contract,
+ assetOut: quoteAsset.contract,
+ amount: amount,
+ tradeType: tradeType,
+ protocols: protocolsStatus.filter(p => p.key !== PlatformType.STELLAR_CLASSIC && p.value).map(p => p.key as Protocol),
+ parts: 10,
+ slippageTolerance: Math.floor(Number(allowedSlippage) * 100).toString(),
+ assetList: ['SOROSWAP'], // TODO: Use actual asset list,
+ maxHops: maxHops,
+ };
+
+ try {
+ const networkName = passphraseToBackendNetworkName[sorobanContext.activeNetwork ?? Networks.TESTNET];
+ const response = await getSwapSplitRoute(networkName, swapSplitRequest);
+ sorobanPath = response;
+ } catch (error) {
+ console.error('Error getting soroban split path:', error);
+ return undefined;
+ }
+
+ } else if (isSoroswapEnabled) {
+ //NOTE: Asset changed to defaultm without reverse
+ const swapRequest: SwapRouteRequest = {
+ assetIn: amountAsset.currency.contract,
+ assetOut: quoteAsset.contract,
+ amount: amount,
+ tradeType: tradeType,
+ slippageTolerance: Math.floor(Number(allowedSlippage) * 100).toString(),
+ assetList: ['SOROSWAP'], // TODO: Use actual asset list,
+ maxHops: maxHops,
+ };
+
+ try {
+ const networkName = passphraseToBackendNetworkName[sorobanContext.activeNetwork ?? Networks.TESTNET];
+ const response = await getSwapRoute(networkName, swapRequest);
+ sorobanPath = response;
+ } catch (error) {
+ console.error('Error getting soroban path:', error);
+ return undefined;
+ }
+ }
+ const bestPath = getBestPath(horizonPath, sorobanPath, tradeType);
+ return bestPath;
+};
+
+export const getDerivedSwapInfoService = async (
+ state: SwapState,
+ sorobanContext: SorobanContextType,
+ account: string | undefined | null,
+ tokensAsMap: TokenMapType,
+ horizonAccount: AccountResponse | undefined, // Accept AccountResponse as argument
+ allowedSlippage: number, // Accept allowedSlippage as argument
+ // Add arguments for calculateBestRouteService dependencies
+ factory: string | undefined,
+ maxHops: number,
+ protocolsStatus: ProtocolsStatus[],
+ isAggregator: boolean,
+): Promise => {
+
+ const {
+ independentField,
+ typedValue,
+ [Field.INPUT]: { currencyId: inputCurrencyId },
+ [Field.OUTPUT]: { currencyId: outputCurrencyId },
+ recipient,
+ } = state;
+
+ // Fetch input and output tokens using findTokenService
+ const inputCurrency = await findTokenService(inputCurrencyId ?? undefined, tokensAsMap, sorobanContext);
+ const outputCurrency = await findTokenService(outputCurrencyId ?? undefined, tokensAsMap, sorobanContext);
+
+ const tokensArray = inputCurrency && outputCurrency ? [inputCurrency, outputCurrency] : undefined;
+
+ // Fetch relevant token balances
+ let relevantTokenBalances: { [field in Field]?: relevantTokensType | string | undefined } | undefined;
+ if (account && tokensArray && horizonAccount) { // Use passed horizonAccount
+ const balancesResult = await tokenBalances(account, tokensArray, sorobanContext, horizonAccount);
+ // Transform the balances array into an object with INPUT and OUTPUT fields
+ relevantTokenBalances = {
+ [Field.INPUT]: balancesResult?.balances?.[0] ?? '', // Assuming the order is always input then output
+ [Field.OUTPUT]: balancesResult?.balances?.[1] ?? '',
+ };
+ }
+
+ const isExactIn: boolean = independentField === Field.INPUT;
+ const parsedAmount = tryParseCurrencyAmount(typedValue, inputCurrency ?? undefined);
+
+ // Fetch best trade using the new service function
+ let trade: any = { trade: undefined, state: TradeState.STALE }; // Initial state
+
+ if (parsedAmount && inputCurrency && outputCurrency) {
+ trade.state = TradeState.LOADING; // Set loading state before the async call
+ try {
+ const route = await calculateBestRouteService({
+ amountAsset: parsedAmount,
+ quoteAsset: outputCurrency,
+ amount: parsedAmount.value, // Use typedValue for the amount
+ tradeType: isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
+ currentProtocolsStatus: protocolsStatus, // Pass protocolsStatus
+ sorobanContext,
+ factory, // Pass factory
+ maxHops, // Pass maxHops
+ protocolsStatus, // Pass protocolsStatus again? Check if needed twice
+ isAggregator, // Pass isAggregator
+ isExactIn: isExactIn,
+ allowedSlippage
+ });
+
+ // Map the route result to the trade structure expected by SwapInfoService
+ let inputAmount: CurrencyAmount | undefined;
+ let outputAmount: CurrencyAmount | undefined;
+
+ if (route) {
+ // Use type guards to narrow down the type of the route and access trade details
+ if (route.tradeType === TradeType.EXACT_INPUT) {
+ const exactInRoute = route as ExactInBuildTradeReturn | ExactInSplitBuildTradeReturn;
+ const tradeDetails = exactInRoute.trade;
+ inputAmount = { currency: inputCurrency, value: tradeDetails.amountIn.toString() };
+ outputAmount = { currency: outputCurrency, value: tradeDetails.amountOutMin.toString() };
+ } else if (route.tradeType === TradeType.EXACT_OUTPUT) { // TradeType.EXACT_OUTPUT
+ const exactOutRoute = route as ExactOutBuildTradeReturn | ExactOutSplitBuildTradeReturn;
+ const tradeDetails = exactOutRoute.trade;
+ inputAmount = { currency: inputCurrency, value: tradeDetails.amountInMax.toString() };
+ outputAmount = { currency: outputCurrency, value: tradeDetails.amountOut.toString() };
+ }
+ }
+
+ trade = {
+ trade: {
+ inputAmount: inputAmount,
+ outputAmount: outputAmount,
+ state: route ? TradeState.VALID : TradeState.NO_ROUTE_FOUND,
+ path: (route?.trade as any)?.path,
+ distribution: (route?.trade as any)?.distribution,
+ expectedAmount: route?.tradeType === TradeType.EXACT_INPUT ? (route?.trade as any)?.amountOutMin?.toString() : (route?.trade as any)?.amountInMax?.toString(),
+ assetIn: route?.assetIn,
+ assetOut: route?.assetOut,
+ priceImpact: route?.priceImpact,
+ platform: route?.platform,
+ tradeType: route?.tradeType,
+ },
+ };
+
+ } catch (error) {
+ console.error("Error calculating best route:", error);
+ trade.state = TradeState.INVALID; // Set state to invalid on error
+ // Optionally set an error message in trade.error
+ }
+ }
+
+
+ // Calculate input error
+ let inputError: ReactNode | undefined;
+
+ if (!account) {
+ inputError = 'Connect Wallet';
+ }
+
+ if (!inputCurrency || !outputCurrency) {
+ inputError = inputError ?? 'Select a token';
+ }
+
+ if (!parsedAmount) {
+ inputError = inputError ?? 'Enter an amount';
+ }
+
+ const formattedTo = isAddress(recipient ?? '');
+ if (!recipient || !formattedTo) {
+ inputError = inputError ?? 'Enter a recipient';
+ }
+
+ // Check for insufficient liquidity
+ if (
+ trade.trade?.outputAmount?.value?.includes('Infinity') ||
+ trade.trade?.outputAmount?.value?.includes('-')
+ ) {
+ inputError = 'Insufficient liquidity for this trade.';
+ }
+
+ // Check for insufficient balance
+ const balanceIn: string | relevantTokensType | undefined = relevantTokenBalances ? relevantTokenBalances[Field.INPUT] : undefined; // Get the balance for the input currency from the object
+ const maxAmountIn: string | number = trade.trade?.inputAmount?.value ?? 0;
+
+ if (
+ balanceIn &&
+ typeof balanceIn !== 'string' &&
+ BigNumber(balanceIn.balance!).isLessThan(BigNumber(maxAmountIn)) // Use isLessThan for strict comparison
+ ) {
+ inputError = `Insufficient ${balanceIn.code} balance`;
+ }
+
+
+ return {
+ currencies: {
+ [Field.INPUT]: inputCurrency,
+ [Field.OUTPUT]: outputCurrency,
+ },
+ currencyBalances: relevantTokenBalances ?? {}, // Use the fetched balances, provide empty object if undefined
+ parsedAmount: parsedAmount, // Use the parsed amount
+ inputError: inputError, // Use the calculated input error
+ trade: trade, // Use the fetched trade
+ allowedSlippage: allowedSlippage, // Use the calculated slippage
+ autoSlippage: undefined, // Auto slippage is not directly used in the return type
+ };
+};
+
+
+
diff --git a/src/services/tokenService.ts b/src/services/tokenService.ts
new file mode 100644
index 00000000..adeec7da
--- /dev/null
+++ b/src/services/tokenService.ts
@@ -0,0 +1,103 @@
+import { SorobanContextType, WalletNetwork } from 'stellar-react';
+import { getClassicAssetSorobanAddress } from 'functions/getClassicAssetSorobanAddress';
+import { getClassicStellarAsset, isAddress } from 'helpers/address';
+import { getTokenName } from 'helpers/soroban';
+import { TokenMapType, TokenType, tokensResponse } from 'interfaces';
+import { getToken, isClassicStellarAsset, tokensToMap } from 'hooks/tokens/utils';
+import { Asset } from '@stellar/stellar-sdk';
+import { fetchTokens } from 'services/tokens';
+import { passphraseToBackendNetworkName } from 'services/pairs';
+
+export const findTokenService = async (
+ tokenAddress: string | undefined,
+ tokensAsMap: TokenMapType,
+ sorobanContext: SorobanContextType
+): Promise => {
+ if (!tokenAddress || tokenAddress === '') return undefined;
+
+ const classicAssetSearch = getClassicAssetSorobanAddress(tokenAddress!, sorobanContext);
+
+ const formattedAddress = isAddress(classicAssetSearch ? classicAssetSearch : tokenAddress);
+ if (!formattedAddress) return undefined;
+
+ const fromMap = tokensAsMap[formattedAddress];
+
+ if (fromMap) return fromMap;
+
+ const token = await getToken(sorobanContext, formattedAddress);
+
+ if (!token?.name || !token?.code) return undefined;
+ // Here from token.name we will try to understand if this is a classic asset (even if we got a soroban contracta as address).
+ const stellarAsset = getClassicStellarAsset(token.name);
+
+ /*
+ TODO: Here we might have a situation where a Soroban Contract has as name a CODE:ISSUER
+ We need to have a beter way to know if a soroban contract its for sure a stellar classic asset, and which.
+ */
+ if (stellarAsset && typeof stellarAsset !== 'boolean') {
+ return {
+ issuer: stellarAsset.issuer,
+ contract: token.contract,
+ name: stellarAsset.asset,
+ code: stellarAsset.assetCode,
+ decimals: 7,
+ icon: '',
+ };
+ }
+ else {
+ return token
+ };
+};
+
+// Helper function to check if a token is a classic stellar asset
+export const isClassicStellarAssetService = async (
+ tokenAddress: string | undefined,
+ sorobanContext: SorobanContextType
+): Promise => {
+ if (!tokenAddress) return undefined;
+ return isClassicStellarAsset(tokenAddress, sorobanContext);
+};
+
+// Helper function to get token name
+export const getTokenNameService = async (
+ tokenAddress: string | undefined,
+ sorobanContext: SorobanContextType,
+
+): Promise => {
+ if (!tokenAddress) return undefined;
+ return getTokenName(tokenAddress, sorobanContext);
+};
+
+export const getAllTokensService = async (
+ sorobanContext: SorobanContextType,
+
+): Promise<{ tokens: TokenType[]; tokensAsMap: TokenMapType }> => {
+ const { activeNetwork } = sorobanContext;
+ if (!activeNetwork) {
+ return { tokens: [], tokensAsMap: {} };
+ }
+
+ const activeChain = passphraseToBackendNetworkName[activeNetwork].toLowerCase();
+
+ // Fetch API tokens
+ const apiTokensData = await fetchTokens(activeChain);
+
+ // If test net: apiTokens has strucuture [{network: 'mainnet': assets: []...}],
+ // for mainNet: {network: 'mainnet', assets: []}
+ const apiTokens = (apiTokensData && apiTokensData?.length) ?
+ apiTokensData?.find((tkn: { network: string; }) => tkn && tkn.
+ network === activeChain
+ )?.assets : apiTokensData?.assets;
+ // Get user-added tokens from localStorage
+ const userAddedTokensStr = localStorage.getItem(`userAddedTokens`) || '[]';
+ const userAddedTokens = (JSON.parse(userAddedTokensStr) ?? []) as tokensResponse[];
+ const userTokensFromActiveChain = userAddedTokens?.find(
+ (item: tokensResponse) => item?.network === activeChain,
+ )?.tokens ?? [];
+
+ // Combine tokens
+ const allTokens = [...apiTokens, ...userTokensFromActiveChain];
+ const allTokensAsMap = tokensToMap(allTokens);
+
+ return { tokens: allTokens, tokensAsMap: allTokensAsMap };
+};
\ No newline at end of file
diff --git a/src/services/tokens.ts b/src/services/tokens.ts
index 63955aaf..e7d91a84 100644
--- a/src/services/tokens.ts
+++ b/src/services/tokens.ts
@@ -23,3 +23,4 @@ export const fetchTokens = async (network: string) => {
return tokensList;
};
+
\ No newline at end of file
diff --git a/src/state/swap/hooks.tsx b/src/state/swap/hooks.tsx
index 168fde32..3056a399 100644
--- a/src/state/swap/hooks.tsx
+++ b/src/state/swap/hooks.tsx
@@ -5,7 +5,6 @@ import { relevantTokensType, tokenBalances } from 'hooks';
import { useToken } from 'hooks/tokens/useToken';
import { useBestTrade } from 'hooks/useBestTrade';
import useHorizonLoadAccount from 'hooks/useHorizonLoadAccount';
-import { TokenType } from 'interfaces';
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount';
import { ParsedQs } from 'qs';
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
@@ -15,6 +14,7 @@ import { useUserSlippageToleranceWithDefault } from 'state/user/hooks';
import { Field, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions';
import { SwapState } from './reducer';
import { DEFAULT_SLIPPAGE_INPUT_VALUE } from 'components/Settings/MaxSlippageSettings';
+import { TokenType } from 'interfaces';
export function useSwapActionHandlers(dispatch: React.Dispatch): {
onCurrencySelection: (field: Field, currency: TokenType) => void;
@@ -90,14 +90,16 @@ export function useDerivedSwapInfo(state: SwapState) {
} = state;
const { token: inputCurrency } = useToken(inputCurrencyId!);
+
//
const { token: outputCurrency } = useToken(outputCurrencyId!);
+
const recipientLookup = { address: '' }; //TODO: Use ENS useENS(recipient ?? undefined)
//
const to: string | null | undefined = account; //recipient === null ? account : recipientLookup.contract) ?? null
const tokensArray = useMemo(() => {
- return inputCurrency && outputCurrency ? [inputCurrency, outputCurrency] : undefined;
+ return inputCurrency && outputCurrency?.code ? [inputCurrency, outputCurrency] : undefined;
}, [inputCurrency, outputCurrency]);
const [relevantTokenBalances, setRelevantTokenBalances] = useState<
@@ -105,6 +107,7 @@ export function useDerivedSwapInfo(state: SwapState) {
>();
const { account: horizonAccount } = useHorizonLoadAccount();
+
useEffect(() => {
if (account) {
tokenBalances(account, tokensArray, sorobanContext, horizonAccount).then((res) => {
@@ -316,3 +319,4 @@ export function queryParametersToSwapState(parsedQs: ParsedQs): SwapState {
// return parsedSwapState
// }
+