diff --git a/src/app/hooks/use-swap.ts b/src/app/hooks/use-swap.ts index 03c0b3bd7..069d3c639 100644 --- a/src/app/hooks/use-swap.ts +++ b/src/app/hooks/use-swap.ts @@ -16,14 +16,16 @@ export const useSwap = () => { fromRoute3Token: Route3Token, toRoute3Token: Route3Token, inputAmountAtomic: BigNumber, - minimumReceivedAtomic: BigNumber, + expectedReceivedAtomic: BigNumber, + slippageRatio: number, hops: Route3SwapHops | Route3LiquidityBakingHops ) => getSwapTransferParams( fromRoute3Token, toRoute3Token, inputAmountAtomic, - minimumReceivedAtomic, + expectedReceivedAtomic, + slippageRatio, hops, tezos, publicKeyHash diff --git a/src/app/store/swap/state.mock.ts b/src/app/store/swap/state.mock.ts index 2bd53eb0c..413e2ff3a 100644 --- a/src/app/store/swap/state.mock.ts +++ b/src/app/store/swap/state.mock.ts @@ -1,9 +1,24 @@ -import { Route3SwapParamsResponse } from 'lib/route3/interfaces'; +import { Route3SwapParamsResponse, Route3TreeNodeType } from 'lib/route3/interfaces'; import { createEntity, mockPersistedState } from 'lib/store'; import type { SwapState } from './state'; -export const DEFAULT_SWAP_PARAMS: Route3SwapParamsResponse = { input: undefined, output: undefined, hops: [] }; +export const DEFAULT_SWAP_PARAMS: Route3SwapParamsResponse = { + input: undefined, + output: undefined, + hops: [], + tree: { + type: Route3TreeNodeType.Empty, + items: [], + dexId: null, + tokenInId: 0, + tokenOutId: 1, + tokenInAmount: '0', + tokenOutAmount: '0', + width: 0, + height: 0 + } +}; export const mockSwapState = mockPersistedState({ swapParams: createEntity(DEFAULT_SWAP_PARAMS), diff --git a/src/app/templates/InternalConfirmation.tsx b/src/app/templates/InternalConfirmation.tsx index 75c4f5285..07eeb09a6 100644 --- a/src/app/templates/InternalConfirmation.tsx +++ b/src/app/templates/InternalConfirmation.tsx @@ -1,7 +1,6 @@ import React, { FC, useCallback, useEffect, useMemo } from 'react'; import { localForger } from '@taquito/local-forging'; -import BigNumber from 'bignumber.js'; import classNames from 'clsx'; import { useDispatch } from 'react-redux'; @@ -21,8 +20,7 @@ import OperationsBanner from 'app/templates/OperationsBanner/OperationsBanner'; import RawPayloadView from 'app/templates/RawPayloadView'; import ViewsSwitcher from 'app/templates/ViewsSwitcher/ViewsSwitcher'; import { ViewsSwitcherItemProps } from 'app/templates/ViewsSwitcher/ViewsSwitcherItem'; -import { TEZ_TOKEN_SLUG, toTokenSlug } from 'lib/assets'; -import { useRawBalance } from 'lib/balances'; +import { toTokenSlug } from 'lib/assets'; import { T, t } from 'lib/i18n'; import { useRetryableSWR } from 'lib/swr'; import { useChainIdValue, useNetwork, useRelevantAccounts, tryParseExpenses } from 'lib/temple/front'; @@ -87,24 +85,29 @@ const InternalConfirmation: FC = ({ payload, onConfir })); }, [rawExpensesData]); - const { value: tezBalance } = useRawBalance(TEZ_TOKEN_SLUG, account.publicKeyHash); + useEffect(() => { + try { + const { errorDetails, errors, name } = payloadError.error[0]; + if ( + payload.type !== 'operations' || + !errorDetails.toLowerCase().includes('estimation') || + name !== 'TezosOperationError' || + !Array.isArray(errors) + ) { + return; + } - const totalTransactionCost = useMemo(() => { - if (payload.type === 'operations') { - return payload.opParams.reduce( - (accumulator, currentOpParam) => accumulator.plus(currentOpParam.amount), - new BigNumber(0) - ); - } + const tezBalanceTooLow = errors.some(error => { + const { id, contract } = error ?? {}; - return new BigNumber(0); - }, [payload]); + return id?.includes('balance_too_low') && contract === payload.sourcePkh; + }); - useEffect(() => { - if (tezBalance && new BigNumber(tezBalance).isLessThanOrEqualTo(totalTransactionCost)) { - dispatch(setOnRampPossibilityAction(true)); - } - }, [dispatch, tezBalance, totalTransactionCost]); + if (tezBalanceTooLow) { + dispatch(setOnRampPossibilityAction(true)); + } + } catch {} + }, [dispatch, payload.sourcePkh, payload.type, payloadError]); const signPayloadFormats: ViewsSwitcherItemProps[] = useMemo(() => { if (payload.type === 'operations') { diff --git a/src/app/templates/SwapForm/SwapForm.tsx b/src/app/templates/SwapForm/SwapForm.tsx index de8cd6a7e..6b715c031 100644 --- a/src/app/templates/SwapForm/SwapForm.tsx +++ b/src/app/templates/SwapForm/SwapForm.tsx @@ -43,8 +43,9 @@ import { ZERO } from 'lib/utils/numbers'; import { parseTransferParamsToParamsWithKind } from 'lib/utils/parse-transfer-params'; import { calculateSidePaymentsFromInput, - calculateOutputFeeAtomic, - getRoutingFeeTransferParams + getRoutingFeeTransferParams, + multiplyAtomicAmount, + calculateOutputAmounts } from 'lib/utils/swap.utils'; import { HistoryAction, navigate } from 'lib/woozie'; @@ -104,15 +105,23 @@ export const SwapForm: FC = () => { const [isAlertVisible, setIsAlertVisible] = useState(false); const slippageRatio = useMemo(() => getPercentageRatio(slippageTolerance ?? 0), [slippageTolerance]); - const minimumReceivedAmountAtomic = useMemo(() => { - if (isDefined(swapParams.data.output)) { - return tokensToAtoms(new BigNumber(swapParams.data.output), outputAssetMetadata.decimals) - .multipliedBy(slippageRatio) - .integerValue(BigNumber.ROUND_DOWN); - } else { - return ZERO; - } - }, [swapParams.data.output, outputAssetMetadata.decimals, slippageRatio]); + const { outputAtomicAmountBeforeFee, minimumReceivedAtomic, outputFeeAtomicAmount } = useMemo( + () => + calculateOutputAmounts( + inputValue.amount, + inputAssetMetadata.decimals, + swapParams.data.output, + outputAssetMetadata.decimals, + slippageRatio + ), + [ + inputValue.amount, + inputAssetMetadata.decimals, + swapParams.data.output, + outputAssetMetadata.decimals, + slippageRatio + ] + ); const hopsAreAbsent = isLiquidityBakingParamsResponse(swapParams.data) ? swapParams.data.tzbtcHops.length === 0 && swapParams.data.xtzHops.length === 0 @@ -217,14 +226,16 @@ export const SwapForm: FC = () => { amount: undefined }); } else { - const outputAtomicAmountBeforeFee = tokensToAtoms(new BigNumber(currentOutput), outputAssetMetadata.decimals); - const outputFeeAtomic = calculateOutputFeeAtomic( - tokensToAtoms(inputValue.amount ?? ZERO, inputAssetMetadata.decimals), - outputAtomicAmountBeforeFee + const { expectedReceivedAtomic } = calculateOutputAmounts( + inputValue.amount, + inputAssetMetadata.decimals, + currentOutput, + outputAssetMetadata.decimals, + slippageRatio ); setValue('output', { assetSlug: outputValue.assetSlug, - amount: atomsToTokens(outputAtomicAmountBeforeFee.minus(outputFeeAtomic), outputAssetMetadata.decimals) + amount: atomsToTokens(expectedReceivedAtomic, outputAssetMetadata.decimals) }); } @@ -232,6 +243,7 @@ export const SwapForm: FC = () => { triggerValidation(); } }, [ + slippageRatio, swapParams.data.output, setValue, triggerValidation, @@ -287,7 +299,6 @@ export const SwapForm: FC = () => { inputFeeAtomic: routingFeeFromInputAtomic, cashbackSwapInputAtomic: cashbackSwapInputFromInAtomic } = calculateSidePaymentsFromInput(atomsInputValue); - const routingFeeFromOutputAtomic = calculateOutputFeeAtomic(atomsInputValue, minimumReceivedAmountAtomic); if (!fromRoute3Token || !toRoute3Token || !swapParams.data.output || !inputValue.assetSlug) { return; @@ -299,7 +310,7 @@ export const SwapForm: FC = () => { const allSwapParams: Array = []; let routingOutputFeeTransferParams: TransferParams[] = await getRoutingFeeTransferParams( toRoute3Token, - routingFeeFromOutputAtomic, + outputFeeAtomicAmount, publicKeyHash, ROUTING_FEE_ADDRESS, tezos @@ -309,7 +320,8 @@ export const SwapForm: FC = () => { fromRoute3Token, toRoute3Token, swapInputMinusFeeAtomic, - minimumReceivedAmountAtomic, + outputAtomicAmountBeforeFee, + slippageRatio, swapParams.data ); @@ -350,18 +362,22 @@ export const SwapForm: FC = () => { rpcUrl: tezos.rpc.getRpcUrl() }); - const templeOutputAtomic = tokensToAtoms( + const templeExpectedOutputAtomic = tokensToAtoms( new BigNumber(swapToTempleParams.output ?? ZERO), TEMPLE_TOKEN.decimals - ) - .multipliedBy(ROUTING_FEE_SLIPPAGE_RATIO) - .integerValue(BigNumber.ROUND_DOWN); + ); + const templeMinOutputAtomic = multiplyAtomicAmount( + templeExpectedOutputAtomic, + ROUTING_FEE_SLIPPAGE_RATIO, + BigNumber.ROUND_DOWN + ); const swapToTempleTokenOpParams = await getSwapParams( fromRoute3Token, TEMPLE_TOKEN, routingFeeFromInputAtomic, - templeOutputAtomic, + templeExpectedOutputAtomic, + ROUTING_FEE_SLIPPAGE_RATIO, swapToTempleParams ); @@ -369,7 +385,7 @@ export const SwapForm: FC = () => { const routingFeeOpParams = await getRoutingFeeTransferParams( TEMPLE_TOKEN, - templeOutputAtomic.times(ROUTING_FEE_RATIO - SWAP_CASHBACK_RATIO).dividedToIntegerBy(ROUTING_FEE_RATIO), + templeMinOutputAtomic.times(ROUTING_FEE_RATIO - SWAP_CASHBACK_RATIO).dividedToIntegerBy(ROUTING_FEE_RATIO), publicKeyHash, BURN_ADDREESS, tezos @@ -378,9 +394,7 @@ export const SwapForm: FC = () => { } else if (!isInputTokenTempleToken && isSwapAmountMoreThreshold && isOutputTokenTempleToken) { routingOutputFeeTransferParams = await getRoutingFeeTransferParams( TEMPLE_TOKEN, - routingFeeFromOutputAtomic - .times(ROUTING_FEE_RATIO - SWAP_CASHBACK_RATIO) - .dividedToIntegerBy(ROUTING_FEE_RATIO), + outputFeeAtomicAmount.times(ROUTING_FEE_RATIO - SWAP_CASHBACK_RATIO).dividedToIntegerBy(ROUTING_FEE_RATIO), publicKeyHash, BURN_ADDREESS, tezos @@ -390,29 +404,33 @@ export const SwapForm: FC = () => { fromSymbol: toRoute3Token.symbol, toSymbol: TEMPLE_TOKEN.symbol, toTokenDecimals: TEMPLE_TOKEN.decimals, - amount: atomsToTokens(routingFeeFromOutputAtomic, toRoute3Token.decimals).toFixed(), + amount: atomsToTokens(outputFeeAtomicAmount, toRoute3Token.decimals).toFixed(), dexesLimit: CASHBACK_SWAP_MAX_DEXES, rpcUrl: tezos.rpc.getRpcUrl() }); - const templeOutputAtomic = tokensToAtoms( + const templeExpectedOutputAtomic = tokensToAtoms( new BigNumber(swapToTempleParams.output ?? ZERO), TEMPLE_TOKEN.decimals - ) - .multipliedBy(ROUTING_FEE_SLIPPAGE_RATIO) - .integerValue(BigNumber.ROUND_DOWN); + ); + const templeMinOutputAtomic = multiplyAtomicAmount( + templeExpectedOutputAtomic, + ROUTING_FEE_SLIPPAGE_RATIO, + BigNumber.ROUND_DOWN + ); const swapToTempleTokenOpParams = await getSwapParams( toRoute3Token, TEMPLE_TOKEN, - routingFeeFromOutputAtomic, - templeOutputAtomic, + outputFeeAtomicAmount, + templeExpectedOutputAtomic, + ROUTING_FEE_SLIPPAGE_RATIO, swapToTempleParams ); const routingFeeOpParams = await getRoutingFeeTransferParams( TEMPLE_TOKEN, - templeOutputAtomic.times(ROUTING_FEE_RATIO - SWAP_CASHBACK_RATIO).dividedToIntegerBy(ROUTING_FEE_RATIO), + templeMinOutputAtomic.times(ROUTING_FEE_RATIO - SWAP_CASHBACK_RATIO).dividedToIntegerBy(ROUTING_FEE_RATIO), publicKeyHash, BURN_ADDREESS, tezos @@ -603,7 +621,7 @@ export const SwapForm: FC = () => { diff --git a/src/lib/apis/route3/fetch-route3-swap-params.ts b/src/lib/apis/route3/fetch-route3-swap-params.ts index 544cdc576..3776a0047 100644 --- a/src/lib/apis/route3/fetch-route3-swap-params.ts +++ b/src/lib/apis/route3/fetch-route3-swap-params.ts @@ -1,26 +1,28 @@ -import axios from 'axios'; +import type { TezosToolkit } from '@taquito/taquito'; import BigNumber from 'bignumber.js'; import { intersection, transform } from 'lodash'; import memoizee from 'memoizee'; -import { THREE_ROUTE_SIRS_TOKEN, THREE_ROUTE_TEZ_TOKEN } from 'lib/assets/three-route-tokens'; +import { THREE_ROUTE_SIRS_TOKEN, THREE_ROUTE_TEZ_TOKEN, THREE_ROUTE_TZBTC_TOKEN } from 'lib/assets/three-route-tokens'; import { LIQUIDITY_BAKING_DEX_ADDRESS } from 'lib/constants'; import { EnvVars } from 'lib/env'; -import { BLOCK_DURATION } from 'lib/fixed-times'; +import { SIRS_LIQUIDITY_SLIPPAGE_RATIO } from 'lib/route3/constants'; import { + Route3EmptyTreeNode, Route3LbSwapParamsRequest, Route3LiquidityBakingParamsResponse, Route3SwapParamsRequest, - Route3TraditionalSwapParamsResponse + Route3TraditionalSwapParamsResponse, + Route3TreeNodeType } from 'lib/route3/interfaces'; -import { ONE_MINUTE_S } from 'lib/utils/numbers'; +import { loadContract } from 'lib/temple/contract'; +import { ReactiveTezosToolkit } from 'lib/temple/front'; +import { atomsToTokens, loadFastRpcClient, tokensToAtoms } from 'lib/temple/helpers'; import { ROUTE3_BASE_URL } from './route3.api'; const parser = (origJSON: string): ReturnType => { - const stringedJSON = origJSON - .replace(/input":\s*([-+Ee0-9.]+)/g, 'input":"$1"') - .replace(/output":\s*([-+Ee0-9.]+)/g, 'output":"$1"'); + const stringedJSON = origJSON.replace(/(input|output|tokenInAmount|tokenOutAmount)":\s*([-+Ee0-9.]+)/g, '$1":"$2"'); return JSON.parse(stringedJSON); }; @@ -53,74 +55,129 @@ const fetchRoute3TraditionalSwapParams = ( .then(res => res.text()) .then(res => parser(res)); -const getLbSubsidyCausedXtzDeviation = memoizee( - async (rpcUrl: string) => { - const currentBlockRpcBaseURL = `${rpcUrl}/chains/main/blocks/head/context`; - const [{ data: constants }, { data: rawSirsDexBalance }] = await Promise.all([ - axios.get<{ minimal_block_delay: string; liquidity_baking_subsidy: string }>( - `${currentBlockRpcBaseURL}/constants` - ), - axios.get(`${currentBlockRpcBaseURL}/contracts/${LIQUIDITY_BAKING_DEX_ADDRESS}/balance`) - ]); - const { minimal_block_delay: blockDuration = String(BLOCK_DURATION), liquidity_baking_subsidy: lbSubsidyPerMin } = - constants; - const lbSubsidyPerBlock = Math.floor(Number(lbSubsidyPerMin) / Math.floor(ONE_MINUTE_S / Number(blockDuration))); - - return lbSubsidyPerBlock / Number(rawSirsDexBalance); - }, - { promise: true, maxAge: 1000 * ONE_MINUTE_S * 5 } +const getTezosToolkit = memoizee( + (rpcUrl: string) => new ReactiveTezosToolkit(loadFastRpcClient(rpcUrl), `${rpcUrl}_3route`), + { max: 2 } ); -const fetchRoute3LiquidityBakingParams = ( +export const getLbStorage = async (tezosOrRpc: string | TezosToolkit) => { + const tezos = typeof tezosOrRpc === 'string' ? getTezosToolkit(tezosOrRpc) : tezosOrRpc; + const contract = await loadContract(tezos, LIQUIDITY_BAKING_DEX_ADDRESS, false); + + return contract.storage<{ tokenPool: BigNumber; xtzPool: BigNumber; lqtTotal: BigNumber }>(); +}; + +const makeEmptyTreeNode = ( + tokenInId: number, + tokenOutId: number, + tokenInAmount: string, + tokenOutAmount: string +): Route3EmptyTreeNode => ({ + type: Route3TreeNodeType.Empty, + items: [], + dexId: null, + tokenInId, + tokenOutId, + tokenInAmount, + tokenOutAmount, + width: 0, + height: 0 +}); + +const fetchRoute3LiquidityBakingParams = async ( params: Route3LbSwapParamsRequest -): Promise => - fetch(`${ROUTE3_BASE_URL}/swap-sirs${getRoute3ParametrizedUrlPart(params)}`, { +): Promise => { + const { rpcUrl, toSymbol, toTokenDecimals } = params; + + if (params.fromSymbol === THREE_ROUTE_SIRS_TOKEN.symbol) { + const { tokenPool, xtzPool, lqtTotal } = await getLbStorage(params.rpcUrl); + const sirsAtomicAmount = tokensToAtoms(params.amount, THREE_ROUTE_SIRS_TOKEN.decimals); + const tzbtcAtomicAmount = sirsAtomicAmount + .times(tokenPool) + .times(SIRS_LIQUIDITY_SLIPPAGE_RATIO) + .dividedToIntegerBy(lqtTotal); + const xtzAtomicAmount = sirsAtomicAmount + .times(xtzPool) + .times(SIRS_LIQUIDITY_SLIPPAGE_RATIO) + .dividedToIntegerBy(lqtTotal); + const xtzInAmount = atomsToTokens(xtzAtomicAmount, THREE_ROUTE_TEZ_TOKEN.decimals).toFixed(); + const tzbtcInAmount = atomsToTokens(tzbtcAtomicAmount, THREE_ROUTE_TZBTC_TOKEN.decimals).toFixed(); + const [fromXtzSwapParams, fromTzbtcSwapParams] = await Promise.all([ + toSymbol === THREE_ROUTE_TEZ_TOKEN.symbol + ? { + input: xtzInAmount, + output: xtzInAmount, + hops: [], + tree: makeEmptyTreeNode(THREE_ROUTE_TEZ_TOKEN.id, THREE_ROUTE_TEZ_TOKEN.id, xtzInAmount, xtzInAmount) + } + : fetchRoute3TraditionalSwapParams({ + fromSymbol: THREE_ROUTE_TEZ_TOKEN.symbol, + toSymbol: toSymbol, + amount: xtzInAmount, + toTokenDecimals, + rpcUrl, + dexesLimit: params.xtzDexesLimit, + showTree: true + }), + toSymbol === THREE_ROUTE_TZBTC_TOKEN.symbol + ? { + input: tzbtcInAmount, + output: tzbtcInAmount, + hops: [], + tree: makeEmptyTreeNode( + THREE_ROUTE_TZBTC_TOKEN.id, + THREE_ROUTE_TZBTC_TOKEN.id, + tzbtcInAmount, + tzbtcInAmount + ) + } + : fetchRoute3TraditionalSwapParams({ + fromSymbol: THREE_ROUTE_TZBTC_TOKEN.symbol, + toSymbol: toSymbol, + amount: tzbtcInAmount, + toTokenDecimals, + rpcUrl, + dexesLimit: params.tzbtcDexesLimit, + showTree: true + }) + ]); + + if (fromTzbtcSwapParams.output === undefined || fromXtzSwapParams.output === undefined) { + return { + input: params.amount, + output: undefined, + tzbtcHops: [], + xtzHops: [], + tzbtcTree: makeEmptyTreeNode(THREE_ROUTE_TZBTC_TOKEN.id, -1, tzbtcInAmount, '0'), + xtzTree: makeEmptyTreeNode(THREE_ROUTE_TEZ_TOKEN.id, -1, xtzInAmount, '0') + }; + } + + return { + input: params.amount, + output: new BigNumber(fromTzbtcSwapParams.output).plus(fromXtzSwapParams.output).toFixed(), + tzbtcHops: fromTzbtcSwapParams.hops, + xtzHops: fromXtzSwapParams.hops, + tzbtcTree: fromTzbtcSwapParams.tree, + xtzTree: fromXtzSwapParams.tree + }; + } + + const originalResponse = await fetch(`${ROUTE3_BASE_URL}/swap-sirs${getRoute3ParametrizedUrlPart(params)}`, { headers: { Authorization: EnvVars.TEMPLE_WALLET_ROUTE3_AUTH_TOKEN } - }) - .then(res => res.text()) - .then(async res => { - const { rpcUrl, fromSymbol, toSymbol, toTokenDecimals } = params; - const originalParams: Route3LiquidityBakingParamsResponse = parser(res); - - if ( - fromSymbol !== THREE_ROUTE_SIRS_TOKEN.symbol || - toSymbol === THREE_ROUTE_TEZ_TOKEN.symbol || - originalParams.output === undefined - ) { - return originalParams; - } - - // SIRS -> not XTZ swaps are likely to fail with tez.subtraction_underflow error, preventing it - try { - const lbSubsidyCausedXtzDeviation = await getLbSubsidyCausedXtzDeviation(rpcUrl); - const initialXtzInput = new BigNumber(originalParams.xtzHops[0].tokenInAmount); - const correctedXtzInput = initialXtzInput.times(1 - lbSubsidyCausedXtzDeviation).integerValue(); - const initialOutput = new BigNumber(originalParams.output); - // The difference between inputs is usually pretty small, so we can use the following formula - const correctedOutput = initialOutput - .times(correctedXtzInput) - .div(initialXtzInput) - .decimalPlaces(toTokenDecimals, BigNumber.ROUND_FLOOR); - - return { - ...originalParams, - output: correctedOutput.toString(), - xtzHops: [ - { - ...originalParams.xtzHops[0], - tokenInAmount: correctedXtzInput.toFixed() - } - ].concat(originalParams.xtzHops.slice(1)) - }; - } catch (err) { - console.error(err); - return originalParams; - } - }); - -export const fetchRoute3SwapParams = ({ fromSymbol, toSymbol, dexesLimit, ...restParams }: Route3SwapParamsRequest) => { + }); + + return parser(await originalResponse.text()); +}; + +export const fetchRoute3SwapParams = ({ + fromSymbol, + toSymbol, + dexesLimit, + ...restParams +}: Omit) => { const isLbUnderlyingTokenSwap = intersection([fromSymbol, toSymbol], ['TZBTC', 'XTZ']).length > 0; return [fromSymbol, toSymbol].includes(THREE_ROUTE_SIRS_TOKEN.symbol) @@ -130,7 +187,8 @@ export const fetchRoute3SwapParams = ({ fromSymbol, toSymbol, dexesLimit, ...res // XTZ <-> SIRS and TZBTC <-> SIRS swaps have either XTZ or TZBTC hops, so a total number of hops cannot exceed the limit xtzDexesLimit: isLbUnderlyingTokenSwap ? dexesLimit : Math.ceil(dexesLimit / 2), tzbtcDexesLimit: isLbUnderlyingTokenSwap ? dexesLimit : Math.floor(dexesLimit / 2), + showTree: true, ...restParams }) - : fetchRoute3TraditionalSwapParams({ fromSymbol, toSymbol, dexesLimit, ...restParams }); + : fetchRoute3TraditionalSwapParams({ fromSymbol, toSymbol, dexesLimit, showTree: true, ...restParams }); }; diff --git a/src/lib/assets/three-route-tokens.ts b/src/lib/assets/three-route-tokens.ts index 9276f1d08..bad3d4761 100644 --- a/src/lib/assets/three-route-tokens.ts +++ b/src/lib/assets/three-route-tokens.ts @@ -17,3 +17,12 @@ export const THREE_ROUTE_TEZ_TOKEN: Route3Token = { tokenId: null, decimals: 6 }; + +export const THREE_ROUTE_TZBTC_TOKEN: Route3Token = { + id: 2, + symbol: 'TZBTC', + standard: Route3TokenStandardEnum.fa12, + contract: 'KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn', + tokenId: null, + decimals: 8 +}; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a7da07100..b0b619035 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -50,10 +50,10 @@ export const MAX_SHOW_AGREEMENTS_COUNTER = 1; const isMacOS = /Mac OS/.test(navigator.userAgent); export const searchHotkey = ` (${isMacOS ? '⌘' : 'Ctrl + '}K)`; -export const FEE_PER_GAS_UNIT = 0.1; - export const LIQUIDITY_BAKING_DEX_ADDRESS = 'KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5'; +export const FEE_PER_GAS_UNIT = 0.1; + export const THEME_COLOR_SEARCH_PARAM_NAME = 'tc'; export const FONT_SIZE_SEARCH_PARAM_NAME = 'fs'; export const LINE_HEIGHT_SEARCH_PARAM_NAME = 'lh'; diff --git a/src/lib/route3/constants.ts b/src/lib/route3/constants.ts index 7e7f0c341..f4ba995c1 100644 --- a/src/lib/route3/constants.ts +++ b/src/lib/route3/constants.ts @@ -2,10 +2,10 @@ import { Route3Token, Route3TokenStandardEnum } from 'lib/apis/route3/fetch-rout import { TempleToken } from 'lib/assets/known-tokens'; export const ROUTE3_CONTRACT = 'KT1V5XKmeypanMS9pR65REpqmVejWBZURuuT'; -export const LIQUIDITY_BAKING_PROXY_CONTRACT = 'KT1DJRF7pTocLsoVgA9KQPBtrDrbzNUceSFK'; export const BURN_ADDREESS = 'tz1burnburnburnburnburnburnburjAYjjX'; export const ROUTING_FEE_ADDRESS = 'tz1UbRzhYjQKTtWYvGUWcRtVT4fN3NESDVYT'; +export const SIRS_LIQUIDITY_SLIPPAGE_RATIO = 0.9999; export const ROUTING_FEE_RATIO = 0.006; export const SWAP_CASHBACK_RATIO = 0.003; export const ROUTING_FEE_SLIPPAGE_RATIO = 0.995; diff --git a/src/lib/route3/interfaces/index.ts b/src/lib/route3/interfaces/index.ts index e288714e1..3aafe3f73 100644 --- a/src/lib/route3/interfaces/index.ts +++ b/src/lib/route3/interfaces/index.ts @@ -10,7 +10,6 @@ export interface Route3SwapParamsRequestRaw { rpcUrl: string; } -// TODO: add `showTree: boolean` when adding route view interface Route3SwapParamsRequestBase { fromSymbol: string; toSymbol: string; @@ -18,6 +17,8 @@ interface Route3SwapParamsRequestBase { amount: string; /** Needed to make a correction of params if input is SIRS */ rpcUrl: string; + /** 3route API does not require it but the extension needs swaps trees */ + showTree: true; } export interface Route3SwapParamsRequest extends Route3SwapParamsRequestBase { @@ -45,22 +46,26 @@ export interface Route3Hop { params: string | null; } -export interface Route3TraditionalSwapParamsResponse { - input: string | undefined; - output: string | undefined; +export interface Route3SwapHops { hops: Route3Hop[]; + tree: Route3TreeNode; } -export interface Route3LiquidityBakingParamsResponse { +interface Route3SwapParamsResponseBase { input: string | undefined; output: string | undefined; +} + +export interface Route3TraditionalSwapParamsResponse extends Route3SwapHops, Route3SwapParamsResponseBase {} + +export interface Route3LiquidityBakingHops { tzbtcHops: Route3Hop[]; xtzHops: Route3Hop[]; + tzbtcTree: Route3TreeNode; + xtzTree: Route3TreeNode; } -export type Route3SwapHops = Pick; - -export type Route3LiquidityBakingHops = Pick; +export interface Route3LiquidityBakingParamsResponse extends Route3LiquidityBakingHops, Route3SwapParamsResponseBase {} export type Route3SwapParamsResponse = Route3TraditionalSwapParamsResponse | Route3LiquidityBakingParamsResponse; @@ -69,3 +74,44 @@ export const isSwapHops = (hops: Route3SwapHops | Route3LiquidityBakingHops): ho export const isLiquidityBakingParamsResponse = ( response: Route3SwapParamsResponse ): response is Route3LiquidityBakingParamsResponse => 'tzbtcHops' in response && 'xtzHops' in response; + +export enum Route3TreeNodeType { + Empty = 'Empty', + High = 'High', + Dex = 'Dex', + Wide = 'Wide' +} + +interface Route3TreeNodeBase { + type: Route3TreeNodeType; + tokenInId: number; + tokenOutId: number; + tokenInAmount: string; + tokenOutAmount: string; + width: number; + height: number; + items: Route3NonEmptyNode[] | null; + dexId: number | null; +} + +export interface Route3EmptyTreeNode extends Route3TreeNodeBase { + type: Route3TreeNodeType.Empty; + items: []; + dexId: null; +} + +interface Route3NonTerminalTreeNode extends Route3TreeNodeBase { + type: Route3TreeNodeType.High | Route3TreeNodeType.Wide; + items: Route3NonEmptyNode[]; + dexId: null; +} + +interface Route3DexTreeNode extends Route3TreeNodeBase { + type: Route3TreeNodeType.Dex; + items: null; + dexId: number; +} + +type Route3NonEmptyNode = Route3NonTerminalTreeNode | Route3DexTreeNode; + +type Route3TreeNode = Route3EmptyTreeNode | Route3NonEmptyNode; diff --git a/src/lib/temple/back/dryrun.ts b/src/lib/temple/back/dryrun.ts index 783f29789..4d7eab325 100644 --- a/src/lib/temple/back/dryrun.ts +++ b/src/lib/temple/back/dryrun.ts @@ -1,6 +1,6 @@ import { localForger } from '@taquito/local-forging'; import { ForgeOperationsParams } from '@taquito/rpc'; -import { Estimate, TezosToolkit } from '@taquito/taquito'; +import { Estimate, TezosOperationError, TezosToolkit } from '@taquito/taquito'; import { FEE_PER_GAS_UNIT } from 'lib/constants'; import { formatOpParamsBeforeSend, michelEncoder, loadFastRpcClient } from 'lib/temple/helpers'; @@ -11,6 +11,8 @@ type DryRunParams = { networkRpc: string; sourcePkh: string; sourcePublicKey: string; + attemptCounter?: number; + prevFailedOperationIndex?: number; }; export interface DryRunResult { @@ -27,7 +29,9 @@ export async function dryRunOpParams({ opParams, networkRpc, sourcePkh, - sourcePublicKey + sourcePublicKey, + attemptCounter = 0, + prevFailedOperationIndex = -1 }: DryRunParams): Promise { try { const tezos = new TezosToolkit(loadFastRpcClient(networkRpc)); @@ -44,32 +48,63 @@ export async function dryRunOpParams({ let error: any = []; try { const formatted = opParams.map(operation => formatOpParamsBeforeSend(operation, sourcePkh)); - const result = [ - await tezos.estimate.batch(formatted).catch(e => ({ ...e, isError: true })), - await tezos.contract - .batch(formatted) - .send() - .catch(e => ({ ...e, isError: true })) - ]; - if (result.every(x => x.isError)) { - error = result; + const [estimationResult] = await Promise.allSettled([tezos.estimate.batch(formatted)]); + const [contractBatchResult] = await Promise.allSettled([tezos.contract.batch(formatted).send()]); + if (estimationResult.status === 'rejected' && contractBatchResult.status === 'rejected') { + if ( + estimationResult.reason instanceof TezosOperationError && + estimationResult.reason.errors.some(error => error.id.includes('gas_exhausted')) + ) { + const { operationsWithResults } = estimationResult.reason; + const firstSkippedOperationIndex = operationsWithResults.findIndex( + op => + 'metadata' in op && 'operation_result' in op.metadata && op.metadata.operation_result.status === 'skipped' + ); + // An internal operation of this operation may be marked as failed but this one as backtracked + const failedOperationIndex = + firstSkippedOperationIndex === -1 ? operationsWithResults.length - 1 : firstSkippedOperationIndex - 1; + const failedOperationWithResult = operationsWithResults[failedOperationIndex]; + if ('gas_limit' in failedOperationWithResult) { + const newOpParams = Array.from(opParams); + newOpParams[failedOperationIndex].gasLimit = + Math.max(opParams[failedOperationIndex].gasLimit ?? 0, Number(failedOperationWithResult.gas_limit)) * 2; + + if (attemptCounter < 3) { + return dryRunOpParams({ + opParams: newOpParams, + networkRpc, + sourcePkh, + sourcePublicKey, + attemptCounter: failedOperationIndex > prevFailedOperationIndex ? 0 : attemptCounter + 1, + prevFailedOperationIndex: Math.max(failedOperationIndex, prevFailedOperationIndex) + }); + } + } + } + error = [ + { ...estimationResult.reason, isError: true }, + { ...contractBatchResult.reason, isError: true } + ]; + } + + if (estimationResult.status === 'fulfilled') { + estimates = estimationResult.value.map( + (e, i) => + ({ + ...e, + burnFeeMutez: e.burnFeeMutez, + consumedMilligas: e.consumedMilligas, + gasLimit: e.gasLimit, + minimalFeeMutez: e.minimalFeeMutez, + suggestedFeeMutez: + e.suggestedFeeMutez + + (opParams[i]?.gasLimit ? Math.ceil((opParams[i].gasLimit - e.gasLimit) * FEE_PER_GAS_UNIT) : 0), + storageLimit: opParams[i]?.storageLimit ? +opParams[i].storageLimit : e.storageLimit, + totalCost: e.totalCost, + usingBaseFeeMutez: e.usingBaseFeeMutez + } as Estimate) + ); } - estimates = result[0]?.map( - (e: any, i: number) => - ({ - ...e, - burnFeeMutez: e.burnFeeMutez, - consumedMilligas: e.consumedMilligas, - gasLimit: e.gasLimit, - minimalFeeMutez: e.minimalFeeMutez, - storageLimit: opParams[i]?.storageLimit ? +opParams[i].storageLimit : e.storageLimit, - suggestedFeeMutez: - e.suggestedFeeMutez + - (opParams[i]?.gasLimit ? Math.ceil((opParams[i].gasLimit - e.gasLimit) * FEE_PER_GAS_UNIT) : 0), - totalCost: e.totalCost, - usingBaseFeeMutez: e.usingBaseFeeMutez - } as Estimate) - ); } catch {} if (bytesToSign && estimates) { diff --git a/src/lib/utils/swap.utils.ts b/src/lib/utils/swap.utils.ts index df1ace906..4861ed67a 100644 --- a/src/lib/utils/swap.utils.ts +++ b/src/lib/utils/swap.utils.ts @@ -1,85 +1,49 @@ -import { ContractMethodObject, ContractProvider, OpKind, TezosToolkit, TransferParams, Wallet } from '@taquito/taquito'; +import { isDefined } from '@rnw-community/shared'; +import { TezosToolkit, TransferParams } from '@taquito/taquito'; import { BigNumber } from 'bignumber.js'; +import { getLbStorage } from 'lib/apis/route3/fetch-route3-swap-params'; import { Route3Token } from 'lib/apis/route3/fetch-route3-tokens'; -import { FEE_PER_GAS_UNIT } from 'lib/constants'; +import { THREE_ROUTE_SIRS_TOKEN, THREE_ROUTE_TEZ_TOKEN, THREE_ROUTE_TZBTC_TOKEN } from 'lib/assets/three-route-tokens'; +import { LIQUIDITY_BAKING_DEX_ADDRESS } from 'lib/constants'; import { APP_ID, ATOMIC_INPUT_THRESHOLD_FOR_FEE_FROM_INPUT, - LIQUIDITY_BAKING_PROXY_CONTRACT, ROUTE3_CONTRACT, ROUTING_FEE_RATIO, + SIRS_LIQUIDITY_SLIPPAGE_RATIO, SWAP_CASHBACK_RATIO } from 'lib/route3/constants'; import { isSwapHops, Route3LiquidityBakingHops, Route3SwapHops } from 'lib/route3/interfaces'; import { isRoute3GasToken } from 'lib/route3/utils/assets.utils'; import { mapToRoute3ExecuteHops } from 'lib/route3/utils/map-to-route3-hops'; import { loadContract } from 'lib/temple/contract'; +import { tokensToAtoms } from 'lib/temple/helpers'; import { getTransferPermissions } from './get-transfer-permissions'; -import { ZERO } from './numbers'; - -const GAS_CAP_PER_INTERNAL_OPERATION = 1000; - -const isSwapTransaction = (params: TransferParams) => - params.to === ROUTE3_CONTRACT || params.to === LIQUIDITY_BAKING_PROXY_CONTRACT; - -/** - * Estimates a batch of transfers and applies the estimations to the transfer params. If the estimation fails, - * the transfer params are returned as is. - * @param transfersParams The transfer params to estimate and apply the estimations to. - * @param tezos The TezosToolkit instance to use for the estimations. - * @param sourcePkh The public key hash of the sender. - * @param gasCapFn A function that returns the gas cap for a given transfer params. - */ -const withBatchEstimations = async ( - transfersParams: TransferParams[], - tezos: TezosToolkit, - sourcePkh: string, - gasCapFn: (params: TransferParams) => number = () => GAS_CAP_PER_INTERNAL_OPERATION -) => { - if (transfersParams.length === 0) { - return []; - } - - try { - const estimations = await tezos.estimate.batch( - transfersParams.map(params => ({ kind: OpKind.TRANSACTION, source: sourcePkh, ...params })) - ); - - return transfersParams.map((params, index) => { - const { suggestedFeeMutez, storageLimit, gasLimit } = estimations[index]; - const gasCap = gasCapFn(params); - - return { - ...params, - fee: suggestedFeeMutez + Math.ceil(gasCap * FEE_PER_GAS_UNIT), - storageLimit, - gasLimit: gasLimit + gasCap - }; - }); - } catch (e) { - console.error(e); - - return transfersParams; - } -}; +import { ONE_MINUTE_S, ZERO } from './numbers'; export const getSwapTransferParams = async ( fromRoute3Token: Route3Token, toRoute3Token: Route3Token, inputAmountAtomic: BigNumber, - minimumReceivedAtomic: BigNumber, + expectedReceivedAtomic: BigNumber, + slippageRatio: number, chains: Route3LiquidityBakingHops | Route3SwapHops, tezos: TezosToolkit, accountPkh: string ) => { - const swapParams: Array = []; - let swapMethod: ContractMethodObject; + const minimumReceivedAtomic = multiplyAtomicAmount(expectedReceivedAtomic, slippageRatio, BigNumber.ROUND_FLOOR); + let burnSirsBeforeEstimateParams: TransferParams[] = []; + let approvesBeforeEstimateParams: TransferParams[]; + let swapBeforeEstimateParams: TransferParams[]; + let revokesBeforeEstimateParams: TransferParams[]; + let mintSirsBeforeEstimateParams: TransferParams[] = []; + const swapContract = await loadContract(tezos, ROUTE3_CONTRACT, false); + const lbDexContract = await loadContract(tezos, LIQUIDITY_BAKING_DEX_ADDRESS, false); if (isSwapHops(chains)) { - const swapContract = await loadContract(tezos, ROUTE3_CONTRACT, false); - swapMethod = swapContract.methodsObject.execute({ + const swapMethod = swapContract.methodsObject.execute({ token_in_id: fromRoute3Token.id, token_out_id: toRoute3Token.id, min_out: minimumReceivedAtomic, @@ -87,63 +51,169 @@ export const getSwapTransferParams = async ( hops: mapToRoute3ExecuteHops(chains.hops), app_id: APP_ID }); - } else { - const liquidityBakingProxyContract = await loadContract(tezos, LIQUIDITY_BAKING_PROXY_CONTRACT, false); - swapMethod = liquidityBakingProxyContract.methodsObject.swap({ - token_in_id: fromRoute3Token.id, - token_out_id: toRoute3Token.id, - tez_hops: mapToRoute3ExecuteHops(chains.xtzHops), - tzbtc_hops: mapToRoute3ExecuteHops(chains.tzbtcHops), - amount_in: inputAmountAtomic, - min_out: minimumReceivedAtomic, - receiver: accountPkh, - app_id: APP_ID - }); - } - if (fromRoute3Token.symbol.toLowerCase() === 'xtz') { - swapParams.push( + swapBeforeEstimateParams = [ swapMethod.toTransferParams({ - amount: inputAmountAtomic.toNumber(), + amount: fromRoute3Token.symbol.toLowerCase() === 'xtz' ? inputAmountAtomic.toNumber() : 0, mutez: true }) + ]; + + const { approve, revoke } = await getTransferPermissions( + tezos, + ROUTE3_CONTRACT, + accountPkh, + fromRoute3Token, + inputAmountAtomic + ); + approvesBeforeEstimateParams = approve; + revokesBeforeEstimateParams = revoke; + } else if (fromRoute3Token.id === THREE_ROUTE_SIRS_TOKEN.id) { + const xtzFromBurnAmount = tokensToAtoms(chains.xtzTree.tokenInAmount, THREE_ROUTE_TEZ_TOKEN.decimals); + const tzbtcFromBurnAmount = tokensToAtoms(chains.tzbtcTree.tokenInAmount, THREE_ROUTE_TZBTC_TOKEN.decimals); + burnSirsBeforeEstimateParams = [ + lbDexContract.methodsObject + .removeLiquidity({ + to: accountPkh, + lqtBurned: inputAmountAtomic, + minXtzWithdrawn: xtzFromBurnAmount, + minTokensWithdrawn: tzbtcFromBurnAmount, + deadline: Math.floor(Date.now() / 1000) + ONE_MINUTE_S + }) + .toTransferParams() + ]; + + const { approve: approveTzbtc, revoke: revokeTzbtc } = await getTransferPermissions( + tezos, + ROUTE3_CONTRACT, + accountPkh, + THREE_ROUTE_TZBTC_TOKEN, + tzbtcFromBurnAmount ); + approvesBeforeEstimateParams = approveTzbtc; + revokesBeforeEstimateParams = revokeTzbtc; + swapBeforeEstimateParams = []; + const xtzSwapOut = tokensToAtoms(chains.xtzTree.tokenOutAmount, toRoute3Token.decimals); + const tzbtcSwapOut = tokensToAtoms(chains.tzbtcTree.tokenOutAmount, toRoute3Token.decimals); + if (chains.xtzHops.length > 0) { + const xtzSwapMethod = swapContract.methodsObject.execute({ + token_in_id: THREE_ROUTE_TEZ_TOKEN.id, + token_out_id: toRoute3Token.id, + min_out: multiplyAtomicAmount(xtzSwapOut, slippageRatio, BigNumber.ROUND_FLOOR), + receiver: accountPkh, + hops: mapToRoute3ExecuteHops(chains.xtzHops), + app_id: APP_ID + }); + swapBeforeEstimateParams.push( + xtzSwapMethod.toTransferParams({ amount: Number(chains.xtzTree.tokenInAmount), mutez: false }) + ); + } + if (chains.tzbtcHops.length > 0) { + const tzbtcSwapMethod = swapContract.methodsObject.execute({ + token_in_id: THREE_ROUTE_TZBTC_TOKEN.id, + token_out_id: toRoute3Token.id, + min_out: multiplyAtomicAmount(tzbtcSwapOut, slippageRatio, BigNumber.ROUND_FLOOR), + receiver: accountPkh, + hops: mapToRoute3ExecuteHops(chains.tzbtcHops), + app_id: APP_ID + }); + swapBeforeEstimateParams.push(tzbtcSwapMethod.toTransferParams()); + } } else { - swapParams.push(swapMethod.toTransferParams()); + const { approve: approveInputToken, revoke: revokeInputToken } = await getTransferPermissions( + tezos, + ROUTE3_CONTRACT, + accountPkh, + fromRoute3Token, + inputAmountAtomic + ); + approvesBeforeEstimateParams = approveInputToken; + revokesBeforeEstimateParams = revokeInputToken; + swapBeforeEstimateParams = []; + const xtzSwapOut = tokensToAtoms(chains.xtzTree.tokenOutAmount, THREE_ROUTE_TEZ_TOKEN.decimals); + const tzbtcSwapOut = tokensToAtoms(chains.tzbtcTree.tokenOutAmount, THREE_ROUTE_TZBTC_TOKEN.decimals); + const xtzIsSwapped = chains.xtzHops.length > 0; + const tzbtcIsSwapped = chains.tzbtcHops.length > 0; + const xtzSwapMinOut = xtzIsSwapped + ? multiplyAtomicAmount(xtzSwapOut, slippageRatio, BigNumber.ROUND_FLOOR) + : xtzSwapOut; + const tzbtcAddLiqInput = tzbtcIsSwapped + ? multiplyAtomicAmount(tzbtcSwapOut, slippageRatio, BigNumber.ROUND_FLOOR) + : tzbtcSwapOut; + if (xtzIsSwapped) { + const xtzSwapMethod = swapContract.methodsObject.execute({ + token_in_id: fromRoute3Token.id, + token_out_id: THREE_ROUTE_TEZ_TOKEN.id, + min_out: xtzSwapMinOut, + receiver: accountPkh, + hops: mapToRoute3ExecuteHops(chains.xtzHops), + app_id: APP_ID + }); + swapBeforeEstimateParams.push(xtzSwapMethod.toTransferParams()); + } + if (tzbtcIsSwapped) { + const tzbtcSwapMethod = swapContract.methodsObject.execute({ + token_in_id: fromRoute3Token.id, + token_out_id: THREE_ROUTE_TZBTC_TOKEN.id, + min_out: tzbtcAddLiqInput, + receiver: accountPkh, + hops: mapToRoute3ExecuteHops(chains.tzbtcHops), + app_id: APP_ID + }); + swapBeforeEstimateParams.push( + tzbtcSwapMethod.toTransferParams({ + amount: fromRoute3Token.id === THREE_ROUTE_TEZ_TOKEN.id ? Number(chains.tzbtcTree.tokenInAmount) : 0, + mutez: false + }) + ); + } + + const { approve: approveTzbtc, revoke: revokeTzbtc } = await getTransferPermissions( + tezos, + LIQUIDITY_BAKING_DEX_ADDRESS, + accountPkh, + THREE_ROUTE_TZBTC_TOKEN, + tzbtcAddLiqInput + ); + // Prevent extra TEZ spending + const { xtzPool, lqtTotal } = await getLbStorage(tezos); + const xtzAddLiqInput = BigNumber.min( + xtzSwapMinOut, + xtzPool + .times(expectedReceivedAtomic) + .div(lqtTotal) + .div(SIRS_LIQUIDITY_SLIPPAGE_RATIO) + .integerValue(BigNumber.ROUND_CEIL) + ); + mintSirsBeforeEstimateParams = approveTzbtc.concat( + lbDexContract.methodsObject + .addLiquidity({ + owner: accountPkh, + minLqtMinted: minimumReceivedAtomic, + maxTokensDeposited: tzbtcAddLiqInput, + deadline: Math.floor(Date.now() / 1000) + ONE_MINUTE_S + }) + .toTransferParams({ amount: xtzAddLiqInput.toNumber(), mutez: true }), + revokeTzbtc + ); } - const { approve, revoke } = await getTransferPermissions( - tezos, - isSwapHops(chains) ? ROUTE3_CONTRACT : LIQUIDITY_BAKING_PROXY_CONTRACT, - accountPkh, - fromRoute3Token, - inputAmountAtomic + return burnSirsBeforeEstimateParams.concat( + approvesBeforeEstimateParams, + swapBeforeEstimateParams, + mintSirsBeforeEstimateParams, + revokesBeforeEstimateParams ); - - const [swapWithApproveParams, revokeParams] = await Promise.all([ - withBatchEstimations(approve.concat(swapParams), tezos, accountPkh, params => { - const approximateInternalOperationsCount = isSwapTransaction(params) - ? isSwapHops(chains) - ? 1 + chains.hops.length - : 2 + chains.xtzHops.length + chains.tzbtcHops.length - : 1; - - return approximateInternalOperationsCount * GAS_CAP_PER_INTERNAL_OPERATION; - }), - withBatchEstimations(revoke, tezos, accountPkh) - ]); - - return swapWithApproveParams.concat(revokeParams); }; export const calculateSidePaymentsFromInput = (inputAmount: BigNumber | undefined) => { const swapInputAtomic = (inputAmount ?? ZERO).integerValue(BigNumber.ROUND_DOWN); const shouldTakeFeeFromInput = swapInputAtomic.gte(ATOMIC_INPUT_THRESHOLD_FOR_FEE_FROM_INPUT); const inputFeeAtomic = shouldTakeFeeFromInput - ? swapInputAtomic.times(ROUTING_FEE_RATIO).integerValue(BigNumber.ROUND_CEIL) + ? multiplyAtomicAmount(swapInputAtomic, ROUTING_FEE_RATIO, BigNumber.ROUND_CEIL) : ZERO; const cashbackSwapInputAtomic = shouldTakeFeeFromInput - ? swapInputAtomic.times(SWAP_CASHBACK_RATIO).integerValue() + ? multiplyAtomicAmount(swapInputAtomic, SWAP_CASHBACK_RATIO) : ZERO; const swapInputMinusFeeAtomic = swapInputAtomic.minus(inputFeeAtomic); @@ -154,15 +224,46 @@ export const calculateSidePaymentsFromInput = (inputAmount: BigNumber | undefine }; }; -export const calculateOutputFeeAtomic = (inputAmount: BigNumber | undefined, outputAmount: BigNumber) => { +const calculateOutputFeeAtomic = (inputAmount: BigNumber | undefined, outputAmount: BigNumber) => { const swapInputAtomic = (inputAmount ?? ZERO).integerValue(BigNumber.ROUND_DOWN); return swapInputAtomic.gte(ATOMIC_INPUT_THRESHOLD_FOR_FEE_FROM_INPUT) ? ZERO - : outputAmount.times(ROUTING_FEE_RATIO).integerValue(BigNumber.ROUND_CEIL); + : multiplyAtomicAmount(outputAmount, ROUTING_FEE_RATIO, BigNumber.ROUND_CEIL); }; -const getRoutingFeeTransferParamsBeforeEstimate = async ( +export const calculateOutputAmounts = ( + inputAmount: BigNumber.Value | undefined, + inputAssetDecimals: number, + route3OutputInTokens: string | undefined, + outputAssetDecimals: number, + slippageRatio: number +) => { + const outputAtomicAmountBeforeFee = isDefined(route3OutputInTokens) + ? tokensToAtoms(new BigNumber(route3OutputInTokens), outputAssetDecimals) + : ZERO; + const minOutputAtomicBeforeFee = multiplyAtomicAmount( + outputAtomicAmountBeforeFee, + slippageRatio, + BigNumber.ROUND_FLOOR + ); + const outputFeeAtomicAmount = calculateOutputFeeAtomic( + tokensToAtoms(inputAmount ?? ZERO, inputAssetDecimals), + minOutputAtomicBeforeFee + ); + const expectedReceivedAtomic = outputAtomicAmountBeforeFee.minus(outputFeeAtomicAmount); + const minimumReceivedAtomic = minOutputAtomicBeforeFee.minus(outputFeeAtomicAmount); + + return { outputAtomicAmountBeforeFee, expectedReceivedAtomic, minimumReceivedAtomic, outputFeeAtomicAmount }; +}; + +export const multiplyAtomicAmount = ( + amount: BigNumber, + multiplier: BigNumber.Value, + roundMode?: BigNumber.RoundingMode +) => amount.times(multiplier).integerValue(roundMode); + +export const getRoutingFeeTransferParams = async ( token: Route3Token, feeAmountAtomic: BigNumber, senderPublicKeyHash: string, @@ -213,22 +314,3 @@ const getRoutingFeeTransferParamsBeforeEstimate = async ( return []; }; - -export const getRoutingFeeTransferParams = async ( - token: Route3Token, - feeAmountAtomic: BigNumber, - senderPublicKeyHash: string, - routingFeeAddress: string, - tezos: TezosToolkit -) => - withBatchEstimations( - await getRoutingFeeTransferParamsBeforeEstimate( - token, - feeAmountAtomic, - senderPublicKeyHash, - routingFeeAddress, - tezos - ), - tezos, - senderPublicKeyHash - );