From 00fd88e14293c78af6efb3fd613ff4b1ffb2b3c1 Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Tue, 15 Oct 2024 15:28:07 +0400 Subject: [PATCH 01/10] Re-generate types from OpenAPI schema --- package.json | 2 +- src/generated/coolerLoans.ts | 291 ++++++----------------------------- 2 files changed, 49 insertions(+), 244 deletions(-) diff --git a/package.json b/package.json index f26c8f82f..1d3ca9d20 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "typechain:build": "yarn run typechain --target ethers-v5 --out-dir src/typechain src/abi/*.json src/abi/**/*.json", "postinstall": "yarn typechain:build", "prepare": "husky install", - "codegen:bundle": "redocly bundle https://raw.githubusercontent.com/OlympusDAO/cooler-loans-api/main/openapi/openapi.yml -o tmp/openapi.yaml", + "codegen:bundle": "redocly bundle https://raw.githubusercontent.com/OlympusDAO/cooler-loans-api/main/apps/get/openapi/openapi.yml -o tmp/openapi.yaml", "codegen": "yarn codegen:bundle && orval && yarn lint:fix" }, "resolutions": { diff --git a/src/generated/coolerLoans.ts b/src/generated/coolerLoans.ts index 3a4543e54..1be0086a0 100644 --- a/src/generated/coolerLoans.ts +++ b/src/generated/coolerLoans.ts @@ -2,7 +2,7 @@ * Generated by orval v6.23.0 🍺 * Do not edit manually. * Cooler Loans - * OpenAPI spec version: 1.0 + * OpenAPI spec version: 1.1 */ import type { QueryFunction, QueryKey, UseQueryOptions, UseQueryResult } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; @@ -26,297 +26,102 @@ export type GetSnapshotsParams = { * Represents the state of the Treasury at the time of the snapshot. */ export type SnapshotTreasury = { + /** Total balance of DAI in the active treasury */ daiBalance: number; + /** Total balance of sDAI in the active treasury */ sDaiBalance: number; + /** Total balance of sDAI in terms of DAI in the active treasury */ sDaiInDaiBalance: number; }; +/** + * Current Clearinghouse terms + */ export type SnapshotTerms = { + /** Duration of the loan in seconds */ duration: number; + /** Interest rate as a decimal + +e.g. 0.005 = 0.5% */ interestRate: number; + /** Value of the loan (in DAI) provided against the collateral */ loanToCollateral: number; }; -/** - * Status of the loan - */ -export type SnapshotLoansStatus = (typeof SnapshotLoansStatus)[keyof typeof SnapshotLoansStatus]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const SnapshotLoansStatus = { - Active: "Active", - Expired: "Expired", - Reclaimed: "Reclaimed", - Repaid: "Repaid", -} as const; - -/** - * Dictionary of the loans that had been created by this date. - -Key: `cooler address`-`loanId` -Value: Loan record - -Will only be fetched if explicitly specified. - */ -export type SnapshotLoans = { - [key: string]: { - borrowerAddress: string; - collateralClaimedQuantity: number; - collateralClaimedValue: number; - /** The current quantity of the collateral token that is deposited. - -As the loan is repaid, this will decrease. */ - collateralDeposited: number; - collateralIncome: number; - coolerAddress: string; - /** Timestamp of the loan creation, in seconds */ - createdTimestamp: number; - /** The loan duration, in seconds. */ - durationSeconds: number; - /** Timestamp of the expected loan expiry, in seconds */ - expiryTimestamp: number; - /** Loan id unique across the clearinghouse and its coolers - -Format: cooler-loanId */ - id: string; - /** The total interest charged on the loan. - -When the loan is extended, this number will be increased. */ - interest: number; - /** Cumulative interest paid on the loan. - -Any outstanding interest is paid first, followed by principal. */ - interestPaid: number; - /** The interest rate, stored as a decimal. - -e.g. 0.5% = 0.005 */ - interestRate: number; - lenderAddress: string; - /** Loan id unique to the cooler */ - loanId: number; - /** The loan principal. Will not change after loan creation. */ - principal: number; - /** Cumulative principal paid on the loan. */ - principalPaid: number; - secondsToExpiry: number; - /** Status of the loan */ - status: SnapshotLoansStatus; - }; -}; - /** * Principal due for each expiry bucket. */ export type SnapshotExpiryBuckets = { - "121Days": number; - "30Days": number; + /** Principal due for loans that are active */ active: number; + /** Principal due for loans that are within 121 days of expiry */ + days121: number; + /** Principal due for loans that are within 30 days of expiry */ + days30: number; + /** Principal due for loans that are expired */ expired: number; }; /** - * Represents the state of the Clearinghouse at the time of the snapshot. + * Totals for the Clearinghouses at the time of the snapshot. */ -export type SnapshotClearinghouse = { +export type SnapshotClearinghouseTotals = { + /** Total balance of DAI across all Clearinghouses */ + daiBalance: number; + /** Total balance of sDAI across all Clearinghouses */ + sDaiBalance: number; + /** Total balance of sDAI in terms of DAI across all Clearinghouses */ + sDaiInDaiBalance: number; +}; + +export type SnapshotClearinghousesItem = { + /** Address of the Clearinghouse */ + address: string; + /** The address of the collateral */ collateralAddress: string; + /** The address of the CoolerFactory */ coolerFactoryAddress: string; + /** Balance of DAI */ daiBalance: number; + /** The address of the debt */ debtAddress: string; + /** Amount of DAI that the Clearinghouse should be funded with */ fundAmount: number; + /** The cadence of the funding */ fundCadence: number; + /** Balance of sDAI */ sDaiBalance: number; + /** Balance of sDAI in terms of DAI */ sDaiInDaiBalance: number; }; -export type RepayLoanEventOptionalAllOfLoan = { - id: string; -}; - -export type RepayLoanEventOptionalAllOf = { - loan: RepayLoanEventOptionalAllOfLoan; -}; - -export type OmitRepayLoanEventLoan = { - amountPaid: number; - blockNumber: number; - blockTimestamp: number; - clearinghouseDaiBalance: number; - clearinghouseSDaiBalance: number; - clearinghouseSDaiInDaiBalance: number; - collateralDeposited: number; - date: string; - id: string; - interestPayable: number; - principalPayable: number; - secondsToExpiry: number; - transactionHash: string; - treasuryDaiBalance: number; - treasurySDaiBalance: number; - treasurySDaiInDaiBalance: number; -}; - -export type RepayLoanEventOptional = OmitRepayLoanEventLoan & RepayLoanEventOptionalAllOf; - -export type ExtendLoanEventOptionalAllOfLoan = { - id: string; -}; - -export type ExtendLoanEventOptionalAllOf = { - loan: ExtendLoanEventOptionalAllOfLoan; -}; - -export type OmitExtendLoanEventLoan = { - blockNumber: number; - blockTimestamp: number; - clearinghouseDaiBalance: number; - clearinghouseSDaiBalance: number; - clearinghouseSDaiInDaiBalance: number; - date: string; - expiryTimestamp: number; - id: string; - interestDue: number; - periods: number; - transactionHash: string; - treasuryDaiBalance: number; - treasurySDaiBalance: number; - treasurySDaiInDaiBalance: number; -}; - -export type ExtendLoanEventOptional = OmitExtendLoanEventLoan & ExtendLoanEventOptionalAllOf; - -export type ClaimDefaultedLoanEventOptionalAllOfLoan = { - id: string; -}; - -export type ClaimDefaultedLoanEventOptionalAllOf = { - loan: ClaimDefaultedLoanEventOptionalAllOfLoan; -}; - -export type OmitClaimDefaultedLoanEventLoan = { - blockNumber: number; - blockTimestamp: number; - collateralPrice: number; - collateralQuantityClaimed: number; - collateralValueClaimed: number; - date: string; - id: string; - secondsSinceExpiry: number; - transactionHash: string; -}; - -export type ClaimDefaultedLoanEventOptional = OmitClaimDefaultedLoanEventLoan & ClaimDefaultedLoanEventOptionalAllOf; - -export type ClearLoanRequestEventOptional = OmitClearLoanRequestEventLoanRequest & ClearLoanRequestEventOptionalAllOf; - export type Snapshot = { - /** Represents the state of the Clearinghouse at the time of the snapshot. */ - clearinghouse: SnapshotClearinghouse; - clearinghouseEvents: ClearinghouseSnapshotOptional[]; + /** State of the Clearinghouses at the time of the snapshot. */ + clearinghouses: SnapshotClearinghousesItem[]; + /** Totals for the Clearinghouses at the time of the snapshot. */ + clearinghouseTotals: SnapshotClearinghouseTotals; /** Quantity of collateral deposited across all Coolers */ collateralDeposited: number; /** Income from collateral reclaimed on this date. */ collateralIncome: number; - creationEvents: ClearLoanRequestEventOptional[]; - date: string; - defaultedClaimEvents: ClaimDefaultedLoanEventOptional[]; /** Principal due for each expiry bucket. */ expiryBuckets: SnapshotExpiryBuckets; - extendEvents: ExtendLoanEventOptional[]; /** Income from interest payments made on this date. */ interestIncome: number; /** Interest receivable across all Coolers */ interestReceivables: number; - /** Dictionary of the loans that had been created by this date. - -Key: `cooler address`-`loanId` -Value: Loan record - -Will only be fetched if explicitly specified. */ - loans: SnapshotLoans; /** Principal receivable across all Coolers */ principalReceivables: number; - repaymentEvents: RepayLoanEventOptional[]; + /** Date of the snapshot. + +Times are stored at UTC. */ + snapshotDate: string; + /** Current Clearinghouse terms */ terms: SnapshotTerms; - /** Timestamp of the snapshot, in milliseconds */ - timestamp: number; /** Represents the state of the Treasury at the time of the snapshot. */ treasury: SnapshotTreasury; }; -export type CoolerLoanOptionalAllOfRequest = { - durationSeconds: number; - interestPercentage: number; -}; - -export type CoolerLoanOptionalAllOf = { - request: CoolerLoanOptionalAllOfRequest; -}; - -export type OmitCoolerLoanRequestCreationEventsDefaultedClaimEventsExtendEventsRepaymentEvents = { - borrower: string; - collateral: number; - collateralToken: string; - cooler: string; - createdBlock: number; - createdTimestamp: number; - createdTransaction: string; - debtToken: string; - expiryTimestamp: number; - hasCallback: boolean; - id: string; - interest: number; - lender: string; - loanId: number; - principal: number; -}; - -export type CoolerLoanOptional = OmitCoolerLoanRequestCreationEventsDefaultedClaimEventsExtendEventsRepaymentEvents & - CoolerLoanOptionalAllOf; - -export type ClearLoanRequestEventOptionalAllOf = { - loan: CoolerLoanOptional; -}; - -export type OmitClearLoanRequestEventLoanRequest = { - blockNumber: number; - blockTimestamp: number; - clearinghouseDaiBalance: number; - clearinghouseSDaiBalance: number; - clearinghouseSDaiInDaiBalance: number; - date: string; - id: string; - transactionHash: string; - treasuryDaiBalance: number; - treasurySDaiBalance: number; - treasurySDaiInDaiBalance: number; -}; - -export type ClearinghouseSnapshotOptional = { - blockNumber: number; - blockTimestamp: number; - clearinghouse: string; - collateralAddress: string; - coolerFactoryAddress: string; - daiBalance: number; - date: string; - debtAddress: string; - duration: number; - fundAmount: number; - fundCadence: number; - id: string; - interestRate: number; - interestReceivables: number; - isActive: boolean; - loanToCollateral: number; - nextRebalanceTimestamp: number; - principalReceivables: number; - sDaiBalance: number; - sDaiInDaiBalance: number; - treasuryDaiBalance: number; - treasurySDaiBalance: number; - treasurySDaiInDaiBalance: number; -}; - /** * @summary Retrieves all Cooler Loans snapshots between the given dates. */ From 374b811d48593b58d934367381fd745aaed69bc9 Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Tue, 15 Oct 2024 15:28:35 +0400 Subject: [PATCH 02/10] Update hooks and components with changes to OpenAPI schema/data structure --- .../Lending/Cooler/dashboard/IncomeGraph.tsx | 2 +- .../Cooler/dashboard/MaturityGraph.tsx | 6 +-- .../Lending/Cooler/dashboard/Metrics.tsx | 42 +++---------------- .../Cooler/dashboard/UtilisationGraph.tsx | 2 +- .../Lending/Cooler/hooks/useSnapshot.tsx | 8 ++-- 5 files changed, 14 insertions(+), 46 deletions(-) diff --git a/src/views/Lending/Cooler/dashboard/IncomeGraph.tsx b/src/views/Lending/Cooler/dashboard/IncomeGraph.tsx index 25f0465f4..eb5cf1e14 100644 --- a/src/views/Lending/Cooler/dashboard/IncomeGraph.tsx +++ b/src/views/Lending/Cooler/dashboard/IncomeGraph.tsx @@ -26,7 +26,7 @@ export const IncomeGraph = ({ startDate }: { startDate?: Date }) => { } // Sort in descending order - data.sort((a, b) => b.timestamp - a.timestamp); + data.sort((a, b) => new Date(b.snapshotDate).getTime() - new Date(a.snapshotDate).getTime()); setCoolerSnapshots(data); }, [data]); diff --git a/src/views/Lending/Cooler/dashboard/MaturityGraph.tsx b/src/views/Lending/Cooler/dashboard/MaturityGraph.tsx index 865a10439..74255039a 100644 --- a/src/views/Lending/Cooler/dashboard/MaturityGraph.tsx +++ b/src/views/Lending/Cooler/dashboard/MaturityGraph.tsx @@ -35,7 +35,7 @@ export const MaturityGraph = () => { // Sort in descending order const _coolerSnapshots = data.slice(); - _coolerSnapshots.sort((a, b) => b.timestamp - a.timestamp); + _coolerSnapshots.sort((a, b) => new Date(b.snapshotDate).getTime() - new Date(a.snapshotDate).getTime()); setCoolerSnapshots(_coolerSnapshots); }, [data]); @@ -44,8 +44,8 @@ export const MaturityGraph = () => { */ const dataKeys: string[] = [ "expiryBuckets.expired", - "expiryBuckets.30Days", - "expiryBuckets.121Days", + "expiryBuckets.days30", + "expiryBuckets.days121", "expiryBuckets.active", ]; const itemNames: string[] = ["Expired", "< 30 Days", "< 121 Days", ">= 121 Days"]; diff --git a/src/views/Lending/Cooler/dashboard/Metrics.tsx b/src/views/Lending/Cooler/dashboard/Metrics.tsx index ae22ca0ce..480c87dc2 100644 --- a/src/views/Lending/Cooler/dashboard/Metrics.tsx +++ b/src/views/Lending/Cooler/dashboard/Metrics.tsx @@ -2,7 +2,6 @@ import { Skeleton } from "@mui/material"; import { Metric } from "@olympusdao/component-library"; import { BigNumber, ethers } from "ethers"; import { useMemo, useState } from "react"; -import { SnapshotLoansStatus } from "src/generated/coolerLoans"; import { formatCurrency, formatNumber } from "src/helpers"; import { getTotalCapacity, @@ -112,8 +111,8 @@ export const TotalCapacityRemaining = () => { tooltip={`The capacity remaining is the sum of the DAI and sDAI in the clearinghouse and treasury. As of the latest snapshot, the values (in DAI) are: Clearinghouse: -DAI: ${formatCurrency(latestSnapshot?.clearinghouse?.daiBalance || 0, 0, "DAI")} -sDAI: ${formatCurrency(latestSnapshot?.clearinghouse?.sDaiInDaiBalance || 0, 0, "DAI")} +DAI: ${formatCurrency(latestSnapshot?.clearinghouseTotals.daiBalance || 0, 0, "DAI")} +sDAI: ${formatCurrency(latestSnapshot?.clearinghouseTotals.sDaiInDaiBalance || 0, 0, "DAI")} Treasury: DAI: ${formatCurrency(latestSnapshot?.treasury?.daiBalance || 0, 0, "DAI")} @@ -139,12 +138,12 @@ export const PrincipalMaturingInUnder = ({ days, previousBucket }: { days: numbe } if (days == 30) { - setPrincipalMaturing(latestSnapshot.expiryBuckets["30Days"]); + setPrincipalMaturing(latestSnapshot.expiryBuckets.days30); return; } if (days == 121) { - setPrincipalMaturing(latestSnapshot.expiryBuckets["121Days"]); + setPrincipalMaturing(latestSnapshot.expiryBuckets.days121); return; } @@ -153,7 +152,7 @@ export const PrincipalMaturingInUnder = ({ days, previousBucket }: { days: numbe return ( { - const { latestSnapshot } = useCoolerSnapshotLatest(); - - const [principalExpired, setPrincipalExpired] = useState(); - useMemo(() => { - if (!latestSnapshot) { - setPrincipalExpired(undefined); - return; - } - - let _principalExpired = 0; - for (const loan of Object.values(latestSnapshot.loans)) { - if (loan.status != SnapshotLoansStatus.Expired) { - continue; - } - - _principalExpired += loan.principal; - } - - setPrincipalExpired(_principalExpired); - }, [latestSnapshot]); - - return ( - - ); -}; - export const BorrowRate = () => { const { latestSnapshot } = useCoolerSnapshotLatest(); diff --git a/src/views/Lending/Cooler/dashboard/UtilisationGraph.tsx b/src/views/Lending/Cooler/dashboard/UtilisationGraph.tsx index 2d9597480..43637a5c1 100644 --- a/src/views/Lending/Cooler/dashboard/UtilisationGraph.tsx +++ b/src/views/Lending/Cooler/dashboard/UtilisationGraph.tsx @@ -29,7 +29,7 @@ export const UtilisationGraph = ({ startDate }: { startDate?: Date }) => { const _coolerSnapshotsWithTotals = data.slice(); // Sort in descending order - _coolerSnapshotsWithTotals.sort((a, b) => b.timestamp - a.timestamp); + _coolerSnapshotsWithTotals.sort((a, b) => new Date(b.snapshotDate).getTime() - new Date(a.snapshotDate).getTime()); setCoolerSnapshots(_coolerSnapshotsWithTotals); }, [data]); diff --git a/src/views/Lending/Cooler/hooks/useSnapshot.tsx b/src/views/Lending/Cooler/hooks/useSnapshot.tsx index 8d35a5830..94a006961 100644 --- a/src/views/Lending/Cooler/hooks/useSnapshot.tsx +++ b/src/views/Lending/Cooler/hooks/useSnapshot.tsx @@ -47,8 +47,8 @@ export const getClearinghouseCapacity = (snapshot: Snapshot | undefined): number return 0; } - const daiBalance = snapshot.clearinghouse?.daiBalance || 0; - const sDaiInDaiBalance = snapshot.clearinghouse?.sDaiInDaiBalance || 0; + const daiBalance = snapshot.clearinghouseTotals.daiBalance || 0; + const sDaiInDaiBalance = snapshot.clearinghouseTotals.sDaiInDaiBalance || 0; return daiBalance + sDaiInDaiBalance; }; @@ -72,8 +72,8 @@ export const getTotalCapacity = (snapshot: Snapshot | undefined): number => { const treasuryDaiBalance = snapshot.treasury?.daiBalance || 0; const treasurySDaiInDaiBalance = snapshot.treasury?.sDaiInDaiBalance || 0; - const clearinghouseDaiBalance = snapshot.clearinghouse?.daiBalance || 0; - const clearinghouseSDaiInDaiBalance = snapshot.clearinghouse?.sDaiInDaiBalance || 0; + const clearinghouseDaiBalance = snapshot.clearinghouseTotals.daiBalance || 0; + const clearinghouseSDaiInDaiBalance = snapshot.clearinghouseTotals.sDaiInDaiBalance || 0; return treasuryDaiBalance + treasurySDaiInDaiBalance + clearinghouseDaiBalance + clearinghouseSDaiInDaiBalance; }; From 467a4ad42fc22c692e661d7fc00a6a146847fa75 Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Tue, 15 Oct 2024 15:55:47 +0400 Subject: [PATCH 03/10] Add missing timestamp field, used in charts --- src/views/Lending/Cooler/hooks/useSnapshot.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/views/Lending/Cooler/hooks/useSnapshot.tsx b/src/views/Lending/Cooler/hooks/useSnapshot.tsx index 94a006961..501bfefc9 100644 --- a/src/views/Lending/Cooler/hooks/useSnapshot.tsx +++ b/src/views/Lending/Cooler/hooks/useSnapshot.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { Snapshot, useGetSnapshots } from "src/generated/coolerLoans"; import { getISO8601String } from "src/helpers/DateHelper"; @@ -21,8 +22,22 @@ export const useCoolerSnapshot = (startDate?: Date, beforeDate?: Date) => { }, ); + // Add a timestamp field to each snapshot, and cache the result + const cachedData = useMemo(() => { + if (!data || !data.records) { + return undefined; + } + + return data.records.map(snapshot => { + return { + ...snapshot, + timestamp: new Date(snapshot.snapshotDate).getTime(), + }; + }); + }, [data]); + return { - data: data?.records, + data: cachedData, isLoading, }; }; From 56a2184cb48159f16ac951bd003cc1922cf716ec Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Tue, 15 Oct 2024 16:07:03 +0400 Subject: [PATCH 04/10] Tweak to fetching latest snapshot --- src/views/Lending/Cooler/hooks/useSnapshot.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/views/Lending/Cooler/hooks/useSnapshot.tsx b/src/views/Lending/Cooler/hooks/useSnapshot.tsx index 501bfefc9..87e758aaf 100644 --- a/src/views/Lending/Cooler/hooks/useSnapshot.tsx +++ b/src/views/Lending/Cooler/hooks/useSnapshot.tsx @@ -43,9 +43,10 @@ export const useCoolerSnapshot = (startDate?: Date, beforeDate?: Date) => { }; export const useCoolerSnapshotLatest = () => { - // Go back 2 days + // Go back 1 day + // In case there is no snapshot (yet) for today, use yesterday's snapshot const startDate = new Date(); - startDate.setDate(startDate.getDate() - 2); + startDate.setDate(startDate.getDate() - 1); const { data, isLoading } = useCoolerSnapshot(startDate); From 9ff736f6af326a106cf0736875235bd69d4d40f4 Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Wed, 16 Oct 2024 15:14:06 +0400 Subject: [PATCH 05/10] Update Cooler Loans API hooks with changes: sorting, current snapshot route --- src/generated/coolerLoans.ts | 65 ++++++++++++++++++- .../Lending/Cooler/dashboard/IncomeGraph.tsx | 3 +- .../Cooler/dashboard/MaturityGraph.tsx | 6 +- .../Lending/Cooler/dashboard/Metrics.tsx | 12 ++-- .../Cooler/dashboard/UtilisationGraph.tsx | 4 +- .../Lending/Cooler/hooks/useSnapshot.tsx | 37 ++++++++--- 6 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/generated/coolerLoans.ts b/src/generated/coolerLoans.ts index 1be0086a0..c2a76cd3d 100644 --- a/src/generated/coolerLoans.ts +++ b/src/generated/coolerLoans.ts @@ -7,6 +7,10 @@ import type { QueryFunction, QueryKey, UseQueryOptions, UseQueryResult } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; import { customHttpClient } from "src/views/Lending/Cooler/hooks/customHttpClient"; +export type GetCurrentSnapshot200 = { + record?: Snapshot; +}; + export type GetSnapshots200 = { records?: Snapshot[]; }; @@ -20,6 +24,10 @@ export type GetSnapshotsParams = { * The date (YYYY-MM-DD) up to (but not including) which records should be retrieved */ beforeDate: string; + /** + * The order in which to return the snapshots. ASC or DESC + */ + orderBy?: string; }; /** @@ -126,11 +134,11 @@ Times are stored at UTC. */ * @summary Retrieves all Cooler Loans snapshots between the given dates. */ export const getSnapshots = (params: GetSnapshotsParams, signal?: AbortSignal) => { - return customHttpClient({ url: `/`, method: "GET", params, signal }); + return customHttpClient({ url: `/snapshots`, method: "GET", params, signal }); }; export const getGetSnapshotsQueryKey = (params: GetSnapshotsParams) => { - return [`/`, ...(params ? [params] : [])] as const; + return [`/snapshots`, ...(params ? [params] : [])] as const; }; export const getGetSnapshotsQueryOptions = >, TError = unknown>( @@ -168,3 +176,56 @@ export const useGetSnapshots = > return query; }; + +/** + * The current snapshot is the most recent one up to the current date. + * @summary Retrieves the current Cooler Loans snapshot + */ +export const getCurrentSnapshot = (signal?: AbortSignal) => { + return customHttpClient({ url: `/snapshots/current`, method: "GET", signal }); +}; + +export const getGetCurrentSnapshotQueryKey = () => { + return [`/snapshots/current`] as const; +}; + +export const getGetCurrentSnapshotQueryOptions = < + TData = Awaited>, + TError = unknown, +>(options?: { + query?: UseQueryOptions>, TError, TData>; +}) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetCurrentSnapshotQueryKey(); + + const queryFn: QueryFunction>> = ({ signal }) => + getCurrentSnapshot(signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetCurrentSnapshotQueryResult = NonNullable>>; +export type GetCurrentSnapshotQueryError = unknown; + +/** + * @summary Retrieves the current Cooler Loans snapshot + */ +export const useGetCurrentSnapshot = < + TData = Awaited>, + TError = unknown, +>(options?: { + query?: UseQueryOptions>, TError, TData>; +}): UseQueryResult & { queryKey: QueryKey } => { + const queryOptions = getGetCurrentSnapshotQueryOptions(options); + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey; + + return query; +}; diff --git a/src/views/Lending/Cooler/dashboard/IncomeGraph.tsx b/src/views/Lending/Cooler/dashboard/IncomeGraph.tsx index eb5cf1e14..e45b16740 100644 --- a/src/views/Lending/Cooler/dashboard/IncomeGraph.tsx +++ b/src/views/Lending/Cooler/dashboard/IncomeGraph.tsx @@ -25,8 +25,7 @@ export const IncomeGraph = ({ startDate }: { startDate?: Date }) => { return; } - // Sort in descending order - data.sort((a, b) => new Date(b.snapshotDate).getTime() - new Date(a.snapshotDate).getTime()); + // Cache. Already sorted in descending order. setCoolerSnapshots(data); }, [data]); diff --git a/src/views/Lending/Cooler/dashboard/MaturityGraph.tsx b/src/views/Lending/Cooler/dashboard/MaturityGraph.tsx index 74255039a..26de85614 100644 --- a/src/views/Lending/Cooler/dashboard/MaturityGraph.tsx +++ b/src/views/Lending/Cooler/dashboard/MaturityGraph.tsx @@ -33,10 +33,8 @@ export const MaturityGraph = () => { return; } - // Sort in descending order - const _coolerSnapshots = data.slice(); - _coolerSnapshots.sort((a, b) => new Date(b.snapshotDate).getTime() - new Date(a.snapshotDate).getTime()); - setCoolerSnapshots(_coolerSnapshots); + // Cache. Already sorted in descending order. + setCoolerSnapshots(data); }, [data]); /** diff --git a/src/views/Lending/Cooler/dashboard/Metrics.tsx b/src/views/Lending/Cooler/dashboard/Metrics.tsx index 480c87dc2..8024ce978 100644 --- a/src/views/Lending/Cooler/dashboard/Metrics.tsx +++ b/src/views/Lending/Cooler/dashboard/Metrics.tsx @@ -6,7 +6,7 @@ import { formatCurrency, formatNumber } from "src/helpers"; import { getTotalCapacity, useCoolerSnapshot, - useCoolerSnapshotLatest, + useCurrentCoolerSnapshot, } from "src/views/Lending/Cooler/hooks/useSnapshot"; export const CumulativeInterestIncome = ({ startDate }: { startDate?: Date }) => { @@ -66,7 +66,7 @@ export const CumulativeCollateralIncome = ({ startDate }: { startDate?: Date }) }; export const CollateralDeposited = () => { - const { latestSnapshot } = useCoolerSnapshotLatest(); + const { latestSnapshot } = useCurrentCoolerSnapshot(); return ( { }; export const OutstandingPrincipal = () => { - const { latestSnapshot } = useCoolerSnapshotLatest(); + const { latestSnapshot } = useCurrentCoolerSnapshot(); return ( { - const { latestSnapshot } = useCoolerSnapshotLatest(); + const { latestSnapshot } = useCurrentCoolerSnapshot(); return ( (); useMemo(() => { @@ -161,7 +161,7 @@ export const PrincipalMaturingInUnder = ({ days, previousBucket }: { days: numbe }; export const BorrowRate = () => { - const { latestSnapshot } = useCoolerSnapshotLatest(); + const { latestSnapshot } = useCurrentCoolerSnapshot(); const [borrowRate, setBorrowRate] = useState(); useMemo(() => { diff --git a/src/views/Lending/Cooler/dashboard/UtilisationGraph.tsx b/src/views/Lending/Cooler/dashboard/UtilisationGraph.tsx index 43637a5c1..7a29e741c 100644 --- a/src/views/Lending/Cooler/dashboard/UtilisationGraph.tsx +++ b/src/views/Lending/Cooler/dashboard/UtilisationGraph.tsx @@ -28,9 +28,7 @@ export const UtilisationGraph = ({ startDate }: { startDate?: Date }) => { const _coolerSnapshotsWithTotals = data.slice(); - // Sort in descending order - _coolerSnapshotsWithTotals.sort((a, b) => new Date(b.snapshotDate).getTime() - new Date(a.snapshotDate).getTime()); - + // Cache. Already sorted in descending order. setCoolerSnapshots(_coolerSnapshotsWithTotals); }, [data]); diff --git a/src/views/Lending/Cooler/hooks/useSnapshot.tsx b/src/views/Lending/Cooler/hooks/useSnapshot.tsx index 87e758aaf..ec86d16e0 100644 --- a/src/views/Lending/Cooler/hooks/useSnapshot.tsx +++ b/src/views/Lending/Cooler/hooks/useSnapshot.tsx @@ -1,7 +1,14 @@ import { useMemo } from "react"; -import { Snapshot, useGetSnapshots } from "src/generated/coolerLoans"; +import { Snapshot, useGetCurrentSnapshot, useGetSnapshots } from "src/generated/coolerLoans"; import { getISO8601String } from "src/helpers/DateHelper"; +/** + * Get the Cooler Loans snapshots for a given date range + * + * @param startDate - The start date of the range + * @param beforeDate - The end date of the range + * @returns The snapshots for the given date range, sorted in descending order + */ export const useCoolerSnapshot = (startDate?: Date, beforeDate?: Date) => { let _beforeDate = beforeDate; // If there is no beforeDate, set it to tomorrow @@ -42,18 +49,28 @@ export const useCoolerSnapshot = (startDate?: Date, beforeDate?: Date) => { }; }; -export const useCoolerSnapshotLatest = () => { - // Go back 1 day - // In case there is no snapshot (yet) for today, use yesterday's snapshot - const startDate = new Date(); - startDate.setDate(startDate.getDate() - 1); - - const { data, isLoading } = useCoolerSnapshot(startDate); +/** + * Get the latest Cooler Loans snapshot + * + * @returns The latest snapshot, or undefined if there is no snapshot + */ +export const useCurrentCoolerSnapshot = () => { + const { data, isLoading } = useGetCurrentSnapshot(); + + // Add a timestamp field to the snapshot, and cache the result + const cachedData: Snapshot | undefined = useMemo(() => { + if (!data || !data.record) { + return undefined; + } - const latestSnapshot = data ? data[data.length - 1] : undefined; + return { + ...data.record, + timestamp: new Date(data.record.snapshotDate).getTime(), + }; + }, [data]); return { - latestSnapshot, + latestSnapshot: cachedData, isLoading, }; }; From ea2dc459095ecae57d5cb0b930cf77f57d310d6c Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Fri, 18 Oct 2024 12:17:08 +0400 Subject: [PATCH 06/10] Clarify collateral income metric --- src/views/Lending/Cooler/dashboard/Metrics.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Lending/Cooler/dashboard/Metrics.tsx b/src/views/Lending/Cooler/dashboard/Metrics.tsx index 8024ce978..1beb0428b 100644 --- a/src/views/Lending/Cooler/dashboard/Metrics.tsx +++ b/src/views/Lending/Cooler/dashboard/Metrics.tsx @@ -60,7 +60,7 @@ export const CumulativeCollateralIncome = ({ startDate }: { startDate?: Date }) label="Income From Defaults" metric={cumulativeCollateralIncome !== undefined ? formatCurrency(cumulativeCollateralIncome, 0, "USD") : ""} isLoading={cumulativeCollateralIncome === undefined} - tooltip="The value of collateral reclaimed due to defaults during the selected time period" + tooltip="The residual value of collateral (collateral value - outstanding principal) reclaimed due to defaults during the selected time period" /> ); }; From 9642626501831f0bebfe75ee9883abd139efc5dc Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Fri, 18 Oct 2024 12:50:17 +0400 Subject: [PATCH 07/10] Enforce use of yarn --- package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1d3ca9d20..702e7fb8f 100644 --- a/package.json +++ b/package.json @@ -179,5 +179,11 @@ "prettier --write" ] }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", + "engines": { + "node": ">=18", + "npm": "please-use-yarn", + "pnpm": "please-use-yarn", + "yarn": ">=1.22.0 <2.0.0" + } } From 5fb9905d1c3d9e2881cf5461a6fdfe315cbb3f88 Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Fri, 18 Oct 2024 12:50:42 +0400 Subject: [PATCH 08/10] Update OpenAPI schema --- src/generated/coolerLoans.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/generated/coolerLoans.ts b/src/generated/coolerLoans.ts index c2a76cd3d..19d354aa4 100644 --- a/src/generated/coolerLoans.ts +++ b/src/generated/coolerLoans.ts @@ -15,6 +15,14 @@ export type GetSnapshots200 = { records?: Snapshot[]; }; +export type GetSnapshotsOrderBy = (typeof GetSnapshotsOrderBy)[keyof typeof GetSnapshotsOrderBy]; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const GetSnapshotsOrderBy = { + ASC: "ASC", + DESC: "DESC", +} as const; + export type GetSnapshotsParams = { /** * The start date (YYYY-MM-DD) of the loan period @@ -27,7 +35,7 @@ export type GetSnapshotsParams = { /** * The order in which to return the snapshots. ASC or DESC */ - orderBy?: string; + orderBy?: GetSnapshotsOrderBy; }; /** @@ -108,9 +116,19 @@ export type Snapshot = { clearinghouses: SnapshotClearinghousesItem[]; /** Totals for the Clearinghouses at the time of the snapshot. */ clearinghouseTotals: SnapshotClearinghouseTotals; + /** Quantity of collateral reclaimed on this date. */ + collateralClaimedQuantity: number; + /** USD value of collateral claimed on this date. */ + collateralClaimedValue: number; /** Quantity of collateral deposited across all Coolers */ collateralDeposited: number; - /** Income from collateral reclaimed on this date. */ + /** USD value of the income recognised from claiming the loan's collateral. + +As collateral is returned to the borrower as they repay the loan principal, the collateral at any point in time covers the principal outstanding. + +The income is therefore calculated as: + +collateralValueAtClaim - principalOutstanding */ collateralIncome: number; /** Principal due for each expiry bucket. */ expiryBuckets: SnapshotExpiryBuckets; From 6c0d56460631171872b6d645295f7f3a3a7dcef2 Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Fri, 18 Oct 2024 15:28:32 +0400 Subject: [PATCH 09/10] Remove extraneous log --- src/helpers/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index 1e70ae65b..9ee68b87a 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -42,7 +42,6 @@ export function shorten(str: string) { } export function formatCurrency(c: number, precision = 0, currency = "USD") { - console.log(c, precision, currency); const formatted = new Intl.NumberFormat("en-US", { style: currency === "USD" || currency === "" ? "currency" : undefined, currency: "USD", From 3756790397a8cdf0febf2b12bfed0be5f36ee74d Mon Sep 17 00:00:00 2001 From: Jem <0x0xJem@gmail.com> Date: Fri, 18 Oct 2024 15:29:10 +0400 Subject: [PATCH 10/10] Update OpenAPI schema. Add "Max" option to Cooler Metrics date range. --- src/generated/coolerLoans.ts | 56 +++++++++++++++++++ .../Lending/Cooler/dashboard/Dashboard.tsx | 26 ++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/generated/coolerLoans.ts b/src/generated/coolerLoans.ts index 19d354aa4..7de9ad75f 100644 --- a/src/generated/coolerLoans.ts +++ b/src/generated/coolerLoans.ts @@ -7,6 +7,10 @@ import type { QueryFunction, QueryKey, UseQueryOptions, UseQueryResult } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; import { customHttpClient } from "src/views/Lending/Cooler/hooks/customHttpClient"; +export type GetEarliestSnapshot200 = { + record?: Snapshot; +}; + export type GetCurrentSnapshot200 = { record?: Snapshot; }; @@ -247,3 +251,55 @@ export const useGetCurrentSnapshot = < return query; }; + +/** + * @summary Retrieves the earliest Cooler Loans snapshot + */ +export const getEarliestSnapshot = (signal?: AbortSignal) => { + return customHttpClient({ url: `/snapshots/earliest`, method: "GET", signal }); +}; + +export const getGetEarliestSnapshotQueryKey = () => { + return [`/snapshots/earliest`] as const; +}; + +export const getGetEarliestSnapshotQueryOptions = < + TData = Awaited>, + TError = unknown, +>(options?: { + query?: UseQueryOptions>, TError, TData>; +}) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetEarliestSnapshotQueryKey(); + + const queryFn: QueryFunction>> = ({ signal }) => + getEarliestSnapshot(signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetEarliestSnapshotQueryResult = NonNullable>>; +export type GetEarliestSnapshotQueryError = unknown; + +/** + * @summary Retrieves the earliest Cooler Loans snapshot + */ +export const useGetEarliestSnapshot = < + TData = Awaited>, + TError = unknown, +>(options?: { + query?: UseQueryOptions>, TError, TData>; +}): UseQueryResult & { queryKey: QueryKey } => { + const queryOptions = getGetEarliestSnapshotQueryOptions(options); + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey; + + return query; +}; diff --git a/src/views/Lending/Cooler/dashboard/Dashboard.tsx b/src/views/Lending/Cooler/dashboard/Dashboard.tsx index a2fdc8901..78450b16d 100644 --- a/src/views/Lending/Cooler/dashboard/Dashboard.tsx +++ b/src/views/Lending/Cooler/dashboard/Dashboard.tsx @@ -2,6 +2,7 @@ import { Grid, useMediaQuery, useTheme } from "@mui/material"; import { Paper, TabBar } from "@olympusdao/component-library"; import { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; +import { useGetEarliestSnapshot } from "src/generated/coolerLoans"; import { adjustDateByDays } from "src/helpers/DateHelper"; import { updateSearchParams } from "src/helpers/SearchParamsHelper"; import { IncomeGraph } from "src/views/Lending/Cooler/dashboard/IncomeGraph"; @@ -10,11 +11,29 @@ import { UtilisationGraph } from "src/views/Lending/Cooler/dashboard/Utilisation const PARAM_DAYS = "days"; const DEFAULT_DAYS = 30; +// Needs to be different from other values, otherwise two tabs will be active +const MAX_DAYS_UNSET = 181; export const CoolerDashboard = () => { const theme = useTheme(); const hidePaperSidePadding = useMediaQuery(theme.breakpoints.down("md")); + // Fetch the date of the earliest snapshot + const { data: earliestSnapshot } = useGetEarliestSnapshot(); + const [daysPriorMax, setDaysPriorMax] = useState(MAX_DAYS_UNSET); + useEffect(() => { + if (!earliestSnapshot || !earliestSnapshot.record) { + console.log(`No earliest snapshot found, setting max days prior to ${MAX_DAYS_UNSET}`); + setDaysPriorMax(MAX_DAYS_UNSET); + return; + } + + const earliestDate = new Date(earliestSnapshot.record.snapshotDate); + // Get the difference in days between the earliest date and today + const diffInDays = Math.floor((Date.now() - earliestDate.getTime()) / (1000 * 60 * 60 * 24)); + setDaysPriorMax(diffInDays); + }, [earliestSnapshot]); + /** * Date selection */ @@ -66,7 +85,7 @@ export const CoolerDashboard = () => { {/* Line one */} - + { to: `/lending/cooler?${getSearchParamsWithUpdatedRecordCount(180)}`, isActive: isActiveRecordCount(180), }, + { + label: "Max", + to: `/lending/cooler?${getSearchParamsWithUpdatedRecordCount(daysPriorMax)}`, + isActive: isActiveRecordCount(daysPriorMax), + }, ]} />