diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index b464f1579..67537d4e6 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -38,8 +38,18 @@ type Query { tokenPrices(where: tokenPriceFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): tokenPricePage! _meta: Meta - """Get total assets""" - totalAssets(days: queryInput_totalAssets_days = _7d): [query_totalAssets_items] + """ + Get historical Liquid Treasury (treasury without DAO tokens) from external providers (DefiLlama/Dune) + """ + getLiquidTreasury(days: queryInput_getLiquidTreasury_days = _365d, order: queryInput_getLiquidTreasury_order = asc): getLiquidTreasury_200_response + + """ + Get historical DAO Token Treasury value (governance token quantity × token price) + """ + getDaoTokenTreasury(days: queryInput_getDaoTokenTreasury_days = _365d, order: queryInput_getDaoTokenTreasury_order = asc): getDaoTokenTreasury_200_response + + """Get historical Total Treasury (liquid treasury + DAO token treasury)""" + getTotalTreasury(days: queryInput_getTotalTreasury_days = _365d, order: queryInput_getTotalTreasury_order = asc): getTotalTreasury_200_response """Get historical market data for a specific token""" historicalTokenData(skip: NonNegativeInt, limit: Float = 365): [query_historicalTokenData_items] @@ -1330,12 +1340,78 @@ input tokenPriceFilter { timestamp_lte: BigInt } -type query_totalAssets_items { - totalAssets: String! - date: String! +type getLiquidTreasury_200_response { + items: [query_getLiquidTreasury_items_items]! + + """Total number of items""" + totalCount: Float! +} + +type query_getLiquidTreasury_items_items { + """Treasury value in USD""" + value: Float! + + """Unix timestamp in milliseconds""" + date: Float! +} + +enum queryInput_getLiquidTreasury_days { + _7d + _30d + _90d + _180d + _365d +} + +enum queryInput_getLiquidTreasury_order { + asc + desc +} + +type getDaoTokenTreasury_200_response { + items: [query_getDaoTokenTreasury_items_items]! + + """Total number of items""" + totalCount: Float! +} + +type query_getDaoTokenTreasury_items_items { + """Treasury value in USD""" + value: Float! + + """Unix timestamp in milliseconds""" + date: Float! +} + +enum queryInput_getDaoTokenTreasury_days { + _7d + _30d + _90d + _180d + _365d +} + +enum queryInput_getDaoTokenTreasury_order { + asc + desc } -enum queryInput_totalAssets_days { +type getTotalTreasury_200_response { + items: [query_getTotalTreasury_items_items]! + + """Total number of items""" + totalCount: Float! +} + +type query_getTotalTreasury_items_items { + """Treasury value in USD""" + value: Float! + + """Unix timestamp in milliseconds""" + date: Float! +} + +enum queryInput_getTotalTreasury_days { _7d _30d _90d @@ -1343,6 +1419,11 @@ enum queryInput_totalAssets_days { _365d } +enum queryInput_getTotalTreasury_order { + asc + desc +} + type query_historicalTokenData_items { price: String! timestamp: Float! diff --git a/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx b/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx index 786e5898a..5b621c5e1 100644 --- a/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx +++ b/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx @@ -29,7 +29,7 @@ import { mockedAttackCostBarData } from "@/shared/constants/mocked-data/mocked-a import { useDaoTokenHistoricalData, useTopTokenHolderNonDao, - useTreasuryAssetNonDaoToken, + useTreasury, useVetoCouncilVotingPower, } from "@/features/attack-profitability/hooks"; import daoConfigByDaoId from "@/shared/dao-config"; @@ -71,10 +71,8 @@ export const AttackCostBarChart = ({ const selectedDaoId = daoId.toUpperCase() as DaoIdEnum; const timeInterval = TimeInterval.NINETY_DAYS; - const liquidTreasury = useTreasuryAssetNonDaoToken( - selectedDaoId, - timeInterval, - ); + const { data: liquidTreasuryData, loading: liquidTreasuryLoading } = + useTreasury(selectedDaoId, "liquid", TimeInterval.SEVEN_DAYS); const delegatedSupply = useDelegatedSupply(selectedDaoId, timeInterval); const activeSupply = useActiveSupply(selectedDaoId, timeInterval); const averageTurnout = useAverageTurnout(selectedDaoId, timeInterval); @@ -104,7 +102,7 @@ export const AttackCostBarChart = ({ const { isMobile } = useScreenSize(); const isLoading = - liquidTreasury.loading || + liquidTreasuryLoading || delegatedSupply.isLoading || activeSupply.isLoading || averageTurnout.isLoading || @@ -152,10 +150,10 @@ export const AttackCostBarChart = ({ id: "liquidTreasury", name: "Liquid Treasury", type: BarChartEnum.REGULAR, - value: Number(liquidTreasury.data?.[0]?.totalAssets || 0), + value: Number(liquidTreasuryData?.[0]?.value || 0), customColor: "#EC762EFF", displayValue: - Number(liquidTreasury.data?.[0]?.totalAssets || 0) > 10000 + Number(liquidTreasuryData?.[0]?.value || 0) > 10000 ? undefined : "<$10,000", }, @@ -226,7 +224,7 @@ export const AttackCostBarChart = ({ mocked, daoTokenPriceHistoricalData, valueMode, - liquidTreasury.data, + liquidTreasuryData, delegatedSupply.data?.currentDelegatedSupply, activeSupply.data?.activeSupply, averageTurnout.data?.currentAverageTurnout, diff --git a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx index dc6cfdeb9..0f83f8820 100644 --- a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx +++ b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx @@ -27,18 +27,14 @@ import { ResearchPendingChartBlur } from "@/shared/components/charts/ResearchPen import { AttackProfitabilityCustomTooltip } from "@/features/attack-profitability/components"; import { useDaoTokenHistoricalData, - useTreasuryAssetNonDaoToken, + useTreasury, } from "@/features/attack-profitability/hooks"; import { cn, formatNumberUserReadable, timestampToReadableDate, } from "@/shared/utils"; -import { - normalizeDataset, - normalizeDatasetTreasuryNonDaoToken, - normalizeDatasetAllTreasury, -} from "@/features/attack-profitability/utils"; +import { normalizeDataset } from "@/features/attack-profitability/utils"; import daoConfigByDaoId from "@/shared/dao-config"; import { AnticaptureWatermark } from "@/shared/components/icons/AnticaptureWatermark"; import { Data } from "react-csv/lib/core"; @@ -62,14 +58,20 @@ export const MultilineChartAttackProfitability = ({ const { data: daoData } = useDaoData(daoEnum); const daoConfig = daoConfigByDaoId[daoEnum]; - const { data: treasuryAssetNonDAOToken = [] } = useTreasuryAssetNonDaoToken( + const { data: liquidTreasuryData } = useTreasury( daoEnum, - days, + "liquid", + days as TimeInterval, + ); + const { data: totalTreasuryData } = useTreasury( + daoEnum, + "total", + days as TimeInterval, ); const { data: daoTokenPriceHistoricalData } = useDaoTokenHistoricalData({ daoId: daoEnum, - limit: Number(days.split("d")[0]) - 7, + limit: Number(days.split("d")[0]), }); const { data: timeSeriesData } = useTimeSeriesData( @@ -108,9 +110,7 @@ export const MultilineChartAttackProfitability = ({ const chartData = useMemo(() => { let delegatedSupplyChart: DaoMetricsDayBucket[] = []; - let treasurySupplyChart: DaoMetricsDayBucket[] = []; if (timeSeriesData) { - treasurySupplyChart = timeSeriesData[MetricTypesEnum.TREASURY]; delegatedSupplyChart = timeSeriesData[MetricTypesEnum.DELEGATED_SUPPLY]; } @@ -119,17 +119,14 @@ export const MultilineChartAttackProfitability = ({ datasets = mockedAttackProfitabilityDatasets; } else { datasets = { - treasuryNonDAO: normalizeDatasetTreasuryNonDaoToken( - treasuryAssetNonDAOToken, - "treasuryNonDAO", - ).reverse(), - all: normalizeDatasetAllTreasury( - daoTokenPriceHistoricalData, - "all", - treasuryAssetNonDAOToken, - treasurySupplyChart, - daoConfig.decimals, - ), + treasuryNonDAO: liquidTreasuryData.map((item) => ({ + date: item.date, + treasuryNonDAO: item.value, + })), + all: totalTreasuryData.map((item) => ({ + date: item.date, + all: item.value, + })), quorum: daoConfig?.attackProfitability?.dynamicQuorum?.percentage ? normalizeDataset( daoTokenPriceHistoricalData, @@ -139,9 +136,11 @@ export const MultilineChartAttackProfitability = ({ ).map((datasetpoint) => ({ ...datasetpoint, quorum: - datasetpoint.quorum * - (daoConfig?.attackProfitability?.dynamicQuorum?.percentage ?? - 0), + datasetpoint.quorum !== null + ? datasetpoint.quorum * + (daoConfig?.attackProfitability?.dynamicQuorum + ?.percentage ?? 0) + : null, })) : quorumValue ? normalizeDataset( @@ -195,7 +194,8 @@ export const MultilineChartAttackProfitability = ({ mocked, quorumValue, daoTokenPriceHistoricalData, - treasuryAssetNonDAOToken, + liquidTreasuryData, + totalTreasuryData, timeSeriesData, daoConfig?.attackProfitability?.dynamicQuorum?.percentage, daoConfig.decimals, diff --git a/apps/dashboard/features/attack-profitability/hooks/index.ts b/apps/dashboard/features/attack-profitability/hooks/index.ts index f05ea3c6b..c65bd20ab 100644 --- a/apps/dashboard/features/attack-profitability/hooks/index.ts +++ b/apps/dashboard/features/attack-profitability/hooks/index.ts @@ -1,4 +1,4 @@ export * from "@/features/attack-profitability/hooks/useVetoCouncilVotingPower"; export * from "@/features/attack-profitability/hooks/useDaoTokenHistoricalData"; export * from "@/features/attack-profitability/hooks/useTopTokenHolderNonDao"; -export * from "@/features/attack-profitability/hooks/useTreasuryAssetNonDaoToken"; +export * from "@/features/attack-profitability/hooks/useTreasury"; diff --git a/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts new file mode 100644 index 000000000..75bfe7dc0 --- /dev/null +++ b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts @@ -0,0 +1,81 @@ +import useSWR from "swr"; +import axios from "axios"; +import { DaoIdEnum } from "@/shared/types/daos"; +import { TimeInterval } from "@/shared/types/enums/TimeInterval"; +import { BACKEND_ENDPOINT } from "@/shared/utils/server-utils"; + +export type TreasuryType = "liquid" | "dao-token" | "total"; + +export interface TreasuryDataPoint { + value: number; + date: number; +} + +export interface TreasuryResponse { + items: TreasuryDataPoint[]; + totalCount: number; +} + +const QUERY_NAME_MAP: Record = { + liquid: "getLiquidTreasury", + "dao-token": "getDaoTokenTreasury", + total: "getTotalTreasury", +}; + +const fetchTreasury = async ({ + daoId, + type = "total", + days = TimeInterval.ONE_YEAR, + order = "asc", +}: { + daoId: DaoIdEnum; + type?: TreasuryType; + days?: TimeInterval; + order?: "asc" | "desc"; +}): Promise => { + const queryName = QUERY_NAME_MAP[type]; + const daysParam = `_${days}`; + + const query = `query GetTreasury { + ${queryName}(days: ${daysParam}, order: ${order}) { + items { + date + value + } + totalCount + } + }`; + + const response: { + data: { data: { [key: string]: TreasuryResponse } }; + } = await axios.post( + `${BACKEND_ENDPOINT}`, + { query }, + { headers: { "anticapture-dao-id": daoId } }, + ); + + return response.data.data[queryName]; +}; + +export const useTreasury = ( + daoId: DaoIdEnum, + type: TreasuryType = "total", + days: TimeInterval = TimeInterval.ONE_YEAR, + order: "asc" | "desc" = "asc", +) => { + const { data, error, isValidating } = useSWR( + ["treasury", daoId, type, days, order], + () => fetchTreasury({ daoId, type, days, order }), + { + revalidateOnFocus: false, + shouldRetryOnError: false, + }, + ); + + return { + data: data?.items ?? [], + totalCount: data?.totalCount ?? 0, + loading: isValidating, + error, + }; +}; diff --git a/apps/dashboard/features/attack-profitability/hooks/useTreasuryAssetNonDaoToken.ts b/apps/dashboard/features/attack-profitability/hooks/useTreasuryAssetNonDaoToken.ts deleted file mode 100644 index 58e924eff..000000000 --- a/apps/dashboard/features/attack-profitability/hooks/useTreasuryAssetNonDaoToken.ts +++ /dev/null @@ -1,78 +0,0 @@ -import daoConfigByDaoId from "@/shared/dao-config"; -import { BACKEND_ENDPOINT } from "@/shared/utils/server-utils"; -import { DaoIdEnum } from "@/shared/types/daos"; -import useSWR, { SWRConfiguration } from "swr"; -import axios from "axios"; -export interface TreasuryAssetNonDaoToken { - date: string; - totalAssets: string; -} - -export const fetchTreasuryAssetNonDaoToken = async ({ - daoId, - days, -}: { - daoId: DaoIdEnum; - days: string; -}): Promise => { - const query = ` - query getTotalAssets { - totalAssets(days:_${days}){ - totalAssets - date - } -}`; - const response = await axios.post( - `${BACKEND_ENDPOINT}`, - { - query, - }, - { - headers: { - "anticapture-dao-id": daoId, - }, - }, - ); - const { totalAssets } = response.data.data as { - totalAssets: TreasuryAssetNonDaoToken[]; - }; - return totalAssets; -}; - -export const useTreasuryAssetNonDaoToken = ( - daoId: DaoIdEnum, - days: string, - config?: Partial>, -) => { - const key = daoId && days ? [`treasury-assets`, daoId, days] : null; - - const supportsLiquidTreasuryCall = - daoConfigByDaoId[daoId].attackProfitability?.supportsLiquidTreasuryCall; - const fixedTreasuryValue = - daoConfigByDaoId[daoId].attackProfitability?.liquidTreasury; - - // Only create a valid key if the DAO supports liquid treasury calls - const fetchKey = supportsLiquidTreasuryCall ? key : null; - - const { data, error, isValidating, mutate } = useSWR< - TreasuryAssetNonDaoToken[] - >(fetchKey, () => fetchTreasuryAssetNonDaoToken({ daoId, days }), { - revalidateOnFocus: false, - shouldRetryOnError: false, - ...config, - }); - - // Return default data (empty array) when liquid treasury is not supported - const finalData = supportsLiquidTreasuryCall - ? data - : fixedTreasuryValue - ? [fixedTreasuryValue] - : []; - - return { - data: finalData, - loading: isValidating, - error, - refetch: mutate, - }; -}; diff --git a/apps/dashboard/features/attack-profitability/utils/index.ts b/apps/dashboard/features/attack-profitability/utils/index.ts index 39f9c3188..67808531d 100644 --- a/apps/dashboard/features/attack-profitability/utils/index.ts +++ b/apps/dashboard/features/attack-profitability/utils/index.ts @@ -1,3 +1 @@ export * from "@/features/attack-profitability/utils/normalizeDataset"; -export * from "@/features/attack-profitability/utils/normalizeDatasetTreasuryNonDaoToken"; -export * from "@/features/attack-profitability/utils/normalizeDatasetAllTreasury"; diff --git a/apps/dashboard/features/attack-profitability/utils/normalizeDataset.ts b/apps/dashboard/features/attack-profitability/utils/normalizeDataset.ts index 4b4409349..9d1abc93f 100644 --- a/apps/dashboard/features/attack-profitability/utils/normalizeDataset.ts +++ b/apps/dashboard/features/attack-profitability/utils/normalizeDataset.ts @@ -30,10 +30,13 @@ export function normalizeDataset( {} as Record, ); - // Multiply using the exact timestamp's multiplier (may be undefined if missing) + // Multiply using the exact timestamp's multiplier return [...tokenPrices].reverse().map(({ timestamp, price }) => ({ date: timestamp, - [key]: Number(price) * (multipliersByTs[timestamp] ?? 0), + [key]: + multipliersByTs[timestamp] !== undefined + ? Number(price) * multipliersByTs[timestamp] + : null, })); } diff --git a/apps/dashboard/features/attack-profitability/utils/normalizeDatasetAllTreasury.ts b/apps/dashboard/features/attack-profitability/utils/normalizeDatasetAllTreasury.ts deleted file mode 100644 index 5a16ceb8d..000000000 --- a/apps/dashboard/features/attack-profitability/utils/normalizeDatasetAllTreasury.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { TreasuryAssetNonDaoToken } from "@/features/attack-profitability/hooks"; -import { - PriceEntry, - DaoMetricsDayBucket, - MultilineChartDataSetPoint, -} from "@/shared/dao-config/types"; -import { formatUnits } from "viem"; - -/** - * Calculates per-day total treasury value: - * total = (gov token treasury * gov token price) + non-DAO asset treasuries - * Uses exact-day values only (no forward-fill). Any continuity should come from upstream. - */ -export function normalizeDatasetAllTreasury( - tokenPrices: PriceEntry[], - key: string, - assetTreasuries: TreasuryAssetNonDaoToken[], - govTreasuries: DaoMetricsDayBucket[] = [], - decimals: number, // decimals for the governance token (used with formatUnits) -): MultilineChartDataSetPoint[] { - // Map: timestamp (ms) -> non-DAO assets value - const assetTreasuriesMap = assetTreasuries.map((item) => ({ - date: new Date(item.date).getTime(), - totalAssets: Number(item.totalAssets), - })); - - // Map: timestamp (ms) -> governance treasury amount (normalized by decimals for ERC20) - const govTreasuriesMap = govTreasuries.reduce( - (acc, item) => ({ - ...acc, - [Number(item.date) * 1000]: Number( - formatUnits(BigInt(item.close), decimals), - ), - }), - {} as Record, - ); - - let currentAssetIndex = 0; - return tokenPrices.map(({ timestamp, price }) => { - if ( - timestamp > assetTreasuriesMap[currentAssetIndex]?.date && - currentAssetIndex < assetTreasuriesMap.length - 1 - ) { - currentAssetIndex++; - } - - return { - date: timestamp, - [key]: - Number(price) * (govTreasuriesMap[timestamp] ?? 1) + - (assetTreasuriesMap[currentAssetIndex]?.totalAssets ?? 0), - }; - }); -} diff --git a/apps/dashboard/features/attack-profitability/utils/normalizeDatasetTreasuryNonDaoToken.ts b/apps/dashboard/features/attack-profitability/utils/normalizeDatasetTreasuryNonDaoToken.ts deleted file mode 100644 index eca8f468a..000000000 --- a/apps/dashboard/features/attack-profitability/utils/normalizeDatasetTreasuryNonDaoToken.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { MultilineChartDataSetPoint } from "@/shared/dao-config/types"; -import { TreasuryAssetNonDaoToken } from "@/features/attack-profitability/hooks"; - -export function normalizeDatasetTreasuryNonDaoToken( - tokenPrices: TreasuryAssetNonDaoToken[], - key: string, -): MultilineChartDataSetPoint[] { - return tokenPrices.map((item) => { - return { - date: new Date(item.date).getTime(), - [key]: Number(item.totalAssets), - }; - }); -} diff --git a/apps/dashboard/features/dao-overview/hooks/useDaoOverviewData.ts b/apps/dashboard/features/dao-overview/hooks/useDaoOverviewData.ts index 83ee51fa7..61120dedb 100644 --- a/apps/dashboard/features/dao-overview/hooks/useDaoOverviewData.ts +++ b/apps/dashboard/features/dao-overview/hooks/useDaoOverviewData.ts @@ -6,10 +6,8 @@ import { useAverageTurnout, useTokenData, } from "@/shared/hooks"; -import { useTreasuryAssetNonDaoToken } from "@/features/attack-profitability/hooks"; import { DaoIdEnum } from "@/shared/types/daos"; import { TimeInterval } from "@/shared/types/enums"; -import { useCompareTreasury } from "@/features/dao-overview/hooks/useCompareTreasury"; import { useTopDelegatesToPass } from "@/features/dao-overview/hooks/useTopDelegatesToPass"; import { useDaoTreasuryStats } from "@/features/dao-overview/hooks/useDaoTreasuryStats"; import { formatNumberUserReadable } from "@/shared/utils"; @@ -28,11 +26,6 @@ export const useDaoOverviewData = ({ const daoData = useDaoData(daoId); const activeSupply = useActiveSupply(daoId, TimeInterval.NINETY_DAYS); const averageTurnout = useAverageTurnout(daoId, TimeInterval.NINETY_DAYS); - const treasuryNonDao = useTreasuryAssetNonDaoToken( - daoId, - TimeInterval.NINETY_DAYS, - ); - const treasuryAll = useCompareTreasury(daoId, TimeInterval.NINETY_DAYS); const tokenData = useTokenData(daoId); const delegates = useGetDelegatesQuery({ @@ -70,10 +63,8 @@ export const useDaoOverviewData = ({ ); const treasuryStats = useDaoTreasuryStats({ - treasuryAll, - treasuryNonDao, + daoId, tokenData, - decimals, }); const topDelegatesToPass = useTopDelegatesToPass({ @@ -110,8 +101,6 @@ export const useDaoOverviewData = ({ activeSupply.isLoading || averageTurnout.isLoading || tokenData.isLoading || - treasuryNonDao.loading || - treasuryAll.loading || delegates.loading, }; }; diff --git a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts index 7bd90bfa6..8926d8d4e 100644 --- a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts +++ b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts @@ -1,40 +1,51 @@ -import { TreasuryAssetNonDaoToken } from "@/features/attack-profitability/hooks"; -import { TokenDataResponse } from "@/shared/hooks"; -import { CompareTreasury_200_Response } from "@anticapture/graphql-client"; import { useMemo } from "react"; -import { formatUnits } from "viem"; +import { useTreasury } from "@/features/attack-profitability/hooks/useTreasury"; +import { TokenDataResponse } from "@/shared/hooks"; +import { DaoIdEnum } from "@/shared/types/daos"; +import { TimeInterval } from "@/shared/types/enums/TimeInterval"; export const useDaoTreasuryStats = ({ - treasuryAll, - treasuryNonDao, + daoId, tokenData, - decimals, }: { - treasuryAll: { data?: CompareTreasury_200_Response | null }; - treasuryNonDao: { data?: TreasuryAssetNonDaoToken[] | null }; + daoId: DaoIdEnum; tokenData: { data?: TokenDataResponse | null }; - decimals: number; }) => { + // Use 7 days (minimum supported) with desc order to get most recent first + const { data: liquidTreasury } = useTreasury( + daoId, + "liquid", + TimeInterval.SEVEN_DAYS, + "desc", + ); + const { data: tokenTreasury } = useTreasury( + daoId, + "dao-token", + TimeInterval.SEVEN_DAYS, + "desc", + ); + const { data: allTreasury } = useTreasury( + daoId, + "total", + TimeInterval.SEVEN_DAYS, + "desc", + ); + return useMemo(() => { const lastPrice = Number(tokenData.data?.price) || 0; - const liquidTreasuryUSD = Number( - treasuryNonDao.data?.[0]?.totalAssets || 0, - ); - const daoTreasuryTokens = Number(treasuryAll.data?.currentTreasury || 0); - const govTreasuryUSD = - Number(formatUnits(BigInt(daoTreasuryTokens), decimals)) * lastPrice; + const liquidValue = liquidTreasury[0]?.value ?? 0; + const tokenValue = tokenTreasury[0]?.value ?? 0; + const totalValue = allTreasury[0]?.value ?? 0; - const liquidTreasuryAllPercent = govTreasuryUSD - ? Math.round( - (govTreasuryUSD / (govTreasuryUSD + liquidTreasuryUSD)) * 100, - ).toString() + const liquidTreasuryAllPercent = totalValue + ? Math.round((tokenValue / totalValue) * 100).toString() : "0"; return { lastPrice, - liquidTreasuryNonDaoValue: liquidTreasuryUSD, - liquidTreasuryAllValue: govTreasuryUSD, + liquidTreasuryNonDaoValue: liquidValue, + liquidTreasuryAllValue: tokenValue, liquidTreasuryAllPercent, }; - }, [tokenData, treasuryAll.data, treasuryNonDao.data, decimals]); + }, [liquidTreasury, tokenTreasury, allTreasury, tokenData]); }; diff --git a/apps/dashboard/shared/dao-config/comp.ts b/apps/dashboard/shared/dao-config/comp.ts index 42ba3dabb..84bf85b01 100644 --- a/apps/dashboard/shared/dao-config/comp.ts +++ b/apps/dashboard/shared/dao-config/comp.ts @@ -42,11 +42,6 @@ export const COMP: DaoConfiguration = { }, attackProfitability: { riskLevel: RiskLevel.HIGH, - supportsLiquidTreasuryCall: false, - liquidTreasury: { - date: (Date.now() / 1000).toString(), - totalAssets: "150000000.00000000", - }, attackCostBarChart: { // 41 addresses -> You can check all the addresses in this dashboard: https://encurtador.com.br/kDHn Timelock: "0x6d903f6003cca6255D85CcA4D3B5E5146dC33925", diff --git a/apps/dashboard/shared/dao-config/ens.ts b/apps/dashboard/shared/dao-config/ens.ts index 277839e0b..33f375a7f 100644 --- a/apps/dashboard/shared/dao-config/ens.ts +++ b/apps/dashboard/shared/dao-config/ens.ts @@ -60,7 +60,6 @@ export const ENS: DaoConfiguration = { }, attackProfitability: { riskLevel: RiskLevel.HIGH, - supportsLiquidTreasuryCall: true, attackCostBarChart: { ENSTokenTimelock: "0xd7A029Db2585553978190dB5E85eC724Aa4dF23f", ENSDaoWallet: "0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7", diff --git a/apps/dashboard/shared/dao-config/gtc.ts b/apps/dashboard/shared/dao-config/gtc.ts index 6a6b88ec5..c68c25057 100644 --- a/apps/dashboard/shared/dao-config/gtc.ts +++ b/apps/dashboard/shared/dao-config/gtc.ts @@ -40,15 +40,12 @@ export const GTC: DaoConfiguration = { proposalThreshold: "150k GTC", }, }, - // attackProfitability: { - // riskLevel: RiskLevel.HIGH, - // supportsLiquidTreasuryCall: false, - // attackCostBarChart: { - // OptimismTimelock: "", - // OptimismTokenDistributor: "", - // OptimismUniv3Uni: "", - // }, - // }, + attackProfitability: { + riskLevel: RiskLevel.HIGH, + attackCostBarChart: { + GitcoinTimelock: "0x57a8865cfB1eCEf7253c27da6B4BC3dAEE5Be518", + }, + }, riskAnalysis: true, governanceImplementation: { // Fields are sorted alphabetically by GovernanceImplementationEnum for readability diff --git a/apps/dashboard/shared/dao-config/nouns.ts b/apps/dashboard/shared/dao-config/nouns.ts index 691c9ede7..eeff0f5d8 100644 --- a/apps/dashboard/shared/dao-config/nouns.ts +++ b/apps/dashboard/shared/dao-config/nouns.ts @@ -40,7 +40,6 @@ export const NOUNS: DaoConfiguration = { }, attackProfitability: { riskLevel: RiskLevel.LOW, - supportsLiquidTreasuryCall: false, attackCostBarChart: { NounsTimelock: "0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71", PayerContract: "0xd97Bcd9f47cEe35c0a9ec1dc40C1269afc9E8E1D", diff --git a/apps/dashboard/shared/dao-config/obol.ts b/apps/dashboard/shared/dao-config/obol.ts index f4e0069cf..4ea7ac9e8 100644 --- a/apps/dashboard/shared/dao-config/obol.ts +++ b/apps/dashboard/shared/dao-config/obol.ts @@ -42,7 +42,6 @@ export const OBOL: DaoConfiguration = { }, attackProfitability: { riskLevel: RiskLevel.LOW, - supportsLiquidTreasuryCall: false, attackCostBarChart: {}, }, riskAnalysis: true, diff --git a/apps/dashboard/shared/dao-config/op.ts b/apps/dashboard/shared/dao-config/op.ts index e9c67c8df..71574a9d0 100644 --- a/apps/dashboard/shared/dao-config/op.ts +++ b/apps/dashboard/shared/dao-config/op.ts @@ -43,7 +43,6 @@ export const OP: DaoConfiguration = { }, attackProfitability: { riskLevel: RiskLevel.LOW, - supportsLiquidTreasuryCall: false, attackCostBarChart: { OptimismTimelock: "", OptimismTokenDistributor: "", diff --git a/apps/dashboard/shared/dao-config/scr.ts b/apps/dashboard/shared/dao-config/scr.ts index 854fe3c1b..f32c152cf 100644 --- a/apps/dashboard/shared/dao-config/scr.ts +++ b/apps/dashboard/shared/dao-config/scr.ts @@ -39,7 +39,6 @@ export const SCR: DaoConfiguration = { }, attackProfitability: { riskLevel: RiskLevel.LOW, - supportsLiquidTreasuryCall: false, attackCostBarChart: {}, }, riskAnalysis: true, diff --git a/apps/dashboard/shared/dao-config/types.ts b/apps/dashboard/shared/dao-config/types.ts index a713ccda4..bdbd56252 100644 --- a/apps/dashboard/shared/dao-config/types.ts +++ b/apps/dashboard/shared/dao-config/types.ts @@ -4,7 +4,6 @@ import { DaoIdEnum } from "@/shared/types/daos"; import { MetricTypesEnum } from "@/shared/types/enums/metric-type"; import { RiskLevel, GovernanceImplementationEnum } from "@/shared/types/enums"; import { DaoIconProps } from "@/shared/components/icons/types"; -import { TreasuryAssetNonDaoToken } from "@/features/attack-profitability/hooks"; export type DaoMetricsDayBucket = { date: string; @@ -24,7 +23,7 @@ export type PriceEntry = { timestamp: number; price: string }; export interface MultilineChartDataSetPoint { date: number; - [key: string]: number; + [key: string]: number | null; } export interface ChartDataSetPoint { @@ -185,8 +184,6 @@ export interface DaoAddresses { export interface AttackProfitabilityConfig { riskLevel?: RiskLevel; - liquidTreasury?: TreasuryAssetNonDaoToken; // FIXME(DEV-161): Remove once treasury fetching from Octav is operational - supportsLiquidTreasuryCall?: boolean; attackCostBarChart: DaoAddresses[DaoIdEnum]; dynamicQuorum?: { percentage: number; diff --git a/apps/dashboard/shared/dao-config/uni.ts b/apps/dashboard/shared/dao-config/uni.ts index 117a28c4b..25732ec25 100644 --- a/apps/dashboard/shared/dao-config/uni.ts +++ b/apps/dashboard/shared/dao-config/uni.ts @@ -194,7 +194,6 @@ export const UNI: DaoConfiguration = { }, attackProfitability: { riskLevel: RiskLevel.LOW, - supportsLiquidTreasuryCall: false, attackCostBarChart: { UniTimelock: "0x1a9C8182C09F50C8318d769245beA52c32BE35BC", UniTokenDistributor: "0x090D4613473dEE047c3f2706764f49E0821D256e", diff --git a/apps/indexer/.env.example b/apps/indexer/.env.example index 10ee84370..dd2de7e63 100644 --- a/apps/indexer/.env.example +++ b/apps/indexer/.env.example @@ -3,6 +3,11 @@ DATABASE_URL=postgresql://postgres:admin@localhost:5432/postgres DAO_ID=ENS CHAIN_ID=31337 +# Treasury Provider +DEFILLAMA_API_URL=https://api.llama.fi/treasury +# Examples: ENS, uniswap, optimism-foundation, arbitrum-dao +TREASURY_PROVIDER_PROTOCOL_ID=ENS + # Petition COINGECKO_API_KEY= DUNE_API_KEY= diff --git a/apps/indexer/package.json b/apps/indexer/package.json index 7c05324e5..1cf856696 100644 --- a/apps/indexer/package.json +++ b/apps/indexer/package.json @@ -29,7 +29,7 @@ "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^20.16.5", - "@types/pg": "^8.11.10", + "@types/pg": "^8.15.6", "dotenv": "^16.5.0", "eslint": "^8.53.0", "eslint-config-ponder": "^0.5.6", diff --git a/apps/indexer/src/api/controllers/assets/index.ts b/apps/indexer/src/api/controllers/assets/index.ts deleted file mode 100644 index 6980a6492..000000000 --- a/apps/indexer/src/api/controllers/assets/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi"; - -import { DaysOpts } from "@/lib/enums"; -import { DuneResponse } from "@/api/services/"; - -interface AssetsClient { - fetchTotalAssets(size: number): Promise; -} - -export function totalAssets(app: Hono, service: AssetsClient) { - app.openapi( - createRoute({ - method: "get", - operationId: "totalAssets", - path: "/total-assets", - summary: "Get total assets", - description: "Get total assets", - tags: ["assets"], - request: { - query: z.object({ - // TODO add sort by date and remove sorting from apps/dashboard/features/attack-profitability/utils/normalizeDataset.ts:19 - days: z - .enum(DaysOpts) - .default("7d") - .transform((val) => parseInt(val.replace("d", ""))), - }), - }, - responses: { - 200: { - description: "Returns the total assets by day", - content: { - "application/json": { - schema: z.array( - z.object({ - totalAssets: z.string(), - date: z.string(), - }), - ), - }, - }, - }, - }, - }), - async (context) => { - const { days } = context.req.valid("query"); - const data = await service.fetchTotalAssets(days); - return context.json(data.result.rows); - }, - ); -} diff --git a/apps/indexer/src/api/controllers/index.ts b/apps/indexer/src/api/controllers/index.ts index 72c486a84..b84689940 100644 --- a/apps/indexer/src/api/controllers/index.ts +++ b/apps/indexer/src/api/controllers/index.ts @@ -1,5 +1,4 @@ export * from "./account-balance"; -export * from "./assets"; export * from "./delegation-percentage"; export * from "./governance-activity"; export * from "./last-update"; @@ -8,3 +7,4 @@ export * from "./token"; export * from "./transactions"; export * from "./voting-power"; export * from "./dao"; +export * from "./treasury"; diff --git a/apps/indexer/src/api/controllers/treasury/index.ts b/apps/indexer/src/api/controllers/treasury/index.ts new file mode 100644 index 000000000..b2ab323bd --- /dev/null +++ b/apps/indexer/src/api/controllers/treasury/index.ts @@ -0,0 +1,117 @@ +import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; + +import { TreasuryService } from "@/api/services/treasury"; +import { + TreasuryResponseSchema, + TreasuryQuerySchema, +} from "@/api/mappers/treasury"; +import { CONTRACT_ADDRESSES } from "@/lib/constants"; +import { env } from "@/env"; + +export function treasury(app: Hono, treasuryService: TreasuryService) { + app.openapi( + createRoute({ + method: "get", + operationId: "getLiquidTreasury", + path: "/treasury/liquid", + summary: "Get liquid treasury data", + description: + "Get historical Liquid Treasury (treasury without DAO tokens) from external providers (DefiLlama/Dune)", + tags: ["treasury"], + request: { + query: TreasuryQuerySchema, + }, + responses: { + 200: { + description: "Returns liquid treasury history", + content: { + "application/json": { + schema: TreasuryResponseSchema, + }, + }, + }, + }, + }), + async (context) => { + const { days, order } = context.req.valid("query"); + const result = await treasuryService.getLiquidTreasury(days, order); + return context.json(result); + }, + ); + + app.openapi( + createRoute({ + method: "get", + operationId: "getDaoTokenTreasury", + path: "/treasury/dao-token", + summary: "Get DAO token treasury data", + description: + "Get historical DAO Token Treasury value (governance token quantity × token price)", + tags: ["treasury"], + request: { + query: TreasuryQuerySchema, + }, + responses: { + 200: { + description: "Returns DAO token treasury history", + content: { + "application/json": { + schema: TreasuryResponseSchema, + }, + }, + }, + 400: { + description: "Invalid DAO ID or missing configuration", + }, + }, + }), + async (context) => { + const { days, order } = context.req.valid("query"); + const decimals = CONTRACT_ADDRESSES[env.DAO_ID].token.decimals; + const result = await treasuryService.getTokenTreasury( + days, + order, + decimals, + ); + return context.json(result); + }, + ); + + app.openapi( + createRoute({ + method: "get", + operationId: "getTotalTreasury", + path: "/treasury/total", + summary: "Get total treasury data", + description: + "Get historical Total Treasury (liquid treasury + DAO token treasury)", + tags: ["treasury"], + request: { + query: TreasuryQuerySchema, + }, + responses: { + 200: { + description: "Returns total treasury history", + content: { + "application/json": { + schema: TreasuryResponseSchema, + }, + }, + }, + 400: { + description: "Invalid DAO ID or missing configuration", + }, + }, + }), + async (context) => { + const { days, order } = context.req.valid("query"); + const decimals = CONTRACT_ADDRESSES[env.DAO_ID].token.decimals; + const result = await treasuryService.getTotalTreasury( + days, + order, + decimals, + ); + return context.json(result); + }, + ); +} diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index f31d2c557..0efffc93e 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -16,13 +16,13 @@ import { transactions, proposals, lastUpdate, - totalAssets, votingPower, delegationPercentage, votingPowerVariations, accountBalanceVariations, dao, accountInteractions, + treasury, } from "@/api/controllers"; import { docs } from "@/api/docs"; import { env } from "@/env"; @@ -38,6 +38,7 @@ import { DrizzleProposalsActivityRepository, NounsVotingPowerRepository, AccountInteractionsRepository, + TreasuryRepository, } from "@/api/repositories"; import { errorHandler } from "@/api/middlewares"; import { getClient } from "@/lib/client"; @@ -48,7 +49,6 @@ import { VotingPowerService, TransactionsService, ProposalsService, - DuneService, CoingeckoService, NFTPriceService, TokenService, @@ -58,6 +58,7 @@ import { } from "@/api/services"; import { CONTRACT_ADDRESSES } from "@/lib/constants"; import { DaoIdEnum } from "@/lib/enums"; +import { createTreasuryService } from "./services/treasury/treasury-provider-factory"; const app = new Hono({ defaultHook: (result, c) => { @@ -129,11 +130,6 @@ const accountBalanceService = new BalanceVariationsService( accountInteractionRepo, ); -if (env.DUNE_API_URL && env.DUNE_API_KEY) { - const duneClient = new DuneService(env.DUNE_API_URL, env.DUNE_API_KEY); - totalAssets(app, duneClient); -} - const tokenPriceClient = env.DAO_ID === DaoIdEnum.NOUNS ? new NFTPriceService( @@ -147,6 +143,16 @@ const tokenPriceClient = env.DAO_ID, ); +const treasuryService = createTreasuryService( + new TreasuryRepository(), + tokenPriceClient, + env.DEFILLAMA_API_URL, + env.TREASURY_PROVIDER_PROTOCOL_ID, + env.DUNE_API_URL, + env.DUNE_API_KEY, +); +treasury(app, treasuryService); + tokenHistoricalData(app, tokenPriceClient); token( app, diff --git a/apps/indexer/src/api/mappers/index.ts b/apps/indexer/src/api/mappers/index.ts index a40f25bdd..0f6695787 100644 --- a/apps/indexer/src/api/mappers/index.ts +++ b/apps/indexer/src/api/mappers/index.ts @@ -6,3 +6,4 @@ export * from "./token"; export * from "./delegation-percentage"; export * from "./account-balance"; export * from "./dao"; +export * from "./treasury"; diff --git a/apps/indexer/src/api/mappers/treasury/index.ts b/apps/indexer/src/api/mappers/treasury/index.ts new file mode 100644 index 000000000..aeaeefec8 --- /dev/null +++ b/apps/indexer/src/api/mappers/treasury/index.ts @@ -0,0 +1,22 @@ +import { z } from "@hono/zod-openapi"; +import { DaysOpts } from "@/lib/enums"; + +export const TreasuryResponseSchema = z.object({ + items: z.array( + z.object({ + value: z.number().describe("Treasury value in USD"), + date: z.number().describe("Unix timestamp in milliseconds"), + }), + ), + totalCount: z.number().describe("Total number of items"), +}); + +export type TreasuryResponse = z.infer; + +export const TreasuryQuerySchema = z.object({ + days: z + .enum(DaysOpts) + .default("365d") + .transform((val) => parseInt(val.replace("d", ""))), + order: z.enum(["asc", "desc"]).optional().default("asc"), +}); diff --git a/apps/indexer/src/api/repositories/index.ts b/apps/indexer/src/api/repositories/index.ts index d6a6a116e..26e796a46 100644 --- a/apps/indexer/src/api/repositories/index.ts +++ b/apps/indexer/src/api/repositories/index.ts @@ -6,3 +6,4 @@ export * from "./transactions"; export * from "./voting-power"; export * from "./token"; export * from "./account-balance"; +export * from "./treasury/index"; diff --git a/apps/indexer/src/api/repositories/treasury/index.ts b/apps/indexer/src/api/repositories/treasury/index.ts new file mode 100644 index 000000000..d4bc4ae9f --- /dev/null +++ b/apps/indexer/src/api/repositories/treasury/index.ts @@ -0,0 +1,57 @@ +import { db } from "ponder:api"; +import { daoMetricsDayBucket } from "ponder:schema"; +import { and, eq, gte, lte, desc } from "ponder"; +import { MetricTypesEnum } from "@/lib/constants"; + +/** + * Repository for treasury-related database queries. + */ +export class TreasuryRepository { + /** + * Fetch DAO token quantities from daoMetricsDayBucket table + * @param cutoffTimestamp - The timestamp to filter the data + * @returns Map of timestamp (ms) to token quantity (bigint) + */ + async getTokenQuantities( + cutoffTimestamp: number, + ): Promise> { + const results = await db.query.daoMetricsDayBucket.findMany({ + columns: { + date: true, + close: true, + }, + where: and( + eq(daoMetricsDayBucket.metricType, MetricTypesEnum.TREASURY), + gte(daoMetricsDayBucket.date, BigInt(cutoffTimestamp)), + ), + orderBy: (fields, { asc }) => [asc(fields.date)], + }); + + const map = new Map(); + results.forEach((item) => { + const timestampMs = Number(item.date) * 1000; + map.set(timestampMs, item.close); + }); + + return map; + } + + /** + * Fetch the last token quantity before a given cutoff timestamp. + * Used to get initial value for forward-fill when no data exists in the requested range. + * @param cutoffTimestamp - The timestamp to search before + * @returns The last known token quantity or null if not found + */ + async getLastTokenQuantityBeforeDate( + cutoffTimestamp: number, + ): Promise { + const result = await db.query.daoMetricsDayBucket.findFirst({ + where: and( + eq(daoMetricsDayBucket.metricType, MetricTypesEnum.TREASURY), + lte(daoMetricsDayBucket.date, BigInt(cutoffTimestamp)), + ), + orderBy: desc(daoMetricsDayBucket.date), + }); + return result?.close ?? null; + } +} diff --git a/apps/indexer/src/api/services/coingecko/index.ts b/apps/indexer/src/api/services/coingecko/index.ts index 8a476d9cb..49e3ac6e5 100644 --- a/apps/indexer/src/api/services/coingecko/index.ts +++ b/apps/indexer/src/api/services/coingecko/index.ts @@ -11,6 +11,8 @@ import { import { DAYS_IN_YEAR } from "@/lib/constants"; import { DaoIdEnum } from "@/lib/enums"; import { TokenHistoricalPriceResponse } from "@/api/mappers"; +import { PriceProvider } from "@/api/services/treasury/types"; +import { truncateTimestampTimeMs } from "@/eventHandlers/shared"; const createCoingeckoTokenPriceDataSchema = ( tokenContractAddress: string, @@ -22,7 +24,7 @@ const createCoingeckoTokenPriceDataSchema = ( }), }); -export class CoingeckoService { +export class CoingeckoService implements PriceProvider { private readonly client: AxiosInstance; constructor( @@ -38,6 +40,18 @@ export class CoingeckoService { }); } + async getHistoricalPricesMap(days: number): Promise> { + const priceData = await this.getHistoricalTokenData(days); + + const priceMap = new Map(); + priceData.forEach((item) => { + const normalizedTimestamp = truncateTimestampTimeMs(item.timestamp); + priceMap.set(normalizedTimestamp, Number(item.price)); + }); + + return priceMap; + } + async getHistoricalTokenData( days: number = DAYS_IN_YEAR, ): Promise { diff --git a/apps/indexer/src/api/services/dune/index.ts b/apps/indexer/src/api/services/dune/index.ts index 4593a61f8..6261f8963 100644 --- a/apps/indexer/src/api/services/dune/index.ts +++ b/apps/indexer/src/api/services/dune/index.ts @@ -1,2 +1 @@ export * from "./service"; -export * from "./types"; diff --git a/apps/indexer/src/api/services/dune/service.ts b/apps/indexer/src/api/services/dune/service.ts index 08b5e3251..b978cc525 100644 --- a/apps/indexer/src/api/services/dune/service.ts +++ b/apps/indexer/src/api/services/dune/service.ts @@ -1,5 +1,5 @@ import { HTTPException } from "hono/http-exception"; -import { DuneResponse } from "./types"; +import { DuneResponse } from "../treasury/providers/dune-provider"; export class DuneService { constructor( @@ -7,7 +7,7 @@ export class DuneService { private readonly apiKey: string, ) {} - async fetchTotalAssets(size: number): Promise { + async fetchLiquidTreasury(size: number): Promise { try { const response = await fetch(this.apiUrl + `?limit=${size}`, { headers: { @@ -23,7 +23,7 @@ export class DuneService { return data as DuneResponse; } catch (error) { throw new HTTPException(503, { - message: "Failed to fetch total assets data", + message: "Failed to fetch liquid treasury data", cause: error, }); } diff --git a/apps/indexer/src/api/services/dune/types.ts b/apps/indexer/src/api/services/dune/types.ts deleted file mode 100644 index dcd284d2a..000000000 --- a/apps/indexer/src/api/services/dune/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface DuneResponse { - execution_id: string; - query_id: number; - is_execution_finished: boolean; - state: string; - submitted_at: string; - expires_at: string; - execution_started_at: string; - execution_ended_at: string; - result: { - rows: TotalAssetsByDay[]; - }; - next_uri: string; - next_offset: number; -} - -export interface TotalAssetsByDay { - totalAssets: string; - date: string; -} diff --git a/apps/indexer/src/api/services/nft-price/index.ts b/apps/indexer/src/api/services/nft-price/index.ts index ede3b8162..2d3067033 100644 --- a/apps/indexer/src/api/services/nft-price/index.ts +++ b/apps/indexer/src/api/services/nft-price/index.ts @@ -2,6 +2,15 @@ import { formatEther } from "viem"; import axios, { AxiosInstance } from "axios"; import { TokenHistoricalPriceResponse } from "@/api/mappers"; +import { PriceProvider } from "@/api/services/treasury/types"; +import { + truncateTimestampTimeMs, + calculateCutoffTimestamp, +} from "@/eventHandlers/shared"; +import { + forwardFill, + createDailyTimelineFromData, +} from "@/api/services/treasury/forward-fill"; // TODO: move to shared folder interface Repository { getHistoricalNFTPrice( @@ -11,7 +20,7 @@ interface Repository { getTokenPrice(): Promise; } -export class NFTPriceService { +export class NFTPriceService implements PriceProvider { private readonly client: AxiosInstance; constructor( @@ -50,12 +59,32 @@ export class NFTPriceService { .reverse() .slice(0, limit); - return auctionPrices.map(({ price, timestamp }, index) => ({ + const rawPrices = auctionPrices.map(({ price, timestamp }, index) => ({ price: ( Number(formatEther(BigInt(price))) * ethPriceResponse[index]![1] ).toFixed(2), timestamp: timestamp * 1000, })); + + // Create map with normalized timestamps (midnight UTC) + const priceMap = new Map(); + rawPrices.forEach((item) => { + const normalizedTs = truncateTimestampTimeMs(item.timestamp); + priceMap.set(normalizedTs, item.price); + }); + + // Create timeline and forward-fill gaps + const timeline = createDailyTimelineFromData([...priceMap.keys()]); + const filledPrices = forwardFill(timeline, priceMap); + + // Filter to only include last `limit` days + const cutoffMs = calculateCutoffTimestamp(limit) * 1000; + const filteredTimeline = timeline.filter((ts) => ts >= cutoffMs); + + return filteredTimeline.map((timestamp) => ({ + price: filledPrices.get(timestamp) ?? "0", + timestamp, + })); } async getTokenPrice(_: string, __: string): Promise { @@ -69,4 +98,16 @@ export class NFTPriceService { const ethPriceResponse = ethCurrentPrice.data.prices.reverse().slice(0, 1); return (nftEthValue * ethPriceResponse[0]![1]).toFixed(2); } + + async getHistoricalPricesMap(days: number): Promise> { + const priceData = await this.getHistoricalTokenData(days, 0); + + const priceMap = new Map(); + priceData.forEach((item) => { + const normalizedTimestamp = truncateTimestampTimeMs(item.timestamp); + priceMap.set(normalizedTimestamp, Number(item.price)); + }); + + return priceMap; + } } diff --git a/apps/indexer/src/api/services/treasury/forward-fill.ts b/apps/indexer/src/api/services/treasury/forward-fill.ts new file mode 100644 index 000000000..755cf535b --- /dev/null +++ b/apps/indexer/src/api/services/treasury/forward-fill.ts @@ -0,0 +1,58 @@ +/** + * Forward-fill interpolation utility for time-series data. + * + * Forward-fill means: use the last known value for any missing data points. + * This is commonly used in financial data where values persist until they change. + * + */ + +import { truncateTimestampTimeMs } from "@/eventHandlers/shared"; +import { ONE_DAY_MS } from "@/lib/enums"; + +/** + * Forward-fill sparse data across a master timeline. + * + * @param timeline - Sorted array of timestamps + * @param sparseData - Map of timestamp to value (may have gaps) + * @param initialValue - Optional initial value to use when no data exists before the first timeline entry + * @returns Map with values filled for all timeline timestamps + */ +export function forwardFill( + timeline: number[], + sparseData: Map, + initialValue?: T, +): Map { + const result = new Map(); + let lastKnownValue: T | undefined = initialValue; + + for (const timestamp of timeline) { + // Update last known value if we have data at this timestamp + if (sparseData.has(timestamp)) { + lastKnownValue = sparseData.get(timestamp); + } + + // Use last known value (or undefined if no data yet) + if (lastKnownValue !== undefined) { + result.set(timestamp, lastKnownValue); + } + } + + return result; +} + +/** + * Create daily timeline from first data point to today (midnight UTC) + */ +export function createDailyTimelineFromData(timestamps: number[]): number[] { + if (timestamps.length === 0) return []; + + const firstTimestamp = Math.min(...timestamps); + const todayMidnight = truncateTimestampTimeMs(Date.now()); + const totalDays = + Math.floor((todayMidnight - firstTimestamp) / ONE_DAY_MS) + 1; + + return Array.from( + { length: totalDays }, + (_, i) => firstTimestamp + i * ONE_DAY_MS, + ); +} diff --git a/apps/indexer/src/api/services/treasury/index.ts b/apps/indexer/src/api/services/treasury/index.ts new file mode 100644 index 000000000..c9b31e527 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/index.ts @@ -0,0 +1,5 @@ +export * from "./providers"; +export * from "./types"; +export * from "./treasury.service"; +export * from "../../repositories/treasury"; +export * from "./forward-fill"; diff --git "a/apps/indexer/src/api/services/treasury/providers/defillama\342\200\223provider.ts" "b/apps/indexer/src/api/services/treasury/providers/defillama\342\200\223provider.ts" new file mode 100644 index 000000000..d41fab178 --- /dev/null +++ "b/apps/indexer/src/api/services/treasury/providers/defillama\342\200\223provider.ts" @@ -0,0 +1,123 @@ +import { AxiosInstance } from "axios"; +import { TreasuryProvider } from "./treasury-provider.interface"; +import { LiquidTreasuryDataPoint } from "../types"; +import { truncateTimestampTime } from "@/eventHandlers/shared"; + +interface RawDefiLlamaResponse { + chainTvls: Record< + string, + { + tvl: Array<{ + date: number; // Unix timestamp in seconds + totalLiquidityUSD: number; + }>; + tokensInUsd?: Array; + tokens?: Array; + } + >; +} + +export class DefiLlamaProvider implements TreasuryProvider { + private readonly client: AxiosInstance; + private readonly providerDaoId: string; + + constructor(client: AxiosInstance, providerDaoId: string) { + this.client = client; + this.providerDaoId = providerDaoId; + } + + async fetchTreasury( + cutoffTimestamp: number, + ): Promise { + try { + const response = await this.client.get( + `/${this.providerDaoId}`, + ); + + return this.transformData(response.data, cutoffTimestamp); + } catch (error) { + console.error( + `[DefiLlamaProvider] Failed to fetch treasury data for ${this.providerDaoId}:`, + error, + ); + return []; + } + } + + /** + * Transforms DeFi Llama's raw response into our standardized format. + */ + private transformData( + rawData: RawDefiLlamaResponse, + cutoffTimestamp: number, + ): LiquidTreasuryDataPoint[] { + const { chainTvls } = rawData; + + // Map: chainKey → Map(dayTimestamp → latest dataPoint) + const chainsByDate = new Map< + string, + Map + >(); + + // For each chain, keep only the latest timestamp per date + for (const [chainKey, chainData] of Object.entries(chainTvls)) { + // Only process base chains and global OwnTokens + if (chainKey.includes("-")) { + continue; // Skip {Chain}-OwnTokens variants + } + + const dateMap = new Map(); + + for (const dataPoint of chainData.tvl || []) { + const dayTimestamp = truncateTimestampTime(dataPoint.date); + const existing = dateMap.get(dayTimestamp); + + // Keep only the latest timestamp for each date + if (!existing || dataPoint.date > existing.timestamp) { + dateMap.set(dayTimestamp, { + timestamp: dataPoint.date, + value: dataPoint.totalLiquidityUSD, + }); + } + } + + chainsByDate.set(chainKey, dateMap); + } + + // Aggregate across chains + const aggregatedByDate = new Map< + number, + { total: number; withoutOwnToken: number } + >(); + + for (const [chainKey, dateMap] of chainsByDate.entries()) { + const isGlobalOwnTokens = chainKey === "OwnTokens"; + + for (const [dayTimestamp, { value }] of dateMap.entries()) { + let entry = aggregatedByDate.get(dayTimestamp); + if (!entry) { + entry = { total: 0, withoutOwnToken: 0 }; + aggregatedByDate.set(dayTimestamp, entry); + } + + if (isGlobalOwnTokens) { + // OwnTokens → adds to total only + entry.total += value; + } else { + // Regular chain → adds to both + entry.total += value; + entry.withoutOwnToken += value; + } + } + } + + // Convert map to array, filter by cutoff, and format + return Array.from(aggregatedByDate.entries()) + .filter(([dayTimestamp]) => dayTimestamp >= cutoffTimestamp) + .map(([dayTimestamp, values]) => ({ + date: dayTimestamp, + liquidTreasury: values.withoutOwnToken, // Liquid Treasury + })) + .sort((a, b) => a.date - b.date); // Sort by timestamp ascending + } +} diff --git a/apps/indexer/src/api/services/treasury/providers/dune-provider.ts b/apps/indexer/src/api/services/treasury/providers/dune-provider.ts new file mode 100644 index 000000000..f78f806cf --- /dev/null +++ b/apps/indexer/src/api/services/treasury/providers/dune-provider.ts @@ -0,0 +1,69 @@ +import { HTTPException } from "hono/http-exception"; +import { LiquidTreasuryDataPoint } from "../types"; +import { TreasuryProvider } from "./treasury-provider.interface"; +import { AxiosInstance } from "axios"; + +export interface DuneResponse { + execution_id: string; + query_id: number; + is_execution_finished: boolean; + state: string; + submitted_at: string; + expires_at: string; + execution_started_at: string; + execution_ended_at: string; + result: { + rows: { + date: string; + totalAssets: number; + }[]; + }; + next_uri: string; + next_offset: number; +} + +export class DuneProvider implements TreasuryProvider { + constructor( + private readonly client: AxiosInstance, + private readonly apiKey: string, + ) {} + + async fetchTreasury( + cutoffTimestamp: number, + ): Promise { + try { + const response = await this.client.get("/", { + headers: { + "X-Dune-API-Key": this.apiKey, + }, + }); + + return this.transformData(response.data, cutoffTimestamp); + } catch (error) { + throw new HTTPException(503, { + message: "Failed to fetch total assets data", + cause: error, + }); + } + } + + private transformData( + data: DuneResponse, + cutoffTimestamp: number, + ): LiquidTreasuryDataPoint[] { + return data.result.rows + .map((row) => { + // Parse date string "YYYY-MM-DD" and convert to Unix timestamp (seconds) + const [year, month, day] = row.date.split("-").map(Number); + if (!year || !month || !day) { + throw new Error(`Invalid date string: ${row.date}`); + } + const timestamp = Math.floor(Date.UTC(year, month - 1, day) / 1000); + return { + date: timestamp, + liquidTreasury: row.totalAssets ?? 0, + }; + }) + .filter((item) => item.date >= cutoffTimestamp); + } +} diff --git a/apps/indexer/src/api/services/treasury/providers/index.ts b/apps/indexer/src/api/services/treasury/providers/index.ts new file mode 100644 index 000000000..73710af82 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/providers/index.ts @@ -0,0 +1,3 @@ +export * from "./treasury-provider.interface"; +export * from "./defillama–provider"; +export * from "./dune-provider"; diff --git a/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts b/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts new file mode 100644 index 000000000..ea342ee93 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts @@ -0,0 +1,11 @@ +import { LiquidTreasuryDataPoint } from "../types"; + +export interface TreasuryProvider { + /** + * Fetches historical treasury data from the configured provider. + * Provider-specific DAO ID is configured during instantiation. + * @param cutoffTimestamp - Only return data points with date >= this timestamp (Unix seconds) + * @returns Array of historical treasury data points, or empty array if provider is not configured + */ + fetchTreasury(cutoffTimestamp: number): Promise; +} diff --git a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts new file mode 100644 index 000000000..c5d746480 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -0,0 +1,42 @@ +import axios from "axios"; +import { DefiLlamaProvider } from "./providers/defillama–provider"; +import { DuneProvider } from "./providers/dune-provider"; +import { TreasuryService } from "./treasury.service"; +import { TreasuryRepository } from "@/api/repositories/treasury"; +import { PriceProvider } from "./types"; + +/** + * Creates a treasury service with optional liquid treasury provider. + * The service is created if at least the tokenPriceProvider is available, + * allowing dao-token and total treasury endpoints to work even without + * a liquid treasury provider (DefiLlama/Dune). + * + * @returns TreasuryService instance + */ +export function createTreasuryService( + repository: TreasuryRepository, + tokenPriceProvider: PriceProvider, + defiLlamaApiUrl?: string, + defiLlamaProtocolId?: string, + duneApiUrl?: string, + duneApiKey?: string, +): TreasuryService { + let liquidProvider: DefiLlamaProvider | DuneProvider | undefined; + if (defiLlamaProtocolId && defiLlamaApiUrl) { + const axiosClient = axios.create({ + baseURL: defiLlamaApiUrl, + }); + liquidProvider = new DefiLlamaProvider(axiosClient, defiLlamaProtocolId); + } else if (duneApiUrl && duneApiKey) { + const axiosClient = axios.create({ + baseURL: duneApiUrl, + }); + liquidProvider = new DuneProvider(axiosClient, duneApiKey); + } else { + console.warn( + "Liquid treasury provider not configured. Only dao-token treasury will be available.", + ); + } + + return new TreasuryService(repository, liquidProvider, tokenPriceProvider); +} diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts new file mode 100644 index 000000000..b328e2092 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -0,0 +1,156 @@ +import { formatUnits } from "viem"; +import { TreasuryProvider } from "./providers"; +import { PriceProvider } from "./types"; +import { TreasuryResponse } from "@/api/mappers/treasury"; +import { TreasuryRepository } from "../../repositories/treasury"; +import { forwardFill, createDailyTimelineFromData } from "./forward-fill"; +import { + calculateCutoffTimestamp, + truncateTimestampTimeMs, + normalizeMapTimestamps, +} from "@/eventHandlers/shared"; + +/** + * Treasury Service - Orchestrates treasury data retrieval and calculation. + * Responsibility: Coordinate between provider, repository, and business logic. + */ +export class TreasuryService { + constructor( + private repository: TreasuryRepository, + private provider?: TreasuryProvider, + private priceProvider?: PriceProvider, + ) {} + + /** + * Get liquid treasury only (from external providers) + */ + async getLiquidTreasury( + days: number, + order: "asc" | "desc", + ): Promise { + if (!this.provider) { + return { items: [], totalCount: 0 }; + } + + const cutoffTimestamp = calculateCutoffTimestamp(days); + const data = await this.provider.fetchTreasury(cutoffTimestamp); + + if (data.length === 0) { + return { items: [], totalCount: 0 }; + } + + // Convert to map with normalized timestamps (midnight UTC) + const liquidMap = new Map(); + data.forEach((item) => { + const timestampMs = truncateTimestampTimeMs(item.date * 1000); + liquidMap.set(timestampMs, item.liquidTreasury); + }); + + // Create timeline from first data point to today + const timeline = createDailyTimelineFromData([...liquidMap.keys()]); + + // Forward-fill to remove gaps + const filledValues = forwardFill(timeline, liquidMap); + + // Build response + const items = timeline + .map((timestamp) => ({ + date: timestamp, + value: filledValues.get(timestamp) ?? 0, + })) + .sort((a, b) => (order === "desc" ? b.date - a.date : a.date - b.date)); + + return { items, totalCount: items.length }; + } + + /** + * Get DAO token treasury only (token quantity × price) + */ + async getTokenTreasury( + days: number, + order: "asc" | "desc", + decimals: number, + ): Promise { + if (!this.priceProvider) { + return { items: [], totalCount: 0 }; + } + + const cutoffTimestamp = calculateCutoffTimestamp(days); + + // Fetch token quantities from DB and prices from CoinGecko + const [tokenQuantities, historicalPrices] = await Promise.all([ + this.repository.getTokenQuantities(cutoffTimestamp), + this.priceProvider.getHistoricalPricesMap(days), + ]); + + if (tokenQuantities.size === 0 && historicalPrices.size === 0) { + return { items: [], totalCount: 0 }; + } + + // Normalize all timestamps to midnight UTC + const normalizedQuantities = normalizeMapTimestamps(tokenQuantities); + const normalizedPrices = normalizeMapTimestamps(historicalPrices); + + // Create timeline from first data point to today + const timeline = createDailyTimelineFromData([ + ...normalizedQuantities.keys(), + ...normalizedPrices.keys(), + ]); + + // Get last known quantity before cutoff to use as initial value for forward-fill + const lastKnownQuantity = + await this.repository.getLastTokenQuantityBeforeDate(cutoffTimestamp); + + // Forward-fill both quantities and prices + const filledQuantities = forwardFill( + timeline, + normalizedQuantities, + lastKnownQuantity ?? undefined, + ); + const filledPrices = forwardFill(timeline, normalizedPrices); + + // Calculate token treasury values + const items = timeline + .map((timestamp) => { + const quantity = filledQuantities.get(timestamp) ?? 0n; + const price = filledPrices.get(timestamp) ?? 0; + const tokenAmount = Number(formatUnits(quantity, decimals)); + + return { date: timestamp, value: price * tokenAmount }; + }) + .sort((a, b) => (order === "desc" ? b.date - a.date : a.date - b.date)); + + return { items, totalCount: items.length }; + } + + /** + * Get total treasury (liquid + token) + */ + async getTotalTreasury( + days: number, + order: "asc" | "desc", + decimals: number, + ): Promise { + const [liquidResult, tokenResult] = await Promise.all([ + this.getLiquidTreasury(days, order), + this.getTokenTreasury(days, order, decimals), + ]); + + if (liquidResult.items.length === 0 && tokenResult.items.length === 0) { + return { items: [], totalCount: 0 }; + } + + // Use the timeline with more data points (liquid or token could be empty) + const baseItems = + liquidResult.items.length > 0 ? liquidResult.items : tokenResult.items; + + const items = baseItems.map((item, i) => ({ + date: item.date, + value: + (liquidResult.items[i]?.value ?? 0) + + (tokenResult.items[i]?.value ?? 0), + })); + + return { items, totalCount: items.length }; + } +} diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts new file mode 100644 index 000000000..24640fe3b --- /dev/null +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -0,0 +1,14 @@ +/** + * Interface to represent a treasury's data point + */ +export interface LiquidTreasuryDataPoint { + date: number; // Unix timestamp in seconds (start of day) + liquidTreasury: number; +} + +/** + * Interface for fetching historical token prices + */ +export interface PriceProvider { + getHistoricalPricesMap(days: number): Promise>; +} diff --git a/apps/indexer/src/env.ts b/apps/indexer/src/env.ts index ae25c056f..a8041c42e 100644 --- a/apps/indexer/src/env.ts +++ b/apps/indexer/src/env.ts @@ -11,6 +11,11 @@ const envSchema = z.object({ MAX_REQUESTS_PER_SECOND: z.coerce.number().default(20), DAO_ID: z.nativeEnum(DaoIdEnum), CHAIN_ID: z.coerce.number(), + + // Treasury provider configuration + DEFILLAMA_API_URL: z.string().optional(), + TREASURY_PROVIDER_PROTOCOL_ID: z.string().optional(), + DUNE_API_URL: z.string().optional(), DUNE_API_KEY: z.string().optional(), COINGECKO_API_URL: z.string(), diff --git a/apps/indexer/src/eventHandlers/shared.ts b/apps/indexer/src/eventHandlers/shared.ts index ab8fcf4d8..df9197c9c 100644 --- a/apps/indexer/src/eventHandlers/shared.ts +++ b/apps/indexer/src/eventHandlers/shared.ts @@ -3,6 +3,7 @@ import { Context } from "ponder:registry"; import { account, daoMetricsDayBucket, transaction } from "ponder:schema"; import { MetricTypesEnum } from "@/lib/constants"; +import { SECONDS_IN_DAY, ONE_DAY_MS } from "@/lib/enums"; import { delta, max, min } from "@/lib/utils"; export const ensureAccountExists = async ( @@ -42,7 +43,7 @@ export const storeDailyBucket = async ( await context.db .insert(daoMetricsDayBucket) .values({ - date: truncateTimestampTime(timestamp), + date: BigInt(truncateTimestampTime(Number(timestamp))), tokenId: tokenAddress, metricType, daoId, @@ -144,7 +145,36 @@ export const handleTransaction = async ( ); }; -export const truncateTimestampTime = (timestampSeconds: bigint): bigint => { - const SECONDS_IN_DAY = BigInt(86400); // 24 * 60 * 60 - return (timestampSeconds / SECONDS_IN_DAY) * SECONDS_IN_DAY; +/** + * Truncate timestamp (seconds) to midnight UTC + */ +export const truncateTimestampTime = (timestampSeconds: number): number => { + return Math.floor(timestampSeconds / SECONDS_IN_DAY) * SECONDS_IN_DAY; +}; + +/** + * Truncate timestamp (milliseconds) to midnight UTC + */ +export const truncateTimestampTimeMs = (timestampMs: number): number => { + return Math.floor(timestampMs / ONE_DAY_MS) * ONE_DAY_MS; +}; + +/** + * Calculate cutoff timestamp for filtering data by days + */ +export const calculateCutoffTimestamp = (days: number): number => { + return Math.floor(Date.now() / 1000) - days * SECONDS_IN_DAY; +}; + +/** + * Normalize all timestamps in a Map to midnight UTC + */ +export const normalizeMapTimestamps = ( + map: Map, +): Map => { + const normalized = new Map(); + map.forEach((value, ts) => { + normalized.set(truncateTimestampTimeMs(ts), value); + }); + return normalized; }; diff --git a/apps/indexer/src/indexer/nouns/governor.ts b/apps/indexer/src/indexer/nouns/governor.ts index dc503f2ae..c9d311728 100644 --- a/apps/indexer/src/indexer/nouns/governor.ts +++ b/apps/indexer/src/indexer/nouns/governor.ts @@ -69,7 +69,7 @@ export function GovernorIndexer(blockTime: number) { ponder.on(`NounsAuction:AuctionSettled`, async ({ event, context }) => { await context.db.insert(tokenPrice).values({ price: event.args.amount, - timestamp: truncateTimestampTime(event.block.timestamp), + timestamp: BigInt(truncateTimestampTime(Number(event.block.timestamp))), }); }); } diff --git a/apps/indexer/src/lib/enums.ts b/apps/indexer/src/lib/enums.ts index 64fd68c89..d8dafbb84 100644 --- a/apps/indexer/src/lib/enums.ts +++ b/apps/indexer/src/lib/enums.ts @@ -13,6 +13,7 @@ export enum DaoIdEnum { } export const SECONDS_IN_DAY = 24 * 60 * 60; +export const ONE_DAY_MS = SECONDS_IN_DAY * 1000; /** * Gets the current day timestamp (midnight UTC) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4537dad3d..fa278ba78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,10 +91,10 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + version: 29.7.0(@types/node@24.10.1) ts-jest: specifier: ^29.4.1 - version: 29.4.6(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.1))(typescript@5.9.3) tsx: specifier: ^4.19.4 version: 4.21.0 @@ -275,7 +275,7 @@ importers: version: 9.1.16(eslint@8.57.1)(storybook@9.1.16(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.7.4)(utf-8-validate@5.0.10)(vite@7.0.5(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 29.7.0(@types/node@20.19.25) playwright: specifier: ^1.52.0 version: 1.57.0 @@ -296,7 +296,7 @@ importers: version: 4.1.17 ts-jest: specifier: ^29.2.6 - version: 29.4.6(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) tw-animate-css: specifier: ^1.3.0 version: 1.4.0 @@ -344,7 +344,7 @@ importers: specifier: ^20.16.5 version: 20.19.25 "@types/pg": - specifier: ^8.11.10 + specifier: ^8.15.6 version: 8.15.6 dotenv: specifier: ^16.5.0 @@ -369,7 +369,7 @@ importers: version: 3.7.4 ts-jest: specifier: ^29.4.1 - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) @@ -423,7 +423,7 @@ importers: version: 4.9.6 forge-std: specifier: github:foundry-rs/forge-std - version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/26048ab55c64519ce21e032fdb49df1d5a2a7eb4 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/27ba11c86ac93d8d4a50437ae26621468fe63c20 packages: "@adobe/css-tools@4.4.3": @@ -10969,10 +10969,10 @@ packages: } engines: { node: ">=14" } - forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/26048ab55c64519ce21e032fdb49df1d5a2a7eb4: + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/27ba11c86ac93d8d4a50437ae26621468fe63c20: resolution: { - tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/26048ab55c64519ce21e032fdb49df1d5a2a7eb4, + tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/27ba11c86ac93d8d4a50437ae26621468fe63c20, } version: 1.12.0 @@ -13522,6 +13522,7 @@ packages: integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==, } engines: { node: ^18.18.0 || ^19.8.0 || >= 20.0.0 } + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: "@opentelemetry/api": ^1.1.0 @@ -18326,23 +18327,11 @@ snapshots: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 @@ -18358,12 +18347,6 @@ snapshots: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 @@ -18384,34 +18367,16 @@ snapshots: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 @@ -18427,34 +18392,16 @@ snapshots: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 @@ -18470,45 +18417,21 @@ snapshots: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 "@babel/helper-plugin-utils": 7.27.1 - "@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)": - dependencies: - "@babel/core": 7.28.5 - "@babel/helper-plugin-utils": 7.27.1 - optional: true - "@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)": dependencies: "@babel/core": 7.28.0 @@ -21505,41 +21428,6 @@ snapshots: - supports-color - ts-node - "@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))": - dependencies: - "@jest/console": 29.7.0 - "@jest/reporters": 29.7.0 - "@jest/test-result": 29.7.0 - "@jest/transform": 29.7.0 - "@jest/types": 29.6.3 - "@types/node": 20.19.25 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - "@jest/environment@29.7.0": dependencies: "@jest/fake-timers": 29.7.0 @@ -25182,20 +25070,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-jest@29.7.0(@babel/core@7.28.5): - dependencies: - "@babel/core": 7.28.5 - "@jest/transform": 29.7.0 - "@types/babel__core": 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.5) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - optional: true - babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.103.0(esbuild@0.25.8)): dependencies: "@babel/core": 7.28.0 @@ -25265,26 +25139,6 @@ snapshots: "@babel/plugin-syntax-private-property-in-object": 7.14.5(@babel/core@7.28.0) "@babel/plugin-syntax-top-level-await": 7.14.5(@babel/core@7.28.0) - babel-preset-current-node-syntax@1.1.0(@babel/core@7.28.5): - dependencies: - "@babel/core": 7.28.5 - "@babel/plugin-syntax-async-generators": 7.8.4(@babel/core@7.28.5) - "@babel/plugin-syntax-bigint": 7.8.3(@babel/core@7.28.5) - "@babel/plugin-syntax-class-properties": 7.12.13(@babel/core@7.28.5) - "@babel/plugin-syntax-class-static-block": 7.14.5(@babel/core@7.28.5) - "@babel/plugin-syntax-import-attributes": 7.27.1(@babel/core@7.28.5) - "@babel/plugin-syntax-import-meta": 7.10.4(@babel/core@7.28.5) - "@babel/plugin-syntax-json-strings": 7.8.3(@babel/core@7.28.5) - "@babel/plugin-syntax-logical-assignment-operators": 7.10.4(@babel/core@7.28.5) - "@babel/plugin-syntax-nullish-coalescing-operator": 7.8.3(@babel/core@7.28.5) - "@babel/plugin-syntax-numeric-separator": 7.10.4(@babel/core@7.28.5) - "@babel/plugin-syntax-object-rest-spread": 7.8.3(@babel/core@7.28.5) - "@babel/plugin-syntax-optional-catch-binding": 7.8.3(@babel/core@7.28.5) - "@babel/plugin-syntax-optional-chaining": 7.8.3(@babel/core@7.28.5) - "@babel/plugin-syntax-private-property-in-object": 7.14.5(@babel/core@7.28.5) - "@babel/plugin-syntax-top-level-await": 7.14.5(@babel/core@7.28.5) - optional: true - babel-preset-fbjs@3.4.0(@babel/core@7.28.5): dependencies: "@babel/core": 7.28.5 @@ -25324,13 +25178,6 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.0) - babel-preset-jest@29.6.3(@babel/core@7.28.5): - dependencies: - "@babel/core": 7.28.5 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.5) - optional: true - balanced-match@1.0.2: {} base-x@3.0.11: @@ -25865,13 +25712,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + create-jest@29.7.0(@types/node@24.10.1): dependencies: "@jest/types": 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -27084,7 +26931,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/26048ab55c64519ce21e032fdb49df1d5a2a7eb4: + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/27ba11c86ac93d8d4a50437ae26621468fe63c20: {} fork-ts-checker-webpack-plugin@8.0.0(typescript@5.9.3)(webpack@5.103.0(esbuild@0.25.8)): @@ -27922,7 +27769,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@20.19.25): dependencies: "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) "@jest/test-result": 29.7.0 @@ -27941,16 +27788,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: - "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) "@jest/test-result": 29.7.0 "@jest/types": 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -27960,38 +27807,26 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@24.10.1): dependencies: - "@babel/core": 7.28.0 - "@jest/test-sequencer": 29.7.0 + "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + "@jest/test-result": 29.7.0 "@jest/types": 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.0) chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 + create-jest: 29.7.0(@types/node@24.10.1) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - "@types/node": 20.19.25 - ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) + yargs: 17.7.2 transitivePeerDependencies: + - "@types/node" - babel-plugin-macros - supports-color + - ts-node - jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: "@babel/core": 7.28.0 "@jest/test-sequencer": 29.7.0 @@ -28017,12 +27852,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: "@types/node": 20.19.25 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) + ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@24.10.1): dependencies: "@babel/core": 7.28.0 "@jest/test-sequencer": 29.7.0 @@ -28048,7 +27883,6 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: "@types/node": 24.10.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -28274,6 +28108,18 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@29.7.0(@types/node@20.19.25): + dependencies: + "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + "@jest/types": 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.25) + transitivePeerDependencies: + - "@types/node" + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) @@ -28286,12 +28132,12 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest@29.7.0(@types/node@24.10.1): dependencies: - "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) "@jest/types": 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-cli: 29.7.0(@types/node@24.10.1) transitivePeerDependencies: - "@types/node" - babel-plugin-macros @@ -30879,12 +30725,12 @@ snapshots: esbuild: 0.25.8 jest-util: 29.7.0 - ts-jest@29.4.6(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest: 29.7.0(@types/node@20.19.25) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -30897,14 +30743,15 @@ snapshots: "@jest/transform": 29.7.0 "@jest/types": 29.6.3 babel-jest: 29.7.0(@babel/core@7.28.0) + esbuild: 0.25.8 jest-util: 29.7.0 - ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.1))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest: 29.7.0(@types/node@24.10.1) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -30913,11 +30760,10 @@ snapshots: typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - "@babel/core": 7.28.5 + "@babel/core": 7.28.0 "@jest/transform": 29.7.0 "@jest/types": 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - esbuild: 0.25.8 + babel-jest: 29.7.0(@babel/core@7.28.0) jest-util: 29.7.0 ts-log@2.2.7: {} @@ -30942,25 +30788,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): - dependencies: - "@cspotcode/source-map-support": 0.8.1 - "@tsconfig/node10": 1.0.11 - "@tsconfig/node12": 1.0.11 - "@tsconfig/node14": 1.0.3 - "@tsconfig/node16": 1.0.4 - "@types/node": 24.10.1 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optional: true - tsconfck@3.1.6(typescript@5.9.3): optionalDependencies: typescript: 5.9.3