From 805ba1002ac8765b079e9a9b999e1f9ba162d037 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Thu, 8 Feb 2024 11:22:10 +0200 Subject: [PATCH] TW-1282: Use /prices 3route API entry (#143) * TW-1282 Use /prices 3route API entry * TW-1282 Fix throwing an error Co-authored-by: Alex * TW-1282 Fix ts errors --------- Co-authored-by: Alex --- src/index.ts | 4 +- src/utils/block-finder.ts | 57 ------- src/utils/three-route.ts | 37 +---- src/utils/tokens.ts | 316 +++++--------------------------------- 4 files changed, 45 insertions(+), 369 deletions(-) delete mode 100644 src/utils/block-finder.ts diff --git a/src/index.ts b/src/index.ts index aff53ba..19ca0cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,7 @@ import { getSignedMoonPayUrl } from './utils/moonpay/get-signed-moonpay-url'; import { getSigningNonce } from './utils/signing-nonce'; import SingleQueryDataProvider from './utils/SingleQueryDataProvider'; import { tezExchangeRateProvider } from './utils/tezos'; -import { getExchangeRatesFromDB } from './utils/tokens'; +import { getExchangeRates } from './utils/tokens'; const PINO_LOGGER = { logger: logger.child({ name: 'web' }), @@ -172,7 +172,7 @@ app.get('/api/abtest', (_, res) => { app.get('/api/exchange-rates/tez', makeProviderDataRequestHandler(tezExchangeRateProvider)); app.get('/api/exchange-rates', async (_req, res) => { - const tokensExchangeRates = await getExchangeRatesFromDB(); + const tokensExchangeRates = await getExchangeRates(); const { data: tezExchangeRate, error: tezExchangeRateError } = await getProviderStateWithTimeout( tezExchangeRateProvider ); diff --git a/src/utils/block-finder.ts b/src/utils/block-finder.ts deleted file mode 100644 index 3070459..0000000 --- a/src/utils/block-finder.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { BlockResponse, BlockFullHeader } from '@taquito/rpc'; - -import { sleep } from './helpers'; -import logger from './logger'; -import { tezosToolkit } from './tezos'; - -export interface BlockInterface extends Pick { - header: Pick; -} - -export const EMPTY_BLOCK: BlockInterface = { - protocol: '', - chain_id: '', - hash: '', - header: { - level: 0, - timestamp: '' - } -}; - -export const blockFinder = async ( - prevBlock: BlockInterface, - onNewBlock: (block: BlockInterface) => Promise -): Promise => { - const block = await tezosToolkit.rpc - .getBlock() - .then( - (blockResponse): BlockInterface => ({ - protocol: blockResponse.protocol, - chain_id: blockResponse.chain_id, - hash: blockResponse.hash, - header: { - level: blockResponse.header.level, - timestamp: blockResponse.header.timestamp - } - }) - ) - .catch(e => { - logger.error(e); - - return prevBlock; - }); - - const isNewBlock = block.header.level > prevBlock.header.level; - const realBlock = isNewBlock ? block : prevBlock; - - if (isNewBlock) { - await onNewBlock(realBlock).catch(e => { - logger.error('blockFinder error'); - logger.error(e); - }); - } else { - await sleep(200); - } - - return blockFinder(realBlock, onNewBlock); -}; diff --git a/src/utils/three-route.ts b/src/utils/three-route.ts index 1a89c89..0a49dff 100644 --- a/src/utils/three-route.ts +++ b/src/utils/three-route.ts @@ -24,20 +24,6 @@ export interface ThreeRouteChain { hops: ThreeRouteHop[]; } -// TODO: add axios adapter and change type if precision greater than of standard js number type is necessary -export interface ThreeRouteClassicSwapResponse { - input: number; - output: number; - chains: ThreeRouteChain[]; -} - -export interface ThreeRouteSirsSwapResponse { - input: number; - output: number; - tzbtcChain: ThreeRouteClassicSwapResponse; - xtzChain: ThreeRouteClassicSwapResponse; -} - interface ThreeRouteTokenCommon { id: number; symbol: string; @@ -96,14 +82,10 @@ export interface ThreeRouteDex { token2: ThreeRouteToken; } -type ThreeRouteQueryParams = object | SwapQueryParams; -type ThreeRouteQueryResponse = - | ThreeRouteClassicSwapResponse - | ThreeRouteSirsSwapResponse - | ThreeRouteDex[] - | ThreeRouteToken[]; +type ThreeRouteExchangeRates = Record; -export type ThreeRouteSwapResponse = ThreeRouteClassicSwapResponse | ThreeRouteSirsSwapResponse; +type ThreeRouteQueryParams = object | SwapQueryParams; +type ThreeRouteQueryResponse = ThreeRouteExchangeRates | ThreeRouteToken[]; export const THREE_ROUTE_SIRS_SYMBOL = 'SIRS'; @@ -113,17 +95,6 @@ const threeRouteBuildQueryFn = makeBuildQueryFn( - ({ inputTokenSymbol, outputTokenSymbol, realAmount }) => { - const isSirsSwap = inputTokenSymbol === THREE_ROUTE_SIRS_SYMBOL || outputTokenSymbol === THREE_ROUTE_SIRS_SYMBOL; - - return `/${isSirsSwap ? 'swap-sirs' : 'swap'}/${inputTokenSymbol}/${outputTokenSymbol}/${realAmount}`; - } -); - -export const getThreeRouteDexes = threeRouteBuildQueryFn('/dexes', []); - export const getThreeRouteTokens = threeRouteBuildQueryFn('/tokens', []); -export const getChains = (response: ThreeRouteSwapResponse) => - 'chains' in response ? response.chains : [...response.xtzChain.chains, ...response.tzbtcChain.chains]; +export const getThreeRouteExchangeRates = threeRouteBuildQueryFn('/prices', []); diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 578f957..c468e6d 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -1,35 +1,19 @@ import { BigNumber } from 'bignumber.js'; -import { differenceBy, isEqual } from 'lodash'; import { redisClient } from '../redis'; -import { blockFinder, EMPTY_BLOCK } from './block-finder'; -import DataProvider from './DataProvider'; -import { getRecentDestinations } from './get-recent-destinations'; import { isDefined } from './helpers'; import logger from './logger'; -import PromisifiedSemaphore from './PromisifiedSemaphore'; -import SingleQueryDataProvider from './SingleQueryDataProvider'; +import SingleQueryDataProvider, { SingleQueryDataProviderState } from './SingleQueryDataProvider'; import { tezExchangeRateProvider } from './tezos'; import { - getThreeRouteDexes, - getThreeRouteSwap, + getThreeRouteExchangeRates, getThreeRouteTokens, ThreeRouteStandardEnum, ThreeRouteFa12Token, - ThreeRouteFa2Token, - getChains, - THREE_ROUTE_SIRS_SYMBOL, - ThreeRouteSwapResponse, - ThreeRouteDex + ThreeRouteFa2Token } from './three-route'; import { BcdTokenData, mapTzktTokenDataToBcdTokenData, tokensMetadataProvider } from './tzkt'; -interface SwapsResponse { - directSwap: ThreeRouteSwapResponse; - invertedSwap: ThreeRouteSwapResponse; - updatedAt: string; -} - interface TokenExchangeRateEntry { tokenAddress: string; tokenId?: number; @@ -38,299 +22,77 @@ interface TokenExchangeRateEntry { swapsUpdatedAt?: string; } -class TimeoutError extends Error {} - const tokensListProvider = new SingleQueryDataProvider(30000, () => getThreeRouteTokens({})); -const dexesListProvider = new SingleQueryDataProvider(30000, () => getThreeRouteDexes({})); - -const THREE_ROUTE_TEZ_SYMBOL = 'XTZ'; -const EMPTY_SWAP = { - input: 0, - output: 0, - chains: [] -}; - -const ASPENCOIN_ADDRESS = 'KT1S5iPRQ612wcNm6mXDqDhTNegGFcvTV7vM'; +const threeRouteExchangeRatesProvider = new SingleQueryDataProvider(30000, () => getThreeRouteExchangeRates({})); const EXCHANGE_RATES_STORAGE_KEY = 'exchange_rates'; -const PREV_DEXES_LIST_STORAGE_KEY = 'prev_dexes_list'; - -const getToTezExchangeRatesVersions = ({ directSwap, invertedSwap }: SwapsResponse) => { - const toTezExchangeRatesVersions: BigNumber[] = []; - if (directSwap.output !== 0) { - toTezExchangeRatesVersions.push(new BigNumber(directSwap.input).div(directSwap.output)); - } - if (invertedSwap.output !== 0) { - toTezExchangeRatesVersions.push(new BigNumber(invertedSwap.output).div(invertedSwap.input)); - } - - return toTezExchangeRatesVersions; -}; - -const assertSmallRatesDifference = (swapResponse: SwapsResponse) => { - const toTezExchangeRatesVersions = getToTezExchangeRatesVersions(swapResponse); - const minRate = BigNumber.min(...toTezExchangeRatesVersions); - const maxRate = BigNumber.max(...toTezExchangeRatesVersions); - if (minRate.div(maxRate).lt(0.9)) { - throw new Error('Prices difference is too big'); - } -}; - -const getSwapsByTezAmount = async (outputTokenSymbol: string, tezAmount: number) => { - const updatedAt = new Date().toISOString(); - const directSwap = await getThreeRouteSwap({ - inputTokenSymbol: THREE_ROUTE_TEZ_SYMBOL, - outputTokenSymbol, - realAmount: tezAmount - }); - - if (directSwap.output === 0) { - throw new Error('Failed to get direct swap'); - } - - const invertedSwap = await getThreeRouteSwap({ - inputTokenSymbol: outputTokenSymbol, - outputTokenSymbol: THREE_ROUTE_TEZ_SYMBOL, - realAmount: directSwap.output - }); - - const response = { directSwap, invertedSwap, updatedAt }; - assertSmallRatesDifference(response); - - return response; -}; - -const getSwapsByOneToken = async (outputTokenSymbol: string) => { - const updatedAt = new Date().toISOString(); - const invertedSwap = await getThreeRouteSwap({ - inputTokenSymbol: outputTokenSymbol, - outputTokenSymbol: THREE_ROUTE_TEZ_SYMBOL, - realAmount: 1 - }); - - if (invertedSwap.output === 0) { - throw new Error(`Failed to get swaps for 1 ${outputTokenSymbol}`); - } - - return { directSwap: EMPTY_SWAP, invertedSwap, updatedAt }; -}; - -const getSwaps = async (outputTokenSymbol: string) => { - try { - return await getSwapsByTezAmount(outputTokenSymbol, 10); - } catch {} - - try { - return await getSwapsByTezAmount(outputTokenSymbol, 1); - } catch {} - - try { - return await getSwapsByOneToken(outputTokenSymbol); - } catch { - return { directSwap: EMPTY_SWAP, invertedSwap: EMPTY_SWAP, updatedAt: new Date().toISOString() }; - } -}; - -const probeSwapsProvider = new DataProvider(Infinity, getSwaps); - -const rejectOnTimeout = (timeoutMs: number) => - new Promise((_, rej) => setTimeout(() => rej(new TimeoutError()), timeoutMs)); - -tokensMetadataProvider.subscribe(ASPENCOIN_ADDRESS); const getTokensExchangeRates = async (): Promise => { logger.info('Getting tokens exchange rates...'); logger.info('Getting exchange rates of tokens which are known to 3route...'); const { data: tokens, error: tokensError } = await tokensListProvider.getState(); + const { data: exchangeRatesInputs, error: exchangeRatesError } = await threeRouteExchangeRatesProvider.getState(); const { data: tezExchangeRate, error: tezExchangeRateError } = await tezExchangeRateProvider.getState(); - if (tokensError ?? tezExchangeRateError) { - throw tokensError ?? tezExchangeRateError; + const error = tokensError ?? exchangeRatesError ?? tezExchangeRateError; + if (error) { + throw error; } const exchangeRatesWithHoles = await Promise.all( - tokens + tokens! .filter( (token): token is ThreeRouteFa12Token | ThreeRouteFa2Token => token.standard !== ThreeRouteStandardEnum.xtz ) .map(async (token): Promise => { - const { contract, tokenId: rawTokenId } = token; + const { contract, tokenId: rawTokenId, symbol } = token; const tokenId = isDefined(rawTokenId) ? Number(rawTokenId) : undefined; - await probeSwapsProvider.subscribe(token.symbol); - try { - const { data: probeSwaps, error: swapError } = await Promise.race([ - probeSwapsProvider.get(token.symbol), - rejectOnTimeout(10000) - ]); - await tokensMetadataProvider.subscribe(contract, tokenId); - const { data: metadata } = await tokensMetadataProvider.get(contract, tokenId); - - if (swapError) { - logger.error(`Failed to get exchange rate for token ${token.symbol}`); - throw swapError; - } - - const toTezExchangeRatesVersions = getToTezExchangeRatesVersions(probeSwaps); - const exchangeRate = - toTezExchangeRatesVersions.length === 0 - ? new BigNumber(0) - : BigNumber.sum(...toTezExchangeRatesVersions) - .div(toTezExchangeRatesVersions.length) - .times(tezExchangeRate); + const { ask, bid } = exchangeRatesInputs![symbol] ?? { ask: 0, bid: 0 }; - return { - tokenAddress: contract, - tokenId, - exchangeRate, - metadata: mapTzktTokenDataToBcdTokenData(metadata?.[0]), - swapsUpdatedAt: probeSwaps.updatedAt - }; - } catch (e) { - if (e instanceof TimeoutError) { - logger.error(`Timeout error while getting exchange rate for token ${token.symbol}`); + if (ask === 0 && bid === 0) { + logger.error(`Failed to get exchange rate for token ${token.symbol}`); - return undefined; - } - - throw e; + return undefined; } + + const { data: metadata } = await tokensMetadataProvider.get(contract, tokenId); + const tokensPerTez = ask === 0 ? bid : ask; + const exchangeRate = new BigNumber(1).div(tokensPerTez).times(tezExchangeRate!); + + return { + tokenAddress: contract, + tokenId, + exchangeRate, + metadata: mapTzktTokenDataToBcdTokenData(metadata?.[0]), + swapsUpdatedAt: new Date().toISOString() + }; }) ); const exchangeRates = exchangeRatesWithHoles.filter(isDefined); logger.info('Successfully got tokens exchange rates'); + const newExchangeRates = [...exchangeRates].filter(({ exchangeRate }) => !exchangeRate.eq(0)); + await redisClient.set(EXCHANGE_RATES_STORAGE_KEY, JSON.stringify(newExchangeRates)); - return [...exchangeRates].filter(({ exchangeRate }) => !exchangeRate.eq(0)); + return newExchangeRates; }; -const tokensExchangeRatesProvider = new SingleQueryDataProvider(60000, getTokensExchangeRates); +const tokensExchangeRatesProvider = new SingleQueryDataProvider(30000, getTokensExchangeRates); -export const getExchangeRatesFromDB = async (): Promise => { +const getExchangeRatesFromDB = async (): Promise => { const rawValue = await redisClient.get(EXCHANGE_RATES_STORAGE_KEY); return JSON.parse(rawValue ?? '[]'); }; -const updateExchangeRatesInDB = async () => { - const prevExchangeRates = await getExchangeRatesFromDB(); - const prevIndexedExchangeRates = Object.fromEntries( - prevExchangeRates.map(exchangeRate => [`${exchangeRate.tokenAddress}_${exchangeRate.tokenId}`, exchangeRate]) - ); - const { data: exchangeRatesUpdates, error: exchangeRatesError } = await tokensExchangeRatesProvider.getState(); - - if (exchangeRatesError) { - return; - } - - const indexedExchangeRatesUpdates = Object.fromEntries( - exchangeRatesUpdates.map(exchangeRate => [`${exchangeRate.tokenAddress}_${exchangeRate.tokenId}`, exchangeRate]) - ); - - const newExchangeRates = Object.values({ - ...prevIndexedExchangeRates, - ...indexedExchangeRatesUpdates - }); - - await redisClient.set(EXCHANGE_RATES_STORAGE_KEY, JSON.stringify(newExchangeRates)); -}; -updateExchangeRatesInDB().catch(logger.error); - -const getPrevDexesList = async (): Promise => { - const rawValue = await redisClient.get(PREV_DEXES_LIST_STORAGE_KEY); +export const getExchangeRates = async () => { + const providerState = await Promise.race([ + tokensExchangeRatesProvider.getState(), + new Promise>(res => + setTimeout(() => res({ error: new Error('Timeout') }), 1000) + ) + ]); - return isDefined(rawValue) ? JSON.parse(rawValue) : null; + return providerState.data ?? (await getExchangeRatesFromDB()); }; - -const setPrevDexesList = async (dexesList: ThreeRouteDex[]) => - redisClient.set(PREV_DEXES_LIST_STORAGE_KEY, JSON.stringify(dexesList)); - -const swapsUpdateSemaphore = new PromisifiedSemaphore(); -blockFinder(EMPTY_BLOCK, async block => - swapsUpdateSemaphore.exec(async () => { - logger.info(`updating stats for level ${block.header.level}`); - const recentDestinations = await getRecentDestinations(block.header.level); - const { data: tokens, error: tokensError } = await tokensListProvider.getState(); - const { data: dexes, error: dexesError } = await dexesListProvider.getState(); - - if (tokensError ?? dexesError) { - throw tokensError ?? dexesError; - } - - let dexesListChanged = false; - const prevDexesList = await getPrevDexesList(); - if (prevDexesList) { - const createdOrUpdatedDexesList = differenceBy(dexes, prevDexesList, isEqual); - const deletedOrUpdatedDexesList = differenceBy(prevDexesList, dexes, isEqual); - dexesListChanged = createdOrUpdatedDexesList.length > 0 || deletedOrUpdatedDexesList.length > 0; - } - await setPrevDexesList(dexes); - - if (dexesListChanged) { - logger.info('dexes list changed, refreshing all tokens exchange rates'); - } - - const outputsUpdatesFlags = await Promise.all( - tokens.map(async token => { - if (token.symbol === THREE_ROUTE_TEZ_SYMBOL) { - return false; - } - - if (dexesListChanged || token.symbol === THREE_ROUTE_SIRS_SYMBOL) { - // Swap output for SIRS should be updated each block because of baking subsidy - await probeSwapsProvider.refetchInSubscription(token.symbol); - - return true; - } - - try { - await probeSwapsProvider.subscribe(token.symbol); - const { data: probeSwaps, error: swapError } = await Promise.race([ - probeSwapsProvider.get(token.symbol), - rejectOnTimeout(10000) - ]); - - if (swapError) { - throw swapError; - } - - const { directSwap, invertedSwap } = probeSwaps; - const directSwapChains = getChains(directSwap); - const invertedSwapChains = getChains(invertedSwap); - - if (directSwapChains.length === 0) { - logger.info(`updating swap output for token ${token.symbol} because of direct swap chains absence`); - await probeSwapsProvider.refetchInSubscription(token.symbol); - - return true; - } - - const dexesAddresses = directSwapChains - .concat(invertedSwapChains) - .map(chain => chain.hops.map(hop => dexes.find(dex => dex.id === hop.dex)?.contract).filter(isDefined)) - .flat(); - - const firstUpdatedDexAddress = dexesAddresses.find(dexAddress => recentDestinations.includes(dexAddress)); - if (isDefined(firstUpdatedDexAddress)) { - logger.info(`updating swap output for token ${token.symbol} because of dex ${firstUpdatedDexAddress}`); - await probeSwapsProvider.refetchInSubscription(token.symbol); - - return true; - } - - return false; - } catch (e) { - logger.error(e as Error); - - return false; - } - }) - ); - if (outputsUpdatesFlags.some(flag => flag)) { - logger.info('refreshing tokens exchange rates because of swaps outputs updates'); - await tokensExchangeRatesProvider.refetch(); - await updateExchangeRatesInDB(); - } - logger.info(`stats updated for level ${block.header.level}`); - }) -);