Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TW-1622 Implement swapping without 3Route liquidity baking proxy #1242

Merged
merged 11 commits into from
Dec 30, 2024
39 changes: 21 additions & 18 deletions src/app/templates/InternalConfirmation.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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';
Expand Down Expand Up @@ -87,24 +85,29 @@ const InternalConfirmation: FC<InternalConfiramtionProps> = ({ 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') {
Expand Down
204 changes: 114 additions & 90 deletions src/lib/apis/route3/fetch-route3-swap-params.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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,
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 { ROUTE3_BASE_URL } from './route3.api';

Expand Down Expand Up @@ -53,100 +55,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<string>(`${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 = <T extends Route3TreeNode>(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<Route3LiquidityBakingParamsResponse> => {
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<Route3TraditionalSwapParamsResponse>([
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<Route3LiquidityBakingParamsResponse> =>
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,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
1 change: 1 addition & 0 deletions src/lib/route3/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/route3/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ interface Route3TreeNodeBase {
dexId: number | null;
}

interface Route3EmptyTreeNode extends Route3TreeNodeBase {
export interface Route3EmptyTreeNode extends Route3TreeNodeBase {
type: Route3TreeNodeType.Empty;
items: [];
dexId: null;
Expand Down
54 changes: 27 additions & 27 deletions src/lib/temple/back/dryrun.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -44,32 +44,32 @@ 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') {
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),
keshan3262 marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down
Loading