From e3312a65a35bfbee1741643577a9c8918e3b125a Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Fri, 13 Dec 2024 12:56:36 +0200 Subject: [PATCH 01/10] TW-1615 Make limits for dexes amount in a swap stricter --- src/app/templates/SwapForm/SwapForm.tsx | 26 ++++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/app/templates/SwapForm/SwapForm.tsx b/src/app/templates/SwapForm/SwapForm.tsx index 474d26a69..de8cd6a7e 100644 --- a/src/app/templates/SwapForm/SwapForm.tsx +++ b/src/app/templates/SwapForm/SwapForm.tsx @@ -58,10 +58,10 @@ import { slippageToleranceInputValidationFn } from './SwapFormInput/SlippageTole import { SwapFormInput } from './SwapFormInput/SwapFormInput'; import { SwapMinimumReceived } from './SwapMinimumReceived/SwapMinimumReceived'; -// These values have been set after some experimentation. They are different to the respective values in -// templewallet-mobile because the mobile app still uses taquito v19.0.0, which has a different gas estimation algorithm. -const SINGLE_SWAP_IN_BATCH_MAX_DEXES = 12; -const LB_OPERATION_DEXES_COST = 3; +const CASHBACK_SWAP_MAX_DEXES = 3; +// Actually, at most 2 dexes for each of underlying SIRS -> tzBTC -> X swap and SIRS -> XTZ -> X swap +const MAIN_SIRS_SWAP_MAX_DEXES = 4; +const MAIN_NON_SIRS_SWAP_MAX_DEXES = 3; export const SwapForm: FC = () => { const dispatch = useDispatch(); @@ -134,17 +134,13 @@ export const SwapForm: FC = () => { const isOutputTokenTempleToken = outputAssetSlug === KNOWN_TOKENS_SLUGS.TEMPLE; const isSirsSwap = inputAssetSlug === KNOWN_TOKENS_SLUGS.SIRS || outputAssetSlug === KNOWN_TOKENS_SLUGS.SIRS; const isSwapAmountMoreThreshold = inputAmountInUsd.isGreaterThanOrEqualTo(SWAP_THRESHOLD_TO_GET_CASHBACK); - const totalMaxDexes = SINGLE_SWAP_IN_BATCH_MAX_DEXES - (isSirsSwap ? LB_OPERATION_DEXES_COST : 0); - const cashbackSwapMaxDexes = Math.ceil(totalMaxDexes / (isSirsSwap ? 3 : 2)); - const mainSwapMaxDexes = - totalMaxDexes - (isSwapAmountMoreThreshold && !isInputTokenTempleToken ? cashbackSwapMaxDexes : 0); + const mainSwapMaxDexes = isSirsSwap ? MAIN_SIRS_SWAP_MAX_DEXES : MAIN_NON_SIRS_SWAP_MAX_DEXES; return { isInputTokenTempleToken, isOutputTokenTempleToken, isSwapAmountMoreThreshold, - mainSwapMaxDexes, - cashbackSwapMaxDexes + mainSwapMaxDexes }; }, [allUsdToTokenRates] @@ -321,8 +317,10 @@ export const SwapForm: FC = () => { return; } - const { isInputTokenTempleToken, isOutputTokenTempleToken, isSwapAmountMoreThreshold, cashbackSwapMaxDexes } = - getSwapWithFeeParams(inputValue, outputValue); + const { isInputTokenTempleToken, isOutputTokenTempleToken, isSwapAmountMoreThreshold } = getSwapWithFeeParams( + inputValue, + outputValue + ); if (isInputTokenTempleToken && isSwapAmountMoreThreshold) { const routingInputFeeOpParams = await getRoutingFeeTransferParams( @@ -348,7 +346,7 @@ export const SwapForm: FC = () => { toSymbol: TEMPLE_TOKEN.symbol, toTokenDecimals: TEMPLE_TOKEN.decimals, amount: atomsToTokens(routingFeeFromInputAtomic, fromRoute3Token.decimals).toFixed(), - dexesLimit: cashbackSwapMaxDexes, + dexesLimit: CASHBACK_SWAP_MAX_DEXES, rpcUrl: tezos.rpc.getRpcUrl() }); @@ -393,7 +391,7 @@ export const SwapForm: FC = () => { toSymbol: TEMPLE_TOKEN.symbol, toTokenDecimals: TEMPLE_TOKEN.decimals, amount: atomsToTokens(routingFeeFromOutputAtomic, toRoute3Token.decimals).toFixed(), - dexesLimit: cashbackSwapMaxDexes, + dexesLimit: CASHBACK_SWAP_MAX_DEXES, rpcUrl: tezos.rpc.getRpcUrl() }); From 5f55cbb662e891d7ad553ee3dafac7865381eff3 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Wed, 18 Dec 2024 16:12:07 +0200 Subject: [PATCH 02/10] TW-1622 Implement swapping without 3Route liquidity baking proxy --- src/app/hooks/use-swap.ts | 6 +- src/app/store/swap/state.mock.ts | 19 +- src/app/templates/SwapForm/SwapForm.tsx | 92 ++++--- .../apis/route3/fetch-route3-swap-params.ts | 58 +++- src/lib/assets/three-route-tokens.ts | 9 + src/lib/route3/interfaces/index.ts | 62 ++++- src/lib/utils/swap.utils.ts | 249 ++++++++++++++---- 7 files changed, 387 insertions(+), 108 deletions(-) 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/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..113add3f6 100644 --- a/src/lib/apis/route3/fetch-route3-swap-params.ts +++ b/src/lib/apis/route3/fetch-route3-swap-params.ts @@ -11,16 +11,16 @@ import { Route3LbSwapParamsRequest, Route3LiquidityBakingParamsResponse, Route3SwapParamsRequest, - Route3TraditionalSwapParamsResponse + Route3TraditionalSwapParamsResponse, + Route3TreeNode, + Route3TreeNodeType } from 'lib/route3/interfaces'; import { ONE_MINUTE_S } from 'lib/utils/numbers'; 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); }; @@ -71,6 +71,34 @@ const getLbSubsidyCausedXtzDeviation = memoizee( { promise: true, maxAge: 1000 * ONE_MINUTE_S * 5 } ); +const correctFinalOutput = (tree: T, multiplier: BigNumber, decimals: number): T => { + const correctedTokenOutAmount = new BigNumber(tree.tokenOutAmount) + .times(multiplier) + .decimalPlaces(decimals, BigNumber.ROUND_FLOOR) + .toFixed(); + + switch (tree.type) { + case Route3TreeNodeType.Empty: + case Route3TreeNodeType.Dex: + return { ...tree, tokenOutAmount: correctedTokenOutAmount }; + case Route3TreeNodeType.High: + return { + ...tree, + tokenOutAmount: correctedTokenOutAmount, + // TODO: Fix output value for the last item; the sum of outputs for all subitems should be equal to the output of the parent item + items: tree.items.map(item => correctFinalOutput(item, multiplier, decimals)) + }; + default: + return { + ...tree, + tokenOutAmount: correctedTokenOutAmount, + items: tree.items.map((item, index, subitems) => + index === subitems.length - 1 ? correctFinalOutput(item, multiplier, decimals) : item + ) + }; + } +}; + const fetchRoute3LiquidityBakingParams = ( params: Route3LbSwapParamsRequest ): Promise => @@ -98,21 +126,21 @@ const fetchRoute3LiquidityBakingParams = ( const initialXtzInput = new BigNumber(originalParams.xtzHops[0].tokenInAmount); const correctedXtzInput = initialXtzInput.times(1 - lbSubsidyCausedXtzDeviation).integerValue(); const initialOutput = new BigNumber(originalParams.output); + const multiplier = new BigNumber(correctedXtzInput).div(initialXtzInput); // 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); + const correctedOutput = initialOutput.times(multiplier).decimalPlaces(toTokenDecimals, BigNumber.ROUND_FLOOR); + const correctedXtzTree = correctFinalOutput(originalParams.xtzTree, multiplier, toTokenDecimals); return { ...originalParams, - output: correctedOutput.toString(), + output: correctedOutput.toFixed(), xtzHops: [ { ...originalParams.xtzHops[0], tokenInAmount: correctedXtzInput.toFixed() } - ].concat(originalParams.xtzHops.slice(1)) + ].concat(originalParams.xtzHops.slice(1)), + xtzTree: correctedXtzTree }; } catch (err) { console.error(err); @@ -120,7 +148,12 @@ const fetchRoute3LiquidityBakingParams = ( } }); -export const fetchRoute3SwapParams = ({ fromSymbol, toSymbol, dexesLimit, ...restParams }: Route3SwapParamsRequest) => { +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 +163,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/route3/interfaces/index.ts b/src/lib/route3/interfaces/index.ts index e288714e1..841c6630d 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; +} + +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; + +export type Route3TreeNode = Route3EmptyTreeNode | Route3NonEmptyNode; diff --git a/src/lib/utils/swap.utils.ts b/src/lib/utils/swap.utils.ts index df1ace906..954e04947 100644 --- a/src/lib/utils/swap.utils.ts +++ b/src/lib/utils/swap.utils.ts @@ -1,8 +1,10 @@ -import { ContractMethodObject, ContractProvider, OpKind, TezosToolkit, TransferParams, Wallet } from '@taquito/taquito'; +import { isDefined } from '@rnw-community/shared'; +import { OpKind, TezosToolkit, TransferParams } from '@taquito/taquito'; import { BigNumber } from 'bignumber.js'; 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 { FEE_PER_GAS_UNIT, LIQUIDITY_BAKING_DEX_ADDRESS } from 'lib/constants'; import { APP_ID, ATOMIC_INPUT_THRESHOLD_FOR_FEE_FROM_INPUT, @@ -15,9 +17,10 @@ import { isSwapHops, Route3LiquidityBakingHops, Route3SwapHops } from 'lib/route 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'; +import { ONE_MINUTE_S, ZERO } from './numbers'; const GAS_CAP_PER_INTERNAL_OPERATION = 1000; @@ -69,17 +72,23 @@ 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,50 +96,165 @@ 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); + const xtzSwapMinOut = multiplyAtomicAmount(xtzSwapOut, slippageRatio, BigNumber.ROUND_FLOOR); + const tzbtcSwapMinOut = multiplyAtomicAmount(tzbtcSwapOut, slippageRatio, BigNumber.ROUND_FLOOR); + 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: xtzSwapMinOut, + receiver: accountPkh, + hops: mapToRoute3ExecuteHops(chains.xtzHops), + app_id: APP_ID + }); + swapBeforeEstimateParams.push( + xtzSwapMethod.toTransferParams({ amount: Number(chains.xtzHops[0].tokenInAmount), mutez: true }) + ); + } + 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: tzbtcSwapMinOut, + 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 xtzAddLiqInput = 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: xtzAddLiqInput, + 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.tzbtcHops[0].tokenInAmount) : 0, + mutez: true + }) + ); + } - const { approve, revoke } = await getTransferPermissions( - tezos, - isSwapHops(chains) ? ROUTE3_CONTRACT : LIQUIDITY_BAKING_PROXY_CONTRACT, - accountPkh, - fromRoute3Token, - inputAmountAtomic - ); + const { approve: approveTzbtc, revoke: revokeTzbtc } = await getTransferPermissions( + tezos, + LIQUIDITY_BAKING_DEX_ADDRESS, + accountPkh, + THREE_ROUTE_TZBTC_TOKEN, + tzbtcAddLiqInput + ); + 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 [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) + withBatchEstimations( + burnSirsBeforeEstimateParams.concat( + approvesBeforeEstimateParams, + swapBeforeEstimateParams, + mintSirsBeforeEstimateParams + ), + tezos, + accountPkh, + params => { + const approximateInternalOperationsCount = isSwapTransaction(params) + ? isSwapHops(chains) + ? chains.hops.length + : chains.xtzHops.length + chains.tzbtcHops.length + : 1; + + return approximateInternalOperationsCount * GAS_CAP_PER_INTERNAL_OPERATION; + } + ), + withBatchEstimations(revokesBeforeEstimateParams, tezos, accountPkh) ]); return swapWithApproveParams.concat(revokeParams); @@ -140,10 +264,10 @@ export const calculateSidePaymentsFromInput = (inputAmount: BigNumber | undefine 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,14 +278,45 @@ 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); }; +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); + const getRoutingFeeTransferParamsBeforeEstimate = async ( token: Route3Token, feeAmountAtomic: BigNumber, From 504eb760b8db860a25d8f9ad80aaaaa4372b6bb1 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Wed, 18 Dec 2024 17:01:18 +0200 Subject: [PATCH 03/10] TW-1622 Minor refactoring --- src/lib/utils/swap.utils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/utils/swap.utils.ts b/src/lib/utils/swap.utils.ts index 954e04947..c0db99686 100644 --- a/src/lib/utils/swap.utils.ts +++ b/src/lib/utils/swap.utils.ts @@ -140,13 +140,11 @@ export const getSwapTransferParams = async ( swapBeforeEstimateParams = []; const xtzSwapOut = tokensToAtoms(chains.xtzTree.tokenOutAmount, toRoute3Token.decimals); const tzbtcSwapOut = tokensToAtoms(chains.tzbtcTree.tokenOutAmount, toRoute3Token.decimals); - const xtzSwapMinOut = multiplyAtomicAmount(xtzSwapOut, slippageRatio, BigNumber.ROUND_FLOOR); - const tzbtcSwapMinOut = multiplyAtomicAmount(tzbtcSwapOut, slippageRatio, BigNumber.ROUND_FLOOR); 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: xtzSwapMinOut, + min_out: multiplyAtomicAmount(xtzSwapOut, slippageRatio, BigNumber.ROUND_FLOOR), receiver: accountPkh, hops: mapToRoute3ExecuteHops(chains.xtzHops), app_id: APP_ID @@ -159,7 +157,7 @@ export const getSwapTransferParams = async ( const tzbtcSwapMethod = swapContract.methodsObject.execute({ token_in_id: THREE_ROUTE_TZBTC_TOKEN.id, token_out_id: toRoute3Token.id, - min_out: tzbtcSwapMinOut, + min_out: multiplyAtomicAmount(tzbtcSwapOut, slippageRatio, BigNumber.ROUND_FLOOR), receiver: accountPkh, hops: mapToRoute3ExecuteHops(chains.tzbtcHops), app_id: APP_ID From 91cc115ee784ed92edbd572793de839cf71ce584 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Mon, 23 Dec 2024 12:30:40 +0200 Subject: [PATCH 04/10] TW-1622 Fix SIRS swaps logic --- src/app/templates/InternalConfirmation.tsx | 39 ++-- .../SwapForm/SwapFormInput/SwapFormInput.tsx | 2 +- .../apis/route3/fetch-route3-swap-params.ts | 206 ++++++++++-------- src/lib/constants.ts | 2 +- src/lib/route3/constants.ts | 1 + src/lib/route3/interfaces/index.ts | 2 +- src/lib/temple/back/dryrun.ts | 67 +++--- src/lib/utils/swap.utils.ts | 123 +++-------- 8 files changed, 207 insertions(+), 235 deletions(-) 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/SwapFormInput/SwapFormInput.tsx b/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx index 63f290f05..8eec8160c 100644 --- a/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx +++ b/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx @@ -29,7 +29,7 @@ import { AssetOption } from './AssetsMenu/AssetOption'; import { PercentageButton } from './PercentageButton/PercentageButton'; import { SwapFormInputProps } from './SwapFormInput.props'; -const EXCHANGE_XTZ_RESERVE = new BigNumber('0.3'); +const EXCHANGE_XTZ_RESERVE = new BigNumber('0'); const PERCENTAGE_BUTTONS = [25, 50, 75, 100]; const LEADING_ASSETS = [TEZ_TOKEN_SLUG]; diff --git a/src/lib/apis/route3/fetch-route3-swap-params.ts b/src/lib/apis/route3/fetch-route3-swap-params.ts index 113add3f6..fbdee8a8a 100644 --- a/src/lib/apis/route3/fetch-route3-swap-params.ts +++ b/src/lib/apis/route3/fetch-route3-swap-params.ts @@ -1,21 +1,25 @@ -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 { BLOCK_DURATION } from 'lib/fixed-times'; +import { SIRS_LIQUIDITY_SLIPPAGE_RATIO } from 'lib/route3/constants'; import { + Route3EmptyTreeNode, Route3LbSwapParamsRequest, Route3LiquidityBakingParamsResponse, Route3SwapParamsRequest, Route3TraditionalSwapParamsResponse, - Route3TreeNode, 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 { ONE_MINUTE_S } from 'lib/utils/numbers'; import { ROUTE3_BASE_URL } from './route3.api'; @@ -53,100 +57,122 @@ 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 correctFinalOutput = (tree: T, multiplier: BigNumber, decimals: number): T => { - const correctedTokenOutAmount = new BigNumber(tree.tokenOutAmount) - .times(multiplier) - .decimalPlaces(decimals, BigNumber.ROUND_FLOOR) - .toFixed(); - - switch (tree.type) { - case Route3TreeNodeType.Empty: - case Route3TreeNodeType.Dex: - return { ...tree, tokenOutAmount: correctedTokenOutAmount }; - case Route3TreeNodeType.High: - return { - ...tree, - tokenOutAmount: correctedTokenOutAmount, - // TODO: Fix output value for the last item; the sum of outputs for all subitems should be equal to the output of the parent item - items: tree.items.map(item => correctFinalOutput(item, multiplier, decimals)) - }; - default: +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 => { + 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 { - ...tree, - tokenOutAmount: correctedTokenOutAmount, - items: tree.items.map((item, index, subitems) => - index === subitems.length - 1 ? correctFinalOutput(item, multiplier, decimals) : item - ) + 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 fetchRoute3LiquidityBakingParams = ( - params: Route3LbSwapParamsRequest -): Promise => - fetch(`${ROUTE3_BASE_URL}/swap-sirs${getRoute3ParametrizedUrlPart(params)}`, { + 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); - const multiplier = new BigNumber(correctedXtzInput).div(initialXtzInput); - // The difference between inputs is usually pretty small, so we can use the following formula - const correctedOutput = initialOutput.times(multiplier).decimalPlaces(toTokenDecimals, BigNumber.ROUND_FLOOR); - const correctedXtzTree = correctFinalOutput(originalParams.xtzTree, multiplier, toTokenDecimals); - - return { - ...originalParams, - output: correctedOutput.toFixed(), - xtzHops: [ - { - ...originalParams.xtzHops[0], - tokenInAmount: correctedXtzInput.toFixed() - } - ].concat(originalParams.xtzHops.slice(1)), - xtzTree: correctedXtzTree - }; - } catch (err) { - console.error(err); - return originalParams; - } - }); + }); + + return parser(await originalResponse.text()); +}; export const fetchRoute3SwapParams = ({ fromSymbol, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a7da07100..26693503b 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -50,7 +50,7 @@ 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 MINIMAL_FEE_MUTEZ = 100; export const LIQUIDITY_BAKING_DEX_ADDRESS = 'KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5'; diff --git a/src/lib/route3/constants.ts b/src/lib/route3/constants.ts index 7e7f0c341..db3d84369 100644 --- a/src/lib/route3/constants.ts +++ b/src/lib/route3/constants.ts @@ -6,6 +6,7 @@ export const LIQUIDITY_BAKING_PROXY_CONTRACT = 'KT1DJRF7pTocLsoVgA9KQPBtrDrbzNUc 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 841c6630d..d79d279a1 100644 --- a/src/lib/route3/interfaces/index.ts +++ b/src/lib/route3/interfaces/index.ts @@ -94,7 +94,7 @@ interface Route3TreeNodeBase { dexId: number | null; } -interface Route3EmptyTreeNode extends Route3TreeNodeBase { +export interface Route3EmptyTreeNode extends Route3TreeNodeBase { type: Route3TreeNodeType.Empty; items: []; dexId: null; diff --git a/src/lib/temple/back/dryrun.ts b/src/lib/temple/back/dryrun.ts index 783f29789..8ff691d06 100644 --- a/src/lib/temple/back/dryrun.ts +++ b/src/lib/temple/back/dryrun.ts @@ -1,8 +1,8 @@ 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 { MINIMAL_FEE_MUTEZ } from 'lib/constants'; import { formatOpParamsBeforeSend, michelEncoder, loadFastRpcClient } from 'lib/temple/helpers'; import { ReadOnlySigner } from 'lib/temple/read-only-signer'; @@ -44,32 +44,45 @@ 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) { + const { operationsWithResults, errors } = estimationResult.reason; + if (errors.some(error => error.id.includes('gas_exhausted'))) { + const firstSkippedIndex = operationsWithResults.findIndex( + op => + 'metadata' in op && + 'operation_result' in op.metadata && + op.metadata.operation_result.status === 'skipped' + ); + console.log('firstSkippedIndex', firstSkippedIndex); + console.log(operationsWithResults); + } + } + 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, + storageLimit: opParams[i]?.storageLimit ? +opParams[i].storageLimit : e.storageLimit, + // @ts-expect-error: accessing private field + suggestedFeeMutez: Math.ceil(e.operationFeeMutez + MINIMAL_FEE_MUTEZ * 1.2), + 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 c0db99686..4861ed67a 100644 --- a/src/lib/utils/swap.utils.ts +++ b/src/lib/utils/swap.utils.ts @@ -1,16 +1,17 @@ import { isDefined } from '@rnw-community/shared'; -import { OpKind, TezosToolkit, TransferParams } from '@taquito/taquito'; +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 { THREE_ROUTE_SIRS_TOKEN, THREE_ROUTE_TEZ_TOKEN, THREE_ROUTE_TZBTC_TOKEN } from 'lib/assets/three-route-tokens'; -import { FEE_PER_GAS_UNIT, LIQUIDITY_BAKING_DEX_ADDRESS } from 'lib/constants'; +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'; @@ -22,52 +23,6 @@ import { tokensToAtoms } from 'lib/temple/helpers'; import { getTransferPermissions } from './get-transfer-permissions'; import { ONE_MINUTE_S, 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; - } -}; - export const getSwapTransferParams = async ( fromRoute3Token: Route3Token, toRoute3Token: Route3Token, @@ -150,7 +105,7 @@ export const getSwapTransferParams = async ( app_id: APP_ID }); swapBeforeEstimateParams.push( - xtzSwapMethod.toTransferParams({ amount: Number(chains.xtzHops[0].tokenInAmount), mutez: true }) + xtzSwapMethod.toTransferParams({ amount: Number(chains.xtzTree.tokenInAmount), mutez: false }) ); } if (chains.tzbtcHops.length > 0) { @@ -179,7 +134,7 @@ export const getSwapTransferParams = async ( const tzbtcSwapOut = tokensToAtoms(chains.tzbtcTree.tokenOutAmount, THREE_ROUTE_TZBTC_TOKEN.decimals); const xtzIsSwapped = chains.xtzHops.length > 0; const tzbtcIsSwapped = chains.tzbtcHops.length > 0; - const xtzAddLiqInput = xtzIsSwapped + const xtzSwapMinOut = xtzIsSwapped ? multiplyAtomicAmount(xtzSwapOut, slippageRatio, BigNumber.ROUND_FLOOR) : xtzSwapOut; const tzbtcAddLiqInput = tzbtcIsSwapped @@ -189,7 +144,7 @@ export const getSwapTransferParams = async ( const xtzSwapMethod = swapContract.methodsObject.execute({ token_in_id: fromRoute3Token.id, token_out_id: THREE_ROUTE_TEZ_TOKEN.id, - min_out: xtzAddLiqInput, + min_out: xtzSwapMinOut, receiver: accountPkh, hops: mapToRoute3ExecuteHops(chains.xtzHops), app_id: APP_ID @@ -207,8 +162,8 @@ export const getSwapTransferParams = async ( }); swapBeforeEstimateParams.push( tzbtcSwapMethod.toTransferParams({ - amount: fromRoute3Token.id === THREE_ROUTE_TEZ_TOKEN.id ? Number(chains.tzbtcHops[0].tokenInAmount) : 0, - mutez: true + amount: fromRoute3Token.id === THREE_ROUTE_TEZ_TOKEN.id ? Number(chains.tzbtcTree.tokenInAmount) : 0, + mutez: false }) ); } @@ -220,6 +175,16 @@ export const getSwapTransferParams = async ( 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({ @@ -233,29 +198,12 @@ export const getSwapTransferParams = async ( ); } - const [swapWithApproveParams, revokeParams] = await Promise.all([ - withBatchEstimations( - burnSirsBeforeEstimateParams.concat( - approvesBeforeEstimateParams, - swapBeforeEstimateParams, - mintSirsBeforeEstimateParams - ), - tezos, - accountPkh, - params => { - const approximateInternalOperationsCount = isSwapTransaction(params) - ? isSwapHops(chains) - ? chains.hops.length - : chains.xtzHops.length + chains.tzbtcHops.length - : 1; - - return approximateInternalOperationsCount * GAS_CAP_PER_INTERNAL_OPERATION; - } - ), - withBatchEstimations(revokesBeforeEstimateParams, tezos, accountPkh) - ]); - - return swapWithApproveParams.concat(revokeParams); + return burnSirsBeforeEstimateParams.concat( + approvesBeforeEstimateParams, + swapBeforeEstimateParams, + mintSirsBeforeEstimateParams, + revokesBeforeEstimateParams + ); }; export const calculateSidePaymentsFromInput = (inputAmount: BigNumber | undefined) => { @@ -315,7 +263,7 @@ export const multiplyAtomicAmount = ( roundMode?: BigNumber.RoundingMode ) => amount.times(multiplier).integerValue(roundMode); -const getRoutingFeeTransferParamsBeforeEstimate = async ( +export const getRoutingFeeTransferParams = async ( token: Route3Token, feeAmountAtomic: BigNumber, senderPublicKeyHash: string, @@ -366,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 - ); From ec7e0f858aae572c377936aa6fb479616daf4bca Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Mon, 23 Dec 2024 12:37:24 +0200 Subject: [PATCH 05/10] TW-1622 Revert harmful changes --- .../SwapForm/SwapFormInput/SwapFormInput.tsx | 2 +- src/lib/apis/route3/fetch-route3-swap-params.ts | 2 -- src/lib/temple/back/dryrun.ts | 13 ------------- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx b/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx index 8eec8160c..63f290f05 100644 --- a/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx +++ b/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx @@ -29,7 +29,7 @@ import { AssetOption } from './AssetsMenu/AssetOption'; import { PercentageButton } from './PercentageButton/PercentageButton'; import { SwapFormInputProps } from './SwapFormInput.props'; -const EXCHANGE_XTZ_RESERVE = new BigNumber('0'); +const EXCHANGE_XTZ_RESERVE = new BigNumber('0.3'); const PERCENTAGE_BUTTONS = [25, 50, 75, 100]; const LEADING_ASSETS = [TEZ_TOKEN_SLUG]; diff --git a/src/lib/apis/route3/fetch-route3-swap-params.ts b/src/lib/apis/route3/fetch-route3-swap-params.ts index fbdee8a8a..3776a0047 100644 --- a/src/lib/apis/route3/fetch-route3-swap-params.ts +++ b/src/lib/apis/route3/fetch-route3-swap-params.ts @@ -6,7 +6,6 @@ import memoizee from 'memoizee'; 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, @@ -19,7 +18,6 @@ import { import { loadContract } from 'lib/temple/contract'; import { ReactiveTezosToolkit } from 'lib/temple/front'; import { atomsToTokens, loadFastRpcClient, tokensToAtoms } from 'lib/temple/helpers'; -// import { ONE_MINUTE_S } from 'lib/utils/numbers'; import { ROUTE3_BASE_URL } from './route3.api'; diff --git a/src/lib/temple/back/dryrun.ts b/src/lib/temple/back/dryrun.ts index 8ff691d06..4561a4dca 100644 --- a/src/lib/temple/back/dryrun.ts +++ b/src/lib/temple/back/dryrun.ts @@ -47,19 +47,6 @@ export async function dryRunOpParams({ 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) { - const { operationsWithResults, errors } = estimationResult.reason; - if (errors.some(error => error.id.includes('gas_exhausted'))) { - const firstSkippedIndex = operationsWithResults.findIndex( - op => - 'metadata' in op && - 'operation_result' in op.metadata && - op.metadata.operation_result.status === 'skipped' - ); - console.log('firstSkippedIndex', firstSkippedIndex); - console.log(operationsWithResults); - } - } error = [ { ...estimationResult.reason, isError: true }, { ...contractBatchResult.reason, isError: true } From 9e7e8b45131b0b6adc42e737e1876fed968f3b18 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Mon, 23 Dec 2024 15:38:45 +0200 Subject: [PATCH 06/10] TW-1622 Refactoring according to comments + remove dead code --- src/lib/constants.ts | 2 -- src/lib/route3/constants.ts | 1 - src/lib/route3/interfaces/index.ts | 2 +- src/lib/temple/back/dryrun.ts | 5 +---- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 26693503b..1fb86e033 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -50,8 +50,6 @@ export const MAX_SHOW_AGREEMENTS_COUNTER = 1; const isMacOS = /Mac OS/.test(navigator.userAgent); export const searchHotkey = ` (${isMacOS ? '⌘' : 'Ctrl + '}K)`; -export const MINIMAL_FEE_MUTEZ = 100; - export const LIQUIDITY_BAKING_DEX_ADDRESS = 'KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5'; export const THEME_COLOR_SEARCH_PARAM_NAME = 'tc'; diff --git a/src/lib/route3/constants.ts b/src/lib/route3/constants.ts index db3d84369..f4ba995c1 100644 --- a/src/lib/route3/constants.ts +++ b/src/lib/route3/constants.ts @@ -2,7 +2,6 @@ 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'; diff --git a/src/lib/route3/interfaces/index.ts b/src/lib/route3/interfaces/index.ts index d79d279a1..3aafe3f73 100644 --- a/src/lib/route3/interfaces/index.ts +++ b/src/lib/route3/interfaces/index.ts @@ -114,4 +114,4 @@ interface Route3DexTreeNode extends Route3TreeNodeBase { type Route3NonEmptyNode = Route3NonTerminalTreeNode | Route3DexTreeNode; -export type Route3TreeNode = Route3EmptyTreeNode | Route3NonEmptyNode; +type Route3TreeNode = Route3EmptyTreeNode | Route3NonEmptyNode; diff --git a/src/lib/temple/back/dryrun.ts b/src/lib/temple/back/dryrun.ts index 4561a4dca..4afe05953 100644 --- a/src/lib/temple/back/dryrun.ts +++ b/src/lib/temple/back/dryrun.ts @@ -1,8 +1,7 @@ import { localForger } from '@taquito/local-forging'; import { ForgeOperationsParams } from '@taquito/rpc'; -import { Estimate, TezosOperationError, TezosToolkit } from '@taquito/taquito'; +import { Estimate, TezosToolkit } from '@taquito/taquito'; -import { MINIMAL_FEE_MUTEZ } from 'lib/constants'; import { formatOpParamsBeforeSend, michelEncoder, loadFastRpcClient } from 'lib/temple/helpers'; import { ReadOnlySigner } from 'lib/temple/read-only-signer'; @@ -63,8 +62,6 @@ export async function dryRunOpParams({ gasLimit: e.gasLimit, minimalFeeMutez: e.minimalFeeMutez, storageLimit: opParams[i]?.storageLimit ? +opParams[i].storageLimit : e.storageLimit, - // @ts-expect-error: accessing private field - suggestedFeeMutez: Math.ceil(e.operationFeeMutez + MINIMAL_FEE_MUTEZ * 1.2), totalCost: e.totalCost, usingBaseFeeMutez: e.usingBaseFeeMutez } as Estimate) From 9a2153a872eaa7825536d684b86ca819cbec1f7b Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Mon, 23 Dec 2024 16:46:32 +0200 Subject: [PATCH 07/10] TW-1622 Fix displaying fee --- src/lib/temple/back/dryrun.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/temple/back/dryrun.ts b/src/lib/temple/back/dryrun.ts index 4afe05953..d5300e724 100644 --- a/src/lib/temple/back/dryrun.ts +++ b/src/lib/temple/back/dryrun.ts @@ -5,6 +5,8 @@ import { Estimate, TezosToolkit } from '@taquito/taquito'; import { formatOpParamsBeforeSend, michelEncoder, loadFastRpcClient } from 'lib/temple/helpers'; import { ReadOnlySigner } from 'lib/temple/read-only-signer'; +const MINIMAL_FEE_MUTEZ = 100; + type DryRunParams = { opParams: any[]; networkRpc: string; @@ -61,6 +63,9 @@ export async function dryRunOpParams({ consumedMilligas: e.consumedMilligas, gasLimit: e.gasLimit, minimalFeeMutez: e.minimalFeeMutez, + // The field below does not appear after spreading `e` + // @ts-expect-error: accessing private field + suggestedFeeMutez: Math.ceil(e.operationFeeMutez + MINIMAL_FEE_MUTEZ * 1.2), storageLimit: opParams[i]?.storageLimit ? +opParams[i].storageLimit : e.storageLimit, totalCost: e.totalCost, usingBaseFeeMutez: e.usingBaseFeeMutez From a1c907d2783ffd4308cbed028d614eff2b01fa98 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Mon, 23 Dec 2024 17:52:13 +0200 Subject: [PATCH 08/10] TW-1622 Minor refactoring --- src/lib/temple/back/dryrun.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/temple/back/dryrun.ts b/src/lib/temple/back/dryrun.ts index d5300e724..886fed7f7 100644 --- a/src/lib/temple/back/dryrun.ts +++ b/src/lib/temple/back/dryrun.ts @@ -63,9 +63,7 @@ export async function dryRunOpParams({ consumedMilligas: e.consumedMilligas, gasLimit: e.gasLimit, minimalFeeMutez: e.minimalFeeMutez, - // The field below does not appear after spreading `e` - // @ts-expect-error: accessing private field - suggestedFeeMutez: Math.ceil(e.operationFeeMutez + MINIMAL_FEE_MUTEZ * 1.2), + suggestedFeeMutez: e.suggestedFeeMutez, storageLimit: opParams[i]?.storageLimit ? +opParams[i].storageLimit : e.storageLimit, totalCost: e.totalCost, usingBaseFeeMutez: e.usingBaseFeeMutez From 316ddb938bd1435c89955718af51b56abb95c049 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Thu, 26 Dec 2024 09:28:44 +0200 Subject: [PATCH 09/10] TW-1622 Remove an unused variable --- src/lib/temple/back/dryrun.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/temple/back/dryrun.ts b/src/lib/temple/back/dryrun.ts index 886fed7f7..21191953b 100644 --- a/src/lib/temple/back/dryrun.ts +++ b/src/lib/temple/back/dryrun.ts @@ -5,8 +5,6 @@ import { Estimate, TezosToolkit } from '@taquito/taquito'; import { formatOpParamsBeforeSend, michelEncoder, loadFastRpcClient } from 'lib/temple/helpers'; import { ReadOnlySigner } from 'lib/temple/read-only-signer'; -const MINIMAL_FEE_MUTEZ = 100; - type DryRunParams = { opParams: any[]; networkRpc: string; From 958ca58abe6af97f064d09835971edb7b1d54aa5 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Thu, 26 Dec 2024 12:54:03 +0200 Subject: [PATCH 10/10] TW-1622 Implement estimation retry if 'gas_exhausted' error is received --- src/lib/constants.ts | 2 ++ src/lib/temple/back/dryrun.ts | 43 ++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 1fb86e033..b0b619035 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -52,6 +52,8 @@ export const searchHotkey = ` (${isMacOS ? '⌘' : 'Ctrl + '}K)`; 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/temple/back/dryrun.ts b/src/lib/temple/back/dryrun.ts index 21191953b..4d7eab325 100644 --- a/src/lib/temple/back/dryrun.ts +++ b/src/lib/temple/back/dryrun.ts @@ -1,7 +1,8 @@ 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'; import { ReadOnlySigner } from 'lib/temple/read-only-signer'; @@ -10,6 +11,8 @@ type DryRunParams = { networkRpc: string; sourcePkh: string; sourcePublicKey: string; + attemptCounter?: number; + prevFailedOperationIndex?: number; }; export interface DryRunResult { @@ -26,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)); @@ -46,6 +51,36 @@ export async function dryRunOpParams({ 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 } @@ -61,7 +96,9 @@ export async function dryRunOpParams({ consumedMilligas: e.consumedMilligas, gasLimit: e.gasLimit, minimalFeeMutez: e.minimalFeeMutez, - suggestedFeeMutez: e.suggestedFeeMutez, + 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