From 80edd55b407a4177a42a3ec71cd9944d95508d04 Mon Sep 17 00:00:00 2001 From: Matias Poblete Date: Wed, 19 Jun 2024 09:01:41 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8Add=20horizon=20trade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useBestTrade.ts | 193 ++++++++++++++++++++++++++++++++++++- src/state/routing/types.ts | 7 ++ src/state/swap/hooks.tsx | 1 + 3 files changed, 198 insertions(+), 3 deletions(-) diff --git a/src/hooks/useBestTrade.ts b/src/hooks/useBestTrade.ts index 54bf5678..ec7321e0 100644 --- a/src/hooks/useBestTrade.ts +++ b/src/hooks/useBestTrade.ts @@ -2,8 +2,12 @@ import { useRouterSDK } from 'functions/generateRoute'; import { CurrencyAmount, TokenType } from 'interfaces'; import { useEffect, useMemo, useState } from 'react'; import { TradeType as SdkTradeType } from 'soroswap-router-sdk'; -import { InterfaceTrade, QuoteState, TradeState, TradeType } from 'state/routing/types'; +import { InterfaceTrade, QuoteState, TradeState, TradeType, TransactionType } from 'state/routing/types'; import useSWR from 'swr'; +import {Asset} from '@stellar/stellar-sdk' +import { SorobanContextType } from '@soroban-react/core'; +import BigNumber from 'bignumber.js'; +import { ServerApi } from '@stellar/stellar-sdk/lib/horizon'; const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, @@ -11,6 +15,129 @@ const TRADE_NOT_FOUND = { } as const; const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const; +const getClassicAsset = (currency: TokenType) => { + if (!currency) return + if (currency?.code === 'XLM') { + const nativeAsset = Asset.native() + return nativeAsset + } + if (!currency.issuer) { + throw new Error(`Can't convert ${currency.code} to stellar classic asset`) + } + const asset = new Asset(currency.code, currency.issuer) + return asset +} + +const getAmount = (amount: string) => { + return new BigNumber(amount).dividedBy(10000000).toString() +} + +const parseHorizonResult = (payload: ServerApi.PaymentPathRecord, tradeType: TradeType) =>{ + console.log('🔴🔴Object', payload) + const currecnyIn: TokenType = payload.source_asset_type == 'native' ? { + code: 'XLM', + contract: '', + } : { + code: payload.source_asset_code, + issuer: payload.source_asset_issuer, + contract: `${payload.source_asset_code}:${payload.source_asset_issuer}` + } + const currencyOut: TokenType = payload.destination_asset_type == 'native' ? { + code: 'XLM', + contract: '', + } : { + code: payload.destination_asset_code, + issuer: payload.destination_asset_issuer, + contract: `${payload.destination_asset_code}:${payload.destination_asset_issuer}` + } + const inputAmount: CurrencyAmount = { + currency: currecnyIn, + value: new BigNumber(payload.source_amount).multipliedBy(10000000).toString() + } + const outputAmount: CurrencyAmount = { + currency: currencyOut, + value: new BigNumber(payload.destination_amount).multipliedBy(10000000).toString() + } + const path = [currecnyIn, ...payload.path, currencyOut] + const parsedResult = { + inputAmount: inputAmount, + outputAmount: outputAmount, + tradeType: tradeType, + path: path, + priceImpact: undefined, + transctionType: TransactionType.STELLAR_CLASSIC, + } + return parsedResult +} + +function getHorizonBestPath( + payload: any, + sorobanContext: SorobanContextType +) { + if (!payload.assetFrom || !payload.assetTo || !payload.amount || !sorobanContext) { + return; + } + + const { serverHorizon, activeChain } = sorobanContext; + if (!serverHorizon || !activeChain) { + console.log('no serverHorizon or activeChain'); + } + + const args = { + assetFrom: getClassicAsset(payload.assetFrom), + assetTo: getClassicAsset(payload.assetTo), + amount: getAmount(payload.amount) + }; + + if (payload.tradeType === TradeType.EXACT_INPUT) { + try { + const send = serverHorizon?.strictSendPaths( + args.assetFrom!, + args?.amount, + [args.assetTo!] + ).call().then((res) => { + return res.records; + }); + return send?.then(res => { + return res.reduce((maxObj, obj) => { + console.log(maxObj) + if (obj.destination_amount > maxObj.destination_amount) { + return obj; + } else { + return maxObj; + } + }); + }); + } catch (error) { + console.log(error); + } + } + + if (payload.tradeType === TradeType.EXACT_OUTPUT) { + try { + const receive = serverHorizon?.strictReceivePaths( + [args.assetFrom!], + args.assetTo!, + args?.amount, + ).call().then((res) => { + return res.records; + }); + + return receive?.then(res => { + return res.reduce((maxObj, obj) => { + if (obj.destination_amount > maxObj.destination_amount) { + return obj; + } else { + return maxObj; + } + }); + }); + } catch (error) { + console.log(error); + } + } +} + /** * Returns the best v2+v3 trade for a desired swap. * @param tradeType whether the swap is an exact in/out @@ -18,6 +145,7 @@ const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const; * @param otherCurrency the desired output/payment currency */ export function useBestTrade( + sorobanContext: SorobanContextType, tradeType: TradeType, amountSpecified?: CurrencyAmount, otherCurrency?: TokenType, @@ -28,7 +156,64 @@ export function useBestTrade( resetRouterSdkCache: () => void; } { const { generateRoute, resetRouterSdkCache, maxHops } = useRouterSDK(); + /** + * Custom hook that fetches the best trade based on the specified amount and trade type. + * + * @param {object} amountSpecified - The specified amount for the trade. + * @param {object} otherCurrency - The other currency involved in the trade. + * @param {number} maxHops - The maximum number of hops allowed for the trade. + * @returns {object} - The data, isLoading, and isValidating values from the SWR hook. + */ + const payload = { + assetFrom: amountSpecified?.currency, + assetTo: otherCurrency, + amount: amountSpecified?.value, + tradeType: tradeType, + } + const [horizonPath, setHorizonPath] = useState('') + const [soroswapPath, setSoroswapPath] = useState('') + + const calculatePaths = async () => { + if(!amountSpecified || !otherCurrency) return; + await getHorizonBestPath(payload, sorobanContext)?.then(res=>{ + const parsedResult = parseHorizonResult(res, tradeType) + setHorizonPath(parsedResult); + }) + await generateRoute({ + amountTokenAddress: amountSpecified?.currency?.contract!, + quoteTokenAddress: otherCurrency?.contract!, + amount: amountSpecified?.value!, + tradeType: tradeType === TradeType.EXACT_INPUT ? SdkTradeType.EXACT_INPUT : SdkTradeType.EXACT_OUTPUT })?.then(res=>{ + const updatedResult = { + ...res, + transactionType: TransactionType.SOROBAN + }; + setSoroswapPath(updatedResult); + }) + } + + const chooseBestPath = () => { + if(!horizonPath || !soroswapPath) return; + if(horizonPath.destination_amount > soroswapPath.trade.amountOutMin) { + return horizonPath + } else { + return soroswapPath + } + } + useEffect(() => { + calculatePaths() + }, [amountSpecified, otherCurrency, tradeType]) + + useEffect(() => { + if(!amountSpecified || !otherCurrency) return; + const bestPath = chooseBestPath() + console.log(bestPath) + console.log('🔵', horizonPath) + }, [horizonPath, soroswapPath]) + + + // Fetch or save the route in cache const { data, isLoading: isLoadingSWR, @@ -59,9 +244,9 @@ export function useBestTrade( refreshInterval: 0, }, ); - const isLoading = isLoadingSWR || isValidating; + //Define the input and output currency based on the trade type const [currencyIn, currencyOut]: [TokenType | undefined, TokenType | undefined] = useMemo( () => tradeType === TradeType.EXACT_INPUT @@ -118,6 +303,7 @@ export function useBestTrade( } }, [data, currencyIn, currencyOut, tradeType]); + // Create the trade object const trade: InterfaceTrade = useMemo(() => { return { inputAmount, @@ -127,9 +313,10 @@ export function useBestTrade( tradeType: tradeType, rawRoute: data, priceImpact: data?.priceImpact, + transctionType: TransactionType.SOROBAN, }; }, [expectedAmount, inputAmount, outputAmount, tradeType, data]); - + console.log(trade) /* If the pairAddress or the trades chenges, we upgrade the tradeResult trade can change by changing the amounts, as well as the independent value diff --git a/src/state/routing/types.ts b/src/state/routing/types.ts index c4f1a9c0..0cf96fcb 100644 --- a/src/state/routing/types.ts +++ b/src/state/routing/types.ts @@ -232,6 +232,12 @@ export enum TradeType { EXACT_INPUT = 'EXACT_INPUT', EXACT_OUTPUT = 'EXACT_OUTPUT', } + +export enum TransactionType { + SOROBAN = 'SOROBAN', + STELLAR_CLASSIC = 'STELLAR_CLASSIC' +} + export type InterfaceTrade = { inputAmount: CurrencyAmount | undefined; outputAmount: CurrencyAmount | undefined; @@ -239,6 +245,7 @@ export type InterfaceTrade = { path: string[] | undefined; priceImpact?: Percent | undefined; [x: string]: any; + transctionType: TransactionType; }; export enum QuoteState { diff --git a/src/state/swap/hooks.tsx b/src/state/swap/hooks.tsx index 33e2ad30..08098621 100644 --- a/src/state/swap/hooks.tsx +++ b/src/state/swap/hooks.tsx @@ -124,6 +124,7 @@ export function useDerivedSwapInfo(state: SwapState) { ); const trade = useBestTrade( + sorobanContext, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, parsedAmount, (isExactIn ? outputCurrency : inputCurrency) ?? undefined,