From edbd68e125b8d38d9e2454ed5be9834a35b32168 Mon Sep 17 00:00:00 2001 From: wackloner Date: Tue, 16 Apr 2024 08:17:25 +0800 Subject: [PATCH 1/4] fetch lp states from backend api --- src/Farm/store.ts | 122 +++++++++++++++++------------------ src/dexes/index.ts | 3 +- src/providers/apiProvider.ts | 31 +++++++-- 3 files changed, 85 insertions(+), 71 deletions(-) diff --git a/src/Farm/store.ts b/src/Farm/store.ts index dce747dc..e690f4cc 100644 --- a/src/Farm/store.ts +++ b/src/Farm/store.ts @@ -8,7 +8,6 @@ import { ascend, descend, min, Ord, sort, zip, zipWith } from 'ramda'; import { Asset, AssetId, - assetId, buildContractsStore, ContractState, Amount, @@ -19,7 +18,6 @@ import { $algoUsdPrice, fetchAsset, ALGO_ASSET, - fetchAlgoPriceFx, $pricedAssets, $networkTime, Contract, @@ -30,9 +28,9 @@ import { } from '../common/store'; import { nonConcurrent } from '../common/store/utils'; import { AllDefined, Backend } from '../types'; -import { LPTokenInfo, DexProvider, makeDex, PoolInfo } from '../dexes'; +import { LPTokenInfo, DexProvider } from '../dexes'; import { fromSmallestUnits, YEAR } from '../common/lib'; -import { getLpState } from '../providers/apiProvider'; +import { getPricedLpInfo, getPricedLpInfos, PricedLpInfo } from '../providers/apiProvider'; import { calculateAlgoReward, convertAmountToUSD, getPoolState } from './PoolList/Pool/utils'; import { PoolState } from './PoolList/Pool/types'; import { ColumnType } from './PoolList/PoolList'; @@ -61,57 +59,36 @@ export function detectAssetProvider({ name }: { name: string }): DexProvider { return 'MOCK'; } -export async function getLPTokenInfoBackend(asset: Asset, provider: DexProvider): Promise> { - const lpState = await getLpState(asset.id); +function formatPricedLPInfo(lpInfo: PricedLpInfo, asset: Asset): Priced { return { ...asset, - poolId: lpState.id, - asset1: lpState.asset1_id, - asset2: lpState.asset2_id, + poolId: lpInfo.id, + asset1: lpInfo.asset1_id, + asset2: lpInfo.asset2_id, liquidityAsset: asset.id, - asset1Reserve: BigInt(lpState.asset1_reserve), - asset2Reserve: BigInt(lpState.asset2_reserve), - totalLiquidity: BigInt(lpState.issued_tokens), - poolDex: provider, - dexFeeApr: 0, // TODO - price: lpState.token_price_usd, - priceInAlgo: lpState.token_price, + asset1Reserve: BigInt(lpInfo.asset1_reserve_micros), + asset2Reserve: BigInt(lpInfo.asset2_reserve_micros), + totalLiquidity: BigInt(lpInfo.issued_tokens_micros), + poolDex: lpInfo.dex_provider, + dexFeeApr: lpInfo.swap_fee_apr || 0, // TODO + price: lpInfo.token_price_usd, + priceInAlgo: lpInfo.token_price_algo, }; } -export async function getLPTokenInfo( - asset: Asset, - algoPrice: number | null, - provider?: DexProvider -): Promise> { - if (provider === undefined) { - provider = detectAssetProvider(asset); - } - if (provider === 'PT') { - // Pact pools fix - return await getLPTokenInfoBackend(asset, provider); - } - - if (algoPrice === null) { - algoPrice = await fetchAlgoPriceFx(); - } +export async function getLPTokenInfoBackend(asset: Asset): Promise> { + const lpInfo = await getPricedLpInfo(asset.id); + return formatPricedLPInfo(lpInfo, asset); +} - const dex = makeDex(provider); - const poolInfo = await dex.getPoolByAddress(asset.creator).catch(() => dex.getPoolByAddress(asset.reserve)); - - const firstAsset = await fetchAsset(poolInfo.asset1); - let fstAssetPrice; - if (poolInfo.asset1 === 0) { - fstAssetPrice = algoPrice; - } else { - const algoPool = await dex.getPoolByAssets(firstAsset, ALGO_ASSET); - const priceInAlgo = (await algoPool.getSwap(firstAsset, BigInt(10 ** firstAsset.decimals), 0.01)).price; - fstAssetPrice = algoPrice * priceInAlgo; +export async function getManyLPInfosBackend(): Promise[]> { + const lpInfos = await getPricedLpInfos(); + const processedInfos = []; + for (const lpInfo of lpInfos) { + const asset = await fetchAsset(lpInfo.token_id); + processedInfos.push(formatPricedLPInfo(lpInfo, asset)); } - const asset1Reserve = fromSmallestUnits(firstAsset, poolInfo.asset1Reserve); - const totalLiquidity = fromSmallestUnits(asset, poolInfo.totalLiquidity); - const price = (asset1Reserve / totalLiquidity) * fstAssetPrice * 2; - return { ...asset, ...poolInfo, price, priceInAlgo: price / algoPrice }; + return processedInfos; } const BIG_NUM = BigInt('1000000000000000000'); @@ -176,21 +153,43 @@ type LPTokenStore = Map>; export const $lpTokenInfos = createStore(Map()); -export const getLPTokenInfoFx = createEffect( - nonConcurrent( - async ({ - asset, - provider, - algoPrice, - }: { - asset: Asset; - provider: DexProvider | undefined; - algoPrice: number | null; - }) => getLPTokenInfo(asset, algoPrice, provider) +// export const getLPTokenInfoFx = createEffect( +// nonConcurrent( +// async ({ +// asset, +// provider, +// algoPrice, +// }: { +// asset: Asset; +// provider: DexProvider | undefined; +// algoPrice: number | null; +// }) => getLPTokenInfoBackend(asset) +// ) +// ); +// +// $lpTokenInfos.on(getLPTokenInfoFx.done, (state, { params, result }) => state.set(assetId(params.asset), result)); +// +// sample({ +// clock: assetLoaded, +// source: { lpTokens: $lpTokenIds, algoPrice: $algoUsdPrice }, +// filter: ({ lpTokens }, asset) => lpTokens.has(asset.id), +// fn: ({ algoPrice }, asset) => ({ asset, algoPrice, provider: undefined }), +// target: getLPTokenInfoFx, +// }); + +export const getLPTokenInfosFx = createEffect( + nonConcurrent(async ({ lpTokenIds, algoPrice }: { lpTokenIds: AssetId[]; algoPrice: number | null }) => + getManyLPInfosBackend() ) ); -$lpTokenInfos.on(getLPTokenInfoFx.done, (state, { params, result }) => state.set(assetId(params.asset), result)); +$lpTokenInfos.on(getLPTokenInfosFx.done, (state, { result }) => { + let newState = state; + for (const lpTokenInfo of result) { + newState = newState.set(lpTokenInfo.id, lpTokenInfo); + } + return newState; +}); const $lpTokenIds = createStore(Set()).on($farmPools, (state, pools) => { const newIds = Set(pools.filter((pool) => pool.state !== null).map((pool) => pool.state!.initial.stakeToken)); @@ -201,9 +200,8 @@ const $lpTokenIds = createStore(Set()).on($farmPools, (state, pools) => sample({ clock: assetLoaded, source: { lpTokens: $lpTokenIds, algoPrice: $algoUsdPrice }, - filter: ({ lpTokens }, asset) => lpTokens.has(asset.id), - fn: ({ algoPrice }, asset) => ({ asset, algoPrice, provider: undefined }), - target: getLPTokenInfoFx, + fn: ({ lpTokens, algoPrice }) => ({ lpTokenIds: lpTokens.toArray(), algoPrice }), + target: getLPTokenInfosFx, }); $farmPools.watch((farms) => { diff --git a/src/dexes/index.ts b/src/dexes/index.ts index 3012a9a7..28446a46 100644 --- a/src/dexes/index.ts +++ b/src/dexes/index.ts @@ -1,8 +1,7 @@ import { algod, ALGONET, TESTNET } from '../AppContext'; import { AppId, Asset, AssetId, Amount, fetchAsset, ALGO_ASSET } from '../common/store'; -import { WalletTransactionGroup } from '../types'; -import { Dex, PoolInfo, SwapQuote, MintQuote, DexProvider, DexPool } from './common'; +import { Dex, SwapQuote, DexProvider, DexPool } from './common'; import { PactDex } from './pact'; import { TinymanDex } from './tinyman'; import { HumbleDex } from './humble'; diff --git a/src/providers/apiProvider.ts b/src/providers/apiProvider.ts index 0244d7d1..b5f9e095 100644 --- a/src/providers/apiProvider.ts +++ b/src/providers/apiProvider.ts @@ -11,6 +11,7 @@ import { StakingAsset } from '../Farm/AddFarm'; import * as MiniHumble from '../dexes/humbleReexports'; import { TokenOptionType } from '../Components/Select/types'; import { NftLottery } from '../Swap/NftWinModal'; +import { DexProvider } from '../dexes'; export const instance = axios.create({ baseURL: process.env.REACT_APP_COMETA_API_URL, @@ -115,34 +116,50 @@ export async function getContracts( }); } -export type LpState = { +export type PricedLpInfo = { id: number; token_id: number; asset1_id: number; asset2_id: number; - dex_provider: string; + dex_provider: DexProvider; address: string; + asset1_reserve_micros: number; + asset2_reserve_micros: number; + issued_tokens_micros: number; + asset1_reserve: number; asset2_reserve: number; issued_tokens: number; - token_price: number; + token_price_algo: number; token_price_usd: number; swap_fee_apr?: number; }; -export async function getLpState(lp_id: number): Promise { - const res = await instance - .post(`/info/lp/state?lp_token_id=${lp_id}`) +export async function getPricedLpInfo(lp_token_id: number): Promise { + return await instance + .post(`/lp/state/priced?lp_token_id=${lp_token_id}`) .then(({ data }) => data) .catch((error) => { console.log('ERR', error); throw error; }); - return res; } +export async function getPricedLpInfos(lp_token_ids?: number[]): Promise { + const params: { [key: string]: any } = {}; + if (lp_token_ids) { + params['lp_token_ids'] = lp_token_ids; + } + return await instance + .post(`/lp/states/priced`, params) + .then(({ data }) => data) + .catch((error) => { + console.log('ERR', error); + throw error; + }); +} export async function getPoolInfo(asset1: number, asset2: number): Promise { return instance.get(`/pool?asset_1_id=${asset1}&asset_2_id=${asset2}`).then(({ data }) => data); } From 16060267628d8c14e8102e25d5119f3706f70147 Mon Sep 17 00:00:00 2001 From: wackloner Date: Tue, 16 Apr 2024 08:24:09 +0800 Subject: [PATCH 2/4] prod settings to dev env --- .env.dev | 13 +++++-------- .env.testnet | 23 +++++++++++++++++++++++ package.json | 1 + 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 .env.testnet diff --git a/.env.dev b/.env.dev index e27f61bb..61df93d5 100644 --- a/.env.dev +++ b/.env.dev @@ -1,12 +1,11 @@ - -REACT_APP_COMETA_API_URL='https://api.testnet.cometa.farm/' -REACT_APP_BENEFICIARY_ADDR='3ONHOKYJGYREOUEP52JP7YMZDR77ZHM7B4KOJ3AMPSYOJTINL5W5YOX2HM' -REACT_APP_FARM_CREATION_FEE='200' -REACT_APP_FARM_FLAT_ALGO_CREATION_FEE='5' +REACT_APP_COMETA_API_URL='https://api.cometa.farm/' +REACT_APP_BENEFICIARY_ADDR='METAFG5UBD74CKQFIIABWMMQXR45J7BAP3KV6BVR3V7LDPNAEKNEVLMBRE' +REACT_APP_FARM_CREATION_FEE='100' +REACT_APP_FARM_FLAT_ALGO_CREATION_FEE='100' REACH_CONNECTOR_MODE=ALGO-browser # TODO: Make it configurable between deployments/builds (e.g. put it in .env.local and set this as actual env variables in deployments) -ALGO_NETWORK=TestNet +ALGO_NETWORK=MainNet ALGO_TOKEN='' @@ -19,5 +18,3 @@ ALGO_INDEXER_PORT=$ALGO_PORT ALGO_INDEXER_SERVER="https://${ALGO_NETWORK}-idx.algonode.cloud" REACT_APP_ALAMMEX_API_KEY='9a1c1fe8-b49a-4d18-b393-4777e557ff74' - -API_CONTRACTS_MAX_COUNT=10 diff --git a/.env.testnet b/.env.testnet new file mode 100644 index 00000000..e27f61bb --- /dev/null +++ b/.env.testnet @@ -0,0 +1,23 @@ + +REACT_APP_COMETA_API_URL='https://api.testnet.cometa.farm/' +REACT_APP_BENEFICIARY_ADDR='3ONHOKYJGYREOUEP52JP7YMZDR77ZHM7B4KOJ3AMPSYOJTINL5W5YOX2HM' +REACT_APP_FARM_CREATION_FEE='200' +REACT_APP_FARM_FLAT_ALGO_CREATION_FEE='5' +REACH_CONNECTOR_MODE=ALGO-browser + +# TODO: Make it configurable between deployments/builds (e.g. put it in .env.local and set this as actual env variables in deployments) +ALGO_NETWORK=TestNet + +ALGO_TOKEN='' + +ALGO_SERVER="https://${ALGO_NETWORK}-api.algonode.cloud" +ALGO_PORT=443 + +ALGO_INDEXER_TOKEN=$ALGO_TOKEN + +ALGO_INDEXER_PORT=$ALGO_PORT +ALGO_INDEXER_SERVER="https://${ALGO_NETWORK}-idx.algonode.cloud" + +REACT_APP_ALAMMEX_API_KEY='9a1c1fe8-b49a-4d18-b393-4777e557ff74' + +API_CONTRACTS_MAX_COUNT=10 diff --git a/package.json b/package.json index 89fb8c37..e09dd992 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "build": "GENERATE_SOURCEMAP=false node scripts/build.js", "prod": "env-cmd -f .env.prod yarn build", "dev": "env-cmd -f .env.dev yarn build", + "testnet": "env-cmd -f .env.testnet yarn build", "test": "node scripts/test.js", "postinstall": "husky install", "lint": "xo src", From 55ce1c0fee881b14a0aa05729fe3d70aa5326aa1 Mon Sep 17 00:00:00 2001 From: wackloner Date: Wed, 17 Apr 2024 01:24:44 +0800 Subject: [PATCH 3/4] fetch all assets from the backend --- src/Farm/store.ts | 35 ++--------- src/common/store/assets.ts | 99 ++++++++++++++++---------------- src/common/store/prices.ts | 2 +- src/index.tsx | 27 ++++----- src/providers/apiProvider.ts | 45 --------------- src/providers/flexApiProvider.ts | 77 +++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 141 deletions(-) create mode 100644 src/providers/flexApiProvider.ts diff --git a/src/Farm/store.ts b/src/Farm/store.ts index e690f4cc..6147ed68 100644 --- a/src/Farm/store.ts +++ b/src/Farm/store.ts @@ -12,7 +12,7 @@ import { ContractState, Amount, Priced, - assetLoaded, + allAssetsLoaded, registerAsset, registerPricedAsset, $algoUsdPrice, @@ -26,11 +26,11 @@ import { Time, $meanRoundDuration, } from '../common/store'; +import { getPricedLpInfo, getPricedLpInfos, PricedLpInfo } from '../providers/flexApiProvider'; import { nonConcurrent } from '../common/store/utils'; import { AllDefined, Backend } from '../types'; import { LPTokenInfo, DexProvider } from '../dexes'; import { fromSmallestUnits, YEAR } from '../common/lib'; -import { getPricedLpInfo, getPricedLpInfos, PricedLpInfo } from '../providers/apiProvider'; import { calculateAlgoReward, convertAmountToUSD, getPoolState } from './PoolList/Pool/utils'; import { PoolState } from './PoolList/Pool/types'; import { ColumnType } from './PoolList/PoolList'; @@ -153,33 +153,10 @@ type LPTokenStore = Map>; export const $lpTokenInfos = createStore(Map()); -// export const getLPTokenInfoFx = createEffect( -// nonConcurrent( -// async ({ -// asset, -// provider, -// algoPrice, -// }: { -// asset: Asset; -// provider: DexProvider | undefined; -// algoPrice: number | null; -// }) => getLPTokenInfoBackend(asset) -// ) -// ); -// -// $lpTokenInfos.on(getLPTokenInfoFx.done, (state, { params, result }) => state.set(assetId(params.asset), result)); -// -// sample({ -// clock: assetLoaded, -// source: { lpTokens: $lpTokenIds, algoPrice: $algoUsdPrice }, -// filter: ({ lpTokens }, asset) => lpTokens.has(asset.id), -// fn: ({ algoPrice }, asset) => ({ asset, algoPrice, provider: undefined }), -// target: getLPTokenInfoFx, -// }); - export const getLPTokenInfosFx = createEffect( - nonConcurrent(async ({ lpTokenIds, algoPrice }: { lpTokenIds: AssetId[]; algoPrice: number | null }) => - getManyLPInfosBackend() + nonConcurrent( + async ({ lpTokenIds, algoPrice }: { lpTokenIds: AssetId[]; algoPrice: number | null }) => + await getManyLPInfosBackend() ) ); @@ -198,7 +175,7 @@ const $lpTokenIds = createStore(Set()).on($farmPools, (state, pools) => // Automatically fetch LP token infos when general info about them gets fetched the first time sample({ - clock: assetLoaded, + clock: allAssetsLoaded, source: { lpTokens: $lpTokenIds, algoPrice: $algoUsdPrice }, fn: ({ lpTokens, algoPrice }) => ({ lpTokenIds: lpTokens.toArray(), algoPrice }), target: getLPTokenInfosFx, diff --git a/src/common/store/assets.ts b/src/common/store/assets.ts index aae6aed2..94998f62 100644 --- a/src/common/store/assets.ts +++ b/src/common/store/assets.ts @@ -3,9 +3,10 @@ import { createEffect, createEvent, createStore, sample, combine, split, Store, import { algod, USDT_TOKEN_ID } from '../../AppContext'; import { getAlgoRateFromVestige } from '../../providers/coinPriceProvider'; import { pactDex } from '../../dexes'; +import { getAllAssets } from '../../providers/flexApiProvider'; import { $accountInfo } from './account'; import { Asset, AssetId, Amount, Priced } from './types'; -import { nonConcurrent, fetchStore } from './utils'; +import { nonConcurrent } from './utils'; import { doEachTick } from './time'; // Main event to add the asset, adds it to all of the relevant stores @@ -43,62 +44,58 @@ export const ALGO_ASSET: Asset = { decimals: 6, }; -const fetchAssetFx = createEffect( - nonConcurrent(async (id: AssetId): Promise => { - const { params } = await algod.getAssetByID(id).do(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { creator, reserve, decimals } = params; - - const name = params['name'] ?? Buffer.from(params['name-b64'], 'base64').toString(); - const unit_name = params['unit-name'] ?? Buffer.from(params['unit-name-b64'], 'base64').toString(); - - return { - id, - name: name as string, - unitName: unit_name as string, - creator: creator as string, - reserve: reserve as string, - decimals: decimals as number, - } as Asset; - }) -); +export async function fetchAllAssets(): Promise { + console.log('\nfetching all assets\n'); + const assetDetails = await getAllAssets(); + console.log('assetDetails:', assetDetails); + + return assetDetails.map((asset) => ({ + id: asset.id, + name: asset.name, + unitName: asset.unit_name, + creator: asset.creator, + reserve: asset.reserve, + decimals: asset.decimals, + })); +} -export const assetLoaded = fetchAssetFx.doneData; -export const $assets = createStore(Map().set(0, ALGO_ASSET)).on(assetLoaded, (assets, a) => - assets.set(a.id, a) +export const fetchAllAssetsFx = createEffect( + nonConcurrent(async (): Promise => { + console.log('Inside fetchAllAssetsFx'); + try { + const assets = await fetchAllAssets(); + console.log('Assets fetched:', assets); + return assets; + } catch (error) { + console.error('Failed to fetch assets:', error); + return []; // Return an empty array in case of failure + } + }) ); +export const allAssetsLoaded = fetchAllAssetsFx.doneData; -const queryAsset = createEvent(); -const assetFoundInStore = createEvent(); -const storeFetchAttempt = sample({ clock: queryAsset, source: $assets, fn: (assets, id) => assets.get(id) || id }); - -split({ - source: storeFetchAttempt, - match: (a: Asset | AssetId) => (typeof a === 'number' ? 'fetch' : 'return'), - cases: { - fetch: fetchAssetFx, - return: assetFoundInStore, - }, -}); - -// Fetch asset info from algod on asset registration -sample({ - clock: registerAsset, - target: queryAsset, -}); - -/** - * Queries the asset by ID and returns without fetching it from algod if it's already in the store. - * - * @param assetId - * @returns Promise with asset - */ -export const fetchAsset = async (id: AssetId): Promise => { - const saved = await fetchStore($assets.map((assets) => assets.get(id, null))); +export const fetchAsset = async (assetId: AssetId): Promise => { + console.log('fetching asset with id:', assetId); + // eslint-disable-next-line effector/no-getState + const assets = $assets.getState(); + const saved = assets.get(assetId, null); if (saved) return saved; - return fetchAssetFx(id); + throw new Error(`Asset with id ${assetId} not found`); }; +export const fetchAssetFx = createEffect(nonConcurrent(async (id: AssetId): Promise => await fetchAsset(id))); +export const assetLoaded = fetchAssetFx.doneData; + +export const $assets = createStore(Map().set(0, ALGO_ASSET)).on( + allAssetsLoaded, + (assets, newAssets) => { + if (newAssets.length === 0) { + return assets; // Return the current state if no new assets are loaded + } + return newAssets.reduce((acc, asset) => acc.set(asset.id, asset), assets); + } +); + // ================================================================= // ALGO price fetching // (token prices are in separate file to avoid circular import to/from dexesProvider) diff --git a/src/common/store/prices.ts b/src/common/store/prices.ts index d3f348a2..6c6300a7 100644 --- a/src/common/store/prices.ts +++ b/src/common/store/prices.ts @@ -4,7 +4,7 @@ import { Map } from 'immutable'; import { getSwapCostSomewhere } from '../../dexes'; import { META_TOKEN_ID } from '../../AppContext'; import { SLIPPAGE } from '../../Swap/Swap'; -import { $assets, assetLoaded, ALGO_ASSET, $pricedAlgo, registerAsset } from './assets'; +import { $assets, ALGO_ASSET, $pricedAlgo, registerAsset, assetLoaded } from './assets'; import { Asset, AssetId, Priced } from './types'; import { nonConcurrent } from './utils'; diff --git a/src/index.tsx b/src/index.tsx index 44b55634..81267355 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,11 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import * as Sentry from '@sentry/react'; -import { BrowserTracing } from '@sentry/tracing'; - import { ThemeProvider } from 'styled-components'; import { QueryClient, QueryClientProvider, useQuery } from 'react-query'; import { useEffect, useState } from 'react'; import { useModal, ModalProvider } from 'react-hooks-use-modal'; -import { useStoreMap, useUnit } from 'effector-react'; +import { useStore, useStoreMap, useUnit } from 'effector-react'; import { Flip, ToastContainer } from 'react-toastify'; import ReactGA from 'react-ga'; import GlobalStyle from './common/globalStyles'; @@ -21,7 +18,7 @@ import { Zap } from './Zap'; import { MetaDAO } from './MetaDAO'; import { theme } from './theme'; import { Container, ContentContainer } from './common/styled'; -import { $account, ContractInfo, fetchAllPricesFx } from './common/store'; +import { $account, ContractInfo, fetchAllAssetsFx, fetchAllPricesFx } from './common/store'; import { Stake } from './Stake/Stake'; import './css/index.css'; @@ -34,19 +31,18 @@ import { Footer } from './Menu/Footer'; import { notify } from './Components/Notification'; import { AddFarm } from './Farm/AddFarm'; import { LaaS } from './LaaS/LaaS'; -import { setLaasPoolInfos } from './LaaS/store'; import { AddLaaS } from './LaaS/AddLaaS'; import { Metapunks } from './Metapunks/Metapunks'; -Sentry.init({ - dsn: 'https://65dfff9b40a24539b633789b8cfba771@o1313570.ingest.sentry.io/6563864', - integrations: [new BrowserTracing()], - - // Set tracesSampleRate to 1.0 to capture 100% - // of transactions for performance monitoring. - // We recommend adjusting this value in production - tracesSampleRate: 1, -}); +// Sentry.init({ +// dsn: 'https://65dfff9b40a24539b633789b8cfba771@o1313570.ingest.sentry.io/6563864', +// integrations: [new BrowserTracing()], +// +// // Set tracesSampleRate to 1.0 to capture 100% +// // of transactions for performance monitoring. +// // We recommend adjusting this value in production +// tracesSampleRate: 1, +// }); ReactGA.initialize('G-P19KGXJGTP'); // TODO @@ -65,6 +61,7 @@ window.open = (function (open) { const queryClient = new QueryClient(); void fetchAllPricesFx(); +void fetchAllAssetsFx(); const WELCOME_MODAL_KEY = `welcome_cosmaud`; diff --git a/src/providers/apiProvider.ts b/src/providers/apiProvider.ts index b5f9e095..002ecb92 100644 --- a/src/providers/apiProvider.ts +++ b/src/providers/apiProvider.ts @@ -11,7 +11,6 @@ import { StakingAsset } from '../Farm/AddFarm'; import * as MiniHumble from '../dexes/humbleReexports'; import { TokenOptionType } from '../Components/Select/types'; import { NftLottery } from '../Swap/NftWinModal'; -import { DexProvider } from '../dexes'; export const instance = axios.create({ baseURL: process.env.REACT_APP_COMETA_API_URL, @@ -116,50 +115,6 @@ export async function getContracts( }); } -export type PricedLpInfo = { - id: number; - token_id: number; - asset1_id: number; - asset2_id: number; - dex_provider: DexProvider; - address: string; - - asset1_reserve_micros: number; - asset2_reserve_micros: number; - issued_tokens_micros: number; - - asset1_reserve: number; - asset2_reserve: number; - issued_tokens: number; - - token_price_algo: number; - token_price_usd: number; - swap_fee_apr?: number; -}; - -export async function getPricedLpInfo(lp_token_id: number): Promise { - return await instance - .post(`/lp/state/priced?lp_token_id=${lp_token_id}`) - .then(({ data }) => data) - .catch((error) => { - console.log('ERR', error); - throw error; - }); -} - -export async function getPricedLpInfos(lp_token_ids?: number[]): Promise { - const params: { [key: string]: any } = {}; - if (lp_token_ids) { - params['lp_token_ids'] = lp_token_ids; - } - return await instance - .post(`/lp/states/priced`, params) - .then(({ data }) => data) - .catch((error) => { - console.log('ERR', error); - throw error; - }); -} export async function getPoolInfo(asset1: number, asset2: number): Promise { return instance.get(`/pool?asset_1_id=${asset1}&asset_2_id=${asset2}`).then(({ data }) => data); } diff --git a/src/providers/flexApiProvider.ts b/src/providers/flexApiProvider.ts new file mode 100644 index 00000000..3819188c --- /dev/null +++ b/src/providers/flexApiProvider.ts @@ -0,0 +1,77 @@ +import { DexProvider } from '../dexes'; +import { AssetId } from '../common/store'; +import { instance } from './apiProvider'; + +export type AssetDetails = { + id: AssetId; + name: string; + unit_name: string; + creator: string; + reserve: string; + decimals: number; +}; + +export type PricedLpInfo = { + id: number; + token_id: number; + asset1_id: number; + asset2_id: number; + dex_provider: DexProvider; + address: string; + + asset1_reserve_micros: number; + asset2_reserve_micros: number; + issued_tokens_micros: number; + + asset1_reserve: number; + asset2_reserve: number; + issued_tokens: number; + + token_price_algo: number; + token_price_usd: number; + swap_fee_apr?: number; +}; + +export async function getPricedLpInfo(lp_token_id: number): Promise { + return await instance + .post(`/lp/state/priced?lp_token_id=${lp_token_id}`) + .then(({ data }) => data) + .catch((error) => { + console.log('ERR', error); + throw error; + }); +} + +export async function getPricedLpInfos(lp_token_ids?: number[]): Promise { + const params: { [key: string]: any } = {}; + if (lp_token_ids) { + params['lp_token_ids'] = lp_token_ids; + } + return await instance + .post(`/lp/states/priced`, params) + .then(({ data }) => data) + .catch((error) => { + console.log('ERR', error); + throw error; + }); +} + +export async function getAsset(asset_id: number): Promise { + return await instance + .post(`/asset?asset_id=${asset_id}`) + .then(({ data }) => data) + .catch((error) => { + console.log('ERR', error); + throw error; + }); +} + +export async function getAllAssets(asset_ids: number[] | null = null): Promise { + try { + const response = await instance.post(`/assets`, { ids: asset_ids }); + return response.data; + } catch (error) { + console.error('Failed to fetch assets from API:', error); + throw error; // Rethrow the error to be handled by the effect + } +} From 92e985f109fcbbe684fe34f79b7c780c9994e8d8 Mon Sep 17 00:00:00 2001 From: wackloner Date: Wed, 17 Apr 2024 02:31:30 +0800 Subject: [PATCH 4/4] fetch prices from backend by batch --- src/Farm/store.ts | 37 ++---------- src/Farm/utils.ts | 18 ++++++ src/LaaS/store.ts | 6 +- src/Menu/Menu.tsx | 4 +- src/Stake/store.ts | 4 +- src/Swap/Swap.tsx | 14 +---- src/common/store/assets.ts | 57 ------------------ src/common/store/prices.ts | 100 ++++++++++++++----------------- src/providers/flexApiProvider.ts | 29 +++++++++ 9 files changed, 103 insertions(+), 166 deletions(-) diff --git a/src/Farm/store.ts b/src/Farm/store.ts index 6147ed68..24c8e665 100644 --- a/src/Farm/store.ts +++ b/src/Farm/store.ts @@ -14,8 +14,6 @@ import { Priced, allAssetsLoaded, registerAsset, - registerPricedAsset, - $algoUsdPrice, fetchAsset, ALGO_ASSET, $pricedAssets, @@ -25,6 +23,7 @@ import { hasLocalState, Time, $meanRoundDuration, + $pricedAlgo, } from '../common/store'; import { getPricedLpInfo, getPricedLpInfos, PricedLpInfo } from '../providers/flexApiProvider'; import { nonConcurrent } from '../common/store/utils'; @@ -40,25 +39,6 @@ const FARM_BACKENDS = { '17.2.5': farmBackend_17_2_5 as Backend, }; -// TODO: this function is a huge costyl -export function detectAssetProvider({ name }: { name: string }): DexProvider { - if (name.includes('TinymanPool2.0')) { - return 'T3'; - } - name = name.toLowerCase(); - if (name.includes('tinyman')) { - return 'T2'; - } - if (name.includes('humble')) { - return 'H2'; - } - if (name.includes('liquidity') || name.includes('pact')) { - return 'PT'; - } - - return 'MOCK'; -} - function formatPricedLPInfo(lpInfo: PricedLpInfo, asset: Asset): Priced { return { ...asset, @@ -176,7 +156,7 @@ const $lpTokenIds = createStore(Set()).on($farmPools, (state, pools) => // Automatically fetch LP token infos when general info about them gets fetched the first time sample({ clock: allAssetsLoaded, - source: { lpTokens: $lpTokenIds, algoPrice: $algoUsdPrice }, + source: { lpTokens: $lpTokenIds, algoPrice: $pricedAlgo.map((algo) => algo?.price ?? null) }, fn: ({ lpTokens, algoPrice }) => ({ lpTokenIds: lpTokens.toArray(), algoPrice }), target: getLPTokenInfosFx, }); @@ -185,15 +165,6 @@ $farmPools.watch((farms) => { const farmStates = farms.map((farm) => farm.state).filter((s): s is ContractState<'farm'> => s !== null); for (const s of farmStates) { registerAsset(s.initial.stakeToken); - registerPricedAsset(s.initial.rewardToken); - } -}); - -$stakePools.watch((farms) => { - const farmStates = farms.map((farm) => farm.state).filter((s): s is ContractState<'farm'> => s !== null); - for (const s of farmStates) { - registerPricedAsset(s.initial.stakeToken); - registerPricedAsset(s.initial.rewardToken); } }); @@ -307,7 +278,7 @@ export function createAprs( $pools, $networkTime, $meanRoundDuration, - $algoUsdPrice, + $pricedAlgo, $stakeTokens, $farmRewardTokens, (pools, time, meanRoundDuration, algoPrice, stakingTokens, farmRewardTokens) => @@ -350,7 +321,7 @@ export function createAprs( const algoRewardAPR = totalStakedUSD && algoPrice - ? ((algoRewardPerBlock * algoPrice * blocksInAYear) / totalStakedUSD) * 100 + ? ((algoRewardPerBlock * algoPrice.price * blocksInAYear) / totalStakedUSD) * 100 : 0; const feesAPR = ((stakeTokenInfo as Priced).dexFeeApr ?? 0) * 100; diff --git a/src/Farm/utils.ts b/src/Farm/utils.ts index 95e609de..087096c0 100644 --- a/src/Farm/utils.ts +++ b/src/Farm/utils.ts @@ -101,3 +101,21 @@ export const useWalletPersistedState = ( return usePersistedState(walletAddr ? `${walletAddr}-${key}` : undefined, initialValue); }; + +export function detectAssetProvider({ name }: { name: string }): DexProvider { + if (name.includes('TinymanPool2.0')) { + return 'T3'; + } + name = name.toLowerCase(); + if (name.includes('tinyman')) { + return 'T2'; + } + if (name.includes('humble')) { + return 'H2'; + } + if (name.includes('liquidity') || name.includes('pact')) { + return 'PT'; + } + + return 'MOCK'; +} diff --git a/src/LaaS/store.ts b/src/LaaS/store.ts index 12d0f086..0db2d453 100644 --- a/src/LaaS/store.ts +++ b/src/LaaS/store.ts @@ -1,4 +1,4 @@ -import { buildContractsStore, ContractType, registerPricedAsset } from '../common/store'; +import { buildContractsStore } from '../common/store'; import { backend as laasBackend } from '../cometa-laas-tmp/wrapper'; // TODO shall we support multiple backends? @@ -13,8 +13,8 @@ export const initializeLaasPool = initializeContract; $contractStatesWithCache.watch((states) => states.valueSeq().forEach((s) => { - registerPricedAsset(s.initial.aToken); - registerPricedAsset(s.initial.bToken); + // registerPricedAsset(s.initial.aToken); + // registerPricedAsset(s.initial.bToken); }) ); diff --git a/src/Menu/Menu.tsx b/src/Menu/Menu.tsx index 665ba455..080bce54 100644 --- a/src/Menu/Menu.tsx +++ b/src/Menu/Menu.tsx @@ -4,7 +4,7 @@ import logo from '../imgs/logo.png'; import meta_logo from '../imgs/meta_token.svg'; import algo_logo from '../imgs/algo_token.svg'; import burger from '../imgs/burger.svg'; -import { $algoUsdPrice, $pricedAssets, Asset, Priced } from '../common/store'; +import { $pricedAlgo, $pricedAssets, Asset, Priced } from '../common/store'; import { ConnectWallet } from '../wallet/ConnectWallet'; import { ALGONET, isMobile, META_TOKEN_ID, TESTNET } from '../AppContext'; import { getTokenLink } from '../Farm/PoolList/Pool/utils'; @@ -70,7 +70,7 @@ function BurgerMenu({ ALGOPrice, METAPrice }: { ALGOPrice: number | null; METAPr } export function Menu() { - const ALGOPrice = useUnit($algoUsdPrice); + const ALGOPrice = useUnit($pricedAlgo)?.price ?? null; const METAPrice = useStoreMap($pricedAssets, (as) => as.get(META_TOKEN_ID, null)); const [isBurgerOpen, setIsBurgerOpen] = useState(false); diff --git a/src/Stake/store.ts b/src/Stake/store.ts index 4bbfa647..930509f2 100644 --- a/src/Stake/store.ts +++ b/src/Stake/store.ts @@ -4,7 +4,7 @@ import { backend as distribution_17_0_4 } from 'metalabs-distribution-17_0_4'; import { backend as distribution_17_0_5 } from 'metalabs-distribution-17_0_5'; import { combine, Store } from 'effector'; -import { buildContractsStore, registerPricedAsset, $networkTime, $pricedAssets, Contract } from '../common/store'; +import { buildContractsStore, $networkTime, $pricedAssets, Contract } from '../common/store'; import { $stakePools, $farmStakeTokens, @@ -42,7 +42,7 @@ export const $allStakePools = combine( $contractStatesWithCache.watch((states) => states.valueSeq().forEach((s) => { - registerPricedAsset(s.initial.token); + // registerPricedAsset(s.initial.token); }) ); diff --git a/src/Swap/Swap.tsx b/src/Swap/Swap.tsx index 116a87a0..26523a1f 100644 --- a/src/Swap/Swap.tsx +++ b/src/Swap/Swap.tsx @@ -1,21 +1,10 @@ import React, { useEffect, useRef, useState } from 'react'; import { useUnit } from 'effector-react'; import { Account } from '@reach-sh/stdlib/ALGO'; -import { useModal } from 'react-hooks-use-modal'; -import { func } from 'prop-types'; import { DeflexQuote } from '@deflex/deflex-sdk-js'; import { theme } from '../theme'; import { ALGONET, deflexClient, MAINNET, META_TOKEN_ID, reach, TESTNET } from '../AppContext'; -import { - $account, - $balances, - Amount, - Asset, - AssetId, - fetchAsset, - fetchAssetPriceFx, - refreshAccountInfo, -} from '../common/store'; +import { $account, $balances, Amount, Asset, AssetId, fetchAsset, refreshAccountInfo } from '../common/store'; import { logEvent, LogName } from '../logEvent'; import { PacmanButton } from '../Components/PacmanButton/PacmanButton'; @@ -23,7 +12,6 @@ import { algoexplorerTxLink, fromSmallestUnits, getSmallestUnits, - parseTxs, SentTxError, signAndPostTxnGroups, } from '../common/lib'; diff --git a/src/common/store/assets.ts b/src/common/store/assets.ts index 94998f62..90e57bcb 100644 --- a/src/common/store/assets.ts +++ b/src/common/store/assets.ts @@ -1,13 +1,9 @@ import { Map } from 'immutable'; import { createEffect, createEvent, createStore, sample, combine, split, Store, restore } from 'effector'; -import { algod, USDT_TOKEN_ID } from '../../AppContext'; -import { getAlgoRateFromVestige } from '../../providers/coinPriceProvider'; -import { pactDex } from '../../dexes'; import { getAllAssets } from '../../providers/flexApiProvider'; import { $accountInfo } from './account'; import { Asset, AssetId, Amount, Priced } from './types'; import { nonConcurrent } from './utils'; -import { doEachTick } from './time'; // Main event to add the asset, adds it to all of the relevant stores export const registerAsset = createEvent(); @@ -83,9 +79,6 @@ export const fetchAsset = async (assetId: AssetId): Promise => { throw new Error(`Asset with id ${assetId} not found`); }; -export const fetchAssetFx = createEffect(nonConcurrent(async (id: AssetId): Promise => await fetchAsset(id))); -export const assetLoaded = fetchAssetFx.doneData; - export const $assets = createStore(Map().set(0, ALGO_ASSET)).on( allAssetsLoaded, (assets, newAssets) => { @@ -95,53 +88,3 @@ export const $assets = createStore(Map().set(0, ALGO_ASSET)).on( return newAssets.reduce((acc, asset) => acc.set(asset.id, asset), assets); } ); - -// ================================================================= -// ALGO price fetching -// (token prices are in separate file to avoid circular import to/from dexesProvider) -// ================================================================= -export const fetchAlgoPriceFx = createEffect( - nonConcurrent(async () => { - try { - const rate = await getAlgoRateFromVestige(); - if (!rate) { - throw new Error(`Failed to fetch ALGO price from Vestige`); - } - - return Number(rate.price); - } catch (error) { - console.warn('Failed to get price from Vestige, piggybacking on Pact. Error was:', error); - const ALGO = 0; - const pool = await pactDex.getMostLiquidPool(ALGO, USDT_TOKEN_ID); - - return pool.calculator.primaryAssetPrice; - } - }) -); - -export const $algoUsdPrice = restore(fetchAlgoPriceFx.doneData, null); - -export const fetchAllPricesFx = createEffect(async () => { - //console.log('fetching prices...'); - return fetchAlgoPriceFx() - .then(() => { - // console.log('prices fetched'); - }) - .catch(() => { - console.log('failed to fetch prices :('); - }); -}); - -// Re-fetch prices once in say, 1 minute -void doEachTick(60_000, fetchAllPricesFx); - -export const $pricedAlgo: Store | null> = combine( - $assets.map((as) => as.get(0, ALGO_ASSET)), - $algoUsdPrice, - (algoAsset, price) => { - if (price !== null) { - return { ...algoAsset, price, priceInAlgo: 1 }; - } - return null; - } -); diff --git a/src/common/store/prices.ts b/src/common/store/prices.ts index 6c6300a7..896de9a7 100644 --- a/src/common/store/prices.ts +++ b/src/common/store/prices.ts @@ -1,72 +1,60 @@ import { createStore, createEffect, createEvent, combine, sample, Store } from 'effector'; import { Map } from 'immutable'; -import { getSwapCostSomewhere } from '../../dexes'; -import { META_TOKEN_ID } from '../../AppContext'; -import { SLIPPAGE } from '../../Swap/Swap'; -import { $assets, ALGO_ASSET, $pricedAlgo, registerAsset, assetLoaded } from './assets'; +import { AssetPriceInfo, getAllPrices } from '../../providers/flexApiProvider'; +import { $assets, ALGO_ASSET } from './assets'; import { Asset, AssetId, Priced } from './types'; import { nonConcurrent } from './utils'; - -export const fetchAssetPriceFx = createEffect( - nonConcurrent(async (asset: Asset): Promise => { - if (asset.id === 0) { - return 1; +import { doEachTick } from './time'; + +export const fetchAllPricesFx = createEffect( + nonConcurrent(async (): Promise => { + console.log('Inside fetchAllPricesFx'); + try { + const prices = await getAllPrices(); + console.log('Prices fetched:', prices); + return prices; + } catch (error) { + console.error('Failed to fetch prices:', error); + return []; // Return an empty array in case of failure } - const swapQuote = await getSwapCostSomewhere(asset, ALGO_ASSET, BigInt(10 ** asset.decimals), SLIPPAGE); - return swapQuote.price; }) ); -export const $assetAlgoPrices = createStore(Map()).on( - fetchAssetPriceFx.done, - (prices, { params, result }) => prices.set(params.id, result) +export const $assetPrices = createStore(Map()).on( + fetchAllPricesFx.doneData, + (prices, newPrices) => { + return newPrices.reduce((acc, price) => acc.set(price.asset_id, price), prices); + } ); -// Bool flags needed to not fetch swap prices of LP tokens for example -export const requireAssetPrice = createEvent(); -export const $assetIsPriced = createStore(Map()).on(requireAssetPrice, (as, id) => as.set(id, true)); - -export const registerPricedAsset = createEvent(); -sample({ - clock: registerPricedAsset, - target: [registerAsset, requireAssetPrice], -}); - -registerPricedAsset(META_TOKEN_ID); - -// Automatically fetch necessary assets prices when info about them is getting loaded first time -sample({ - clock: assetLoaded, - source: $assetIsPriced, - filter: (pricedFlags, asset) => pricedFlags.get(asset.id, false), - fn: (_, asset) => asset, - target: fetchAssetPriceFx, -}); - -fetchAssetPriceFx.fail.watch((v) => { - console.log('ASSET PRICE FETCHING FAILED', v); +void doEachTick(60_000, fetchAllPricesFx); + +export const $pricedAssets: Store>> = combine($assets, $assetPrices, (assets, prices) => { + return assets.map((asset) => { + const price = prices.get(asset.id); + if (!price) { + return { + ...asset, + price: 0, + priceInAlgo: 0, + }; + } + return { + ...asset, + price: price.price_usd, + priceInAlgo: price.price_algo, + }; + }); }); -export const $pricedAssets: Store>> = combine( - $pricedAlgo, - $assetAlgoPrices, - $assets, - (pricedAlgo: Priced | null, assetAlgoPrices: Map, assets: Map) => { - if (pricedAlgo === null) { - return Map>(); // Empty map, because cannot price anything without algo price +export const $pricedAlgo: Store | null> = combine( + $assets.map((as) => as.get(0, ALGO_ASSET)), + $assetPrices.map((prices) => prices.get(0)), + (algoAsset, algoPrice) => { + if (algoPrice) { + return { ...algoAsset, price: algoPrice.price_usd, priceInAlgo: 1 }; } - - return assetAlgoPrices - .map((priceInAlgo, assetId) => { - const asset = assets.get(assetId); - if (!asset) { - throw new Error(`impossible: having price ${priceInAlgo} for unfetched asset ${assetId}`); - } - - const price = priceInAlgo * pricedAlgo.price; - return { ...asset, priceInAlgo, price }; - }) - .set(0, pricedAlgo); + return null; } ); diff --git a/src/providers/flexApiProvider.ts b/src/providers/flexApiProvider.ts index 3819188c..74108836 100644 --- a/src/providers/flexApiProvider.ts +++ b/src/providers/flexApiProvider.ts @@ -32,6 +32,15 @@ export type PricedLpInfo = { swap_fee_apr?: number; }; +export type AssetPriceInfo = { + asset_id: number; + asset_name: string; + price_usd: number; + price_algo: number; + last_update_round: number; + seconds_since_update?: number; +}; + export async function getPricedLpInfo(lp_token_id: number): Promise { return await instance .post(`/lp/state/priced?lp_token_id=${lp_token_id}`) @@ -75,3 +84,23 @@ export async function getAllAssets(asset_ids: number[] | null = null): Promise { + return await instance + .post(`/asset/price?asset_id=${asset_id}`) + .then(({ data }) => data) + .catch((error) => { + console.log('ERR', error); + throw error; + }); +} + +export async function getAllPrices(asset_ids: number[] | null = null): Promise { + try { + const response = await instance.post(`/assets/price`, { ids: asset_ids }); + return response.data; + } catch (error) { + console.error('Failed to fetch assets from API:', error); + throw error; // Rethrow the error to be handled by the effect + } +}