From 66de463f88a059da460de05262abae81fc1f83b1 Mon Sep 17 00:00:00 2001 From: Harshal Date: Thu, 15 Aug 2024 09:41:54 +0530 Subject: [PATCH] Harshal/v2 split routing (#350) * single hop split routing * wip * wip * remove duplicated pools line * fix infinite loop * multihop and remove consoles * fix page break * console single trade * fix wrong output * log txn * fix type error * wip * increase splits * fix tradedetails dropdown * add loading state * fix big int amount issue * maxhops 2 * no split txn works * fix exact out * fix tradedetails modal * add split * fix swap details dropdown * reset state on currency or input change * fix fetching button state * add v1routes * dont show price until v2 is fetched * 2 hops * update data on the rewards page (#342) * CI: bumps version to v7.2.8 * don't display quote for unsufficient balance (#341) * CI: bumps version to v7.2.9 * fix (#345) * CI: bumps version to v7.2.10 * claim date update (#347) * CI: bumps version to v7.2.11 * vault mobile (#344) * vault landing page mobile optimised * vault details page mobile responsive * vault mobile corrections * misc vault mobile fix * CI: bumps version to v8.0.0 * vault mobile gap fix (#351) * CI: bumps version to v8.0.1 * added link to docs (#354) * CI: bumps version to v8.0.2 * feat: adding pfp (#195) * feat: adding pfp * upgrading starknet react * added proper number type to price (#355) * CI: bumps version to v8.0.3 * add twitter slink * fix twitter:image tag * preview (#368) * preview * update url * update player height * fix link * fix dimentions * decrease size * upldate slinks dimensions * add overflow auto --------- Co-authored-by: Vinay Singh <122733374+vnaysngh-mudrex@users.noreply.github.com> * update site url and twiiter name (#370) * CI: bumps version to v8.1.0 * claim august 01 (#371) * CI: bumps version to v8.1.1 * added allocation error handling (#372) * CI: bumps version to v8.1.2 * block identifier added to claimed rewards contract call to fetch the latest claimed amount (#374) * CI: bumps version to v8.1.3 * fix tvl and user deposit calculations (#375) * CI: bumps version to v8.1.4 * Update VaultWithdrawInput.tsx (#376) * CI: bumps version to v8.1.5 * remove leading zeros (#377) * CI: bumps version to v8.1.6 * hide global data for now (#380) * CI: bumps version to v8.1.7 * fix deposit and withdraw issues (#383) * CI: bumps version to v8.1.8 * CI: bumps version to v8.1.9 * 09 august claim date (#385) * CI: bumps version to v8.1.10 * claim 16 august (#387) * CI: bumps version to v8.1.11 * Revert "feat: adding pfp (#195)" (#390) This reverts commit 28387776a70670bdd4657d81d4bcbea9f40852c6. * CI: bumps version to v9.0.0 * wip * fix tradedetails dropdown * fix tradedetails modal * stop refetch on tab focus * fix singlehop --------- Co-authored-by: iamoskvin <44796732+iamoskvin@users.noreply.github.com> Co-authored-by: Automated Version Bump Co-authored-by: Vinay Singh <122733374+vnaysngh-mudrex@users.noreply.github.com> Co-authored-by: Nico --- package.json | 1 + .../RoutingDiagram/RoutingDiagram.tsx | 17 +- src/hooks/useAllV3Routes.ts | 1 - src/hooks/useBestV3Trade.ts | 870 ++++++++++-------- src/hooks/usePools.ts | 72 +- src/pages/Swap/getBestSwapRoute.ts | 312 +++++++ src/pages/Swap/index.tsx | 333 +++---- src/state/swap/hooks.tsx | 139 ++- yarn.lock | 21 + 9 files changed, 1147 insertions(+), 619 deletions(-) create mode 100644 src/pages/Swap/getBestSwapRoute.ts diff --git a/package.json b/package.json index c5fdf24f..a4239c36 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,7 @@ "@graphql-codegen/typescript-operations": "^3.0.2", "@graphql-codegen/typescript-react-apollo": "^3.3.7", "@graphql-codegen/typescript-resolvers": "^3.2.1", + "@harshalmaniya/jediswap-sdk-v3": "^19.1.1", "@jediswap/sdk": "^2.3.5", "@juggle/resize-observer": "^3.4.0", "@lingui/core": "^4.3.0", diff --git a/src/components/RoutingDiagram/RoutingDiagram.tsx b/src/components/RoutingDiagram/RoutingDiagram.tsx index 75ebd66d..4a31dabc 100644 --- a/src/components/RoutingDiagram/RoutingDiagram.tsx +++ b/src/components/RoutingDiagram/RoutingDiagram.tsx @@ -16,6 +16,7 @@ import { Z_INDEX } from 'theme/zIndex' import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries' import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg' import { MouseoverTooltip, TooltipSize } from '../Tooltip' +import { useFormatter } from 'utils/formatNumbers' const Wrapper = styled(Box)` align-items: center; @@ -107,19 +108,19 @@ export default function RoutingDiagram({ }) { const tokenIn = useTokenInfoFromActiveList(currencyIn) const tokenOut = useTokenInfoFromActiveList(currencyOut) + const { formatPercent } = useFormatter() return ( {routes.map((entry, index) => ( - {index === 0 && ( - - - - {routes?.[0].type} 100% - - - )} + + + + {entry.type} + {formatPercent(entry.percent)}% + + diff --git a/src/hooks/useAllV3Routes.ts b/src/hooks/useAllV3Routes.ts index 8cb15a87..88385800 100644 --- a/src/hooks/useAllV3Routes.ts +++ b/src/hooks/useAllV3Routes.ts @@ -56,7 +56,6 @@ export function useAllV3Routes( ): { loading: boolean; routes: any[] } { const { chainId } = useAccountDetails() const { pools, loading: poolsLoading } = useV3SwapPools(allPools, currencyIn, currencyOut) - // const [singleHopOnly] = useUserSingleHopOnly() const singleHopOnly = false diff --git a/src/hooks/useBestV3Trade.ts b/src/hooks/useBestV3Trade.ts index 81d98eb2..2d26f862 100644 --- a/src/hooks/useBestV3Trade.ts +++ b/src/hooks/useBestV3Trade.ts @@ -1,33 +1,26 @@ -import { Token, Currency, CurrencyAmount, TokenAmount, TradeType } from '@vnaysn/jediswap-sdk-core' -import { encodeRouteToPath, Pool, Route, Trade } from '@vnaysn/jediswap-sdk-v3' -import { BigNumber } from 'ethers' +import { Token, Currency, TradeType, CurrencyAmount } from '@vnaysn/jediswap-sdk-core' +import { Pool, Route } from '@vnaysn/jediswap-sdk-v3' +import { Trade } from '@harshalmaniya/jediswap-sdk-v3' import { useEffect, useMemo, useState } from 'react' -import { useSingleContractMultipleData } from '../state/multicall/hooks' import { useAllV3Routes } from './useAllV3Routes' -import SWAP_QUOTER_ABI from 'contracts/swapquoter/abi.json' import { DEFAULT_CHAIN_ID, SWAP_ROUTER_ADDRESS_V2 } from 'constants/tokens' -import { - BigNumberish, - BlockNumber, - CallData, - Invocation, - InvocationsDetails, - RpcProvider, - TransactionType, - cairo, - encode, - num, -} from 'starknet' +import { BigNumberish, CallData, TransactionType, cairo, num } from 'starknet' import { TradeState } from 'state/routing/types' -import { ec, hash, json, Contract, WeierstrassSignatureType } from 'starknet' +import { ec, hash, WeierstrassSignatureType } from 'starknet' import { useAccountDetails } from './starknet-react' -import { useApprovalCall } from './useApproveCall' -import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import useTransactionDeadline from './useTransactionDeadline' import { useQuery } from 'react-query' -// import { useV3Quoter } from './useContract' -import ERC20_ABI from 'abis/erc20.json' import { providerInstance } from 'utils/getLibrary' +import { getBestSwapRoute } from 'pages/Swap/getBestSwapRoute' +import { getPoolAddress } from './usePools' +import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +function fromUint256ToNumber(uint256: any) { + // Assuming uint256 is an object with 'high' and 'low' properties + const { high } = uint256 + return high +} export enum V3TradeState { LOADING, @@ -37,16 +30,6 @@ export enum V3TradeState { SYNCING, } -// const useResults = (promise: any) => { -// const [data, setData] = useState() -// const results = useMemo(() => { -// if (!promise) return -// promise.then((res: any) => setData(res)) -// }, [promise]) - -// // return data -// } - /** * Returns the best v3 trade for a desired exact input swap * @param amountIn the amount to swap in @@ -54,11 +37,15 @@ export enum V3TradeState { */ export function useBestV3TradeExactIn( allPools: string[], - amountIn?: any, - currencyOut?: Currency + amountIns?: any[], + currencyOut?: Currency, + currencyIn?: Currency, + percents?: number[], + amountIn?: any ): { state: TradeState; trade: any | null } { - const { routes, loading: routesLoading } = useAllV3Routes(allPools, amountIn?.currency, currencyOut) - // State to store the resolved result + const { formatCurrencyAmount } = useFormatter() + + const { routes, loading: routesLoading } = useAllV3Routes(allPools, currencyIn, currencyOut) if (!routes) return { @@ -70,101 +57,112 @@ export function useBestV3TradeExactIn( const swapRouterAddress = SWAP_ROUTER_ADDRESS_V2[chainId ?? DEFAULT_CHAIN_ID] const deadline = useTransactionDeadline() + const [bestRoute, setBestRoute] = useState(null) + const [isFetching, setIsFetching] = useState(false) + + useEffect(() => { + setBestRoute(null) + }, [amountIn, currencyOut, currencyIn, routesLoading]) + const quoteExactInInputs = useMemo(() => { - if (routesLoading || !amountIn || !address || !routes || !routes.length || !deadline) return - return routes.map((route: Route) => { - const isRouteSingleHop = route.pools.length === 1 - - //multi hop - if (!isRouteSingleHop) { - const firstInputToken: Token = route.input.wrapped - //create path - const { path } = route.pools.reduce( - ( - { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, - pool: Pool, - index - ): { inputToken: Token; path: (string | number)[]; types: string[] } => { - const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 - if (index === 0) { - return { - inputToken: outputToken, - types: ['address', 'address', 'uint24'], - path: [inputToken.address, outputToken.address, pool.fee], - } - } else { - return { - inputToken: outputToken, - types: [...types, 'address', 'address', 'uint24'], - path: [...path, inputToken.address, outputToken.address, pool.fee], - } + if (routesLoading || !amountIns || !address || !routes || !routes.length || !deadline) return + return amountIns + .map((amount) => { + return routes.map((route: Route) => { + const isRouteSingleHop = route.pools.length === 1 + + //multi hop + if (!isRouteSingleHop) { + const firstInputToken: Token = route.input.wrapped + //create path + const { path } = route.pools.reduce( + ( + { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, + pool: Pool, + index + ): { inputToken: Token; path: (string | number)[]; types: string[] } => { + const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 + if (index === 0) { + return { + inputToken: outputToken, + types: ['address', 'address', 'uint24'], + path: [inputToken.address, outputToken.address, pool.fee], + } + } else { + return { + inputToken: outputToken, + types: [...types, 'address', 'address', 'uint24'], + path: [...path, inputToken.address, outputToken.address, pool.fee], + } + } + }, + { inputToken: firstInputToken, path: [], types: [] } + ) + + const exactInputParams = { + path, + recipient: address, + deadline: cairo.felt(deadline.toString()), + amount_in: amount ? cairo.uint256(`0x${amount.raw.toString(16)}`) : 0, + amount_out_minimum: cairo.uint256(0), } - }, - { inputToken: firstInputToken, path: [], types: [] } - ) - - const exactInputParams = { - path, - recipient: address, - deadline: cairo.felt(deadline.toString()), - amount_in: amountIn ? cairo.uint256(`0x${amountIn.raw.toString(16)}`) : 0, - amount_out_minimum: cairo.uint256(0), - } - //exact input - const inputSelector = { - contract_address: swapRouterAddress, - entry_point: hash.getSelectorFromName('exact_input'), - } - const input_call_data_length = { input_call_data_length: path.length + 7 } + //exact input + const inputSelector = { + contract_address: swapRouterAddress, + entry_point: hash.getSelectorFromName('exact_input'), + } + const input_call_data_length = { input_call_data_length: path.length + 7 } - const call = { - calldata: exactInputParams, - route, - } + const call = { + calldata: exactInputParams, + route, + } - return { call, inputSelector, input_call_data_length } - } else { - //single hop - const isCurrencyInFirst = amountIn?.currency?.address === route.pools[0].token0.address - const sortedTokens = isCurrencyInFirst - ? [route.pools[0].token0.address, route.pools[0].token1.address] - : [route.pools[0].token1.address, route.pools[0].token0.address] - const exactInputSingleParams = { - token_in: sortedTokens[0], - token_out: sortedTokens[1], - fee: route.pools[0].fee, - recipient: address, - deadline: cairo.felt(deadline.toString()), - amount_in: amountIn ? cairo.uint256(`0x${amountIn.raw.toString(16)}`) : 0, - amount_out_minimum: cairo.uint256(0), - sqrt_price_limit_X96: cairo.uint256(0), - } + return { call, inputSelector, input_call_data_length } + } else { + //single hop + const isCurrencyInFirst = amountIn?.currency?.address === route.pools[0].token0.address + const sortedTokens = isCurrencyInFirst + ? [route.pools[0].token0.address, route.pools[0].token1.address] + : [route.pools[0].token1.address, route.pools[0].token0.address] + const exactInputSingleParams = { + token_in: sortedTokens[0], + token_out: sortedTokens[1], + fee: route.pools[0].fee, + recipient: address, + deadline: cairo.felt(deadline.toString()), + amount_in: !!amount ? cairo.uint256(`0x${amount.raw.toString(16)}`) : 0, + amount_out_minimum: cairo.uint256(0), + sqrt_price_limit_X96: cairo.uint256(0), + } - //exact input - const inputSelector = { - contract_address: swapRouterAddress, - entry_point: hash.getSelectorFromName('exact_input_single'), - } - const input_call_data_length = { input_call_data_length: '0xb' } + //exact input + const inputSelector = { + contract_address: swapRouterAddress, + entry_point: hash.getSelectorFromName('exact_input_single'), + } + const input_call_data_length = { input_call_data_length: '0xb' } - const call = { - calldata: exactInputSingleParams, - route, - } + const call = { + calldata: exactInputSingleParams, + route, + } - return { call, inputSelector, input_call_data_length } - } - }) + return { call, inputSelector, input_call_data_length } + } + }) + }) + .flat(1) }, [routes, amountIn, address, currencyOut, deadline]) const approveSelector = useMemo(() => { - if (!amountIn) return + if (!amountIns) return return { - currency_address: amountIn?.currency?.address, + currency_address: amountIns[0]?.currency?.address, selector: hash.getSelectorFromName('approve'), } - }, [amountIn]) + }, [amountIns]) const totalTx = { totalTx: '0x2', @@ -200,8 +198,9 @@ export function useBestV3TradeExactIn( const message: BigNumberish[] = [1, 128, 18, 14] const msgHash = hash.computeHashOnElements(message) const signature: WeierstrassSignatureType = ec.starkCurve.sign(msgHash, privateKey) - const amountOutResults = useQuery({ + useQuery({ queryKey: ['get_simulation', address, amountIn, nonce_results?.data, currencyOut?.symbol, contract_version?.data], + refetchOnWindowFocus: false, queryFn: async () => { if ( !address || @@ -214,109 +213,138 @@ export function useBestV3TradeExactIn( !deadline ) return - const nonce = Number(nonce_results.data) - const isWalletCairoVersionGreaterThanZero = Boolean(contract_version.data) - const callPromises = quoteExactInInputs.map(async ({ call, input_call_data_length, inputSelector }) => { - const provider = providerInstance(chainId) - if (!provider) return - const payloadForContractType1 = { - contractAddress: address, - calldata: CallData.compile({ - ...totalTx, - ...approveSelector, - ...approve_call_data_length, - ...approve_call_data, - ...inputSelector, - ...input_call_data_length, - ...call.calldata, - }), - } - const payloadForContractType0 = { - contractAddress: address, - calldata: CallData.compile({ - ...totalTx, - ...approveSelector, - ...{ approve_offset: '0x0' }, - ...approve_call_data_length, - ...inputSelector, - ...{ input_offset: approve_call_data_length }, - ...input_call_data_length, - ...{ total_call_data_length: '0xe' }, - ...approve_call_data, - ...call.calldata, - }), - } - const payloadBasedOnCairoVersion = isWalletCairoVersionGreaterThanZero ? payloadForContractType1 : payloadForContractType0 - const response = provider.simulateTransaction( - [{ type: TransactionType.INVOKE, ...payloadBasedOnCairoVersion, signature, nonce }], - { - skipValidate: true, + try { + setIsFetching(true) + const nonce = Number(nonce_results.data) + const isWalletCairoVersionGreaterThanZero = Boolean(contract_version.data) + const callPromises = quoteExactInInputs.map(async ({ call, input_call_data_length, inputSelector }, i) => { + const provider = providerInstance(chainId) + if (!provider) return + + const payloadForContractType1 = { + contractAddress: address, + calldata: CallData.compile({ + ...totalTx, + ...approveSelector, + ...approve_call_data_length, + ...approve_call_data, + ...inputSelector, + ...input_call_data_length, + ...call.calldata, + }), } - ) - - return response - }) - const settledResults = await Promise.allSettled(callPromises as any) - const settledResultsWithRoute = settledResults.map((result, i) => ({ ...result, route: routes[i] })) + const payloadForContractType0 = { + contractAddress: address, + calldata: CallData.compile({ + ...totalTx, + ...approveSelector, + ...{ approve_offset: '0x0' }, + ...approve_call_data_length, + ...inputSelector, + ...{ input_offset: approve_call_data_length }, + ...input_call_data_length, + ...{ total_call_data_length: '0xe' }, + ...approve_call_data, + ...call.calldata, + }), + } + const payloadBasedOnCairoVersion = isWalletCairoVersionGreaterThanZero + ? payloadForContractType1 + : payloadForContractType0 + + const response = provider.simulateTransaction( + [{ type: TransactionType.INVOKE, ...payloadBasedOnCairoVersion, signature, nonce }], + { + skipValidate: true, + } + ) - const resolvedResults = settledResultsWithRoute - .filter((result) => result.status === 'fulfilled') - .map((result: any) => { - const response = { ...result.value, route: result.route } return response }) - return resolvedResults - }, - onSuccess: (data) => { - // Handle the successful data fetching here if needed - }, - }) - - function fromUint256ToNumber(uint256: any) { - // Assuming uint256 is an object with 'high' and 'low' properties - const { high } = uint256 - return high - } + const settledResults = await Promise.allSettled(callPromises as any) + const settledResultsWithRoute = settledResults.map((result, i) => { + if (!amountIns || !percents) return + const amountInsLength = amountIns.length + const routesLength = routes.length - const filteredAmountOutResults = useMemo(() => { - if (!amountOutResults) return - const data = amountOutResults?.data - - if (!data) return - const subRoutesArray = data.map((subArray) => ({ ...subArray[0], route: subArray.route })) - const bestRouteResults = { bestRoute: null, amountOut: null } - - const { bestRoute, amountOut } = subRoutesArray - .filter((result: any) => result?.transaction_trace?.execute_invocation?.result) - .reduce((currentBest: any, result: any, i: any) => { - const selected_tx_result = result?.transaction_trace?.execute_invocation?.result - const value = selected_tx_result[selected_tx_result.length - 2] - const amountOut = fromUint256ToNumber({ high: value }) - if (!result) return currentBest - if (currentBest.amountOut === null) { - return { - bestRoute: result?.route, - amountOut, - } - } else if (Number(cairo.felt(currentBest.amountOut)) < Number(cairo.felt(amountOut))) { + const routeIndex = i % routesLength return { - bestRoute: result?.route, - amountOut, + ...result, + route: routes[routeIndex], } - } + }) - return currentBest - }, bestRouteResults) + const resolvedResults = settledResultsWithRoute + .filter((result) => result?.status === 'fulfilled') + .map((result: any) => { + const response = { ...result.value, route: result.route } + return response + }) + + return resolvedResults + } catch (e) { + console.error(e) + return undefined + } + }, - return { bestRoute, amountOut } - }, [amountOutResults]) + onSuccess: async (data) => { + try { + if (data && currencyOut && amountIns && currencyIn && percents) { + const validQuotes = data + .filter((result: any) => { + return result[0].transaction_trace.execute_invocation.result + }) + .map((result: any) => { + const selected_tx_result = result[0].transaction_trace.execute_invocation.result + const value = selected_tx_result[selected_tx_result.length - 2] + const amountOut = fromUint256ToNumber({ high: value }) + + const selected_call_data = result[0].transaction_trace.execute_invocation.calldata + const isSingleHop = result.route.pools.length === 1 + const inputIndex = isSingleHop ? selected_call_data.length - 6 : selected_call_data.length - 4 + const inputValue = selected_call_data[inputIndex] + const amountIn = fromUint256ToNumber({ high: inputValue }) + + const amountInIndex = amountIns.findIndex((amount) => { + return ( + formatCurrencyAmount({ + amount: amount, + type: NumberType.SwapTradeAmount, + placeholder: '', + }) == + formatCurrencyAmount({ + amount: CurrencyAmount.fromRawAmount(currencyIn!, num.hexToDecimalString(amountIn)), + type: NumberType.SwapTradeAmount, + placeholder: '', + }) + ) + }) - const { bestRoute, amountOut } = useMemo(() => { - if (!filteredAmountOutResults) return { bestRoute: null, amountOut: null } - return { bestRoute: filteredAmountOutResults.bestRoute, amountOut: filteredAmountOutResults.amountOut } - }, [filteredAmountOutResults]) + return { + ...result[0], + route: result.route, + inputAmount: CurrencyAmount.fromRawAmount(currencyIn, num.hexToDecimalString(amountIn)), + percent: percents[amountInIndex], + poolAddresses: result.route.pools.map((pool: Pool) => { + return getPoolAddress(pool.token0, pool.token1, pool.fee, chainId) + }), + outputAmount: CurrencyAmount.fromRawAmount(currencyOut, num.hexToDecimalString(amountOut)), + } + }) + console.log(validQuotes, 'validQuotes') + const route = await getBestSwapRoute(validQuotes, TradeType.EXACT_INPUT, percents ?? []) + setBestRoute(route) + } + } catch (e) { + console.error(e) + } finally { + setIsFetching(false) + } + }, + }) return useMemo(() => { if (!routes.length) { @@ -325,8 +353,7 @@ export function useBestV3TradeExactIn( trade: null, } } - - if (!amountIn || !currencyOut || !filteredAmountOutResults) { + if (!amountIn || !currencyOut || isFetching) { return { state: TradeState.INVALID, trade: null, @@ -340,25 +367,18 @@ export function useBestV3TradeExactIn( } } - if (!bestRoute || !amountOut) { + if (!bestRoute || !amountIns) { return { state: TradeState.NO_ROUTE_FOUND, trade: null, } } - // const isSyncing = quotesResults.some(({ syncing }) => syncing) - return { state: TradeState.VALID, - trade: Trade.createUncheckedTrade({ - route: bestRoute, - tradeType: TradeType.EXACT_INPUT, - inputAmount: amountIn, - outputAmount: CurrencyAmount.fromRawAmount(currencyOut, num.hexToDecimalString(amountOut)), - }), + trade: Trade.createUncheckedTradeWithMultipleRoutes({ routes: bestRoute, tradeType: TradeType.EXACT_INPUT }), } - }, [amountIn, currencyOut, filteredAmountOutResults, routes, routesLoading]) + }, [amountIns, currencyOut, routes, routesLoading]) } /** @@ -368,104 +388,120 @@ export function useBestV3TradeExactIn( */ export function useBestV3TradeExactOut( allPools: string[], + amountOuts?: any[], currencyIn?: Currency, + currencyOut?: Currency, + percents?: number[], amountOut?: any ): { state: TradeState; trade: any | null } { // : { state: V3TradeState; trade: any | null } // const quoter = useV3Quoter() - const { routes, loading: routesLoading } = useAllV3Routes(allPools, currencyIn, amountOut?.currency) + const { routes, loading: routesLoading } = useAllV3Routes(allPools, currencyIn, currencyOut) const { address, account, chainId, connector } = useAccountDetails() const swapRouterAddress = SWAP_ROUTER_ADDRESS_V2[chainId ?? DEFAULT_CHAIN_ID] const deadline = useTransactionDeadline() + const { formatCurrencyAmount } = useFormatter() + + const [bestRoute, setBestRoute] = useState(null) + const [isFetching, setIsFetching] = useState(false) + + useEffect(() => { + setBestRoute(null) + }, [amountOut, currencyOut, currencyIn, routesLoading]) const quoteExactOutInputs = useMemo(() => { - if (routesLoading || !amountOut || !address || !routes || !routes.length || !deadline) return - return routes.map((route: Route) => { - const isRouteSingleHop = route.pools.length === 1 - - //multi hop - if (!isRouteSingleHop) { - const firstInputToken: Token = route.input.wrapped - //create path - const { path } = route.pools.reduce( - ( - { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, - pool: Pool, - index - ): { inputToken: Token; path: (string | number)[]; types: string[] } => { - const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 - if (index === 0) { - return { - inputToken: outputToken, - types: ['uint24', 'address', 'address'], - path: [pool.fee, inputToken.address, outputToken.address], - } - } else { - return { - inputToken: outputToken, - types: [...types, 'uint24', 'address', 'address'], - path: [...path, pool.fee, inputToken.address, outputToken.address], - } + if (routesLoading || !amountOut || !amountOuts || !address || !routes || !routes.length || !deadline) return + + return amountOuts + .map((amount) => + routes.map((route: Route) => { + const isRouteSingleHop = route.pools.length === 1 + + //multi hop + if (!isRouteSingleHop) { + const firstInputToken: Token = route.input.wrapped + //create path + const { path } = route.pools.reduce( + ( + { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, + pool: Pool, + index + ): { inputToken: Token; path: (string | number)[]; types: string[] } => { + const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 + if (index === 0) { + return { + inputToken: outputToken, + types: ['uint24', 'address', 'address'], + path: [pool.fee, inputToken.address, outputToken.address], + } + } else { + return { + inputToken: outputToken, + types: [...types, 'uint24', 'address', 'address'], + path: [...path, pool.fee, inputToken.address, outputToken.address], + } + } + }, + { inputToken: firstInputToken, path: [], types: [] } + ) + + const reversePath = path.reverse() + + const exactOutputParams = { + path: reversePath, + recipient: address, + deadline: cairo.felt(deadline.toString()), + amount_out: amount ? cairo.uint256(`0x${amount.raw.toString(16)}`) : 0, + amount_in_maximum: cairo.uint256(2 ** 128), } - }, - { inputToken: firstInputToken, path: [], types: [] } - ) - - const reversePath = path.reverse() - - const exactOutputParams = { - path: reversePath, - recipient: address, - deadline: cairo.felt(deadline.toString()), - amount_out: amountOut ? cairo.uint256(`0x${amountOut.raw.toString(16)}`) : 0, - amount_in_maximum: cairo.uint256(2 ** 128), - } - //exact input - const outputSelector = { - contract_address: swapRouterAddress, - entry_point: hash.getSelectorFromName('exact_output'), - } - const output_call_data_length = { output_call_data_length: path.length + 7 } + //exact input + const outputSelector = { + contract_address: swapRouterAddress, + entry_point: hash.getSelectorFromName('exact_output'), + } + const output_call_data_length = { output_call_data_length: path.length + 7 } - const call = { - calldata: exactOutputParams, - route, - } + const call = { + calldata: exactOutputParams, + route, + } - return { call, outputSelector, output_call_data_length } - } else { - //single hop - const isCurrencyInFirst = amountOut?.currency?.address === route.pools[0].token0.address - const sortedTokens = isCurrencyInFirst - ? [route.pools[0].token0.address, route.pools[0].token1.address] - : [route.pools[0].token1.address, route.pools[0].token0.address] - const exactOutputSingleParams = { - token_in: sortedTokens[1], - token_out: sortedTokens[0], - fee: route.pools[0].fee, - recipient: address, - deadline: cairo.felt(deadline.toString()), - amount_out: amountOut ? cairo.uint256(`0x${amountOut.raw.toString(16)}`) : 0, - amount_in_maximum: cairo.uint256(2 ** 128), - sqrt_price_limit_X96: cairo.uint256(0), - } + return { call, outputSelector, output_call_data_length } + } else { + //single hop + const isCurrencyInFirst = amountOut?.currency?.address === route.pools[0].token0.address + const sortedTokens = isCurrencyInFirst + ? [route.pools[0].token0.address, route.pools[0].token1.address] + : [route.pools[0].token1.address, route.pools[0].token0.address] + const exactOutputSingleParams = { + token_in: sortedTokens[1], + token_out: sortedTokens[0], + fee: route.pools[0].fee, + recipient: address, + deadline: cairo.felt(deadline.toString()), + amount_out: amount ? cairo.uint256(`0x${amount.raw.toString(16)}`) : 0, + amount_in_maximum: cairo.uint256(2 ** 128), + sqrt_price_limit_X96: cairo.uint256(0), + } - //exact input - const outputSelector = { - contract_address: swapRouterAddress, - entry_point: hash.getSelectorFromName('exact_output_single'), - } - const output_call_data_length = { output_call_data_length: '0xb' } + //exact input + const outputSelector = { + contract_address: swapRouterAddress, + entry_point: hash.getSelectorFromName('exact_output_single'), + } + const output_call_data_length = { output_call_data_length: '0xb' } - const call = { - calldata: exactOutputSingleParams, - route, - } + const call = { + calldata: exactOutputSingleParams, + route, + } - return { call, outputSelector, output_call_data_length } - } - }) + return { call, outputSelector, output_call_data_length } + } + }) + ) + .flat() }, [routes, amountOut, address, currencyIn, deadline]) const approveSelector = useMemo(() => { @@ -512,7 +548,7 @@ export function useBestV3TradeExactOut( const msgHash = hash.computeHashOnElements(message) const signature: WeierstrassSignatureType = ec.starkCurve.sign(msgHash, privateKey) - const amountInResults = useQuery({ + useQuery({ queryKey: [ 'get_simulation', address, @@ -522,6 +558,7 @@ export function useBestV3TradeExactOut( currencyIn?.symbol, contract_version?.data, ], + refetchOnWindowFocus: false, queryFn: async () => { if ( !address || @@ -534,109 +571,136 @@ export function useBestV3TradeExactOut( !deadline ) return - const nonce = Number(nonce_results.data) - const isWalletCairoVersionGreaterThanZero = Boolean(contract_version.data) - const callPromises = quoteExactOutInputs.map(async ({ call, outputSelector, output_call_data_length }) => { - const provider = providerInstance(chainId) - if (!provider) return - const payloadForContractType1 = { - contractAddress: address, - calldata: CallData.compile({ - ...totalTx, - ...approveSelector, - ...approve_call_data_length, - ...approve_call_data, - ...outputSelector, - ...output_call_data_length, - ...call.calldata, - }), - } - - const payloadForContractType0 = { - contractAddress: address, - calldata: CallData.compile({ - ...totalTx, - ...approveSelector, - ...{ approve_offset: '0x0' }, - ...approve_call_data_length, - ...outputSelector, - ...{ input_offset: approve_call_data_length }, - ...output_call_data_length, - ...{ total_call_data_length: '0xe' }, - ...approve_call_data, - ...call.calldata, - }), - } - const payloadBasedOnCairoVersion = isWalletCairoVersionGreaterThanZero ? payloadForContractType1 : payloadForContractType0 - - const response = provider.simulateTransaction( - [{ type: TransactionType.INVOKE, ...payloadBasedOnCairoVersion, signature, nonce }], - { - skipValidate: true, + try { + setIsFetching(true) + const nonce = Number(nonce_results.data) + const isWalletCairoVersionGreaterThanZero = Boolean(contract_version.data) + const callPromises = quoteExactOutInputs.map(async ({ call, outputSelector, output_call_data_length }) => { + const provider = providerInstance(chainId) + if (!provider) return + const payloadForContractType1 = { + contractAddress: address, + calldata: CallData.compile({ + ...totalTx, + ...approveSelector, + ...approve_call_data_length, + ...approve_call_data, + ...outputSelector, + ...output_call_data_length, + ...call.calldata, + }), } - ) - return response - }) - - const settledResults = await Promise.allSettled(callPromises as any) - const settledResultsWithRoute = settledResults.map((result, i) => ({ ...result, route: routes[i] })) + const payloadForContractType0 = { + contractAddress: address, + calldata: CallData.compile({ + ...totalTx, + ...approveSelector, + ...{ approve_offset: '0x0' }, + ...approve_call_data_length, + ...outputSelector, + ...{ input_offset: approve_call_data_length }, + ...output_call_data_length, + ...{ total_call_data_length: '0xe' }, + ...approve_call_data, + ...call.calldata, + }), + } + const payloadBasedOnCairoVersion = isWalletCairoVersionGreaterThanZero + ? payloadForContractType1 + : payloadForContractType0 + + const response = provider.simulateTransaction( + [{ type: TransactionType.INVOKE, ...payloadBasedOnCairoVersion, signature, nonce }], + { + skipValidate: true, + } + ) - const resolvedResults = settledResultsWithRoute - .filter((result) => result.status === 'fulfilled') - .map((result: any) => { - const response = { ...result.value, route: result.route } return response }) - return resolvedResults - }, - onSuccess: (data) => { - // Handle the successful data fetching here if needed - }, - }) - function fromUint256ToNumber(uint256: any) { - // Assuming uint256 is an object with 'high' and 'low' properties - const { high } = uint256 - return high - } + const settledResults = await Promise.allSettled(callPromises as any) + const settledResultsWithRoute = settledResults.map((result, i) => { + if (!amountOuts || !percents) return + const routesLength = routes.length + const routeIndex = i % routesLength - const filteredAmountInResults = useMemo(() => { - if (!amountInResults) return - const data = amountInResults?.data - - if (!data) return - const subRoutesArray = data.map((subArray) => ({ ...subArray[0], route: subArray.route })) - const bestRouteResults = { bestRoute: null, amountIn: null } - const { bestRoute, amountIn } = subRoutesArray - .filter((result: any) => result?.transaction_trace?.execute_invocation?.result) - .reduce((currentBest: any, result: any, i: any) => { - const selected_tx_result = result?.transaction_trace?.execute_invocation?.result - const value = selected_tx_result[selected_tx_result.length - 2] - const amountIn = fromUint256ToNumber({ high: value }) - if (!result) return currentBest - if (currentBest.amountIn === null) { return { - bestRoute: result?.route, - amountIn, + ...result, + route: routes[routeIndex], } - } else if (Number(cairo.felt(currentBest.amountIn)) < Number(cairo.felt(amountIn))) { - return { - bestRoute: result?.route, - amountIn, - } - } - - return currentBest - }, bestRouteResults) + }) + const resolvedResults = settledResultsWithRoute + .filter((result) => result?.status === 'fulfilled') + .map((result: any) => { + const response = { + ...result.value, + route: result.route, + } + return response + }) + return resolvedResults + } catch (e) { + console.error(e) + return undefined + } + }, + onSuccess: async (data) => { + try { + if (data && currencyIn && amountOuts && currencyOut && percents) { + const validQuotes = data + .filter((result: any) => { + return result[0].transaction_trace.execute_invocation.result + }) + .map((result: any) => { + const selected_tx_result = result[0].transaction_trace.execute_invocation.result + const value = selected_tx_result[selected_tx_result.length - 2] + const amountIn = fromUint256ToNumber({ high: value }) + + const selected_call_data = result[0].transaction_trace.execute_invocation.calldata + const isSingleHop = result.route.pools.length === 1 + const outputIndex = isSingleHop ? selected_call_data.length - 6 : selected_call_data.length - 4 + const outputValue = selected_call_data[outputIndex] + const amountOut = fromUint256ToNumber({ high: outputValue }) + + const amountOutIndex = amountOuts.findIndex((amount) => { + return ( + formatCurrencyAmount({ + amount: amount, + type: NumberType.SwapTradeAmount, + placeholder: '', + }) == + formatCurrencyAmount({ + amount: CurrencyAmount.fromRawAmount(currencyOut!, num.hexToDecimalString(amountOut)), + type: NumberType.SwapTradeAmount, + placeholder: '', + }) + ) + }) - return { bestRoute, amountIn } - }, [amountInResults]) + return { + ...result[0], + route: result.route, + outputAmount: CurrencyAmount.fromRawAmount(currencyOut, num.hexToDecimalString(amountOut)), + percent: percents[amountOutIndex], + poolAddresses: result.route.pools.map((pool: Pool) => { + return getPoolAddress(pool.token0, pool.token1, pool.fee, chainId) + }), + inputAmount: CurrencyAmount.fromRawAmount(currencyIn, num.hexToDecimalString(amountIn)), + } + }) - const { bestRoute, amountIn } = useMemo(() => { - if (!filteredAmountInResults) return { bestRoute: null, amountIn: null } - return { bestRoute: filteredAmountInResults.bestRoute, amountIn: filteredAmountInResults.amountIn } - }, [filteredAmountInResults]) + const route = await getBestSwapRoute(validQuotes, TradeType.EXACT_OUTPUT, percents ?? []) + setBestRoute(route) + } + } catch (e) { + console.error(e) + } finally { + setIsFetching(false) + } + }, + }) return useMemo(() => { if (!routes.length) { @@ -645,14 +709,12 @@ export function useBestV3TradeExactOut( trade: null, } } - - if (!amountOut || !currencyIn || !filteredAmountInResults) { + if (!amountOut || !currencyOut || isFetching) { return { state: TradeState.INVALID, trade: null, } } - if (routesLoading) { return { state: TradeState.LOADING, @@ -660,21 +722,15 @@ export function useBestV3TradeExactOut( } } - if (!bestRoute || !amountIn) { + if (!bestRoute || !amountOut) { return { state: TradeState.NO_ROUTE_FOUND, trade: null, } } - return { state: TradeState.VALID, - trade: Trade.createUncheckedTrade({ - route: bestRoute, - tradeType: TradeType.EXACT_OUTPUT, - inputAmount: CurrencyAmount.fromRawAmount(currencyIn, num.hexToDecimalString(amountIn)), - outputAmount: amountOut, - }), + trade: Trade.createUncheckedTradeWithMultipleRoutes({ routes: bestRoute, tradeType: TradeType.EXACT_OUTPUT }), } - }, [amountOut, currencyIn, routesLoading, routes, filteredAmountInResults]) + }, [amountOuts, currencyIn, routesLoading, routes]) } diff --git a/src/hooks/usePools.ts b/src/hooks/usePools.ts index d2f59fe8..ab193c7e 100644 --- a/src/hooks/usePools.ts +++ b/src/hooks/usePools.ts @@ -1,9 +1,9 @@ import { useToken } from 'hooks/Tokens' import { useAccountDetails } from './starknet-react' // import { Interface } from '@ethersproject/abi' -import { BigintIsh, Currency, Token } from '@vnaysn/jediswap-sdk-core' +import { BigintIsh, ChainId, Currency, Token } from '@vnaysn/jediswap-sdk-core' import IUniswapV3PoolStateJSON from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json' -import { computePoolAddress, toHex } from '@vnaysn/jediswap-sdk-v3' +import { POOL_INIT_CODE_HASH, toHex } from '@vnaysn/jediswap-sdk-v3' import { FeeAmount, Pool } from '@vnaysn/jediswap-sdk-v3' import JSBI from 'jsbi' import { useMultipleContractSingleData } from 'lib/hooks/multicall' @@ -11,12 +11,42 @@ import { useMemo } from 'react' import { IUniswapV3PoolStateInterface } from '../types/v3/IUniswapV3PoolState' import { V3_CORE_FACTORY_ADDRESSES } from 'constants/addresses' import { useAllPairs } from 'state/pairs/hooks' -import { BigNumberish, BlockTag, CallData, Contract, ec, hash, num } from 'starknet' +import { BigNumberish, BlockTag, CallData, Contract, ec, getChecksumAddress, hash, num } from 'starknet' import { useContractRead } from '@starknet-react/core' import POOL_ABI from 'contracts/pool/abi.json' import FACTORY_ABI from 'contracts/factoryAddress/abi.json' import { POOL_CLASS_HASH, FACTORY_ADDRESS } from 'constants/tokens' import { toInt } from 'utils/toInt' +import { defaultAbiCoder, getCreate2Address, solidityKeccak256 } from 'ethers/lib/utils' + +function computePoolAddress({ + factoryAddress, + tokenA, + tokenB, + fee, + initCodeHashManualOverride, +}: { + factoryAddress: string + tokenA: Token + tokenB: Token + fee: FeeAmount + initCodeHashManualOverride?: string +}): string { + const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] // does safety checks + return getCreate2Address( + factoryAddress, + solidityKeccak256( + ['bytes'], + [ + defaultAbiCoder.encode( + ['address', 'address', 'uint24'], + [getChecksumAddress(token0.address), getChecksumAddress(token1.address), fee] + ), + ] + ), + initCodeHashManualOverride ?? POOL_INIT_CODE_HASH + ) +} // const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateJSON.abi) as IUniswapV3PoolStateInterface @@ -40,7 +70,6 @@ export class PoolCache { const key = `${factoryAddress}:${addressA}:${addressB}:${fee.toString()}` const found = this.addresses.find((address) => address.key === key) if (found) return found.address - const address = { key, address: computePoolAddress({ @@ -50,6 +79,7 @@ export class PoolCache { fee, }), } + console.log(factoryAddress, tokenA, tokenB, fee, this.addresses, address, 'test') this.addresses.unshift(address) return address.address } @@ -78,6 +108,7 @@ export class PoolCache { }) if (found) return found const pool = new Pool(tokenA, tokenB, fee, sqrtPriceX96, liquidity, tick) + this.pools.unshift(pool) return pool } @@ -209,7 +240,7 @@ export function usePools( export function usePoolsForSwap(results: any): [PoolState, Pool | null][] { return useMemo(() => { return results.map((result: any) => { - const { tickCurrent, liquidity, sqrtPriceX96, token0, token1, fee } = result + const { tickCurrent, liquidity, sqrtPriceX96, token0, token1, fee, address } = result const sqrtPriceHex = sqrtPriceX96 && JSBI.BigInt(num.toHex(sqrtPriceX96 as BigNumberish)) const liquidityHex = Boolean(liquidity) ? JSBI.BigInt(num.toHex(liquidity as BigNumberish)) : JSBI.BigInt('0x0') @@ -238,6 +269,37 @@ export function usePool( return usePools(poolKeys)[0] } +export function getPoolAddress( + currencyA: Currency | undefined, + currencyB: Currency | undefined, + feeAmount: FeeAmount | undefined, + chainId: ChainId | undefined +): string | undefined { + if (currencyA && currencyB && feeAmount && chainId) { + const tokenA = currencyA.wrapped + const tokenB = currencyB.wrapped + if (tokenA.equals(tokenB)) return undefined + const tokens = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] // does safety checks + + //compute pool contract address + const { calculateContractAddressFromHash } = hash + + const salt = ec.starkCurve.poseidonHashMany([ + BigInt(tokens[0].address), + BigInt(tokens[1].address), + BigInt(feeAmount), + ]) + + const contructorCalldata = CallData.compile([tokens[0].address, tokens[1].address, feeAmount, feeAmount / 50]) + + return tokenA && tokenB && !tokenA.equals(tokenB) + ? calculateContractAddressFromHash(salt, POOL_CLASS_HASH[chainId], contructorCalldata, FACTORY_ADDRESS[chainId]) + : undefined + } + + return undefined +} + export function usePoolAddress( currencyA: Currency | undefined, currencyB: Currency | undefined, diff --git a/src/pages/Swap/getBestSwapRoute.ts b/src/pages/Swap/getBestSwapRoute.ts new file mode 100644 index 00000000..7fe8280a --- /dev/null +++ b/src/pages/Swap/getBestSwapRoute.ts @@ -0,0 +1,312 @@ +import { TradeType } from '@jediswap/sdk' +import { CurrencyAmount } from '@vnaysn/jediswap-sdk-core' +import FixedReverseHeap from 'mnemonist/fixed-reverse-heap' +import Queue from 'mnemonist/queue' +import _ from 'lodash' + +const routingConfig = { + minSplits: 0, + maxSplits: 3, + forceCrossProtocol: false, +} + +export async function getBestSwapRoute(routesWithValidQuotes: any[], tradeType: TradeType, percents: number[]) { + const percentToQuotes: { [percent: number]: any[] } = {} + for (const routeWithValidQuote of routesWithValidQuotes) { + if (!percentToQuotes[routeWithValidQuote.percent]) { + percentToQuotes[routeWithValidQuote.percent] = [] + } + percentToQuotes[routeWithValidQuote.percent]!.push(routeWithValidQuote) + } + + // console.log('percentToQuotes', percentToQuotes) + const betRoute = await getBestSwapRouteBy( + tradeType, + percentToQuotes, + percents, + tradeType === TradeType.EXACT_INPUT + ? (routeQuote) => routeQuote.outputAmount + : (routeQuote) => routeQuote.inputAmount + ) + // console.log('betRoute', betRoute) + + return betRoute +} + +async function getBestSwapRouteBy( + routeType: TradeType, + percentToQuotes: { [percent: number]: any[] }, + percents: number[], + by: (routeQuote: any) => CurrencyAmount +) { + // Build a map of percentage to sorted list of quotes, with the biggest quote being first in the list. + const percentToSortedQuotes = _.mapValues(percentToQuotes, (routeQuotes: any[]) => { + return routeQuotes.sort((routeQuoteA, routeQuoteB) => { + if (routeType == TradeType.EXACT_INPUT) { + return by(routeQuoteA).greaterThan(by(routeQuoteB)) ? -1 : 1 + } else { + return by(routeQuoteA).lessThan(by(routeQuoteB)) ? -1 : 1 + } + }) + }) + + const quoteCompFn = + routeType == TradeType.EXACT_INPUT + ? (a: CurrencyAmount, b: CurrencyAmount) => a.greaterThan(b) + : (a: CurrencyAmount, b: CurrencyAmount) => a.lessThan(b) + + const sumFn = (currencyAmounts: CurrencyAmount[]): CurrencyAmount => { + let sum = currencyAmounts[0]! + for (let i = 1; i < currencyAmounts.length; i++) { + sum = sum.add(currencyAmounts[i]!) + } + return sum + } + + let bestQuote: CurrencyAmount | undefined + let bestSwap: any[] | undefined + + // Min-heap for tracking the 5 best swaps given some number of splits. + const bestSwapsPerSplit = new FixedReverseHeap<{ + quote: CurrencyAmount + routes: any[] + }>( + Array, + (a, b) => { + return quoteCompFn(a.quote, b.quote) ? -1 : 1 + }, + 3 + ) + + const { minSplits, maxSplits, forceCrossProtocol } = routingConfig + + if (!percentToSortedQuotes[100] || minSplits > 1 || forceCrossProtocol) { + // log.info( + // { + // percentToSortedQuotes: _.mapValues( + // percentToSortedQuotes, + // (p) => p.length + // ), + // }, + // 'Did not find a valid route without any splits. Continuing search anyway.' + // ); + } else { + bestQuote = by(percentToSortedQuotes[100][0]!) + bestSwap = [percentToSortedQuotes[100][0]!] + + for (const routeWithQuote of percentToSortedQuotes[100].slice(0, 5)) { + bestSwapsPerSplit.push({ + quote: by(routeWithQuote), + routes: [routeWithQuote], + }) + } + } + + // We do a BFS. Each additional node in a path represents us adding an additional split to the route. + const queue = new Queue<{ + percentIndex: number + curRoutes: any[] + remainingPercent: number + special: boolean + }>() + + // First we seed BFS queue with the best quotes for each percentage. + // i.e. [best quote when sending 10% of amount, best quote when sending 20% of amount, ...] + // We will explore the various combinations from each node. + for (let i = percents.length; i >= 0; i--) { + const percent = percents[i]! + + if (!percentToSortedQuotes[percent]) { + continue + } + + queue.enqueue({ + curRoutes: [percentToSortedQuotes[percent]![0]!], + percentIndex: i, + remainingPercent: 100 - percent, + special: false, + }) + + if (!percentToSortedQuotes[percent] || !percentToSortedQuotes[percent]![1]) { + continue + } + + queue.enqueue({ + curRoutes: [percentToSortedQuotes[percent]![1]!], + percentIndex: i, + remainingPercent: 100 - percent, + special: true, + }) + } + + let splits = 1 + + while (queue.size > 0) { + bestSwapsPerSplit.clear() + + // Size of the queue at this point is the number of potential routes we are investigating for the given number of splits. + let layer = queue.size + splits++ + + // If we didn't improve our quote by adding another split, very unlikely to improve it by splitting more after that. + if (splits >= 3 && bestSwap && bestSwap.length < splits - 1) { + break + } + + if (splits > maxSplits) { + break + } + + while (layer > 0) { + layer-- + + const { remainingPercent, curRoutes, percentIndex, special } = queue.dequeue()! + + // For all other percentages, add a new potential route. + // E.g. if our current aggregated route if missing 50%, we will create new nodes and add to the queue for: + // 50% + new 10% route, 50% + new 20% route, etc. + for (let i = percentIndex; i >= 0; i--) { + const percentA = percents[i]! + + if (percentA > remainingPercent) { + continue + } + + // At some point the amount * percentage is so small that the quoter is unable to get + // a quote. In this case there could be no quotes for that percentage. + if (!percentToSortedQuotes[percentA]) { + continue + } + + const candidateRoutesA = percentToSortedQuotes[percentA]! + + // Find the best route in the complimentary percentage that doesn't re-use a pool already + // used in the current route. Re-using pools is not allowed as each swap through a pool changes its liquidity, + // so it would make the quotes inaccurate. + const routeWithQuoteA = findFirstRouteNotUsingUsedPools(curRoutes, candidateRoutesA, forceCrossProtocol) + + if (!routeWithQuoteA) { + continue + } + + const remainingPercentNew = remainingPercent - percentA + const curRoutesNew = [...curRoutes, routeWithQuoteA] + + // If we've found a route combination that uses all 100%, and it has at least minSplits, update our best route. + if (remainingPercentNew == 0 && splits >= minSplits) { + const quotesNew = _.map(curRoutesNew, (r) => by(r)) + const quoteNew = sumFn(quotesNew) + + let gasCostL1QuoteToken = CurrencyAmount.fromRawAmount(quoteNew.currency, 0) + + // if (HAS_L1_FEE.includes(chainId)) { + // if (v2GasModel == undefined && v3GasModel == undefined) { + // throw new Error("Can't compute L1 gas fees."); + // } else { + // const v2Routes = curRoutesNew.filter( + // (routes) => routes.protocol === Protocol.V2 + // ); + // if (v2Routes.length > 0 && V2_SUPPORTED.includes(chainId)) { + // if (v2GasModel) { + // const v2GasCostL1 = await v2GasModel.calculateL1GasFees!( + // v2Routes as V2RouteWithValidQuote[] + // ); + // gasCostL1QuoteToken = gasCostL1QuoteToken.add( + // v2GasCostL1.gasCostL1QuoteToken + // ); + // } + // } + // const v3Routes = curRoutesNew.filter( + // (routes) => routes.protocol === Protocol.V3 + // ); + // if (v3Routes.length > 0) { + // if (v3GasModel) { + // const v3GasCostL1 = await v3GasModel.calculateL1GasFees!( + // v3Routes as V3RouteWithValidQuote[] + // ); + // gasCostL1QuoteToken = gasCostL1QuoteToken.add( + // v3GasCostL1.gasCostL1QuoteToken + // ); + // } + // } + // } + // } + + const quoteAfterL1Adjust = quoteNew + // routeType == TradeType.EXACT_INPUT + // ? quoteNew.subtract(gasCostL1QuoteToken) + // : quoteNew.add(gasCostL1QuoteToken) + + bestSwapsPerSplit.push({ + quote: quoteAfterL1Adjust, + routes: curRoutesNew, + }) + + if (!bestQuote || quoteCompFn(quoteAfterL1Adjust, bestQuote)) { + bestQuote = quoteAfterL1Adjust + bestSwap = curRoutesNew + } + } else { + queue.enqueue({ + curRoutes: curRoutesNew, + remainingPercent: remainingPercentNew, + percentIndex: i, + special, + }) + } + } + } + } + if (!bestSwap) { + // log.info(`Could not find a valid swap`); + return undefined + } + + return bestSwap +} + +// We do not allow pools to be re-used across split routes, as swapping through a pool changes the pools state. +// Given a list of used routes, this function finds the first route in the list of candidate routes that does not re-use an already used pool. +const findFirstRouteNotUsingUsedPools = ( + usedRoutes: any[], + candidateRouteQuotes: any[], + forceCrossProtocol: boolean +): any | null => { + const poolAddressSet = new Set() + const usedPoolAddresses = _(usedRoutes) + .flatMap((r) => r.poolAddresses) + .value() + + for (const poolAddress of usedPoolAddresses) { + poolAddressSet.add(poolAddress) + } + + const protocolsSet = new Set() + const usedProtocols = _(usedRoutes) + .flatMap((r) => r.protocol) + .uniq() + .value() + + for (const protocol of usedProtocols) { + protocolsSet.add(protocol) + } + + for (const routeQuote of candidateRouteQuotes) { + const { poolAddresses, protocol } = routeQuote + + if (poolAddresses.some((poolAddress: any) => poolAddressSet.has(poolAddress))) { + continue + } + + // This code is just for debugging. Allows us to force a cross-protocol split route by skipping + // consideration of routes that come from the same protocol as a used route. + const needToForce = forceCrossProtocol && protocolsSet.size == 1 + if (needToForce && protocolsSet.has(protocol)) { + continue + } + + return routeQuote + } + + return null +} diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index deda5613..33a16c9e 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -619,183 +619,194 @@ export function Swap({ const handleApproval = approveCallback() if (!handleApproval) return const isTradeTypeV2 = (trade as any).swaps - const { inputAmount, outputAmount } = trade - const route = (trade as any).route + // const { inputAmount, outputAmount } = trade + // const route = (trade as any).route + const swaps = (trade as any).swaps const callData = [] callData.push(handleApproval) - const amountIn: string = toHex(trade.maximumAmountIn(allowedSlippage, inputAmount).quotient) - const amountOut: string = toHex(trade.minimumAmountOut(allowedSlippage, outputAmount).quotient) - if (isTradeTypeV2) { - const isRouteSingleHop = route.pools.length === 1 - if (trade.tradeType === TradeType.EXACT_INPUT) { - if (isRouteSingleHop) { - const exactInputSingleParams = { - token_in: route.tokenPath[0].address, - token_out: route.tokenPath[1].address, - fee: route.pools[0].fee, - recipient: address, - deadline: cairo.felt(deadline.toString()), - amount_in: cairo.uint256(inputAmount.raw.toString()), - amount_out_minimum: cairo.uint256(amountOut), - sqrt_price_limit_X96: cairo.uint256(0), - } - const compiledSwapCalls = CallData.compile(exactInputSingleParams) + console.log('tradeswaps', trade, swaps) + + swaps.forEach((swap: any) => { + const { inputAmount, outputAmount, route } = swap + const amountIn: string = toHex(trade.maximumAmountIn(allowedSlippage, inputAmount).quotient) + const amountOut: string = toHex(trade.minimumAmountOut(allowedSlippage, outputAmount).quotient) + if (isTradeTypeV2) { + const isRouteSingleHop = route.pools.length === 1 + console.log('isRouteSingleHop', isRouteSingleHop, route) + if (trade.tradeType === TradeType.EXACT_INPUT) { + if (isRouteSingleHop) { + const exactInputSingleParams = { + token_in: route.tokenPath[0].address, + token_out: route.tokenPath[1].address, + fee: route.pools[0].fee, + recipient: address, + deadline: cairo.felt(deadline.toString()), + amount_in: cairo.uint256(inputAmount.raw.toString()), + amount_out_minimum: cairo.uint256(amountOut), + sqrt_price_limit_X96: cairo.uint256(0), + } + const compiledSwapCalls = CallData.compile(exactInputSingleParams) - const calls = { - contractAddress: swapRouterAddressV2, - entrypoint: 'exact_input_single', - calldata: compiledSwapCalls, - } - callData.push(calls) - } else { - const firstInputToken: Token = route.input.wrapped - //create path - const { path } = route.pools.reduce( - ( - { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, - pool: Pool, - index: number - ): { inputToken: Token; path: (string | number)[]; types: string[] } => { - const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 - if (index === 0) { - return { - inputToken: outputToken, - types: ['address', 'address', 'uint24'], - path: [inputToken.address, outputToken.address, pool.fee], - } - } else { - return { - inputToken: outputToken, - types: [...types, 'address', 'address', 'uint24'], - path: [...path, inputToken.address, outputToken.address, pool.fee], + const calls = { + contractAddress: swapRouterAddressV2, + entrypoint: 'exact_input_single', + calldata: compiledSwapCalls, + } + callData.push(calls) + } else { + const firstInputToken: Token = route.input.wrapped + //create path + const { path } = route.pools.reduce( + ( + { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, + pool: Pool, + index: number + ): { inputToken: Token; path: (string | number)[]; types: string[] } => { + const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 + if (index === 0) { + return { + inputToken: outputToken, + types: ['address', 'address', 'uint24'], + path: [inputToken.address, outputToken.address, pool.fee], + } + } else { + return { + inputToken: outputToken, + types: [...types, 'address', 'address', 'uint24'], + path: [...path, inputToken.address, outputToken.address, pool.fee], + } } - } - }, - { inputToken: firstInputToken, path: [], types: [] } - ) - - const exactInputParams = { - path, - recipient: address, - deadline: cairo.felt(deadline.toString()), - amount_in: cairo.uint256(inputAmount.raw.toString()), - amount_out_minimum: cairo.uint256(amountOut), - } - const compiledSwapCalls = CallData.compile(exactInputParams) - - const calls = { - contractAddress: swapRouterAddressV2, - entrypoint: 'exact_input', - calldata: compiledSwapCalls, - } - callData.push(calls) - } - } else { - if (isRouteSingleHop) { - const exactOutputSingleParams = { - token_in: route.tokenPath[0].address, - token_out: route.tokenPath[1].address, - fee: route.pools[0].fee, - recipient: address, - deadline: cairo.felt(deadline.toString()), - amount_out: cairo.uint256(outputAmount.raw.toString()), - amount_in_maximum: cairo.uint256(amountIn), - sqrt_price_limit_X96: cairo.uint256(0), - } + }, + { inputToken: firstInputToken, path: [], types: [] } + ) - const compiledSwapCalls = CallData.compile(exactOutputSingleParams) + const exactInputParams = { + path, + recipient: address, + deadline: cairo.felt(deadline.toString()), + amount_in: cairo.uint256(inputAmount.raw.toString()), + amount_out_minimum: cairo.uint256(amountOut), + } + const compiledSwapCalls = CallData.compile(exactInputParams) - const calls = { - contractAddress: swapRouterAddressV2, - entrypoint: 'exact_output_single', - calldata: compiledSwapCalls, + const calls = { + contractAddress: swapRouterAddressV2, + entrypoint: 'exact_input', + calldata: compiledSwapCalls, + } + callData.push(calls) } - callData.push(calls) } else { - const firstInputToken: Token = route.input.wrapped - //create path - const { path } = route.pools.reduce( - ( - { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, - pool: Pool, - index: number - ): { inputToken: Token; path: (string | number)[]; types: string[] } => { - const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 - if (index === 0) { - return { - inputToken: outputToken, - types: ['uint24', 'address', 'address'], - path: [pool.fee, inputToken.address, outputToken.address], - } - } else { - return { - inputToken: outputToken, - types: [...types, 'uint24', 'address', 'address'], - path: [...path, pool.fee, inputToken.address, outputToken.address], - } - } - }, - { inputToken: firstInputToken, path: [], types: [] } - ) - - const reversePath = path.reverse() - - const exactOutputParams = { - path: reversePath, - recipient: address, - deadline: cairo.felt(deadline.toString()), - amount_out: cairo.uint256(outputAmount.raw.toString()), - amount_in_maximum: cairo.uint256(amountIn), - } + if (isRouteSingleHop) { + const exactOutputSingleParams = { + token_in: route.tokenPath[0].address, + token_out: route.tokenPath[1].address, + fee: route.pools[0].fee, + recipient: address, + deadline: cairo.felt(deadline.toString()), + amount_out: cairo.uint256(outputAmount.raw.toString()), + amount_in_maximum: cairo.uint256(amountIn), + sqrt_price_limit_X96: cairo.uint256(0), + } - const compiledSwapCalls = CallData.compile(exactOutputParams) + const compiledSwapCalls = CallData.compile(exactOutputSingleParams) - const calls = { - contractAddress: swapRouterAddressV2, - entrypoint: 'exact_output', - calldata: compiledSwapCalls, - } - callData.push(calls) - } - } - } else { - const path: string[] = route.path.map((token: any) => token.address) - if (trade.tradeType === TradeType.EXACT_INPUT) { - const swapArgs = { - amountIn: cairo.uint256(inputAmount.raw.toString()), - amountOutMin: cairo.uint256(amountOut), - path, - to: address, - deadline: cairo.felt(deadline.toString()), - } - const compiledSwapCalls = CallData.compile(swapArgs) + const calls = { + contractAddress: swapRouterAddressV2, + entrypoint: 'exact_output_single', + calldata: compiledSwapCalls, + } + callData.push(calls) + } else { + const firstInputToken: Token = route.input.wrapped + //create path + const { path } = route.pools.reduce( + ( + { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, + pool: Pool, + index: number + ): { inputToken: Token; path: (string | number)[]; types: string[] } => { + const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 + if (index === 0) { + return { + inputToken: outputToken, + types: ['uint24', 'address', 'address'], + path: [pool.fee, inputToken.address, outputToken.address], + } + } else { + return { + inputToken: outputToken, + types: [...types, 'uint24', 'address', 'address'], + path: [...path, pool.fee, inputToken.address, outputToken.address], + } + } + }, + { inputToken: firstInputToken, path: [], types: [] } + ) - const calls = { - contractAddress: swapRouterAddressV1, - entrypoint: 'swap_exact_tokens_for_tokens', - calldata: compiledSwapCalls, - } + console.log('path', path) - callData.push(calls) - } else { - const swapArgs = { - amountOut: cairo.uint256(outputAmount.raw.toString()), - amountInMax: cairo.uint256(amountIn), - path, - to: address, - deadline: cairo.felt(deadline.toString()), - } + const reversePath = path.reverse() - const compiledSwapCalls = CallData.compile(swapArgs) + const exactOutputParams = { + path: reversePath, + recipient: address, + deadline: cairo.felt(deadline.toString()), + amount_out: cairo.uint256(outputAmount.raw.toString()), + amount_in_maximum: cairo.uint256(amountIn), + } - const calls = { - contractAddress: swapRouterAddressV1, - entrypoint: 'swap_tokens_for_exact_tokens', - calldata: compiledSwapCalls, + const compiledSwapCalls = CallData.compile(exactOutputParams) + + const calls = { + contractAddress: swapRouterAddressV2, + entrypoint: 'exact_output', + calldata: compiledSwapCalls, + } + callData.push(calls) + } } - callData.push(calls) } - } + }) + + // else { + // const path: string[] = route.path.map((token: any) => token.address) + // if (trade.tradeType === TradeType.EXACT_INPUT) { + // const swapArgs = { + // amountIn: cairo.uint256(inputAmount.raw.toString()), + // amountOutMin: cairo.uint256(amountOut), + // path, + // to: address, + // deadline: cairo.felt(deadline.toString()), + // } + // const compiledSwapCalls = CallData.compile(swapArgs) + + // const calls = { + // contractAddress: swapRouterAddressV1, + // entrypoint: 'swap_exact_tokens_for_tokens', + // calldata: compiledSwapCalls, + // } + + // callData.push(calls) + // } else { + // const swapArgs = { + // amountOut: cairo.uint256(outputAmount.raw.toString()), + // amountInMax: cairo.uint256(amountIn), + // path, + // to: address, + // deadline: cairo.felt(deadline.toString()), + // } + + // const compiledSwapCalls = CallData.compile(swapArgs) + + // const calls = { + // contractAddress: swapRouterAddressV1, + // entrypoint: 'swap_tokens_for_exact_tokens', + // calldata: compiledSwapCalls, + // } + // callData.push(calls) + // } + // } setSwapCallData(callData) }, [trade, address, deadline, approveCallback]) diff --git a/src/state/swap/hooks.tsx b/src/state/swap/hooks.tsx index 0c35e609..49113652 100644 --- a/src/state/swap/hooks.tsx +++ b/src/state/swap/hooks.tsx @@ -1,12 +1,10 @@ import { Trans } from '@lingui/macro' -import { ChainId, Currency, CurrencyAmount, Percent, TradeType } from '@vnaysn/jediswap-sdk-core' +import { ChainId, Currency, CurrencyAmount, Fraction, Percent, TradeType } from '@vnaysn/jediswap-sdk-core' import { useAccountBalance, useAccountDetails } from 'hooks/starknet-react' import { useConnectionReady } from 'connection/eagerlyConnect' import { useFotAdjustmentsEnabled } from 'featureFlags/flags/fotAdjustments' import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance' -import { useDebouncedTrade } from 'hooks/useDebouncedTrade' import { useSwapTaxes } from 'hooks/useSwapTaxes' -import { useUSDPrice } from 'hooks/useUSDPrice' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { ParsedQs } from 'qs' import { ReactNode, useCallback, useEffect, useMemo } from 'react' @@ -15,10 +13,7 @@ import { useAppDispatch } from 'state/hooks' import { InterfaceTrade, TradeState } from 'state/routing/types' import { isClassicTrade } from 'state/routing/utils' import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' - -// import { TOKEN_SHORTHANDS } from '../../constants/tokens' import { useCurrency } from '../../hooks/Tokens' -import useENS from '../../hooks/useENS' import useParsedQueryString from '../../hooks/useParsedQueryString' import { isAddress } from '../../utils' import { useCurrencyBalances } from '../connection/hooks' @@ -26,8 +21,8 @@ import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies import { SwapState } from './reducer' import { isAddressValidForStarknet } from 'utils/addresses' import { useBestV3TradeExactIn, useBestV3TradeExactOut } from 'hooks/useBestV3Trade' +import { NumberType, useFormatter } from 'utils/formatNumbers' import { useTradeExactIn, useTradeExactOut } from 'hooks/Trades' -import { BigNumber } from 'ethers' export function useSwapActionHandlers(dispatch: React.Dispatch): { onCurrencySelection: (field: Field, currency: Currency) => void @@ -108,6 +103,8 @@ export function useDerivedSwapInfo( allPools: string[], allPairs: string[] ): SwapInfo { + const { formatCurrencyAmount } = useFormatter() + const { address: account } = useAccountDetails() const { independentField, @@ -135,20 +132,36 @@ export function useDerivedSwapInfo( const token1balance = useAccountBalance(outputCurrency ?? undefined) const isExactIn: boolean = independentField === Field.INPUT - const parsedAmount = useMemo( - () => tryParseCurrencyAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined), - [inputCurrency, isExactIn, outputCurrency, typedValue] - ) + // const parsedAmount = useMemo( + // () => tryParseCurrencyAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined), + // [inputCurrency, isExactIn, outputCurrency, typedValue] + // ) + + const parsedAmount = useMemo(() => { + if (!typedValue || !inputCurrency || !outputCurrency) return undefined + return tryParseCurrencyAmount(typedValue, isExactIn ? inputCurrency : outputCurrency) + }, [typedValue]) + + const distributedAmount = useMemo(() => { + if (!parsedAmount) return undefined + return getAmountDistribution(parsedAmount, 25, formatCurrencyAmount) + }, [parsedAmount]) const bestV3TradeExactIn = useBestV3TradeExactIn( allPools, - isExactIn ? parsedAmount : undefined, - outputCurrency ?? undefined + isExactIn && outputCurrency && distributedAmount ? distributedAmount[1] : undefined, + outputCurrency, + inputCurrency, + distributedAmount ? distributedAmount[0] : undefined, + typedValue ) const bestV3TradeExactOut = useBestV3TradeExactOut( allPools, + !isExactIn && inputCurrency && distributedAmount ? distributedAmount[1] : undefined, inputCurrency ?? undefined, - !isExactIn ? parsedAmount : undefined + outputCurrency, + distributedAmount ? distributedAmount[0] : undefined, + typedValue ) const [bestV2TradeExactIn] = useTradeExactIn( @@ -164,18 +177,19 @@ export function useDerivedSwapInfo( ) const bestTradeExactIn = useMemo(() => { - if (bestV2TradeExactIn && bestV3TradeExactIn && bestV3TradeExactIn.trade) { - const v2OutputAmount = BigInt(bestV2TradeExactIn.outputAmount.raw.toString()) - const v3OutputAmount = BigInt(bestV3TradeExactIn.trade.outputAmount.raw.toString()) - return v2OutputAmount > v3OutputAmount - ? { state: TradeState.VALID, trade: bestV2TradeExactIn } - : bestV3TradeExactIn - } else if (!bestV2TradeExactIn && bestV3TradeExactIn) { - return bestV3TradeExactIn - } else if (bestV2TradeExactIn && !bestV3TradeExactIn?.trade) { - return { state: TradeState.VALID, trade: bestV2TradeExactIn } + if (bestV3TradeExactIn.state !== TradeState.INVALID && bestV3TradeExactIn.state !== TradeState.LOADING) { + if (bestV2TradeExactIn && bestV3TradeExactIn && bestV3TradeExactIn.trade) { + const v2OutputAmount = BigInt(bestV2TradeExactIn.outputAmount.raw.toString()) + const v3OutputAmount = BigInt(bestV3TradeExactIn.trade.outputAmount.raw.toString()) + return v2OutputAmount > v3OutputAmount + ? { state: TradeState.VALID, trade: bestV2TradeExactIn } + : bestV3TradeExactIn + } else if (!bestV2TradeExactIn && bestV3TradeExactIn) { + return bestV3TradeExactIn + } else if (bestV2TradeExactIn && !bestV3TradeExactIn?.trade) { + return { state: TradeState.VALID, trade: bestV2TradeExactIn } + } } - return { state: TradeState.INVALID, trade: null, @@ -183,16 +197,18 @@ export function useDerivedSwapInfo( }, [bestV2TradeExactIn, bestV3TradeExactIn]) const bestTradeExactOut = useMemo(() => { - if (bestV2TradeExactOut && bestV3TradeExactOut && bestV3TradeExactOut.trade) { - const v2InputAmount = BigInt(bestV2TradeExactOut.inputAmount.raw.toString()) - const v3InputAmount = BigInt(bestV3TradeExactOut.trade.inputAmount.raw.toString()) - return v2InputAmount < v3InputAmount - ? { state: TradeState.VALID, trade: bestV2TradeExactOut } - : bestV3TradeExactOut - } else if (!bestV2TradeExactOut && bestV3TradeExactOut) { - return bestV3TradeExactOut - } else if (bestV2TradeExactOut && !bestV3TradeExactOut?.trade) { - return { state: TradeState.VALID, trade: bestV2TradeExactOut } + if (bestV3TradeExactOut.state !== TradeState.INVALID && bestV3TradeExactOut.state !== TradeState.LOADING) { + if (bestV2TradeExactOut && bestV3TradeExactOut && bestV3TradeExactOut.trade) { + const v2InputAmount = BigInt(bestV2TradeExactOut.inputAmount.raw.toString()) + const v3InputAmount = BigInt(bestV3TradeExactOut.trade.inputAmount.raw.toString()) + return v2InputAmount < v3InputAmount + ? { state: TradeState.VALID, trade: bestV2TradeExactOut } + : bestV3TradeExactOut + } else if (!bestV2TradeExactOut && bestV3TradeExactOut) { + return bestV3TradeExactOut + } else if (bestV2TradeExactOut && !bestV3TradeExactOut?.trade) { + return { state: TradeState.VALID, trade: bestV2TradeExactOut } + } } return { @@ -209,6 +225,9 @@ export function useDerivedSwapInfo( : isExactIn ? bestTradeExactIn : bestTradeExactOut + + // console.log('finalTrade', trade) + const currencyBalances = useMemo( () => ({ [Field.INPUT]: relevantTokenBalances[0], @@ -259,7 +278,7 @@ export function useDerivedSwapInfo( inputError = inputError ?? Select a token } - if (!parsedAmount) { + if (!typedValue) { inputError = inputError ?? Enter an amount } @@ -268,7 +287,7 @@ export function useDerivedSwapInfo( } return inputError - }, [account, currencies, parsedAmount, currencyBalances, trade?.trade, allowedSlippage, connectionReady]) + }, [account, currencies, typedValue, currencyBalances, trade?.trade, allowedSlippage, connectionReady]) return useMemo( () => ({ @@ -374,3 +393,49 @@ export function useDefaultsFromURLSearch(): SwapState { return parsedSwapState } + +// Note multiplications here can result in a loss of precision in the amounts (e.g. taking 50% of 101) +// This is reconcilled at the end of the algorithm by adding any lost precision to one of +// the splits in the route. +export function getAmountDistribution( + amount: CurrencyAmount, + distributionPercent: number, + formatCurrencyAmount: any +): [number[], CurrencyAmount[]] { + const percents = [] + const amounts = [] + + // console.log( + // 'amount', + // formatCurrencyAmount({ + // amount: amount, + // type: NumberType.SwapTradeAmount, + // placeholder: '', + // }) + // ) + + for (let i = 1; i <= 100 / distributionPercent; i++) { + percents.push(i * distributionPercent) + const partial = amount.multiply(new Fraction(i * distributionPercent, 100)) + const parsedAmount = formatCurrencyAmount({ + amount: partial, + type: NumberType.SwapTradeAmount, + placeholder: '', + }) + amounts.push(tryParseCurrencyAmount(parsedAmount, amount.currency)!) + } + + // amounts.forEach((amount, i) => { + // console.log( + // 'amounts', + // amount, + // formatCurrencyAmount({ + // amount: amount, + // type: NumberType.SwapTradeAmount, + // placeholder: '', + // }) + // ) + // }) + + return [percents, amounts] +} diff --git a/yarn.lock b/yarn.lock index 9328e897..e08db4a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2988,6 +2988,22 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@harshalmaniya/jediswap-sdk-v3@^19.1.1": + version "19.1.1" + resolved "https://registry.yarnpkg.com/@harshalmaniya/jediswap-sdk-v3/-/jediswap-sdk-v3-19.1.1.tgz#7b679b9652d4da5406b7cc67adc3f0432c7babeb" + integrity sha512-iFL6FaDs/ShnRVD3KcJUCibC5JXElcDoSetNlW9xYeU57ACU6cwB0LkTCSxSlP1qaN6Y1Ro/rBPBRLvXdpq7bg== + dependencies: + "@ethersproject/abi" "^5.0.12" + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "^4" + "@uniswap/swap-router-contracts" "^1.2.1" + "@uniswap/v3-periphery" "^1.1.1" + "@uniswap/v3-staker" "1.0.0" + "@vnaysn/jediswap-sdk-core" "^11.1.0" + starknet "^5.24.3" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -5081,6 +5097,11 @@ resolved "https://registry.yarnpkg.com/@starknet-react/chains/-/chains-0.1.3.tgz#8e5d248f860b850b0b03e1059d6a4010d4801818" integrity sha512-dSpLgDS02PmPzFVoW07EBVRbsX+h7MH7DYx2FF7vJv/J6eMGY7KtSupJW9R6ys0iADAxSg0UDcZr7syuy+b/Pg== +"@starknet-react/chains@^0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@starknet-react/chains/-/chains-0.1.7.tgz#58503379e2ffabe33b4f6e0f2aef775e84745a4d" + integrity sha512-UNh97I1SvuJKaAhKOmpEk8JcWuZWMlPG/ba2HcvFYL9x/47BKndJ+Da9V+iJFtkHUjreVnajT1snsaz1XMG+UQ== + "@starknet-react/core@2.0.0-next.0": version "2.0.0-next.0" resolved "https://registry.yarnpkg.com/@starknet-react/core/-/core-2.0.0-next.0.tgz#3aae8f84af791a0b29e0f83fc177b1c2dd66aeab"