added caching for api requests (#513)

This commit is contained in:
Bob van der Helm 2023-10-01 15:56:22 +03:00 committed by GitHub
parent 44196f1a10
commit fe9040b29f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 260 additions and 57 deletions

View File

@ -1,3 +1,4 @@
import { cacheFn, positionsCache } from 'api/cache'
import { getCreditManagerQueryClient } from 'api/cosmwasm-client'
import getDepositedVaults from 'api/vaults/getDepositedVaults'
import { BNCoin } from 'types/classes/BNCoin'
@ -6,9 +7,13 @@ import { Positions } from 'types/generated/mars-credit-manager/MarsCreditManager
export default async function getAccount(accountId: string): Promise<Account> {
const creditManagerQueryClient = await getCreditManagerQueryClient()
const accountPosition: Positions = await creditManagerQueryClient.positions({ accountId })
const accountPosition: Positions = await cacheFn(
() => creditManagerQueryClient.positions({ accountId }),
positionsCache,
`account/${accountId}`,
)
const depositedVaults = await getDepositedVaults(accountId)
const depositedVaults = await getDepositedVaults(accountId, accountPosition)
if (accountPosition) {
return {

62
src/api/cache.ts Normal file
View File

@ -0,0 +1,62 @@
import {
ArrayOfCoin,
Positions,
VaultUtilizationResponse,
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { ArrayOfActiveEmission } from 'types/generated/mars-incentives/MarsIncentives.types'
import { PriceResponse } from 'types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types'
import {
AssetParamsBaseForAddr,
TotalDepositResponse,
VaultConfigBaseForAddr,
} from 'types/generated/mars-params/MarsParams.types'
import { ArrayOfMarket } from 'types/generated/mars-red-bank/MarsRedBank.types'
interface Cache<T> extends Map<string, { data: T | null; timestamp: number }> {}
let totalRequests: number = 0
let cachedRequests: number = 0
export async function cacheFn<T>(
fn: () => Promise<T>,
cache: Cache<T>,
key: string,
staleAfter: number = 5,
) {
const cachedData = cache.get(key)?.data
const isStale = (cache.get(key)?.timestamp || 0) + 1000 * staleAfter < new Date().getTime()
totalRequests += 1
if (cachedData && !isStale) {
cachedRequests += 1
return cachedData
}
const data = await fn()
cache.set(key, { data, timestamp: new Date().getTime() })
return data
}
export const positionsCache: Cache<Positions> = new Map()
export const aprsCacheResponse: Cache<Response> = new Map()
export const aprsCache: Cache<AprResponse> = new Map()
export const vaultConfigsCache: Cache<VaultConfigBaseForAddr[]> = new Map()
export const vaultUtilizationCache: Cache<VaultUtilizationResponse> = new Map()
export const unlockPositionsCache: Cache<VaultExtensionResponse> = new Map()
export const estimateWithdrawCache: Cache<Coin[]> = new Map()
export const previewRedeemCache: Cache<string> = new Map()
export const priceCache: Cache<BigNumber> = new Map()
export const pythPriceCache: Cache<PythConfidenceData> = new Map()
export const oraclePriceCache: Cache<PriceResponse[]> = new Map()
export const poolPriceCache: Cache<PriceResponse[]> = new Map()
export const emissionsCache: Cache<ArrayOfActiveEmission> = new Map()
export const marketCache: Cache<Market> = new Map()
export const marketsCache: Cache<ArrayOfMarket> = new Map()
export const underlyingLiquidityAmountCache: Cache<string> = new Map()
export const unclaimedRewardsCache: Cache<ArrayOfCoin> = new Map()
export const totalDepositCache: Cache<TotalDepositResponse> = new Map()
export const allParamsCache: Cache<AssetParamsBaseForAddr[]> = new Map()
export const underlyingDebtCache: Cache<string> = new Map()
export const previewDepositCache: Cache<{ vaultAddress: string; amount: string }> = new Map()

View File

@ -1,3 +1,4 @@
import { cacheFn, emissionsCache } from 'api/cache'
import { getIncentivesQueryClient } from 'api/cosmwasm-client'
import getPrice from 'api/prices/getPrice'
import { ASSETS } from 'constants/assets'
@ -10,9 +11,15 @@ export default async function getTotalActiveEmissionValue(
): Promise<BigNumber | null> {
try {
const client = await getIncentivesQueryClient()
const activeEmissions = await client.activeEmissions({
collateralDenom: denom,
})
const activeEmissions = await cacheFn(
() =>
client.activeEmissions({
collateralDenom: denom,
}),
emissionsCache,
`emission/${denom}`,
60,
)
if (activeEmissions.length === 0) {
throw 'Asset has no active incentive emission.'

View File

@ -1,3 +1,4 @@
import { cacheFn, unclaimedRewardsCache } from 'api/cache'
import { getIncentivesQueryClient } from 'api/cosmwasm-client'
import { BNCoin } from 'types/classes/BNCoin'
@ -7,11 +8,17 @@ export default async function getUnclaimedRewards(
): Promise<BNCoin[]> {
try {
const client = await getIncentivesQueryClient()
const unclaimedRewards = await client.userUnclaimedRewards({
user,
accountId,
limit: 100,
})
const unclaimedRewards = await cacheFn(
() =>
client.userUnclaimedRewards({
user,
accountId,
limit: 100,
}),
unclaimedRewardsCache,
`incentives/${accountId}`,
60,
)
if (unclaimedRewards.length === 0) return []

View File

@ -1,7 +1,12 @@
import { resolveMarketResponse } from 'utils/resolvers'
import { cacheFn, marketCache } from 'api/cache'
import { getParamsQueryClient, getRedBankQueryClient } from 'api/cosmwasm-client'
import { resolveMarketResponse } from 'utils/resolvers'
export default async function getMarket(denom: string): Promise<Market> {
return cacheFn(() => fetchMarket(denom), marketCache, denom, 60)
}
async function fetchMarket(denom: string) {
try {
const redBankClient = await getRedBankQueryClient()
const paramsClient = await getParamsQueryClient()

View File

@ -1,5 +1,6 @@
import getMarkets from 'api/markets/getMarkets'
import { cacheFn, underlyingDebtCache } from 'api/cache'
import { getRedBankQueryClient } from 'api/cosmwasm-client'
import getMarkets from 'api/markets/getMarkets'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getMarketDebts(): Promise<BNCoin[]> {
@ -8,10 +9,16 @@ export default async function getMarketDebts(): Promise<BNCoin[]> {
const redBankQueryClient = await getRedBankQueryClient()
const debtQueries = markets.map((asset) =>
redBankQueryClient.underlyingDebtAmount({
denom: asset.denom,
amountScaled: asset.debtTotalScaled,
}),
cacheFn(
() =>
redBankQueryClient.underlyingDebtAmount({
denom: asset.denom,
amountScaled: asset.debtTotalScaled,
}),
underlyingDebtCache,
`marketDebts/${asset.denom}/amount/${asset.debtTotalScaled}`,
60,
),
)
const debtsResults = await Promise.all(debtQueries)

View File

@ -1,6 +1,16 @@
import { cacheFn, underlyingLiquidityAmountCache } from 'api/cache'
import { getRedBankQueryClient } from 'api/cosmwasm-client'
export default async function getUnderlyingLiquidityAmount(market: Market): Promise<string> {
return cacheFn(
() => fetchUnderlyingLiquidityAmount(market),
underlyingLiquidityAmountCache,
`underlyingLiquidity/${market.denom}/amount/${market.collateralTotalScaled}`,
60,
)
}
async function fetchUnderlyingLiquidityAmount(market: Market) {
try {
const client = await getRedBankQueryClient()
return await client.underlyingLiquidityAmount({

View File

@ -1,13 +1,14 @@
import { getEnabledMarketAssets } from 'utils/assets'
import { allParamsCache, cacheFn, marketsCache, totalDepositCache } from 'api/cache'
import { getParamsQueryClient, getRedBankQueryClient } from 'api/cosmwasm-client'
import iterateContractQuery from 'utils/iterateContractQuery'
import { byDenom } from 'utils/array'
import { resolveMarketResponse } from 'utils/resolvers'
import { Market as RedBankMarket } from 'types/generated/mars-red-bank/MarsRedBank.types'
import {
AssetParamsBaseForAddr as AssetParams,
TotalDepositResponse,
} from 'types/generated/mars-params/MarsParams.types'
import { Market as RedBankMarket } from 'types/generated/mars-red-bank/MarsRedBank.types'
import { byDenom } from 'utils/array'
import { getEnabledMarketAssets } from 'utils/assets'
import iterateContractQuery from 'utils/iterateContractQuery'
import { resolveMarketResponse } from 'utils/resolvers'
export default async function getMarkets(): Promise<Market[]> {
try {
@ -16,11 +17,21 @@ export default async function getMarkets(): Promise<Market[]> {
const enabledAssets = getEnabledMarketAssets()
const capQueries = enabledAssets.map((asset) =>
paramsClient.totalDeposit({ denom: asset.denom }),
cacheFn(
() => paramsClient.totalDeposit({ denom: asset.denom }),
totalDepositCache,
`enabledMarkets/${asset.denom}`,
60,
),
)
const [markets, assetParams, assetCaps] = await Promise.all([
iterateContractQuery(redBankClient.markets),
iterateContractQuery(paramsClient.allAssetParams),
cacheFn(() => iterateContractQuery(redBankClient.markets), marketsCache, 'markets', 60),
cacheFn(
async () => await iterateContractQuery(paramsClient.allAssetParams),
allParamsCache,
'params',
60,
),
Promise.all(capQueries),
])

View File

@ -1,3 +1,4 @@
import { cacheFn, oraclePriceCache } from 'api/cache'
import { getOracleQueryClient } from 'api/cosmwasm-client'
import { PRICE_ORACLE_DECIMALS } from 'constants/query'
import { BNCoin } from 'types/classes/BNCoin'
@ -11,7 +12,12 @@ export default async function getOraclePrices(...assets: Asset[]): Promise<BNCoi
if (!assets.length) return []
const oracleQueryClient = await getOracleQueryClient()
const priceResults = await iterateContractQuery(oracleQueryClient.prices)
const priceResults = await cacheFn(
() => iterateContractQuery(oracleQueryClient.prices),
oraclePriceCache,
'oraclePrices',
60,
)
return assets.map((asset) => {
const priceResponse = priceResults.find(byDenom(asset.denom)) as PriceResponse

View File

@ -1,3 +1,4 @@
import { cacheFn, poolPriceCache } from 'api/cache'
import getPrice from 'api/prices/getPrice'
import { ENV } from 'constants/env'
import { BN_ONE } from 'constants/math'
@ -34,7 +35,12 @@ export default async function getPoolPrice(
const getAssetRate = async (asset: Asset) => {
const url = `${ENV.URL_REST}osmosis/gamm/v1beta1/pools/${asset.poolId}`
const response = await fetch(url).then((res) => res.json())
const response = await cacheFn(
() => fetch(url).then((res) => res.json()),
poolPriceCache,
`poolPrices/${(asset.poolId || 0).toString()}`,
60,
)
return calculateSpotPrice(response.pool.pool_assets, asset)
}

View File

@ -1,12 +1,17 @@
import { cacheFn, priceCache } from 'api/cache'
import { getOracleQueryClient } from 'api/cosmwasm-client'
import { ASSETS } from 'constants/assets'
import { byDenom } from 'utils/array'
import getPythPrice from 'api/prices/getPythPrices'
import getPoolPrice from 'api/prices/getPoolPrice'
import { BN } from 'utils/helpers'
import getPythPrice from 'api/prices/getPythPrices'
import { ASSETS } from 'constants/assets'
import { PRICE_ORACLE_DECIMALS } from 'constants/query'
import { byDenom } from 'utils/array'
import { BN } from 'utils/helpers'
export default async function getPrice(denom: string): Promise<BigNumber> {
return cacheFn(() => fetchPrice(denom), priceCache, `price/${denom}`, 60)
}
async function fetchPrice(denom: string) {
try {
const asset = ASSETS.find(byDenom(denom)) as Asset

View File

@ -1,12 +1,19 @@
import { ENV } from 'constants/env'
import { BN } from 'utils/helpers'
import { cacheFn, pythPriceCache } from '../cache'
export default async function fetchPythPrices(...priceFeedIds: string[]) {
try {
const pricesUrl = new URL(`${ENV.PYTH_ENDPOINT}/latest_price_feeds`)
priceFeedIds.forEach((id) => pricesUrl.searchParams.append('ids[]', id))
const pythResponse: PythPriceData[] = await fetch(pricesUrl).then((res) => res.json())
const pythResponse: PythPriceData[] = await cacheFn(
() => fetch(pricesUrl).then((res) => res.json()),
pythPriceCache,
`pythPrices/${priceFeedIds.flat().join('-')}`,
30,
)
return pythResponse.map(({ price }) => BN(price.price).shiftedBy(price.expo))
} catch (ex) {

View File

@ -1,5 +1,12 @@
import moment from 'moment'
import {
cacheFn,
estimateWithdrawCache,
positionsCache,
previewRedeemCache,
unlockPositionsCache,
} from 'api/cache'
import { getClient, getCreditManagerQueryClient, getVaultQueryClient } from 'api/cosmwasm-client'
import getPrice from 'api/prices/getPrice'
import getVaults from 'api/vaults/getVaults'
@ -7,6 +14,7 @@ import { BN_ZERO } from 'constants/math'
import { BNCoin } from 'types/classes/BNCoin'
import { VaultStatus } from 'types/enums/vault'
import {
Positions,
VaultPosition,
VaultPositionAmount,
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
@ -17,9 +25,15 @@ async function getUnlocksAtTimestamp(unlockingId: number, vaultAddress: string)
try {
const client = await getClient()
const vaultExtension = (await client.queryContractSmart(vaultAddress, {
vault_extension: { lockup: { unlocking_position: { lockup_id: unlockingId } } },
})) as VaultExtensionResponse
const vaultExtension = (await cacheFn(
() =>
client.queryContractSmart(vaultAddress, {
vault_extension: { lockup: { unlocking_position: { lockup_id: unlockingId } } },
}),
unlockPositionsCache,
`unlockPositions/${vaultAddress}.id/${unlockingId}`,
60,
)) as VaultExtensionResponse
return Number(vaultExtension.release_at.at_time) / 1e6
} catch (ex) {
@ -78,16 +92,28 @@ async function getLpTokensForVaultPosition(
const amounts = flatVaultPositionAmount(vaultPosition.amount)
const totalAmount = amounts.locked.plus(amounts.unlocked).plus(amounts.unlocking).toString()
const lpAmount = await vaultQueryClient.previewRedeem({
amount: totalAmount,
})
const lpAmount = await cacheFn(
() =>
vaultQueryClient.previewRedeem({
amount: totalAmount,
}),
previewRedeemCache,
`previewRedeem/vaults/${vault.address}/amount/${totalAmount}`,
60,
)
const lpTokens = await creditManagerQueryClient.estimateWithdrawLiquidity({
lpToken: {
amount: lpAmount,
denom: vault.denoms.lp,
},
})
const lpTokens = await cacheFn(
() =>
creditManagerQueryClient.estimateWithdrawLiquidity({
lpToken: {
amount: lpAmount,
denom: vault.denoms.lp,
},
}),
estimateWithdrawCache,
`lpToken/${vault.denoms.lp}/amount/${lpAmount}`,
60,
)
const primaryLpToken = lpTokens.find((t) => t.denom === vault.denoms.primary) ?? {
amount: '0',
@ -147,12 +173,23 @@ async function getVaultValuesAndAmounts(
}
}
async function getDepositedVaults(accountId: string): Promise<DepositedVault[]> {
async function getDepositedVaults(
accountId: string,
positions?: Positions,
): Promise<DepositedVault[]> {
try {
const creditManagerQueryClient = await getCreditManagerQueryClient()
const positionsQuery = creditManagerQueryClient.positions({ accountId })
const [positions, allVaults] = await Promise.all([positionsQuery, getVaults()])
if (!positions)
positions = await cacheFn(
() => creditManagerQueryClient.positions({ accountId }),
positionsCache,
`depositedVaults/${accountId}`,
)
if (!positions.vaults.length) return []
const [allVaults] = await Promise.all([getVaults()])
const depositedVaults = positions.vaults.map(async (vaultPosition) => {
const vault = allVaults.find((v) => v.address === vaultPosition.vault.address)

View File

@ -1,19 +1,22 @@
import { aprsCache, aprsCacheResponse, cacheFn } from 'api/cache'
import { ENV } from 'constants/env'
export default async function getAprs() {
try {
const response = await fetch(ENV.URL_VAULT_APR)
const response = await cacheFn(
() => fetch(ENV.URL_VAULT_APR),
aprsCacheResponse,
'aprsResponse',
60,
)
if (response.ok) {
const data: AprResponse = await response.json()
const data: AprResponse = await cacheFn(() => response.json(), aprsCache, 'aprs', 60)
const newAprs = data.vaults.map((aprData) => {
return data.vaults.map((aprData) => {
const finalApr = aprData.apr.projected_apr * 100
return { address: aprData.address, apr: finalApr }
return { address: aprData.address, apr: finalApr } as Apr
})
return newAprs
}
return []

View File

@ -1,3 +1,4 @@
import { cacheFn, vaultConfigsCache } from 'api/cache'
import { getParamsQueryClient } from 'api/cosmwasm-client'
import { VaultConfigBaseForAddr } from 'types/generated/mars-params/MarsParams.types'
import iterateContractQuery from 'utils/iterateContractQuery'
@ -5,7 +6,12 @@ import iterateContractQuery from 'utils/iterateContractQuery'
export const getVaultConfigs = async (): Promise<VaultConfigBaseForAddr[]> => {
try {
const paramsQueryClient = await getParamsQueryClient()
return await iterateContractQuery(paramsQueryClient.allVaultConfigs, 'addr')
return await cacheFn(
() => iterateContractQuery(paramsQueryClient.allVaultConfigs, 'addr'),
vaultConfigsCache,
'vaultConfigs',
600,
)
} catch (ex) {
console.error(ex)
throw ex

View File

@ -1,3 +1,4 @@
import { cacheFn, previewDepositCache } from 'api/cache'
import { getVaultQueryClient } from 'api/cosmwasm-client'
export async function getVaultTokenFromLp(
@ -7,7 +8,13 @@ export async function getVaultTokenFromLp(
try {
const client = await getVaultQueryClient(vaultAddress)
return client.previewDeposit({ amount: lpAmount }).then((amount) => ({ vaultAddress, amount }))
return cacheFn(
() =>
client.previewDeposit({ amount: lpAmount }).then((amount) => ({ vaultAddress, amount })),
previewDepositCache,
`vaults/${vaultAddress}/amounts/${lpAmount}`,
30,
)
} catch (ex) {
throw ex
}

View File

@ -1,3 +1,4 @@
import { cacheFn, vaultUtilizationCache } from 'api/cache'
import { getCreditManagerQueryClient } from 'api/cosmwasm-client'
import { ENV } from 'constants/env'
import { VaultUtilizationResponse } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
@ -10,7 +11,12 @@ export const getVaultUtilizations = async (
const creditManagerQueryClient = await getCreditManagerQueryClient()
try {
const vaultUtilizations$ = vaultConfigs.map((vaultConfig) => {
return creditManagerQueryClient.vaultUtilization({ vault: { address: vaultConfig.addr } })
return cacheFn(
() => creditManagerQueryClient.vaultUtilization({ vault: { address: vaultConfig.addr } }),
vaultUtilizationCache,
`vaultUtilization/${vaultConfig.addr}`,
60,
)
})
return await Promise.all(vaultUtilizations$).then((vaultUtilizations) => vaultUtilizations)

View File

@ -131,7 +131,7 @@ export default function Index(props: Props) {
header: 'APY',
cell: ({ row }) => {
if (row.original.type === 'deposits')
return <span className='w-full text-xs text-center'>&ndash;</span>
return <p className='w-full text-xs text-right number'>&ndash;</p>
const isEnabled = markets.find(byDenom(row.original.denom))?.borrowEnabled ?? false
return (
<AssetRate

View File

@ -3,6 +3,7 @@ import useSWR from 'swr'
import getAccounts from 'api/wallets/getAccounts'
import useStore from 'store'
// TODO: Remove this hook
export default function useAccounts(address?: string) {
return useSWR(`accounts${address}`, () => getAccounts(address), {
suspense: true,

View File

@ -111,3 +111,8 @@ interface AprBreakdown {
period_daily_return: number
projected_apr: number
}
interface Apr {
address: string
apr: number
}