diff --git a/apps/dapp/package.json b/apps/dapp/package.json index 926e318bd..12c49e5d7 100644 --- a/apps/dapp/package.json +++ b/apps/dapp/package.json @@ -52,7 +52,8 @@ "tippy.js": "^6.3.7", "use-debounce": "^9.0.2", "use-interval": "^1.4.0", - "util": "^0.12.4" + "util": "^0.12.4", + "zod": "^3.19.1" }, "devDependencies": { "@babel/core": "^7.16.0", diff --git a/apps/dapp/src/components/Pages/Core/DappPages/Borrow/Chart.tsx b/apps/dapp/src/components/Pages/Core/DappPages/Borrow/Chart.tsx index 9cd7093a7..fa4afd2bd 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Borrow/Chart.tsx +++ b/apps/dapp/src/components/Pages/Core/DappPages/Borrow/Chart.tsx @@ -10,7 +10,7 @@ import { formatNumberFixedDecimals, } from 'utils/formatter'; import { formatDailyDataPoints } from 'utils/charts'; -import { fetchGenericSubgraph } from 'utils/subgraph'; +import { queryTlcDailySnapshots, subgraphQuery } from 'utils/subgraph'; import IntervalToggler from 'components/Charts/IntervalToggler'; import env from 'constants/env'; @@ -50,17 +50,18 @@ export const TlcChart = () => { useEffect(() => { const fetchMetrics = async () => { - const { data } = await fetchGenericSubgraph( + const response = await subgraphQuery( env.subgraph.templeV2, - `{ - tlcDailySnapshots(orderBy: timestamp, orderDirection: desc) { - timestamp - utilRatio - interestYield - } - }` + queryTlcDailySnapshots() + ); + + setMetrics( + response.tlcDailySnapshots.map((r) => ({ + timestamp: parseFloat(r.timestamp), + utilRatio: parseFloat(r.utilRatio), + interestYield: parseFloat(r.interestYield), + })) ); - setMetrics(data.tlcDailySnapshots); }; fetchMetrics(); }, []); diff --git a/apps/dapp/src/components/Pages/Core/DappPages/Borrow/index.tsx b/apps/dapp/src/components/Pages/Core/DappPages/Borrow/index.tsx index 7604c7acc..26a1643dc 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Borrow/index.tsx +++ b/apps/dapp/src/components/Pages/Core/DappPages/Borrow/index.tsx @@ -10,7 +10,11 @@ import { TreasuryReservesVault__factory, } from 'types/typechain'; import { ITlcDataTypes } from 'types/typechain/contracts/interfaces/v2/templeLineOfCredit/ITempleLineOfCredit'; -import { fetchGenericSubgraph } from 'utils/subgraph'; +import { + queryTlcMinBorrowAmount, + queryTlcPrices, + subgraphQuery, +} from 'utils/subgraph'; import { BigNumber, ethers } from 'ethers'; import daiImg from 'assets/images/newui-images/tokens/dai.png'; import templeImg from 'assets/images/newui-images/tokens/temple.png'; @@ -86,23 +90,18 @@ export const BorrowPage = () => { const [metricsLoading, setMetricsLoading] = useState(false); const getPrices = useCallback(async () => { - const { data } = await fetchGenericSubgraph( + const response = await subgraphQuery( env.subgraph.templeV2, - `{ - tokens { - price - symbol - } - treasuryReservesVaults { - treasuryPriceIndex - } - }` + queryTlcPrices() ); setPrices({ - templePrice: data.tokens.filter((t: any) => t.symbol == 'TEMPLE')[0] - .price, - daiPrice: data.tokens.filter((t: any) => t.symbol == 'DAI')[0].price, - tpi: data.treasuryReservesVaults[0].treasuryPriceIndex, + templePrice: parseFloat( + response.tokens.filter((t: any) => t.symbol == 'TEMPLE')[0].price + ), + daiPrice: parseFloat( + response.tokens.filter((t: any) => t.symbol == 'DAI')[0].price + ), + tpi: parseFloat(response.treasuryReservesVaults[0].treasuryPriceIndex), }); }, []); @@ -210,13 +209,9 @@ export const BorrowPage = () => { }; getAccountPosition(); try { - const { data } = await fetchGenericSubgraph( + const response = await subgraphQuery( env.subgraph.templeV2, - `{ - tlcDailySnapshots(orderBy: timestamp, orderDirection: desc, first: 1) { - minBorrowAmount - } - }` + queryTlcMinBorrowAmount() ); const tlcInfoFromContracts = await getTlcInfoFromContracts(); @@ -230,7 +225,7 @@ export const BorrowPage = () => { } setTlcInfo({ - minBorrow: data.tlcDailySnapshots[0].minBorrowAmount, + minBorrow: parseFloat(response.tlcDailySnapshots[0].minBorrowAmount), borrowRate: tlcInfoFromContracts?.borrowRate || 0, liquidationLtv: tlcInfoFromContracts?.liquidationLtv || 0, strategyBalance: tlcInfoFromContracts?.strategyBalance || 0, diff --git a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/Chart/V2StrategyMetricsChart.tsx b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/Chart/V2StrategyMetricsChart.tsx index 6d6965b47..54ee54d09 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/Chart/V2StrategyMetricsChart.tsx +++ b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/Chart/V2StrategyMetricsChart.tsx @@ -7,13 +7,14 @@ import { formatTimestampedChartData } from 'utils/charts'; import useV2StrategySnapshotData, { StrategyTokenField, V2SnapshotMetric, - V2StrategySnapshot, } from '../hooks/use-dashboardv2-daily-snapshots'; import { ALL_STRATEGIES, DashboardData, isTRVDashboard, + StrategyKey, } from '../DashboardConfig'; +import { V2StrategySnapshot } from 'utils/subgraph'; type XAxisTickFormatter = (timestamp: number) => string; @@ -181,12 +182,16 @@ const V2StrategyMetricsChart: React.FC<{ const filteredDaily = dailyMetrics - ?.filter((m) => chartStrategyNames.includes(m.strategy.name)) + ?.filter((m) => + chartStrategyNames.includes(m.strategy.name as StrategyKey) + ) .sort((a, b) => parseInt(a.timestamp) - parseInt(b.timestamp)) ?? []; const filteredHourly = hourlyMetrics - ?.filter((m) => chartStrategyNames.includes(m.strategy.name)) + ?.filter((m) => + chartStrategyNames.includes(m.strategy.name as StrategyKey) + ) .sort((a, b) => parseInt(a.timestamp) - parseInt(b.timestamp)) ?? []; // if we are rendering chart for only one strategy we can use data as is diff --git a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/Table/TxnHistoryTable.tsx b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/Table/TxnHistoryTable.tsx index a63049924..e2e58ebad 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/Table/TxnHistoryTable.tsx +++ b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/Table/TxnHistoryTable.tsx @@ -16,7 +16,12 @@ import { queryMinTablet } from 'styles/breakpoints'; import env from 'constants/env'; import linkSvg from 'assets/icons/link.svg?react'; import { formatNumberWithCommas } from 'utils/formatter'; -import { DashboardData, Dashboards, isTRVDashboard } from '../DashboardConfig'; +import { + DashboardData, + Dashboards, + isTRVDashboard, + StrategyKey, +} from '../DashboardConfig'; type Props = { dashboardData: DashboardData; @@ -271,8 +276,8 @@ const TxnHistoryTable = (props: Props) => { const timeOnly = format(new Date(Number(tx.timestamp) * 1000), 'H:mm:ss'); return { date: isBiggerThanTablet ? datetime : dateOnly, - type: tx.name, - strategy: tx.strategy.name, + type: tx.name as TxType, + strategy: tx.strategy.name as StrategyKey, token: tx.token.symbol, amount: amountFmt, txHash: tx.hash, diff --git a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-daily-snapshots.ts b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-daily-snapshots.ts index 2629078fd..4e96c0a2a 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-daily-snapshots.ts +++ b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-daily-snapshots.ts @@ -1,9 +1,12 @@ import { useQuery } from '@tanstack/react-query'; import env from 'constants/env'; import { getQueryKey } from 'utils/react-query-helpers'; -import { SubGraphResponse } from 'hooks/core/types'; -import { fetchGenericSubgraph } from 'utils/subgraph'; -import { StrategyKey } from '../DashboardConfig'; +import { + queryStrategyDailySnapshots, + queryStrategyHourlySnapshots, + subgraphQuery, + V2StrategySnapshot, +} from 'utils/subgraph'; const V2SnapshotMetrics = [ 'totalMarketValueUSD', @@ -30,38 +33,12 @@ const STRATEGY_TOKEN_FIELDS = [ export type StrategyTokenField = (typeof STRATEGY_TOKEN_FIELDS)[number]; -const QUERIED_FIELDS = ` - strategy{ - name - } - timeframe - timestamp - ${V2SnapshotMetrics.join('\n')} - strategyTokens{ - ${STRATEGY_TOKEN_FIELDS.join('\n')} - } -`; - -export type V2StrategySnapshot = { - timestamp: string; - timeframe: string; - strategy: { name: StrategyKey }; - strategyTokens: { [key in (typeof STRATEGY_TOKEN_FIELDS)[number]]: string }[]; -} & { [key in V2SnapshotMetric]: string }; - export function isV2SnapshotMetric( key?: string | null ): key is V2SnapshotMetric { return V2SnapshotMetrics.some((m) => m === key); } -type FetchV2StrategyDailySnapshotResponse = SubGraphResponse<{ - strategyDailySnapshots: V2StrategySnapshot[]; -}>; -type FetchV2StrategyHourlySnapshotResponse = SubGraphResponse<{ - strategyHourlySnapshots: V2StrategySnapshot[]; -}>; - const ONE_DAY_ONE_HOUR_MS = 25 * 60 * 60 * 1000; const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000; @@ -73,22 +50,17 @@ async function fetchStrategyHourlySnapshots() { ).toString(); // if # of strategies * 24 > 1000 we would be missing data // but we shouldnt be getting anywhere close to that - const query = ` - query { - strategyHourlySnapshots(first: ${itemsPerPage}, - orderBy: timestamp, - orderDirection: asc, - where: {timestamp_gt: ${since}} - ) { - ${QUERIED_FIELDS} - } - }`; - const resp = - await fetchGenericSubgraph( - env.subgraph.templeV2Balances, - query - ); - return resp?.data?.strategyHourlySnapshots ?? []; + const resp = await subgraphQuery( + env.subgraph.templeV2Balances, + queryStrategyHourlySnapshots( + V2SnapshotMetrics, + STRATEGY_TOKEN_FIELDS, + itemsPerPage, + since + ) + ); + + return resp.strategyHourlySnapshots; } async function fetchStrategyDailySnapshots() { @@ -99,26 +71,19 @@ async function fetchStrategyDailySnapshots() { const MAX_PAGE_SIZE = 1000; // current max page size let skip = 0; while (true) { - const query = ` - query { - strategyDailySnapshots(first: ${MAX_PAGE_SIZE}, - orderBy: timestamp, - orderDirection: asc, - where: {timestamp_gt: ${since}} - skip: ${skip}) { - ${QUERIED_FIELDS} - } - }`; - const page = - await fetchGenericSubgraph( - env.subgraph.templeV2Balances, - query - ); - const itemsOnPage = page.data?.strategyDailySnapshots.length ?? 0; - if (page.data) { - result.push(...page.data.strategyDailySnapshots); - skip += itemsOnPage; - } + const page = await subgraphQuery( + env.subgraph.templeV2Balances, + queryStrategyDailySnapshots( + V2SnapshotMetrics, + STRATEGY_TOKEN_FIELDS, + MAX_PAGE_SIZE, + since, + skip + ) + ); + const itemsOnPage = page.strategyDailySnapshots.length ?? 0; + result.push(...page.strategyDailySnapshots); + skip += itemsOnPage; if (itemsOnPage < MAX_PAGE_SIZE) { break; } diff --git a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-metrics.ts b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-metrics.ts index bd6fd9e13..86d98f081 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-metrics.ts +++ b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-metrics.ts @@ -1,6 +1,5 @@ import { useQuery } from '@tanstack/react-query'; import millify from 'millify'; -import { fetchGenericSubgraph } from 'utils/subgraph'; import env from 'constants/env'; import { getQueryKey } from 'utils/react-query-helpers'; import { @@ -9,6 +8,20 @@ import { TrvKey, isTRVDashboard, } from '../DashboardConfig'; +import { + queryBenchmarkRate, + queryRamosData, + queryStrategyBalances, + queryStrategyData, + queryTempleCirculatingSupply, + queryTrvBalances, + queryTrvData, + StrategyBalancesResp, + StrategyDataResp, + subgraphQuery, + TrvBalancesResp, + TrvDataResp, +} from 'utils/subgraph'; export enum TokenSymbols { DAI = 'DAI', @@ -84,80 +97,51 @@ export default function useDashboardV2Metrics(dashboardData: DashboardData) { try { const allMetricsPromises = [ - fetchGenericSubgraph( - env.subgraph.templeV2, - `{ - strategies { - name - isShutdown - id - strategyTokens { - symbol - rate - premiumRate - debtShare - debtCeiling - debtCeilingUtil - } - totalRepaymentUSD - principalUSD - accruedInterestUSD - } - }` - ), + subgraphQuery(env.subgraph.templeV2, queryStrategyData()), + // includes the external balances so has to come from the second subgraph - fetchGenericSubgraph( - env.subgraph.templeV2Balances, - `{ - strategies { - name - isShutdown - id - benchmarkedEquityUSD - totalMarketValueUSD - } - }` - ), + subgraphQuery(env.subgraph.templeV2Balances, queryStrategyBalances()), ]; const [responses, responseExternalBalances] = await Promise.all( allMetricsPromises ); - const subgraphData = responses?.data?.strategies.find( + const subgraphData = (responses as StrategyDataResp).strategies.find( // eslint-disable-next-line @typescript-eslint/no-explicit-any (_strategy: any) => _strategy.name === strategy && _strategy.isShutdown === false ); - const externalBalancesData = - responseExternalBalances?.data?.strategies.find( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (_strategy: any) => - _strategy.name === strategy && _strategy.isShutdown === false - ); + const externalBalancesData = ( + responseExternalBalances as StrategyBalancesResp + ).strategies.find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (_strategy: any) => + _strategy.name === strategy && _strategy.isShutdown === false + ); - const daiStrategyTokenData = subgraphData?.strategyTokens.find( + const daiStrategyTokenData = subgraphData!.strategyTokens.find( // eslint-disable-next-line @typescript-eslint/no-explicit-any (_strategyToken: any) => _strategyToken.symbol === TokenSymbols.DAI ); metrics = { - valueOfHoldings: parseFloat(externalBalancesData.totalMarketValueUSD), + valueOfHoldings: parseFloat(externalBalancesData!.totalMarketValueUSD), benchmarkedEquity: parseFloat( - externalBalancesData.benchmarkedEquityUSD + externalBalancesData!.benchmarkedEquityUSD ), interestRate: - parseFloat(daiStrategyTokenData.rate) + - parseFloat(daiStrategyTokenData.premiumRate), - debtShare: parseFloat(daiStrategyTokenData.debtShare), - debtCeiling: parseFloat(daiStrategyTokenData.debtCeiling), + parseFloat(daiStrategyTokenData!.rate) + + parseFloat(daiStrategyTokenData!.premiumRate), + debtShare: parseFloat(daiStrategyTokenData!.debtShare), + debtCeiling: parseFloat(daiStrategyTokenData!.debtCeiling), debtCeilingUtilization: parseFloat( - daiStrategyTokenData.debtCeilingUtil + daiStrategyTokenData!.debtCeilingUtil ), - totalRepayment: parseFloat(subgraphData.totalRepaymentUSD), - principal: parseFloat(subgraphData.principalUSD), - accruedInterest: parseFloat(subgraphData.accruedInterestUSD), + totalRepayment: parseFloat(subgraphData!.totalRepaymentUSD), + principal: parseFloat(subgraphData!.principalUSD), + accruedInterest: parseFloat(subgraphData!.accruedInterestUSD), }; } catch (error) { console.info(error); @@ -181,26 +165,11 @@ export default function useDashboardV2Metrics(dashboardData: DashboardData) { try { const allMetricsPromises = [ - fetchGenericSubgraph( - env.subgraph.templeV2, - `{ - treasuryReservesVaults { - treasuryPriceIndex - principalUSD - accruedInterestUSD - } - }` - ), + subgraphQuery(env.subgraph.templeV2, queryTrvData()), + // includes the external balances so has to come from the second subgraph - fetchGenericSubgraph( - env.subgraph.templeV2Balances, - `{ - treasuryReservesVaults { - totalMarketValueUSD - benchmarkedEquityUSD - } - }` - ), + subgraphQuery(env.subgraph.templeV2Balances, queryTrvBalances()), + getBenchmarkRate(), getTempleCirculatingSupply(), getTempleSpotPrice(), @@ -214,21 +183,26 @@ export default function useDashboardV2Metrics(dashboardData: DashboardData) { templeSpotPrice, ] = await Promise.all(allMetricsPromises); - const trvSubgraphData = - trvSubgraphResponse?.data?.treasuryReservesVaults[0]; + const trvSubgraphData = (trvSubgraphResponse as TrvDataResp) + .treasuryReservesVaults[0]; - const externalBalancesData = - responseExternalBalances?.data?.treasuryReservesVaults[0]; + const externalBalancesData = ( + responseExternalBalances as TrvBalancesResp + ).treasuryReservesVaults[0]; metrics = { - totalMarketValue: parseFloat(externalBalancesData.totalMarketValueUSD), - spotPrice: parseFloat(templeSpotPrice), + totalMarketValue: parseFloat( + externalBalancesData.totalMarketValueUSD + ), + spotPrice: parseFloat(templeSpotPrice as string), treasuryPriceIndex: parseFloat(trvSubgraphData.treasuryPriceIndex), - circulatingSupply: parseFloat(templeCirculatingSupply), - benchmarkRate: parseFloat(benchmarkRate), + circulatingSupply: parseFloat(templeCirculatingSupply as string), + benchmarkRate: parseFloat(benchmarkRate as string), principal: parseFloat(trvSubgraphData.principalUSD), accruedInterest: parseFloat(trvSubgraphData.accruedInterestUSD), - benchmarkedEquity: parseFloat(externalBalancesData.benchmarkedEquityUSD), + benchmarkedEquity: parseFloat( + externalBalancesData.benchmarkedEquityUSD + ), }; } catch (error) { console.info(error); @@ -238,18 +212,12 @@ export default function useDashboardV2Metrics(dashboardData: DashboardData) { }; const getBenchmarkRate = async () => { - const debtTokensResponse = await fetchGenericSubgraph( + const debtTokensResponse = await subgraphQuery( env.subgraph.templeV2, - `{ - debtTokens { - name - symbol - baseRate - } - }` + queryBenchmarkRate() ); - const debtTokensData = debtTokensResponse?.data?.debtTokens; + const debtTokensData = debtTokensResponse.debtTokens; // eslint-disable-next-line @typescript-eslint/no-explicit-any return debtTokensData.find( @@ -258,33 +226,16 @@ export default function useDashboardV2Metrics(dashboardData: DashboardData) { }; const getTempleCirculatingSupply = async (): Promise => { - const response = await fetchGenericSubgraph( + const response = await subgraphQuery( env.subgraph.protocolMetrics, - `{ - metrics(first: 1, orderBy: timestamp, orderDirection: desc) { - templeCirculatingSupply - } - }` + queryTempleCirculatingSupply() ); - - const data = response?.data?.metrics?.[0] || {}; - - return data.templeCirculatingSupply; + return response.metrics[0].templeCirculatingSupply; }; const getTempleSpotPrice = async () => { - const response = await fetchGenericSubgraph( - env.subgraph.ramos, - `{ - metrics { - spotPrice - } - }` - ); - - const data = response?.data?.metrics?.[0] || {}; - - return data.spotPrice; + const response = await subgraphQuery(env.subgraph.ramos, queryRamosData()); + return response.metrics[0].spotPrice; }; const formatPercent = (input: number) => { diff --git a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-txHistory.ts b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-txHistory.ts index b80d4427e..dba2d90b4 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-txHistory.ts +++ b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/hooks/use-dashboardv2-txHistory.ts @@ -1,32 +1,15 @@ -import { fetchGenericSubgraph } from 'utils/subgraph'; import env from 'constants/env'; -import { SubGraphResponse } from 'hooks/core/types'; import { TxHistoryFilterType } from '../Table'; import { TableHeaders, TxHistoryTableHeader } from '../Table/TxnHistoryTable'; -import { TxType } from '../Table/TxnDataTable'; import { getQueryKey } from 'utils/react-query-helpers'; import { useQuery } from '@tanstack/react-query'; -import { DashboardData, StrategyKey, isTRVDashboard } from '../DashboardConfig'; - -type Transactions = { - hash: string; - strategy: { - id: string; - name: StrategyKey; - }; - token: { - id: string; - name: string; - symbol: string; - }; - amount: string; - amountUsd: string; - id: string; - from: string; - to: string; - name: TxType; - timestamp: string; -}[]; +import { DashboardData, isTRVDashboard } from '../DashboardConfig'; +import { + subgraphQuery, + queryStrategyTransactions, + StrategyTransactions, + queryStrategyTransactionsMeta, +} from 'utils/subgraph'; type AvailableRows = { totalTransactionCount: number; @@ -34,22 +17,6 @@ type AvailableRows = { blockNumber: number; }; -type Metrics = { - strategyTransactionCount: number; -}[]; - -type Meta = { - block: { - number: number; - }; -}; - -type FetchTxnsResponse = SubGraphResponse<{ - strategyTransactions: Transactions; - metrics: Metrics; - _meta: Meta; -}>; - export type RowFilter = { type?: string; strategy?: string; @@ -111,7 +78,7 @@ const useTxHistory = (props: TxHistoryProps) => const fetchTransactions = async ( props: TxHistoryProps -): Promise => { +): Promise => { const { dashboardData, blockNumber, @@ -129,8 +96,6 @@ const fetchTransactions = async ( const blockNumberQueryParam = blockNumber > 0 ? `block: { number: ${blockNumber} }` : ``; - const paginationQuery = `skip: ${offset} first: ${limit}`; - const dateNowSecs = Math.round(Date.now() / 1000); const typeRowFilterQuery = `${ rowFilter.type ? 'name_contains_nocase: "' + rowFilter.type + '"' : '' @@ -166,32 +131,10 @@ const fetchTransactions = async ( : 'asc' : 'desc'; - const subgraphQuery = `{ - strategyTransactions(orderBy: ${orderBy}, orderDirection: ${orderType} ${paginationQuery} ${whereQuery}) { - hash - strategy { - id - name - } - token { - id - name - symbol - } - amount - amountUSD - id - from - name - timestamp - } - }`; - - const { data: res } = await fetchGenericSubgraph( + const res = await subgraphQuery( env.subgraph.templeV2, - subgraphQuery + queryStrategyTransactions(orderBy, orderType, offset, limit, whereQuery) ); - if (!res) return []; return res.strategyTransactions; }; @@ -224,31 +167,18 @@ const fetchTxHistoryAvailableRows = async ( : '' }`; // get the max allowed 1000 records for a more accurate totalPages calculation - const whereQuery = `( first: 1000 + const whereQuery = `first: 1000 where: { ${strategyQuery} timestamp_gt: ${dateNowSecs - txHistoryFilterTypeToSeconds(txFilter)} ${typeRowFilterQuery} ${strategyRowFilterQuery} ${tokenRowFilterQuery} - } - )`; - const subgraphQuery = `{ - metrics { - strategyTransactionCount - } - strategyTransactions${whereQuery} { - hash - } - _meta { - block { - number - } - } - }`; - const { data: res } = await fetchGenericSubgraph( + }`; + + const res = await subgraphQuery( env.subgraph.templeV2, - subgraphQuery + queryStrategyTransactionsMeta(whereQuery) ); let result: AvailableRows = { @@ -261,29 +191,29 @@ const fetchTxHistoryAvailableRows = async ( if (rowFilter.strategy) hasRowFilters = rowFilter.strategy.length > 0; if (rowFilter.token) hasRowFilters = rowFilter.token.length > 0; if (rowFilter.type) hasRowFilters = rowFilter.type.length > 0; - if (res) { - let totalRowCount = 0; - if ( - props.txFilter === TxHistoryFilterType.all && - isTRVDashboard(strategyKey) && - !hasRowFilters - ) { - // if user chooses all transactions, sum the txCountTotal of every strategy, we don't use this - // calc for the last30days or lastweek filters because it could show an incorrect number of totalPages - totalRowCount = res.metrics[0].strategyTransactionCount; - } else { - // if user chooses last30days or lastweek filters, count the length of txs of each strategy - // in this case there maybe a chance of incorrect calc if there are more than 1000 records, - // which is unlikely in foreseeable future. This due to the max 1000 records subgraph limitation - totalRowCount = res.strategyTransactions.length; - } - result = { - totalRowCount, - blockNumber: res._meta.block.number, - totalTransactionCount: totalRowCount, - }; + let totalRowCount = 0; + if ( + props.txFilter === TxHistoryFilterType.all && + isTRVDashboard(strategyKey) && + !hasRowFilters + ) { + // if user chooses all transactions, sum the txCountTotal of every strategy, we don't use this + // calc for the last30days or lastweek filters because it could show an incorrect number of totalPages + totalRowCount = res.metrics[0].strategyTransactionCount; + } else { + // if user chooses last30days or lastweek filters, count the length of txs of each strategy + // in this case there maybe a chance of incorrect calc if there are more than 1000 records, + // which is unlikely in foreseeable future. This due to the max 1000 records subgraph limitation + totalRowCount = res.strategyTransactions.length; } + + result = { + totalRowCount, + blockNumber: res._meta.block.number, + totalTransactionCount: totalRowCount, + }; + return result; }; diff --git a/apps/dapp/src/components/Pages/Core/NewUI/Home.tsx b/apps/dapp/src/components/Pages/Core/NewUI/Home.tsx index 1de33ebf5..121c5972d 100644 --- a/apps/dapp/src/components/Pages/Core/NewUI/Home.tsx +++ b/apps/dapp/src/components/Pages/Core/NewUI/Home.tsx @@ -17,7 +17,7 @@ import { TemplePriceChart } from './PriceChart'; import { RAMOSMetrics } from './RAMOSMetrics'; import { Button } from 'components/Button/Button'; import { useEffect, useState } from 'react'; -import { fetchGenericSubgraph } from 'utils/subgraph'; +import { queryRamosData, queryTrvData, subgraphQuery } from 'utils/subgraph'; import env from 'constants/env'; interface Metrics { @@ -105,23 +105,14 @@ const Home = ({ tlc }: { tlc?: boolean }) => { useEffect(() => { const fetchMetrics = async () => { - const { data: treasuryData } = await fetchGenericSubgraph( + const treasuryData = await subgraphQuery( env.subgraph.templeV2Balances, - `{ - treasuryReservesVaults { - principalUSD - benchmarkedEquityUSD - treasuryPriceIndex - } - }` + queryTrvData() ); - const { data: ramosData } = await fetchGenericSubgraph( + + const ramosData = await subgraphQuery( env.subgraph.ramos, - `{ - metrics { - spotPrice - } - }` + queryRamosData() ); const treasuryMetrics = treasuryData.treasuryReservesVaults[0]; diff --git a/apps/dapp/src/utils/subgraph.ts b/apps/dapp/src/utils/subgraph.ts index 58f88e2ec..0c788b07e 100644 --- a/apps/dapp/src/utils/subgraph.ts +++ b/apps/dapp/src/utils/subgraph.ts @@ -1,40 +1,641 @@ -import { SubGraphResponse } from 'hooks/core/types'; +export const ENABLE_SUBGRAPH_LOGS = false; +import { z } from 'zod'; +import { backOff } from 'exponential-backoff'; -export class SubgraphQueryError extends Error { - constructor(graphqlErrors: any[]) { - super(graphqlErrors.map((errorPath) => errorPath.message).join(';')); - this.name = 'SubgraphQueryError'; - } +/** A typed query to subgraph */ +interface SubGraphQuery { + label: string; + request: string; + parse(response: unknown): T; +} + +//---------------------------------------------------------------------------------------------------- + +export function queryTlcDailySnapshots(): SubGraphQuery { + const label = 'queryTlcDailySnapshots'; + const request = ` + { + tlcDailySnapshots(orderBy: timestamp, orderDirection: desc) { + timestamp + utilRatio + interestYield + } + }`; + return { + label, + request, + parse: TlcDailySnapshotsResp.parse, + }; } -// Preserved to avoid a larger refactor across the code base for now -export const fetchSubgraph = async >( +const TlcDailySnapshotsResp = z.object({ + tlcDailySnapshots: z.array( + z.object({ + timestamp: z.string(), + utilRatio: z.string(), + interestYield: z.string(), + }) + ), +}); +export type TlcDailySnapshotsResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export function queryTlcMinBorrowAmount(): SubGraphQuery { + const label = 'queryTlcMinBorrowAmount'; + const request = ` + { + tlcDailySnapshots(orderBy: timestamp, orderDirection: desc, first: 1) { + minBorrowAmount + } + }`; + return { + label, + request, + parse: TlcMinBorrowAmountResp.parse, + }; +} + +const TlcMinBorrowAmountResp = z.object({ + tlcDailySnapshots: z.array( + z.object({ + minBorrowAmount: z.string(), + }) + ), +}); +export type TlcMinBorrowAmountResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export function queryTrvData(): SubGraphQuery { + const label = 'queryTrvData'; + const request = ` + { + treasuryReservesVaults { + principalUSD + benchmarkedEquityUSD + treasuryPriceIndex + accruedInterestUSD + } + }`; + return { + label, + request, + parse: TrvDataResp.parse, + }; +} + +const TrvDataResp = z.object({ + treasuryReservesVaults: z.array( + z.object({ + principalUSD: z.string(), + benchmarkedEquityUSD: z.string(), + treasuryPriceIndex: z.string(), + accruedInterestUSD: z.string(), + }) + ), +}); +export type TrvDataResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export function queryTrvBalances(): SubGraphQuery { + const label = 'queryTrvBalances'; + const request = ` + { + treasuryReservesVaults { + totalMarketValueUSD + benchmarkedEquityUSD + } + }`; + return { + label, + request, + parse: TrvBalancesResp.parse, + }; +} + +const TrvBalancesResp = z.object({ + treasuryReservesVaults: z.array( + z.object({ + totalMarketValueUSD: z.string(), + benchmarkedEquityUSD: z.string(), + }) + ), +}); +export type TrvBalancesResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export function queryRamosData(): SubGraphQuery { + const label = 'queryRamosData'; + const request = ` + { + metrics { + spotPrice + } + }`; + return { + label, + request, + parse: RamosDataResp.parse, + }; +} + +const RamosDataResp = z.object({ + metrics: z.array( + z.object({ + spotPrice: z.string(), + }) + ), +}); +export type RamosDataResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export function queryTlcPrices(): SubGraphQuery { + const label = 'queryTlcPrices'; + const request = ` + { + tokens { + price + symbol + } + treasuryReservesVaults { + treasuryPriceIndex + } + }`; + return { + label, + request, + parse: TlcPricesResp.parse, + }; +} + +const TlcPricesResp = z.object({ + tokens: z.array( + z.object({ + price: z.string(), + symbol: z.string(), + }) + ), + treasuryReservesVaults: z.array( + z.object({ + treasuryPriceIndex: z.string(), + }) + ), +}); +export type TlcPricesResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +const V2StrategySnapshot = z.object({ + strategy: z.object({ + name: z.string(), + }), + timeframe: z.string(), + timestamp: z.string(), + totalMarketValueUSD: z.string(), + debtUSD: z.string(), + netDebtUSD: z.string(), + creditUSD: z.string(), + principalUSD: z.string(), + accruedInterestUSD: z.string(), + benchmarkedEquityUSD: z.string(), + strategyTokens: z.array( + z.object({ + symbol: z.string(), + debtUSD: z.string(), + creditUSD: z.string(), + assetBalance: z.string(), + marketValueUSD: z.string(), + principalUSD: z.string(), + accruedInterestUSD: z.string(), + }) + ), +}); +export type V2StrategySnapshot = z.infer; + +export function queryStrategyHourlySnapshots( + v2SnapshotMetrics: readonly string[], + strategyTokenFields: readonly string[], + itemsPerPage: number, + since: string +): SubGraphQuery { + const label = 'queryStrategyHourlySnapshots'; + const request = ` + query { + strategyHourlySnapshots( + first: ${itemsPerPage}, + orderBy: timestamp, + orderDirection: asc, + where: {timestamp_gt: ${since}} + ) { + strategy { + name + } + timeframe + timestamp + ${v2SnapshotMetrics.join('\n')} + strategyTokens { + ${strategyTokenFields.join('\n')} + } + } + }`; + return { + label, + request, + parse: StrategyHourlySnapshotsResp.parse, + }; +} + +const StrategyHourlySnapshotsResp = z.object({ + strategyHourlySnapshots: z.array(V2StrategySnapshot), +}); +export type StrategyHourlySnapshotsResp = z.infer< + typeof StrategyHourlySnapshotsResp +>; + +export function queryStrategyDailySnapshots( + v2SnapshotMetrics: readonly string[], + strategyTokenFields: readonly string[], + itemsPerPage: number, + since: string, + skip: number +): SubGraphQuery { + const label = 'queryStrategyDailySnapshots'; + const request = ` + query { + strategyDailySnapshots( + first: ${itemsPerPage}, + orderBy: timestamp, + orderDirection: asc, + where: {timestamp_gt: ${since}} + skip: ${skip} + ) { + strategy { + name + } + timeframe + timestamp + ${v2SnapshotMetrics.join('\n')} + strategyTokens { + ${strategyTokenFields.join('\n')} + } + } + }`; + return { + label, + request, + parse: StrategyDailySnapshotsResp.parse, + }; +} + +const StrategyDailySnapshotsResp = z.object({ + strategyDailySnapshots: z.array(V2StrategySnapshot), +}); +export type StrategyDailySnapshotsResp = z.infer< + typeof StrategyDailySnapshotsResp +>; + +//---------------------------------------------------------------------------------------------------- + +export function queryTempleCirculatingSupply(): SubGraphQuery { + const label = 'queryTempleCirculatingSupply'; + const request = ` + { + metrics(first: 1, orderBy: timestamp, orderDirection: desc) { + templeCirculatingSupply + } + }`; + return { + label, + request, + parse: TempleCirculatingSupplyResp.parse, + }; +} + +const TempleCirculatingSupplyResp = z.object({ + metrics: z.array( + z.object({ + templeCirculatingSupply: z.string(), + }) + ), +}); +export type TempleCirculatingSupplyResp = z.infer< + typeof TempleCirculatingSupplyResp +>; + +//---------------------------------------------------------------------------------------------------- + +export function queryBenchmarkRate(): SubGraphQuery { + const label = 'queryBenchmarkRate'; + const request = ` + { + debtTokens { + name + symbol + baseRate + } + }`; + return { + label, + request, + parse: BenchmarkRateResp.parse, + }; +} + +const BenchmarkRateResp = z.object({ + debtTokens: z.array( + z.object({ + name: z.string(), + symbol: z.string(), + baseRate: z.string(), + }) + ), +}); +export type BenchmarkRateResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export function queryStrategyData(): SubGraphQuery { + const label = 'queryStrategyData'; + const request = ` + { + strategies { + name + isShutdown + id + strategyTokens { + symbol + rate + premiumRate + debtShare + debtCeiling + debtCeilingUtil + } + totalRepaymentUSD + principalUSD + accruedInterestUSD + } + }`; + return { + label, + request, + parse: StrategyDataResp.parse, + }; +} + +const StrategyDataResp = z.object({ + strategies: z.array( + z.object({ + name: z.string(), + isShutdown: z.boolean(), + id: z.string(), + strategyTokens: z.array( + z.object({ + symbol: z.string(), + rate: z.string(), + premiumRate: z.string(), + debtShare: z.string(), + debtCeiling: z.string(), + debtCeilingUtil: z.string(), + }) + ), + totalRepaymentUSD: z.string(), + principalUSD: z.string(), + accruedInterestUSD: z.string(), + }) + ), +}); +export type StrategyDataResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export function queryStrategyTransactions( + orderBy: string, + orderType: string, + offset: number, + limit: number, + whereQuery: string +): SubGraphQuery { + const label = 'queryStrategyTransactions'; + const request = ` + { + strategyTransactions( + orderBy: ${orderBy} + orderDirection: ${orderType} + skip: ${offset} + first: ${limit} + ${whereQuery} + ) { + hash + strategy { + id + name + } + token { + id + name + symbol + } + amount + amountUSD + id + from + name + timestamp + } + }`; + return { + label, + request, + parse: StrategyTransactionsResp.parse, + }; +} + +const StrategyTransactions = z.array( + z.object({ + hash: z.string(), + strategy: z.object({ + id: z.string(), + name: z.string(), + }), + token: z.object({ + id: z.string(), + name: z.string(), + symbol: z.string(), + }), + amount: z.string(), + amountUSD: z.string(), + id: z.string(), + from: z.string(), + name: z.string(), + timestamp: z.string(), + }) +); +export type StrategyTransactions = z.infer; + +const StrategyTransactionsResp = z.object({ + strategyTransactions: StrategyTransactions, +}); +export type StrategyTransactionsResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export function queryStrategyTransactionsMeta( + whereQuery: string +): SubGraphQuery { + const label = 'queryStrategyTransactionsMeta'; + const request = ` + { + metrics { + strategyTransactionCount + } + strategyTransactions( + ${whereQuery} + ) { + hash + } + _meta { + block { + number + } + } + }`; + return { + label, + request, + parse: StrategyTransactionsMetaResp.parse, + }; +} + +const StrategyTransactionsMetaResp = z.object({ + metrics: z.array( + z.object({ + strategyTransactionCount: z.number(), + }) + ), + strategyTransactions: z.array( + z.object({ + hash: z.string(), + }) + ), + _meta: z.object({ + block: z.object({ + number: z.number(), + }), + }), +}); +export type StrategyTransactionsMetaResp = z.infer< + typeof StrategyTransactionsMetaResp +>; + +//---------------------------------------------------------------------------------------------------- + +export function queryStrategyBalances(): SubGraphQuery { + const label = 'queryStrategyBalances'; + const request = ` + { + strategies { + name + isShutdown + id + benchmarkedEquityUSD + totalMarketValueUSD + } + }`; + return { + label, + request, + parse: StrategyBalancesResp.parse, + }; +} + +const StrategyBalancesResp = z.object({ + strategies: z.array( + z.object({ + name: z.string(), + isShutdown: z.boolean(), + id: z.string(), + benchmarkedEquityUSD: z.string(), + totalMarketValueUSD: z.string(), + }) + ), +}); +export type StrategyBalancesResp = z.infer; + +//---------------------------------------------------------------------------------------------------- + +export async function subgraphQuery( + url: string, + query: SubGraphQuery +): Promise { + const response = await rawSubgraphQuery(url, query.label, query.request); + return query.parse(response); +} + +export async function rawSubgraphQuery( + url: string, + label: string, query: string -) => { - return fetchGenericSubgraph( - 'https://subgraph.satsuma-prod.com/a912521dd162/templedao/temple-metrics/api', - query - ); -}; - -export const fetchGenericSubgraph = async >( +): Promise { + return backOff(() => _rawSubgraphQuery(url, label, query), { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + retry: (e: any, attemptNumber: number) => { + if ((e as FetchError).httpStatus === 429) { + console.info( + `received 429 from subgraph api, retry ${attemptNumber} ...` + ); + return true; + } + return false; + }, + }); +} + +async function _rawSubgraphQuery( url: string, + label: string, query: string -) => { - const result = await fetch(url, { +): Promise { + if (ENABLE_SUBGRAPH_LOGS) { + console.log('subgraph-request', label, query); + } + const response = await fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, - body: JSON.stringify({ - query, - }), + body: JSON.stringify({ query }), }); - const response: R = await result.json(); - if (response.errors) { - throw new SubgraphQueryError(response.errors); + if (!response.ok) { + throw new FetchError( + response.status, + `rawSubgraphQuery failed with status: ${ + response.status + }, body: ${await response.text()}` + ); + } + + const rawResults = await response.json(); + + if (ENABLE_SUBGRAPH_LOGS) { + console.log('subgraph-response', label, rawResults); + } + if (rawResults.errors !== undefined) { + throw new Error( + `Unable to fetch ${label} from subgraph: ${rawResults.errors}` + ); + } + + return rawResults.data as unknown; +} + +class FetchError extends Error { + constructor(readonly httpStatus: number, message: string) { + super(message); } - return response; -}; +} diff --git a/apps/dapp/yarn.lock b/apps/dapp/yarn.lock index e124f7818..37d0bcefd 100644 --- a/apps/dapp/yarn.lock +++ b/apps/dapp/yarn.lock @@ -18976,7 +18976,7 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@3.23.8: +zod@3.23.8, zod@^3.19.1: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==