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/TLC/Borrow.tsx b/apps/dapp/src/components/Pages/Core/DappPages/Borrow/TLC/Borrow.tsx index 0f5f6f756..d7c9b109f 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Borrow/TLC/Borrow.tsx +++ b/apps/dapp/src/components/Pages/Core/DappPages/Borrow/TLC/Borrow.tsx @@ -20,7 +20,7 @@ import { TlcInfo, Warning, } from '../index'; -import { fromAtto, toAtto } from 'utils/bigNumber'; +import { fromAtto, toAtto, ZERO } from 'utils/bigNumber'; import styled from 'styled-components'; import { ReactNode, useEffect, useMemo, useState } from 'react'; @@ -67,18 +67,28 @@ export const Borrow: React.FC = ({ const userMaxBorrowBigNumber = toAtto(userMaxBorrow); - if (!tlcInfo) { - return { value: userMaxBorrow, isCircuitBreakerActive: false }; - } + let returnValue = { value: userMaxBorrow, isCircuitBreakerActive: false }; - if (tlcInfo.daiCircuitBreakerRemaining.lt(userMaxBorrowBigNumber)) { - return { + // Check if the dai circuit breaker is active + if ( + tlcInfo && + tlcInfo.daiCircuitBreakerRemaining.lt(userMaxBorrowBigNumber) + ) { + returnValue = { value: fromAtto(tlcInfo.daiCircuitBreakerRemaining), isCircuitBreakerActive: true, }; } - return { value: userMaxBorrow, isCircuitBreakerActive: false }; + // Check if trvAvailable from the contract is less than the user max borrow + if (tlcInfo && tlcInfo.trvAvailable.lt(userMaxBorrowBigNumber)) { + returnValue = { + value: fromAtto(tlcInfo.trvAvailable || ZERO), + isCircuitBreakerActive: true, + }; + } + + return returnValue; }, [tlcInfo, accountPosition, prices.tpi]); return ( 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 c726826aa..f23fd5155 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'; @@ -48,6 +52,8 @@ export type TlcInfo = { debtCeiling: number; daiCircuitBreakerRemaining: BigNumber; templeCircuitBreakerRemaining: BigNumber; + outstandingUserDebt: number; + trvAvailable: BigNumber; }; export const MAX_LTV = 85; @@ -86,23 +92,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: Number(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), }); }, []); @@ -148,6 +149,7 @@ export const BorrowPage = () => { const debtPosition = await tlcContract.totalDebtPosition(); const totalUserDebt = debtPosition.totalDebt; const utilizationRatio = debtPosition.utilizationRatio; + const outstandingUserDebt = debtPosition[2]; // NOTE: We are intentionally rounding here to nearest 1e18 const debtCeiling = totalUserDebt @@ -159,6 +161,9 @@ export const BorrowPage = () => { const trvContract = new TreasuryReservesVault__factory(signer).attach( env.contracts.treasuryReservesVault ); + + const trvAvailable = await trvContract.totalAvailable(env.contracts.dai); + const strategyAvailalableToBorrowFromTrv = await trvContract.availableForStrategyToBorrow( env.contracts.strategies.tlcStrategy, @@ -189,6 +194,8 @@ export const BorrowPage = () => { strategyBalance: fromAtto(maxAvailableToBorrow), borrowRate: currentBorrowInterestRate, liquidationLtv: fromAtto(maxLtv), + outstandingUserDebt: fromAtto(outstandingUserDebt), + trvAvailable: trvAvailable, daiCircuitBreakerRemaining: circuitBreakers?.daiCircuitBreakerRemaining, templeCircuitBreakerRemaining: circuitBreakers?.templeCircuitBreakerRemaining, @@ -210,13 +217,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 +233,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, @@ -239,6 +242,8 @@ export const BorrowPage = () => { tlcInfoFromContracts?.daiCircuitBreakerRemaining || ZERO, templeCircuitBreakerRemaining: tlcInfoFromContracts?.templeCircuitBreakerRemaining || ZERO, + outstandingUserDebt: tlcInfoFromContracts?.outstandingUserDebt || 0, + trvAvailable: tlcInfoFromContracts?.trvAvailable || ZERO, }); } catch (e) { setMetricsLoading(false); @@ -482,14 +487,21 @@ export const BorrowPage = () => { if (!tlcInfo) return '...'; const availableAsBigNumber = toAtto(tlcInfo.strategyBalance); + let borrowableAmount = tlcInfo.strategyBalance; if (tlcInfo.daiCircuitBreakerRemaining.lt(availableAsBigNumber)) { - return `$${Number( - fromAtto(tlcInfo.daiCircuitBreakerRemaining) - ).toLocaleString()}`; + borrowableAmount = fromAtto(tlcInfo.daiCircuitBreakerRemaining); + } + + const trvAvailable = fromAtto(tlcInfo.trvAvailable); + if (trvAvailable < borrowableAmount) { + borrowableAmount = trvAvailable; } - return `$${Number(tlcInfo.strategyBalance).toLocaleString()}`; + return `$${Number(borrowableAmount).toLocaleString('en', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; }, [tlcInfo]); return ( @@ -717,12 +729,29 @@ export const BorrowPage = () => { Current Borrow APY + + {showLoading ? '...' : prices.tpi.toFixed(2)} Current TPI + + + {showLoading + ? '...' + : tlcInfo && + `$${Number(tlcInfo.outstandingUserDebt).toLocaleString( + 'en', + { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + } + )}`} + + Outstanding User Debt + 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/DashboardConfig.tsx b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/DashboardConfig.tsx index 528521ab2..c3134bc0d 100644 --- a/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/DashboardConfig.tsx +++ b/apps/dapp/src/components/Pages/Core/DappPages/Dashboard/DashboardConfig.tsx @@ -38,7 +38,7 @@ export const Dashboards: DashboardData[] = [ title: 'TRV', path: 'treasuryreservesvault', description: - 'Treasury Reserves Vault (TRV) coordinates and manages the flow of capital for current Treasury allocations. When funding and management parameters are approved for a Strategy, the TRV will transfer funds e.g. DAI and issue corresponding debt to the Strategy borrower. The current equity of the Strategy is discounted by the loan principal and accrued interest benchmarked to the prevailing rate of the current Base Strategy for the borrowed token.', + 'Treasury Reserves Vault (TRV) is the central clearinghouse for the Temple Treasury and critical coordinator for current Treasury Strategy allocations. When funding and management parameters are approved for a Strategy, the TRV will transfer funds and issue corresponding debt to the Strategy borrower. The current equity of the Strategy is discounted by the loan principal and accrued interest benchmarked to the prevailing rate of the current Base Strategy for the borrowed token.', contractLink: `${env.etherscan}/address/${env.contracts.treasuryReservesVault}`, }, { @@ -47,7 +47,7 @@ export const Dashboards: DashboardData[] = [ title: 'RAMOS', path: 'ramos', description: - 'Ramos is the automated market operations (AMO) manager that supplies liquidity to the TEMPLE/DAI pool on the Balancer Exchange platform. A bot manages the contract to support TEMPLE trading, reduce price volatility, and earn farming rewards.', + 'Ramos is the automated market operations (AMO) manager that supplies liquidity to the TEMPLE/DAI pool on the Balancer DEX. A bot manages the contract to support TEMPLE trading, reduce price volatility, and earn farming rewards.', contractLink: `${env.etherscan}/address/${env.contracts.strategies.ramosStrategy}`, }, { @@ -56,7 +56,7 @@ export const Dashboards: DashboardData[] = [ title: 'TLC', path: 'tlc', description: - 'Temple Loving Care (also known as Temple Line of Credit) offers DAI lending for users who supply TEMPLE token as collateral. The value of the collateral is not determined by the current $TEMPLE spot price on the Balancer DEX but by the current Treasury Price Index (TPI). Users may borrow up to 75% loan-to-value (LTV) with the liquidation LTV set to 80%. There are no origination fees and users can withdraw their collateral at any time by repaying the DAI loan. TLC interest rate is a variable APR that is dependent on Debt Ceiling Utilisation. Any accrued interest will increase LTV over time. Borrowers can expect the APR to be set no lower than the prevailing APR for the Treasury DAI Base Strategy. Click here to learn more about Temple Loving Care.', + 'Temple Loving Care (also known as Temple Line of Credit) offers DAI lending for users who supply TEMPLE as collateral. The TLC will use the current Treasury Price Index (TPI) Oracle to determine the collateral value of TEMPLE. Users may borrow up to 85% loan-to-value (LTV) with the TEMPLE liquidation LTV set to 90%. There are no origination fees and users can withdraw their TEMPLE at any time by repaying the DAI loan. The TLC interest rate is set to a fixed rate that will be periodically updated to 2X the yield from the current Treasury Base Strategy e.g. sDAI. Click here to learn more about Temple Loving Care.', contractLink: `${env.etherscan}/address/${env.contracts.strategies.tlcStrategy}`, }, { @@ -65,7 +65,7 @@ export const Dashboards: DashboardData[] = [ title: 'TEMPLE BASE', path: 'templebase', description: - 'Temple Base strategy is the source of automated market operations (AMO) TEMPLE tokens in the Treasury framework. The TRV facilitates the withdrawal of newly minted TEMPLE tokens from and the issuance of TEMPLE debt to the Temple Base strategy. These TEMPLE tokens will be borrowed by a Treasury Strategy such as Ramos to generate returns. Once these tokens are repaid to the TRV, they will be deposited to the Temple Base strategy to be burned. From the perspective of the TRV, positive returns will be realized when TEMPLE flows to the Temple Base strategy is net positive.', + 'TEMPLE Base strategy is the funding source for TEMPLE tokens for automated market operations (AMO) in the Treasury framework. The TRV facilitates the withdrawal of newly minted TEMPLE tokens from and the issuance of TEMPLE debt to the TEMPLE Base strategy. These TEMPLE tokens will be borrowed by a Treasury Strategy such as Ramos to generate returns. Once these tokens are repaid to the TRV, they will be deposited to the TEMPLE Base strategy to be burned. Positive returns will be realized when TEMPLE flows to the TEMPLE Base strategy is net positive.', contractLink: `${env.etherscan}/address/${env.contracts.strategies.templeStrategy}`, }, { @@ -74,7 +74,7 @@ export const Dashboards: DashboardData[] = [ title: 'DSR BASE', path: 'dsrbase', description: - 'Idle capital in the Treasury Reserves Vault (TRV) that is not currently deployed to a Strategy borrower will be automatically directed to a Base Strategy to earn yield. Currently, the Base Strategy is set to the Dai Savings Rate (DSR) which makes DAI the base currency of the TRV. The current rate of return for DSR Base also serves as the benchmark interest rate for the Treasury Strategy that borrows DAI from the TRV.', + 'Idle reserve capital in the TRV that is not currently borrowed by a Strategy Borrower will be automatically directed to a Base Strategy to earn yield. The TRV Base Strategy is currently set to the Dai Savings Rate (DSR) or sDAI. The current rate of return for the Base Strategy also serves as the performance benchmark or "risk-free" interest rate for Treasury Strategies.', contractLink: `${env.etherscan}/address/${env.contracts.strategies.dsrBaseStrategy}`, }, { @@ -83,7 +83,7 @@ export const Dashboards: DashboardData[] = [ title: 'TEMPLO MAYOR', path: 'templomayor', description: - 'Templo Mayor is an Gnosis Safe Omnibus strategy. An Omnibus Strategy utilises the same bookkeeping structure and approval process, but may entail several related holdings or sub-positions that are managed as a whole. For instance, deposits into different but similar or co-dependent vaults on the same platform or different platforms may be consolidated into one Omnibus Gnosis Safe. Seed allocations of a target risk profile may also be consolidated into an Omnibus Strategy to reduce the noise. Therefore an Omnibus Strategy may provide additional operational efficiency and allow Stakeholders to evaluate a series of related deployments as one composite position rather than as singletons.', + 'Templo Mayor is an Gnosis Safe Omnibus Strategy that is particularly useful when full automation is not feasible. An Omnibus Strategy utilises the same bookkeeping structure and approval process as the automated Temple v2 Strategies, but may entail several related holdings or sub-positions that are managed holistically. For instance, deposits into different but similar or co-dependent vaults on the same platform or different platforms may be consolidated into one Omnibus Gnosis Safe. Partner seed allocations of a target risk profile may also be consolidated into an Omnibus Strategy to derisk any particular project. An Omnibus Strategy may provide additional operational efficiency and allow Stakeholders to evaluate a series of related deployments as one composite position rather than as singletons.', contractLink: `${env.etherscan}/address/${env.contracts.strategies.temploMayorGnosisStrategy}`, }, { @@ -92,7 +92,7 @@ export const Dashboards: DashboardData[] = [ title: 'FOHMO', path: 'fohmo', description: - 'FOHMO is a strategy that aims to maintain a maxed looped position in OHM', + 'FOHMO is a strategy that aims to maintain a maximally looped position in OHM', contractLink: `${env.etherscan}/address/${env.contracts.strategies.fohmoGnosisStrategy}`, }, ]; 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/constants/env/local.tsx b/apps/dapp/src/constants/env/local.tsx index 6f3ba6217..1933f1ced 100644 --- a/apps/dapp/src/constants/env/local.tsx +++ b/apps/dapp/src/constants/env/local.tsx @@ -1,8 +1,9 @@ import { ADDRESS_ZERO } from 'utils/bigNumber'; import { Environment } from './types'; -const BALANCER_SUBGRAPH_API_KEY = import.meta.env - .VITE_BALANCER_SUBGRAPH_API_KEY; +const ENV_VARS = import.meta.env; +const BALANCER_SUBGRAPH_API_KEY = ENV_VARS.VITE_BALANCER_SUBGRAPH_API_KEY; +const ENABLE_SUBGRAPH_LOGS = ENV_VARS.VITE_ENABLE_SUBGRAPH_LOGS === 'true'; const env: Environment = { alchemyId: '-nNWThz_YpX1cGffGiz-lbSMu7dmp4GK', @@ -140,6 +141,7 @@ const env: Environment = { // address: '0x5CE28cAE5aAb002DcBc076d5A551A473a7C9dF89', // }, ], + enableSubgraphLogs: ENABLE_SUBGRAPH_LOGS, }; export default env; diff --git a/apps/dapp/src/constants/env/preview.tsx b/apps/dapp/src/constants/env/preview.tsx index 1258edd1a..6921b87ee 100644 --- a/apps/dapp/src/constants/env/preview.tsx +++ b/apps/dapp/src/constants/env/preview.tsx @@ -1,8 +1,9 @@ import { ADDRESS_ZERO } from 'utils/bigNumber'; import { Environment } from './types'; -const BALANCER_SUBGRAPH_API_KEY = import.meta.env - .VITE_BALANCER_SUBGRAPH_API_KEY; +const ENV_VARS = import.meta.env; +const BALANCER_SUBGRAPH_API_KEY = ENV_VARS.VITE_BALANCER_SUBGRAPH_API_KEY; +const ENABLE_SUBGRAPH_LOGS = ENV_VARS.VITE_ENABLE_SUBGRAPH_LOGS === 'true'; const env: Environment = { alchemyId: 'AorwfDdHDsEjIX4HPwS70zkVjWqjv5vZ', @@ -152,6 +153,7 @@ const env: Environment = { // address: '0x5CE28cAE5aAb002DcBc076d5A551A473a7C9dF89', // }, ], + enableSubgraphLogs: ENABLE_SUBGRAPH_LOGS, }; export default env; diff --git a/apps/dapp/src/constants/env/production.tsx b/apps/dapp/src/constants/env/production.tsx index d1b1d42cb..039bd8928 100644 --- a/apps/dapp/src/constants/env/production.tsx +++ b/apps/dapp/src/constants/env/production.tsx @@ -1,8 +1,9 @@ import { ADDRESS_ZERO } from 'utils/bigNumber'; import { Environment } from './types'; -const BALANCER_SUBGRAPH_API_KEY = import.meta.env - .VITE_BALANCER_SUBGRAPH_API_KEY; +const ENV_VARS = import.meta.env; +const BALANCER_SUBGRAPH_API_KEY = ENV_VARS.VITE_BALANCER_SUBGRAPH_API_KEY; +const ENABLE_SUBGRAPH_LOGS = ENV_VARS.VITE_ENABLE_SUBGRAPH_LOGS === 'true'; const env: Environment = { alchemyId: 'XiIZxWykHU5AOFBwxKgxseXWN984Mp8F', @@ -302,6 +303,7 @@ const env: Environment = { // address: '0x9f90430179D9b67341BFa50559bc7B8E35629f1b', // }, ], + enableSubgraphLogs: ENABLE_SUBGRAPH_LOGS, }; export default env; diff --git a/apps/dapp/src/constants/env/types.ts b/apps/dapp/src/constants/env/types.ts index ddde881ff..728c58789 100644 --- a/apps/dapp/src/constants/env/types.ts +++ b/apps/dapp/src/constants/env/types.ts @@ -123,4 +123,5 @@ export interface Environment { enableAscend: boolean; }; safes: SafeWallet[]; + enableSubgraphLogs: boolean; } diff --git a/apps/dapp/src/utils/subgraph.ts b/apps/dapp/src/utils/subgraph.ts index 58f88e2ec..274e7caae 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'; +import env from 'constants/env'; +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 (env.enableSubgraphLogs) { + 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 (env.enableSubgraphLogs) { + 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==