From 22216ca2595a1baa684b8170a86a095344ac5ce9 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Wed, 17 Dec 2025 19:05:29 -0300 Subject: [PATCH 01/50] feat: add treasury multi-provider architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add TreasuryService with provider interface - Implement DefiLlama provider for treasury data - Implement Dune provider as fallback option - Add treasury-provider-factory for dynamic provider initialization - Support for liquid treasury historical data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../src/api/services/treasury/index.ts | 3 + .../defillama\342\200\223provider.ts" | 117 ++++++++++++++++++ .../treasury/providers/dune-provider.ts | 62 ++++++++++ .../api/services/treasury/providers/index.ts | 3 + .../providers/treasury-provider.interface.ts | 10 ++ .../treasury/treasury-provider-factory.ts | 37 ++++++ .../api/services/treasury/treasury.service.ts | 36 ++++++ .../src/api/services/treasury/types.ts | 4 + 8 files changed, 272 insertions(+) create mode 100644 apps/indexer/src/api/services/treasury/index.ts create mode 100644 "apps/indexer/src/api/services/treasury/providers/defillama\342\200\223provider.ts" create mode 100644 apps/indexer/src/api/services/treasury/providers/dune-provider.ts create mode 100644 apps/indexer/src/api/services/treasury/providers/index.ts create mode 100644 apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts create mode 100644 apps/indexer/src/api/services/treasury/treasury-provider-factory.ts create mode 100644 apps/indexer/src/api/services/treasury/treasury.service.ts create mode 100644 apps/indexer/src/api/services/treasury/types.ts 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..223ac25c5 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/index.ts @@ -0,0 +1,3 @@ +export * from "./providers"; +export * from "./types"; +export * from "./treasury.service"; 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..b73141f15 --- /dev/null +++ "b/apps/indexer/src/api/services/treasury/providers/defillama\342\200\223provider.ts" @@ -0,0 +1,117 @@ +import { AxiosInstance } from "axios"; +import { TreasuryProvider } from "./treasury-provider.interface"; +import { TreasuryDataPoint } 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(): Promise { + try { + const response = await this.client.get( + `/${this.providerDaoId}`, + ); + + return this.transformData(response.data); + } 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): TreasuryDataPoint[] { + 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(BigInt(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< + bigint, + { 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 and format + return Array.from(aggregatedByDate.entries()) + .map(([dayTimestamp, values]) => ({ + date: dayTimestamp, + liquidTreasury: values.withoutOwnToken, // Liquid Treasury + })) + .sort((a, b) => Number(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..15687f174 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/providers/dune-provider.ts @@ -0,0 +1,62 @@ +import { HTTPException } from "hono/http-exception"; +import { TreasuryDataPoint } 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(): Promise { + try { + const response = await this.client.get("/", { + headers: { + "X-Dune-API-Key": this.apiKey, + }, + }); + + return this.transformData(response.data); + } catch (error) { + throw new HTTPException(503, { + message: "Failed to fetch total assets data", + cause: error, + }); + } + } + + private transformData(data: DuneResponse): TreasuryDataPoint[] { + 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: BigInt(timestamp), + liquidTreasury: row.totalAssets ?? 0, + }; + }); + } +} 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..3bee0f67a --- /dev/null +++ b/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts @@ -0,0 +1,10 @@ +import { TreasuryDataPoint } from "../types"; + +export interface TreasuryProvider { + /** + * Fetches historical treasury data from the configured provider. + * Provider-specific DAO ID is configured during instantiation. + * @returns Array of historical treasury data points, or empty array if provider is not configured + */ + fetchTreasury(): 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..dd1283555 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -0,0 +1,37 @@ +import { env } from "@/env"; +import axios from "axios"; +import { DefiLlamaProvider } from "./providers/defillama–provider"; +import { DuneProvider } from "./providers/dune-provider"; +import { TreasuryService } from "./treasury.service"; +import { assets } from "@/api/controllers"; +import { OpenAPIHono as Hono } from "@hono/zod-openapi"; + +/** + * Creates a treasury provider and registers API routes + * Providers fetch data on-demand + * @param app - The Hono app instance + * @returns void - Returns early if no provider is configured + */ +export function createTreasuryProvider(app: Hono) { + if (env.TREASURY_PROVIDER_PROTOCOL_ID && env.DEFILLAMA_API_URL) { + const axiosClient = axios.create({ + baseURL: env.DEFILLAMA_API_URL, + }); + const defiLlamaProvider = new DefiLlamaProvider( + axiosClient, + env.TREASURY_PROVIDER_PROTOCOL_ID, + ); + const treasuryService = new TreasuryService(defiLlamaProvider); + assets(app, treasuryService); + } else if (env.DUNE_API_URL && env.DUNE_API_KEY) { + const axiosClient = axios.create({ + baseURL: env.DUNE_API_URL, + }); + const duneProvider = new DuneProvider(axiosClient, env.DUNE_API_KEY); + const treasuryService = new TreasuryService(duneProvider); + assets(app, treasuryService); + } else { + console.warn("Treasury provider not configured."); + return; + } +} 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..342bd4f53 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -0,0 +1,36 @@ +import { TreasuryProvider } from "./providers"; + +export interface TreasuryHistoryResponse { + date: number; // Unix timestamp in milliseconds + liquidTreasury: number; +} + +export class TreasuryService { + constructor(private provider: TreasuryProvider) {} + + async getTreasuryHistory( + days: number, + order: "asc" | "desc" = "asc", + ): Promise { + // Fetch from provider + const allData = await this.provider.fetchTreasury(); + + // Filter by days + const cutoffTimestamp = BigInt( + Math.floor(Date.now() / 1000) - days * 24 * 60 * 60, + ); + const filteredData = allData.filter((item) => item.date >= cutoffTimestamp); + + // Sort + const sortedData = + order === "desc" + ? filteredData.sort((a, b) => Number(b.date - a.date)) + : filteredData.sort((a, b) => Number(a.date - b.date)); + + // Transform to response format (seconds to milliseconds) + return sortedData.map((item) => ({ + date: Number(item.date) * 1000, + liquidTreasury: item.liquidTreasury, + })); + } +} 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..fe6f4ad3e --- /dev/null +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -0,0 +1,4 @@ +export interface TreasuryDataPoint { + date: bigint; // Unix timestamp in seconds (start of day) + liquidTreasury: number; +} From 76589f3f41fc59c5d31ee61a2499d10abff57b70 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Wed, 17 Dec 2025 19:05:42 -0300 Subject: [PATCH 02/50] refactor: update environment configuration for treasury MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DEFILLAMA_API_URL optional environment variable - Add TREASURY_PROVIDER_PROTOCOL_ID for provider configuration - Update .env.example with treasury provider examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/indexer/.env.example | 5 +++++ apps/indexer/src/env.ts | 5 +++++ 2 files changed, 10 insertions(+) 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/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(), From 2f6e0b965e46d3dee4dcd9eb2cbc56a4f69cb2b0 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Wed, 17 Dec 2025 19:05:53 -0300 Subject: [PATCH 03/50] refactor: migrate dune service to treasury providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename fetchTotalAssets to fetchLiquidTreasury - Move DuneResponse type to treasury providers - Update import to use treasury provider types - Remove obsolete types.ts file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/indexer/src/api/services/dune/index.ts | 1 - apps/indexer/src/api/services/dune/service.ts | 8 ++++---- apps/indexer/src/api/services/dune/types.ts | 20 ------------------- 3 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 apps/indexer/src/api/services/dune/types.ts 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 b4782434d..b978cc525 100644 --- a/apps/indexer/src/api/services/dune/service.ts +++ b/apps/indexer/src/api/services/dune/service.ts @@ -1,13 +1,13 @@ import { HTTPException } from "hono/http-exception"; -import { DuneResponse } from "./types"; +import { DuneResponse } from "../treasury/providers/dune-provider"; export class DuneService { constructor( private readonly apiUrl: string, 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; -} From 0283395d96cdb1949fd4774db22ffc77aa67576e Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Wed, 17 Dec 2025 19:06:04 -0300 Subject: [PATCH 04/50] refactor: integrate treasury provider factory in API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update assets controller to use TreasuryService - Replace DuneService instantiation with createTreasuryProvider factory - Change endpoint from /total-assets to /liquid-treasury - Add order parameter support for historical data sorting - Remove totalAssets import from controllers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../src/api/controllers/assets/index.ts | 32 ++++++++----------- apps/indexer/src/api/index.ts | 8 ++--- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/apps/indexer/src/api/controllers/assets/index.ts b/apps/indexer/src/api/controllers/assets/index.ts index 6980a6492..1c4e30d19 100644 --- a/apps/indexer/src/api/controllers/assets/index.ts +++ b/apps/indexer/src/api/controllers/assets/index.ts @@ -1,39 +1,35 @@ 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; -} +import { TreasuryService } from "@/api/services/treasury"; -export function totalAssets(app: Hono, service: AssetsClient) { +export function assets(app: Hono, treasuryService: TreasuryService) { app.openapi( createRoute({ method: "get", - operationId: "totalAssets", - path: "/total-assets", - summary: "Get total assets", - description: "Get total assets", + operationId: "liquidTreasury", + path: "/liquid-treasury", + summary: "Get liquid treasury data", + description: + "Get historical Liquid Treasury (treasury without DAO tokens) directly from provider", 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", ""))), + order: z.enum(["asc", "desc"]).optional().default("asc"), }), }, responses: { 200: { - description: "Returns the total assets by day", + description: "Returns the liquid treasury history", content: { "application/json": { schema: z.array( z.object({ - totalAssets: z.string(), - date: z.string(), + date: z.number().describe("Unix timestamp in milliseconds"), + liquidTreasury: z.number(), }), ), }, @@ -42,9 +38,9 @@ export function totalAssets(app: Hono, service: AssetsClient) { }, }), async (context) => { - const { days } = context.req.valid("query"); - const data = await service.fetchTotalAssets(days); - return context.json(data.result.rows); + const { days, order } = context.req.valid("query"); + const response = await treasuryService.getTreasuryHistory(days, order); + return context.json(response); }, ); } diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index f31d2c557..87913a925 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -16,7 +16,6 @@ import { transactions, proposals, lastUpdate, - totalAssets, votingPower, delegationPercentage, votingPowerVariations, @@ -48,7 +47,6 @@ import { VotingPowerService, TransactionsService, ProposalsService, - DuneService, CoingeckoService, NFTPriceService, TokenService, @@ -58,6 +56,7 @@ import { } from "@/api/services"; import { CONTRACT_ADDRESSES } from "@/lib/constants"; import { DaoIdEnum } from "@/lib/enums"; +import { createTreasuryProvider } from "./services/treasury/treasury-provider-factory"; const app = new Hono({ defaultHook: (result, c) => { @@ -129,10 +128,7 @@ 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); -} +createTreasuryProvider(app); const tokenPriceClient = env.DAO_ID === DaoIdEnum.NOUNS From 27c96a9ac9109f79df7617ac9733aef458ba93df Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Wed, 17 Dec 2025 19:06:15 -0300 Subject: [PATCH 05/50] chore: update dependencies for treasury support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update @types/pg to version 8.15.4 - Update pnpm lockfile 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/indexer/package.json | 2 +- pnpm-lock.yaml | 279 ++++++++------------------------------ 2 files changed, 54 insertions(+), 227 deletions(-) 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/pnpm-lock.yaml b/pnpm-lock.yaml index 81f65821d..9314a827d 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 @@ -269,7 +269,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 @@ -290,7 +290,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 @@ -338,7 +338,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 @@ -363,7 +363,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) @@ -417,7 +417,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/ebc60f500bc6870baaf321a0196fddc24d6edb03 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/27ba11c86ac93d8d4a50437ae26621468fe63c20 packages: "@adobe/css-tools@4.4.3": @@ -10925,10 +10925,10 @@ packages: } engines: { node: ">=14" } - forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/ebc60f500bc6870baaf321a0196fddc24d6edb03: + 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/ebc60f500bc6870baaf321a0196fddc24d6edb03, + tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/27ba11c86ac93d8d4a50437ae26621468fe63c20, } version: 1.12.0 @@ -13441,6 +13441,7 @@ packages: integrity: sha512-oI6D1zbbsh6JzzZFDCSHnnx6Qpvd1fSkVJu/5d8uluqnxzuoqtodVZjYvNovooznUq8udSAiKp7MbwlfZ8Gm6w==, } 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 @@ -18184,23 +18185,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 @@ -18216,12 +18205,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 @@ -18242,34 +18225,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 @@ -18285,34 +18250,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 @@ -18328,45 +18275,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 @@ -21363,41 +21286,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 @@ -25036,20 +24924,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 @@ -25119,26 +24993,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 @@ -25178,13 +25032,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: @@ -25709,13 +25556,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: @@ -26912,7 +26759,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/ebc60f500bc6870baaf321a0196fddc24d6edb03: + 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)): @@ -27741,7 +27588,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 @@ -27760,16 +27607,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 @@ -27779,38 +27626,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 @@ -27836,12 +27671,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 @@ -27867,7 +27702,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 @@ -28093,6 +27927,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)) @@ -28105,12 +27951,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 @@ -30614,12 +30460,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 @@ -30632,14 +30478,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 @@ -30648,11 +30495,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: {} @@ -30677,25 +30523,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 From bd68c09acc34446cca44df418a0208fa59488604 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 18 Dec 2025 16:16:34 -0300 Subject: [PATCH 06/50] refactor: createTreasuryProvider --- apps/indexer/src/api/controllers/index.ts | 1 + apps/indexer/src/api/index.ts | 6 +++++- .../api/services/treasury/treasury-provider-factory.ts | 8 +++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/indexer/src/api/controllers/index.ts b/apps/indexer/src/api/controllers/index.ts index 72c486a84..0ee03617b 100644 --- a/apps/indexer/src/api/controllers/index.ts +++ b/apps/indexer/src/api/controllers/index.ts @@ -8,3 +8,4 @@ export * from "./token"; export * from "./transactions"; export * from "./voting-power"; export * from "./dao"; +export * from "./treasury"; diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index 87913a925..ca2340c3c 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -22,6 +22,7 @@ import { accountBalanceVariations, dao, accountInteractions, + treasury, } from "@/api/controllers"; import { docs } from "@/api/docs"; import { env } from "@/env"; @@ -128,7 +129,10 @@ const accountBalanceService = new BalanceVariationsService( accountInteractionRepo, ); -createTreasuryProvider(app); +const treasuryService = createTreasuryProvider(app); +if (treasuryService) { + treasury(app, treasuryService); +} const tokenPriceClient = env.DAO_ID === DaoIdEnum.NOUNS diff --git a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts index dd1283555..163a55bb1 100644 --- a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -10,9 +10,9 @@ import { OpenAPIHono as Hono } from "@hono/zod-openapi"; * Creates a treasury provider and registers API routes * Providers fetch data on-demand * @param app - The Hono app instance - * @returns void - Returns early if no provider is configured + * @returns TreasuryService instance or null if no provider is configured */ -export function createTreasuryProvider(app: Hono) { +export function createTreasuryProvider(app: Hono): TreasuryService | null { if (env.TREASURY_PROVIDER_PROTOCOL_ID && env.DEFILLAMA_API_URL) { const axiosClient = axios.create({ baseURL: env.DEFILLAMA_API_URL, @@ -23,6 +23,7 @@ export function createTreasuryProvider(app: Hono) { ); const treasuryService = new TreasuryService(defiLlamaProvider); assets(app, treasuryService); + return treasuryService; } else if (env.DUNE_API_URL && env.DUNE_API_KEY) { const axiosClient = axios.create({ baseURL: env.DUNE_API_URL, @@ -30,8 +31,9 @@ export function createTreasuryProvider(app: Hono) { const duneProvider = new DuneProvider(axiosClient, env.DUNE_API_KEY); const treasuryService = new TreasuryService(duneProvider); assets(app, treasuryService); + return treasuryService; } else { console.warn("Treasury provider not configured."); - return; + return null; } } From 41b7c0c594118cda6638ddecff17334e5dec1278 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 18 Dec 2025 16:21:27 -0300 Subject: [PATCH 07/50] feat: treasury interfaces and types --- .../src/api/services/treasury/types.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts index fe6f4ad3e..5ef291fab 100644 --- a/apps/indexer/src/api/services/treasury/types.ts +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -1,4 +1,29 @@ +/** + * Interface to represent a treasury's data point + */ export interface TreasuryDataPoint { date: bigint; // Unix timestamp in seconds (start of day) liquidTreasury: number; + tokenTreasury?: number; + totalTreasury?: number; +} + +/** + * Enum to set the type of treasury + */ +export enum TreasuryType { + LIQUID = "liquid", + DAO_TOKEN = "dao-token", + TOTAL = "total", +} + +/** + * Treasury's response to the client + */ +export interface TreasuryResponse { + items: { + value: number; + date: number; + }[]; + totalCount: number; } From f3e7c46635a27dbddf21cfeb30d7d2d67ea14963 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 18 Dec 2025 16:30:58 -0300 Subject: [PATCH 08/50] refactor: dao-config to remove the supportsLiquidTreasuryCall --- apps/dashboard/shared/dao-config/comp.ts | 5 ----- apps/dashboard/shared/dao-config/ens.ts | 1 - apps/dashboard/shared/dao-config/gtc.ts | 1 - apps/dashboard/shared/dao-config/nouns.ts | 1 - apps/dashboard/shared/dao-config/obol.ts | 1 - apps/dashboard/shared/dao-config/op.ts | 1 - apps/dashboard/shared/dao-config/scr.ts | 1 - apps/dashboard/shared/dao-config/uni.ts | 1 - 8 files changed, 12 deletions(-) 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..0bccf4d1e 100644 --- a/apps/dashboard/shared/dao-config/gtc.ts +++ b/apps/dashboard/shared/dao-config/gtc.ts @@ -42,7 +42,6 @@ export const GTC: DaoConfiguration = { }, // attackProfitability: { // riskLevel: RiskLevel.HIGH, - // supportsLiquidTreasuryCall: false, // attackCostBarChart: { // OptimismTimelock: "", // OptimismTokenDistributor: "", 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/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", From f24d6e9c30412beb84ae8ad660fc3972b8285288 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 18 Dec 2025 16:31:35 -0300 Subject: [PATCH 09/50] refactor: delete old treasury-related files --- .../hooks/useTreasuryAssetNonDaoToken.ts | 78 ------------------- .../utils/normalizeDatasetAllTreasury.ts | 54 ------------- .../normalizeDatasetTreasuryNonDaoToken.ts | 14 ---- 3 files changed, 146 deletions(-) delete mode 100644 apps/dashboard/features/attack-profitability/hooks/useTreasuryAssetNonDaoToken.ts delete mode 100644 apps/dashboard/features/attack-profitability/utils/normalizeDatasetAllTreasury.ts delete mode 100644 apps/dashboard/features/attack-profitability/utils/normalizeDatasetTreasuryNonDaoToken.ts 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/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), - }; - }); -} From 5a59b248e221ea54f57864e4cdf9d0b2c4df20e6 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 11:29:03 -0300 Subject: [PATCH 10/50] refactor: remove old imports --- .../components/AttackCostBarChart.tsx | 16 ++++--- .../MultilineChartAttackProfitability.tsx | 42 ++++++++----------- .../attack-profitability/hooks/index.ts | 2 +- .../attack-profitability/utils/index.ts | 2 - .../dao-overview/hooks/useDaoOverviewData.ts | 13 +----- 5 files changed, 26 insertions(+), 49 deletions(-) diff --git a/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx b/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx index 786e5898a..3a387ad1f 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", 90); 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..18dd699df 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,14 @@ export const MultilineChartAttackProfitability = ({ const { data: daoData } = useDaoData(daoEnum); const daoConfig = daoConfigByDaoId[daoEnum]; - const { data: treasuryAssetNonDAOToken = [] } = useTreasuryAssetNonDaoToken( - daoEnum, - days, - ); + const numDays = Number(days.split("d")[0]); + + const { data: liquidTreasuryData } = useTreasury(daoEnum, "liquid", numDays); + const { data: totalTreasuryData } = useTreasury(daoEnum, "total", numDays); const { data: daoTokenPriceHistoricalData } = useDaoTokenHistoricalData({ daoId: daoEnum, - limit: Number(days.split("d")[0]) - 7, + limit: numDays - 7, }); const { data: timeSeriesData } = useTimeSeriesData( @@ -108,9 +104,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 +113,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, @@ -195,7 +186,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/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/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, }; }; From 726062aa4686c6edcea488ec65205954a4373c9a Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 11:29:31 -0300 Subject: [PATCH 11/50] refactor: types.ts --- apps/dashboard/shared/dao-config/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/dashboard/shared/dao-config/types.ts b/apps/dashboard/shared/dao-config/types.ts index 23bb0fd5a..4c1ac6f25 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; @@ -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; From 2509205a70bc8c2680d27057a0e14222d094d7a9 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 11:50:17 -0300 Subject: [PATCH 12/50] refactor: remove old treasury endpoint --- .../src/api/controllers/assets/index.ts | 46 ------------------- apps/indexer/src/api/controllers/index.ts | 1 - 2 files changed, 47 deletions(-) delete mode 100644 apps/indexer/src/api/controllers/assets/index.ts 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 1c4e30d19..000000000 --- a/apps/indexer/src/api/controllers/assets/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi"; -import { DaysOpts } from "@/lib/enums"; -import { TreasuryService } from "@/api/services/treasury"; - -export function assets(app: Hono, treasuryService: TreasuryService) { - app.openapi( - createRoute({ - method: "get", - operationId: "liquidTreasury", - path: "/liquid-treasury", - summary: "Get liquid treasury data", - description: - "Get historical Liquid Treasury (treasury without DAO tokens) directly from provider", - tags: ["assets"], - request: { - query: z.object({ - days: z - .enum(DaysOpts) - .default("7d") - .transform((val) => parseInt(val.replace("d", ""))), - order: z.enum(["asc", "desc"]).optional().default("asc"), - }), - }, - responses: { - 200: { - description: "Returns the liquid treasury history", - content: { - "application/json": { - schema: z.array( - z.object({ - date: z.number().describe("Unix timestamp in milliseconds"), - liquidTreasury: z.number(), - }), - ), - }, - }, - }, - }, - }), - async (context) => { - const { days, order } = context.req.valid("query"); - const response = await treasuryService.getTreasuryHistory(days, order); - return context.json(response); - }, - ); -} diff --git a/apps/indexer/src/api/controllers/index.ts b/apps/indexer/src/api/controllers/index.ts index 0ee03617b..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"; From 733f340a7b720477e282acdefcb4ec534c27950c Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 11:56:16 -0300 Subject: [PATCH 13/50] feat: treasury repository --- .../services/treasury/treasury.repository.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 apps/indexer/src/api/services/treasury/treasury.repository.ts diff --git a/apps/indexer/src/api/services/treasury/treasury.repository.ts b/apps/indexer/src/api/services/treasury/treasury.repository.ts new file mode 100644 index 000000000..7284964b4 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/treasury.repository.ts @@ -0,0 +1,67 @@ +import { db } from "ponder:api"; +import { daoMetricsDayBucket, tokenPrice } from "ponder:schema"; +import { and, eq, gte } from "ponder"; +import { MetricTypesEnum } from "@/lib/constants"; + +/** + * Repository for treasury-related database queries. + */ +export class TreasuryRepository { + /** + * Fetch DAO token quantities from daoMetricsDayBucket table + * @param daoId - The ID of the DAO + * @param cutoffTimestamp - The timestamp to filter the data + * @returns Map of timestamp (ms) to token quantity (bigint) + */ + async getTokenQuantities( + daoId: string, + cutoffTimestamp: bigint, + ): Promise> { + const results = await db.query.daoMetricsDayBucket.findMany({ + columns: { + date: true, + close: true, + }, + where: and( + eq(daoMetricsDayBucket.daoId, daoId), + eq(daoMetricsDayBucket.metricType, MetricTypesEnum.TREASURY), + gte(daoMetricsDayBucket.date, 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 historical token prices from tokenPrice table + * @param cutoffTimestamp - The timestamp to filter the data + * @returns Map of timestamp (ms) to price (number) + */ + async getHistoricalPrices( + cutoffTimestamp: bigint, + ): Promise> { + const results = await db.query.tokenPrice.findMany({ + columns: { + timestamp: true, + price: true, + }, + where: gte(tokenPrice.timestamp, cutoffTimestamp), + orderBy: (fields, { asc }) => [asc(fields.timestamp)], + }); + + const map = new Map(); + results.forEach((item) => { + const timestampMs = Number(item.timestamp) * 1000; + map.set(timestampMs, Number(item.price)); + }); + + return map; + } +} From 72575452b5db44fbe8b8f637bf1d59a6daef71c4 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 11:57:38 -0300 Subject: [PATCH 14/50] refactor: createTreasuryProvider to return the service --- .../treasury/treasury-provider-factory.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts index 163a55bb1..b70666934 100644 --- a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -3,16 +3,13 @@ import axios from "axios"; import { DefiLlamaProvider } from "./providers/defillama–provider"; import { DuneProvider } from "./providers/dune-provider"; import { TreasuryService } from "./treasury.service"; -import { assets } from "@/api/controllers"; -import { OpenAPIHono as Hono } from "@hono/zod-openapi"; /** - * Creates a treasury provider and registers API routes + * Creates a treasury provider * Providers fetch data on-demand - * @param app - The Hono app instance * @returns TreasuryService instance or null if no provider is configured */ -export function createTreasuryProvider(app: Hono): TreasuryService | null { +export function createTreasuryProvider(): TreasuryService | null { if (env.TREASURY_PROVIDER_PROTOCOL_ID && env.DEFILLAMA_API_URL) { const axiosClient = axios.create({ baseURL: env.DEFILLAMA_API_URL, @@ -21,17 +18,13 @@ export function createTreasuryProvider(app: Hono): TreasuryService | null { axiosClient, env.TREASURY_PROVIDER_PROTOCOL_ID, ); - const treasuryService = new TreasuryService(defiLlamaProvider); - assets(app, treasuryService); - return treasuryService; + return new TreasuryService(defiLlamaProvider); } else if (env.DUNE_API_URL && env.DUNE_API_KEY) { const axiosClient = axios.create({ baseURL: env.DUNE_API_URL, }); const duneProvider = new DuneProvider(axiosClient, env.DUNE_API_KEY); - const treasuryService = new TreasuryService(duneProvider); - assets(app, treasuryService); - return treasuryService; + return new TreasuryService(duneProvider); } else { console.warn("Treasury provider not configured."); return null; From 131d2a014f285bf0237ae7d81307a53ae5883046 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 13:06:35 -0300 Subject: [PATCH 15/50] refactor: imports on the /treasury.index.ts --- apps/indexer/src/api/services/treasury/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/indexer/src/api/services/treasury/index.ts b/apps/indexer/src/api/services/treasury/index.ts index 223ac25c5..c42bcbfc5 100644 --- a/apps/indexer/src/api/services/treasury/index.ts +++ b/apps/indexer/src/api/services/treasury/index.ts @@ -1,3 +1,5 @@ export * from "./providers"; export * from "./types"; export * from "./treasury.service"; +export * from "./treasury.repository"; +export * from "./forward-fill.util"; From 1604cb70ad69a00799e7168eb8fa64af12cc8ed2 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 13:22:30 -0300 Subject: [PATCH 16/50] feat: treasury zod schemas --- apps/indexer/src/api/mappers/index.ts | 1 + .../indexer/src/api/mappers/treasury/index.ts | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 apps/indexer/src/api/mappers/treasury/index.ts 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..7612c32f8 --- /dev/null +++ b/apps/indexer/src/api/mappers/treasury/index.ts @@ -0,0 +1,20 @@ +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 const TreasuryQuerySchema = z.object({ + days: z + .enum(DaysOpts) + .default("365d") + .transform((val) => parseInt(val.replace("d", ""))), + order: z.enum(["asc", "desc"]).optional().default("asc"), +}); From 902be537a92892d3c814aa0879c0444a1204d270 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 13:29:31 -0300 Subject: [PATCH 17/50] feat: fowardFill functions --- apps/indexer/src/api/index.ts | 4 +- .../src/api/services/treasury/forward-fill.ts | 55 +++++++++++++++++++ .../src/api/services/treasury/index.ts | 2 +- 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 apps/indexer/src/api/services/treasury/forward-fill.ts diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index ca2340c3c..f6784772f 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -129,9 +129,9 @@ const accountBalanceService = new BalanceVariationsService( accountInteractionRepo, ); -const treasuryService = createTreasuryProvider(app); +const treasuryService = createTreasuryProvider(); if (treasuryService) { - treasury(app, treasuryService); + treasury(app, treasuryService, env.DAO_ID); } const tokenPriceClient = 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..f6dfa3921 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/forward-fill.ts @@ -0,0 +1,55 @@ +/** + * 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. + * + */ + +/** + * Forward-fill sparse data across a master timeline. + * + * @param timeline - Sorted array of timestamps + * @param sparseData - Map of timestamp to value (may have gaps) + * @returns Map with values filled for all timeline timestamps + */ +export function forwardFill( + timeline: number[], + sparseData: Map, +): Map { + const result = new Map(); + let lastKnownValue: T | undefined; + + 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 a sorted timeline from multiple data sources. + * Useful when you need a master timeline from different datasets. + * + * @param dataSources - Array of Maps with timestamp keys + * @returns Sorted unique timestamps + */ +export function createTimeline( + ...dataSources: Array> +): number[] { + const uniqueTimestamps = new Set(); + + for (const source of dataSources) { + source.forEach((_, timestamp) => uniqueTimestamps.add(timestamp)); + } + + return Array.from(uniqueTimestamps).sort((a, b) => a - b); +} diff --git a/apps/indexer/src/api/services/treasury/index.ts b/apps/indexer/src/api/services/treasury/index.ts index c42bcbfc5..728a4b89b 100644 --- a/apps/indexer/src/api/services/treasury/index.ts +++ b/apps/indexer/src/api/services/treasury/index.ts @@ -2,4 +2,4 @@ export * from "./providers"; export * from "./types"; export * from "./treasury.service"; export * from "./treasury.repository"; -export * from "./forward-fill.util"; +export * from "./forward-fill"; From 596f40d057d3dda6e1b03af1b2c7a36838303e76 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 13:40:52 -0300 Subject: [PATCH 18/50] refactor: remove daoId dependecy --- apps/indexer/src/api/index.ts | 2 +- apps/indexer/src/api/services/treasury/treasury.repository.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index f6784772f..539c965a5 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -131,7 +131,7 @@ const accountBalanceService = new BalanceVariationsService( const treasuryService = createTreasuryProvider(); if (treasuryService) { - treasury(app, treasuryService, env.DAO_ID); + treasury(app, treasuryService); } const tokenPriceClient = diff --git a/apps/indexer/src/api/services/treasury/treasury.repository.ts b/apps/indexer/src/api/services/treasury/treasury.repository.ts index 7284964b4..e12e7ae74 100644 --- a/apps/indexer/src/api/services/treasury/treasury.repository.ts +++ b/apps/indexer/src/api/services/treasury/treasury.repository.ts @@ -9,12 +9,10 @@ import { MetricTypesEnum } from "@/lib/constants"; export class TreasuryRepository { /** * Fetch DAO token quantities from daoMetricsDayBucket table - * @param daoId - The ID of the DAO * @param cutoffTimestamp - The timestamp to filter the data * @returns Map of timestamp (ms) to token quantity (bigint) */ async getTokenQuantities( - daoId: string, cutoffTimestamp: bigint, ): Promise> { const results = await db.query.daoMetricsDayBucket.findMany({ @@ -23,7 +21,6 @@ export class TreasuryRepository { close: true, }, where: and( - eq(daoMetricsDayBucket.daoId, daoId), eq(daoMetricsDayBucket.metricType, MetricTypesEnum.TREASURY), gte(daoMetricsDayBucket.date, cutoffTimestamp), ), From a3687028339f46190ebfbad20f9fffc286c4c1f5 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 13:42:35 -0300 Subject: [PATCH 19/50] feat: treasury controller --- .../src/api/controllers/treasury/index.ts | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 apps/indexer/src/api/controllers/treasury/index.ts 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..f72ee9500 --- /dev/null +++ b/apps/indexer/src/api/controllers/treasury/index.ts @@ -0,0 +1,131 @@ +import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; + +import { TreasuryService, TreasuryType } 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.getTreasury( + TreasuryType.LIQUID, + 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.getTreasury( + TreasuryType.DAO_TOKEN, + 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.getTreasury( + TreasuryType.TOTAL, + days, + order, + decimals, + ); + + return context.json(result); + }, + ); +} From 2bb1f041a2083b0a4e3e28c200b55cf196f4418c Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 16:08:18 -0300 Subject: [PATCH 20/50] refactor: removing treasury types em router func --- .../src/api/controllers/treasury/index.ts | 22 ++++--------------- .../src/api/services/treasury/types.ts | 9 -------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/apps/indexer/src/api/controllers/treasury/index.ts b/apps/indexer/src/api/controllers/treasury/index.ts index f72ee9500..b2ab323bd 100644 --- a/apps/indexer/src/api/controllers/treasury/index.ts +++ b/apps/indexer/src/api/controllers/treasury/index.ts @@ -1,6 +1,6 @@ import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; -import { TreasuryService, TreasuryType } from "@/api/services/treasury"; +import { TreasuryService } from "@/api/services/treasury"; import { TreasuryResponseSchema, TreasuryQuerySchema, @@ -34,13 +34,7 @@ export function treasury(app: Hono, treasuryService: TreasuryService) { }), async (context) => { const { days, order } = context.req.valid("query"); - - const result = await treasuryService.getTreasury( - TreasuryType.LIQUID, - days, - order, - ); - + const result = await treasuryService.getLiquidTreasury(days, order); return context.json(result); }, ); @@ -73,16 +67,12 @@ export function treasury(app: Hono, treasuryService: TreasuryService) { }), async (context) => { const { days, order } = context.req.valid("query"); - const decimals = CONTRACT_ADDRESSES[env.DAO_ID].token.decimals; - - const result = await treasuryService.getTreasury( - TreasuryType.DAO_TOKEN, + const result = await treasuryService.getTokenTreasury( days, order, decimals, ); - return context.json(result); }, ); @@ -115,16 +105,12 @@ export function treasury(app: Hono, treasuryService: TreasuryService) { }), async (context) => { const { days, order } = context.req.valid("query"); - const decimals = CONTRACT_ADDRESSES[env.DAO_ID].token.decimals; - - const result = await treasuryService.getTreasury( - TreasuryType.TOTAL, + const result = await treasuryService.getTotalTreasury( days, order, decimals, ); - return context.json(result); }, ); diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts index 5ef291fab..b28640c86 100644 --- a/apps/indexer/src/api/services/treasury/types.ts +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -8,15 +8,6 @@ export interface TreasuryDataPoint { totalTreasury?: number; } -/** - * Enum to set the type of treasury - */ -export enum TreasuryType { - LIQUID = "liquid", - DAO_TOKEN = "dao-token", - TOTAL = "total", -} - /** * Treasury's response to the client */ From b3115c0eb8d8c90504dd14dfb8da036df000f1d3 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 16:09:26 -0300 Subject: [PATCH 21/50] feat: time related funcs to the shared file --- apps/indexer/src/eventHandlers/shared.ts | 38 ++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/indexer/src/eventHandlers/shared.ts b/apps/indexer/src/eventHandlers/shared.ts index ab8fcf4d8..ecc995fcc 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 } from "@/lib/enums"; import { delta, max, min } from "@/lib/utils"; export const ensureAccountExists = async ( @@ -144,7 +145,40 @@ export const handleTransaction = async ( ); }; +// Time constants +export const ONE_DAY_MS = SECONDS_IN_DAY * 1000; + +/** + * Truncate timestamp (seconds) to midnight UTC + */ export const truncateTimestampTime = (timestampSeconds: bigint): bigint => { - const SECONDS_IN_DAY = BigInt(86400); // 24 * 60 * 60 - return (timestampSeconds / SECONDS_IN_DAY) * SECONDS_IN_DAY; + const secondsInDay = BigInt(SECONDS_IN_DAY); + return (timestampSeconds / secondsInDay) * secondsInDay; +}; + +/** + * 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): bigint => { + return BigInt(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; }; From d655049f0dd9346fcc477d1b29193ec987bd973f Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 16:10:08 -0300 Subject: [PATCH 22/50] feat: add cutoffTimestamp to the fetch treasury and providers --- .../defillama\342\200\223provider.ts" | 12 ++++-- .../treasury/providers/dune-provider.ts | 37 +++++++++++-------- .../providers/treasury-provider.interface.ts | 3 +- 3 files changed, 32 insertions(+), 20 deletions(-) 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" index b73141f15..f65953c42 100644 --- "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" @@ -26,13 +26,13 @@ export class DefiLlamaProvider implements TreasuryProvider { this.providerDaoId = providerDaoId; } - async fetchTreasury(): Promise { + async fetchTreasury(cutoffTimestamp: bigint): Promise { try { const response = await this.client.get( `/${this.providerDaoId}`, ); - return this.transformData(response.data); + return this.transformData(response.data, cutoffTimestamp); } catch (error) { console.error( `[DefiLlamaProvider] Failed to fetch treasury data for ${this.providerDaoId}:`, @@ -45,7 +45,10 @@ export class DefiLlamaProvider implements TreasuryProvider { /** * Transforms DeFi Llama's raw response into our standardized format. */ - private transformData(rawData: RawDefiLlamaResponse): TreasuryDataPoint[] { + private transformData( + rawData: RawDefiLlamaResponse, + cutoffTimestamp: bigint, + ): TreasuryDataPoint[] { const { chainTvls } = rawData; // Map: chainKey → Map(dayTimestamp → latest dataPoint) @@ -106,8 +109,9 @@ export class DefiLlamaProvider implements TreasuryProvider { } } - // Convert map to array and format + // 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 diff --git a/apps/indexer/src/api/services/treasury/providers/dune-provider.ts b/apps/indexer/src/api/services/treasury/providers/dune-provider.ts index 15687f174..6660a0f12 100644 --- a/apps/indexer/src/api/services/treasury/providers/dune-provider.ts +++ b/apps/indexer/src/api/services/treasury/providers/dune-provider.ts @@ -28,7 +28,7 @@ export class DuneProvider implements TreasuryProvider { private readonly apiKey: string, ) {} - async fetchTreasury(): Promise { + async fetchTreasury(cutoffTimestamp: bigint): Promise { try { const response = await this.client.get("/", { headers: { @@ -36,7 +36,7 @@ export class DuneProvider implements TreasuryProvider { }, }); - return this.transformData(response.data); + return this.transformData(response.data, cutoffTimestamp); } catch (error) { throw new HTTPException(503, { message: "Failed to fetch total assets data", @@ -45,18 +45,25 @@ export class DuneProvider implements TreasuryProvider { } } - private transformData(data: DuneResponse): TreasuryDataPoint[] { - 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: BigInt(timestamp), - liquidTreasury: row.totalAssets ?? 0, - }; - }); + private transformData( + data: DuneResponse, + cutoffTimestamp: bigint, + ): TreasuryDataPoint[] { + 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 = BigInt( + 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/treasury-provider.interface.ts b/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts index 3bee0f67a..cb0fa75d2 100644 --- a/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts +++ b/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts @@ -4,7 +4,8 @@ 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(): Promise; + fetchTreasury(cutoffTimestamp: bigint): Promise; } From bec59edabf7e434fce9c3deb34c12b729fdc3216 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 16:10:35 -0300 Subject: [PATCH 23/50] feat: move createDailyTimelineFromData to the foward-fill file --- .../src/api/services/treasury/forward-fill.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/indexer/src/api/services/treasury/forward-fill.ts b/apps/indexer/src/api/services/treasury/forward-fill.ts index f6dfa3921..4329341de 100644 --- a/apps/indexer/src/api/services/treasury/forward-fill.ts +++ b/apps/indexer/src/api/services/treasury/forward-fill.ts @@ -6,6 +6,8 @@ * */ +import { truncateTimestampTimeMs, ONE_DAY_MS } from "@/eventHandlers/shared"; + /** * Forward-fill sparse data across a master timeline. * @@ -36,20 +38,23 @@ export function forwardFill( } /** - * Create a sorted timeline from multiple data sources. - * Useful when you need a master timeline from different datasets. - * - * @param dataSources - Array of Maps with timestamp keys - * @returns Sorted unique timestamps + * Create daily timeline from first data point to today (midnight UTC) + * Accepts multiple maps and finds the earliest timestamp across all */ -export function createTimeline( - ...dataSources: Array> +export function createDailyTimelineFromData( + ...dataMaps: Map[] ): number[] { - const uniqueTimestamps = new Set(); + const allTimestamps = dataMaps.flatMap((map) => [...map.keys()]); - for (const source of dataSources) { - source.forEach((_, timestamp) => uniqueTimestamps.add(timestamp)); - } + if (allTimestamps.length === 0) return []; + + const firstTimestamp = Math.min(...allTimestamps); + const todayMidnight = truncateTimestampTimeMs(Date.now()); + const totalDays = + Math.floor((todayMidnight - firstTimestamp) / ONE_DAY_MS) + 1; - return Array.from(uniqueTimestamps).sort((a, b) => a - b); + return Array.from( + { length: totalDays }, + (_, i) => firstTimestamp + i * ONE_DAY_MS, + ); } From d93430750f1a9b82c61b3654a6daedfb9d195886 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 16:10:54 -0300 Subject: [PATCH 24/50] feat: treasury service file --- .../api/services/treasury/treasury.service.ts | 158 +++++++++++++++--- 1 file changed, 131 insertions(+), 27 deletions(-) diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index 342bd4f53..3ea8da5a1 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -1,36 +1,140 @@ +import { formatUnits } from "viem"; import { TreasuryProvider } from "./providers"; +import { TreasuryResponse } from "./types"; +import { TreasuryRepository } from "./treasury.repository"; +import { forwardFill, createDailyTimelineFromData } from "./forward-fill"; +import { + calculateCutoffTimestamp, + truncateTimestampTimeMs, + normalizeMapTimestamps, +} from "@/eventHandlers/shared"; -export interface TreasuryHistoryResponse { - date: number; // Unix timestamp in milliseconds - liquidTreasury: number; -} - +/** + * Treasury Service - Orchestrates treasury data retrieval and calculation. + * Responsibility: Coordinate between provider, repository, and business logic. + */ export class TreasuryService { - constructor(private provider: TreasuryProvider) {} + private repository: TreasuryRepository; + + constructor(private provider: TreasuryProvider) { + this.repository = new TreasuryRepository(); + } + + /** + * Get liquid treasury only (from external providers) + */ + async getLiquidTreasury( + days: number, + order: "asc" | "desc", + ): Promise { + const cutoffTimestamp = calculateCutoffTimestamp(days); + const data = await this.provider.fetchTreasury(cutoffTimestamp); + + if (data.length === 0) { + return { items: [], totalCount: 0 }; + } - async getTreasuryHistory( + // Convert to map with normalized timestamps (midnight UTC) + const liquidMap = new Map(); + data.forEach((item) => { + const timestampMs = truncateTimestampTimeMs(Number(item.date) * 1000); + liquidMap.set(timestampMs, item.liquidTreasury); + }); + + // Create timeline from first data point to today + const timeline = createDailyTimelineFromData(liquidMap); + + // 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" = "asc", - ): Promise { - // Fetch from provider - const allData = await this.provider.fetchTreasury(); - - // Filter by days - const cutoffTimestamp = BigInt( - Math.floor(Date.now() / 1000) - days * 24 * 60 * 60, + order: "asc" | "desc", + decimals: number, + ): Promise { + const cutoffTimestamp = calculateCutoffTimestamp(days); + + // Fetch data from DB + const [tokenQuantities, historicalPrices] = await Promise.all([ + this.repository.getTokenQuantities(cutoffTimestamp), + this.repository.getHistoricalPrices(cutoffTimestamp), + ]); + + 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, + normalizedPrices, ); - const filteredData = allData.filter((item) => item.date >= cutoffTimestamp); - - // Sort - const sortedData = - order === "desc" - ? filteredData.sort((a, b) => Number(b.date - a.date)) - : filteredData.sort((a, b) => Number(a.date - b.date)); - - // Transform to response format (seconds to milliseconds) - return sortedData.map((item) => ({ - date: Number(item.date) * 1000, - liquidTreasury: item.liquidTreasury, + + // Forward-fill both quantities and prices + const filledQuantities = forwardFill(timeline, normalizedQuantities); + 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 }; + } + + // If only one has data, use that timeline; otherwise both have same timeline + const baseItems = + liquidResult.items.length > 0 ? liquidResult.items : tokenResult.items; + + // Sum values (same timeline guaranteed by forward-fill when both have data) + 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 }; } } From ca9659fbda2d9c80eae5c4747f087986228c9ab6 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 19 Dec 2025 16:14:52 -0300 Subject: [PATCH 25/50] refactor: useTreasury function --- .../attack-profitability/hooks/useTreasury.ts | 69 +++++++++++++++++++ .../dao-overview/hooks/useDaoTreasuryStats.ts | 40 +++++------ 2 files changed, 86 insertions(+), 23 deletions(-) create mode 100644 apps/dashboard/features/attack-profitability/hooks/useTreasury.ts 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..c64a8d616 --- /dev/null +++ b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts @@ -0,0 +1,69 @@ +import useSWR, { SWRConfiguration } from "swr"; +import axios from "axios"; +import { DaoIdEnum } from "@/shared/types/daos"; +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 fetchTreasury = async ({ + daoId, + type = "total", + days = 365, + order = "asc", +}: { + daoId: DaoIdEnum; + type?: TreasuryType; + days?: number; + order?: "asc" | "desc"; +}): Promise => { + const url = `${BACKEND_ENDPOINT}/treasury/${type}`; + const headers = { "anticapture-dao-id": daoId }; + + const response = await axios.get(url, { + params: { days: `${days}d`, order }, + headers, + }); + + return response.data; +}; + +export const useTreasury = ( + daoId: DaoIdEnum, + type: TreasuryType = "total", + days: number = 365, + options?: { + order?: "asc" | "desc"; + config?: Partial>; + }, +) => { + const { order = "asc", config } = options || {}; + const key = daoId ? ["treasury", daoId, type, days, order] : null; + + const { data, error, isValidating, mutate } = useSWR( + key, + () => fetchTreasury({ daoId, type, days, order }), + { + revalidateOnFocus: false, + shouldRetryOnError: false, + ...config, + }, + ); + + return { + data: data?.items ?? [], + totalCount: data?.totalCount ?? 0, + loading: isValidating, + error, + refetch: mutate, + }; +}; diff --git a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts index 7bd90bfa6..9a5fc2c75 100644 --- a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts +++ b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts @@ -1,40 +1,34 @@ -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"; 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; }) => { + const { data: liquidTreasury } = useTreasury(daoId, "liquid", 1); + const { data: tokenTreasury } = useTreasury(daoId, "dao-token", 1); + const { data: allTreasury } = useTreasury(daoId, "total", 1); + 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]); }; From 96c956bca028167a3ca4940272aedfcf91ac30e2 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 11:45:18 -0300 Subject: [PATCH 26/50] feat: getLastTokenQuantityBeforeDate --- apps/api-gateway/schema.graphql | 93 +++++++++++++++++-- .../MultilineChartAttackProfitability.tsx | 2 +- .../src/api/services/treasury/forward-fill.ts | 4 +- .../api/services/treasury/providers/index.ts | 1 + .../services/treasury/treasury.repository.ts | 21 ++++- .../api/services/treasury/treasury.service.ts | 15 ++- 6 files changed, 123 insertions(+), 13 deletions(-) diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 39bbe6f9e..cbd16f1e5 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/MultilineChartAttackProfitability.tsx b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx index 18dd699df..9b2556ac3 100644 --- a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx +++ b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx @@ -65,7 +65,7 @@ export const MultilineChartAttackProfitability = ({ const { data: daoTokenPriceHistoricalData } = useDaoTokenHistoricalData({ daoId: daoEnum, - limit: numDays - 7, + limit: numDays, }); const { data: timeSeriesData } = useTimeSeriesData( diff --git a/apps/indexer/src/api/services/treasury/forward-fill.ts b/apps/indexer/src/api/services/treasury/forward-fill.ts index 4329341de..09c19b04e 100644 --- a/apps/indexer/src/api/services/treasury/forward-fill.ts +++ b/apps/indexer/src/api/services/treasury/forward-fill.ts @@ -13,14 +13,16 @@ import { truncateTimestampTimeMs, ONE_DAY_MS } from "@/eventHandlers/shared"; * * @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; + let lastKnownValue: T | undefined = initialValue; for (const timestamp of timeline) { // Update last known value if we have data at this timestamp diff --git a/apps/indexer/src/api/services/treasury/providers/index.ts b/apps/indexer/src/api/services/treasury/providers/index.ts index 73710af82..2978472f1 100644 --- a/apps/indexer/src/api/services/treasury/providers/index.ts +++ b/apps/indexer/src/api/services/treasury/providers/index.ts @@ -1,3 +1,4 @@ export * from "./treasury-provider.interface"; export * from "./defillama–provider"; export * from "./dune-provider"; +export * from "./coingecko-price-provider"; diff --git a/apps/indexer/src/api/services/treasury/treasury.repository.ts b/apps/indexer/src/api/services/treasury/treasury.repository.ts index e12e7ae74..1556ddbe7 100644 --- a/apps/indexer/src/api/services/treasury/treasury.repository.ts +++ b/apps/indexer/src/api/services/treasury/treasury.repository.ts @@ -1,6 +1,6 @@ import { db } from "ponder:api"; import { daoMetricsDayBucket, tokenPrice } from "ponder:schema"; -import { and, eq, gte } from "ponder"; +import { and, eq, gte, lte, desc } from "ponder"; import { MetricTypesEnum } from "@/lib/constants"; /** @@ -61,4 +61,23 @@ export class TreasuryRepository { 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: bigint, + ): Promise { + const result = await db.query.daoMetricsDayBucket.findFirst({ + where: and( + eq(daoMetricsDayBucket.metricType, MetricTypesEnum.TREASURY), + lte(daoMetricsDayBucket.date, cutoffTimestamp), + ), + orderBy: desc(daoMetricsDayBucket.date), + }); + return result?.close ?? null; + } } diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index 3ea8da5a1..ff26d3eee 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -1,6 +1,6 @@ import { formatUnits } from "viem"; import { TreasuryProvider } from "./providers"; -import { TreasuryResponse } from "./types"; +import { PriceProvider, TreasuryResponse } from "./types"; import { TreasuryRepository } from "./treasury.repository"; import { forwardFill, createDailyTimelineFromData } from "./forward-fill"; import { @@ -16,7 +16,10 @@ import { export class TreasuryService { private repository: TreasuryRepository; - constructor(private provider: TreasuryProvider) { + constructor( + private provider: TreasuryProvider, + private priceProvider?: PriceProvider, + ) { this.repository = new TreasuryRepository(); } @@ -66,12 +69,16 @@ export class TreasuryService { order: "asc" | "desc", decimals: number, ): Promise { + if (!this.priceProvider) { + return { items: [], totalCount: 0 }; + } + const cutoffTimestamp = calculateCutoffTimestamp(days); - // Fetch data from DB + // Fetch token quantities from DB and prices from CoinGecko const [tokenQuantities, historicalPrices] = await Promise.all([ this.repository.getTokenQuantities(cutoffTimestamp), - this.repository.getHistoricalPrices(cutoffTimestamp), + this.priceProvider.getHistoricalPrices(days), ]); if (tokenQuantities.size === 0 && historicalPrices.size === 0) { From f94c70d8a72c9822587efce4ccbb880d6482aa35 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 11:46:34 -0300 Subject: [PATCH 27/50] refactor: treasury endpoint usage on the frontend --- .../attack-profitability/hooks/useTreasury.ts | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts index c64a8d616..964812c3f 100644 --- a/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts +++ b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts @@ -15,6 +15,12 @@ export interface TreasuryResponse { totalCount: number; } +const QUERY_NAME_MAP: Record = { + liquid: "getLiquidTreasury", + "dao-token": "getDaoTokenTreasury", + total: "getTotalTreasury", +}; + const fetchTreasury = async ({ daoId, type = "total", @@ -26,15 +32,28 @@ const fetchTreasury = async ({ days?: number; order?: "asc" | "desc"; }): Promise => { - const url = `${BACKEND_ENDPOINT}/treasury/${type}`; - const headers = { "anticapture-dao-id": daoId }; + const queryName = QUERY_NAME_MAP[type]; + const daysParam = `_${days}d`; - const response = await axios.get(url, { - params: { days: `${days}d`, order }, - headers, - }); + 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; + return response.data.data[queryName]; }; export const useTreasury = ( From 1b3bb24dd53a7747413ac14a9811a3c9df04c98b Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 11:47:01 -0300 Subject: [PATCH 28/50] refactor: useDaoTreasuryStats to use 7d instead of 1 --- .../dao-overview/hooks/useDaoTreasuryStats.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts index 9a5fc2c75..d32f3fe80 100644 --- a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts +++ b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts @@ -10,9 +10,16 @@ export const useDaoTreasuryStats = ({ daoId: DaoIdEnum; tokenData: { data?: TokenDataResponse | null }; }) => { - const { data: liquidTreasury } = useTreasury(daoId, "liquid", 1); - const { data: tokenTreasury } = useTreasury(daoId, "dao-token", 1); - const { data: allTreasury } = useTreasury(daoId, "total", 1); + // Use 7 days (minimum supported) with desc order to get most recent first + const { data: liquidTreasury } = useTreasury(daoId, "liquid", 7, { + order: "desc", + }); + const { data: tokenTreasury } = useTreasury(daoId, "dao-token", 7, { + order: "desc", + }); + const { data: allTreasury } = useTreasury(daoId, "total", 7, { + order: "desc", + }); return useMemo(() => { const lastPrice = Number(tokenData.data?.price) || 0; From ab5af3a32883fb9b96b6b21459ec9a0463be33f4 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 11:52:55 -0300 Subject: [PATCH 29/50] feat: createTokenPriceProvider --- apps/indexer/src/api/index.ts | 9 +++++-- .../providers/coingecko-price-provider.ts | 24 +++++++++++++++++ .../treasury/treasury-provider-factory.ts | 26 ++++++++++++++++--- .../src/api/services/treasury/types.ts | 7 +++++ 4 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 apps/indexer/src/api/services/treasury/providers/coingecko-price-provider.ts diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index 539c965a5..aafadb7ea 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -57,7 +57,10 @@ import { } from "@/api/services"; import { CONTRACT_ADDRESSES } from "@/lib/constants"; import { DaoIdEnum } from "@/lib/enums"; -import { createTreasuryProvider } from "./services/treasury/treasury-provider-factory"; +import { + createLiquidTreasuryProvider, + createTokenPriceProvider, +} from "./services/treasury/treasury-provider-factory"; const app = new Hono({ defaultHook: (result, c) => { @@ -129,7 +132,9 @@ const accountBalanceService = new BalanceVariationsService( accountInteractionRepo, ); -const treasuryService = createTreasuryProvider(); +const tokenPriceProvider = createTokenPriceProvider(); +const treasuryService = createLiquidTreasuryProvider(tokenPriceProvider!); + if (treasuryService) { treasury(app, treasuryService); } diff --git a/apps/indexer/src/api/services/treasury/providers/coingecko-price-provider.ts b/apps/indexer/src/api/services/treasury/providers/coingecko-price-provider.ts new file mode 100644 index 000000000..da5b33435 --- /dev/null +++ b/apps/indexer/src/api/services/treasury/providers/coingecko-price-provider.ts @@ -0,0 +1,24 @@ +import { CoingeckoService } from "@/api/services/coingecko"; +import { PriceProvider } from "../types"; +import { truncateTimestampTimeMs } from "@/eventHandlers/shared"; + +/** + * Adapter that wraps CoingeckoService to implement PriceProvider interface. + * Fetches historical token prices from CoinGecko API. + */ +export class CoingeckoPriceProvider implements PriceProvider { + constructor(private coingeckoService: CoingeckoService) {} + + async getHistoricalPrices(days: number): Promise> { + const priceData = await this.coingeckoService.getHistoricalTokenData(days); + + const priceMap = new Map(); + priceData.forEach((item) => { + // Normalize timestamp to midnight UTC + const normalizedTimestamp = truncateTimestampTimeMs(item.timestamp); + priceMap.set(normalizedTimestamp, Number(item.price)); + }); + + return priceMap; + } +} diff --git a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts index b70666934..060068fd3 100644 --- a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -2,14 +2,34 @@ import { env } from "@/env"; import axios from "axios"; import { DefiLlamaProvider } from "./providers/defillama–provider"; import { DuneProvider } from "./providers/dune-provider"; +import { CoingeckoPriceProvider } from "./providers/coingecko-price-provider"; import { TreasuryService } from "./treasury.service"; +import { CoingeckoService } from "@/api/services/coingecko"; +import { PriceProvider } from "./types"; + +/** + * Creates the price provider for token prices (CoinGecko) + */ +export function createTokenPriceProvider(): PriceProvider | undefined { + if (env.COINGECKO_API_URL && env.COINGECKO_API_KEY) { + const coingeckoService = new CoingeckoService( + env.COINGECKO_API_URL, + env.COINGECKO_API_KEY, + env.DAO_ID, + ); + return new CoingeckoPriceProvider(coingeckoService); + } + return undefined; +} /** * Creates a treasury provider * Providers fetch data on-demand * @returns TreasuryService instance or null if no provider is configured */ -export function createTreasuryProvider(): TreasuryService | null { +export function createLiquidTreasuryProvider( + tokenPriceProvider: PriceProvider, +): TreasuryService | null { if (env.TREASURY_PROVIDER_PROTOCOL_ID && env.DEFILLAMA_API_URL) { const axiosClient = axios.create({ baseURL: env.DEFILLAMA_API_URL, @@ -18,13 +38,13 @@ export function createTreasuryProvider(): TreasuryService | null { axiosClient, env.TREASURY_PROVIDER_PROTOCOL_ID, ); - return new TreasuryService(defiLlamaProvider); + return new TreasuryService(defiLlamaProvider, tokenPriceProvider); } else if (env.DUNE_API_URL && env.DUNE_API_KEY) { const axiosClient = axios.create({ baseURL: env.DUNE_API_URL, }); const duneProvider = new DuneProvider(axiosClient, env.DUNE_API_KEY); - return new TreasuryService(duneProvider); + return new TreasuryService(duneProvider, tokenPriceProvider); } else { console.warn("Treasury provider not configured."); return null; diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts index b28640c86..0c538d0e6 100644 --- a/apps/indexer/src/api/services/treasury/types.ts +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -18,3 +18,10 @@ export interface TreasuryResponse { }[]; totalCount: number; } + +/** + * Interface for fetching historical token prices + */ +export interface PriceProvider { + getHistoricalPrices(days: number): Promise>; +} From 77e3d6ff22ac1f366ffdf98f6600243545ee8729 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 12:04:15 -0300 Subject: [PATCH 30/50] refactor: useTreasury to follow the days standard --- .../components/AttackCostBarChart.tsx | 2 +- .../MultilineChartAttackProfitability.tsx | 12 +++++-- .../attack-profitability/hooks/useTreasury.ts | 9 ++--- .../dao-overview/hooks/useDaoTreasuryStats.ts | 34 ++++++++++++++----- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx b/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx index 3a387ad1f..500ce2d3d 100644 --- a/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx +++ b/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx @@ -72,7 +72,7 @@ export const AttackCostBarChart = ({ const timeInterval = TimeInterval.NINETY_DAYS; const { data: liquidTreasuryData, loading: liquidTreasuryLoading } = - useTreasury(selectedDaoId, "liquid", 90); + useTreasury(selectedDaoId, "liquid", timeInterval); const delegatedSupply = useDelegatedSupply(selectedDaoId, timeInterval); const activeSupply = useActiveSupply(selectedDaoId, timeInterval); const averageTurnout = useAverageTurnout(selectedDaoId, timeInterval); diff --git a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx index 9b2556ac3..0799e4935 100644 --- a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx +++ b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx @@ -60,8 +60,16 @@ export const MultilineChartAttackProfitability = ({ const numDays = Number(days.split("d")[0]); - const { data: liquidTreasuryData } = useTreasury(daoEnum, "liquid", numDays); - const { data: totalTreasuryData } = useTreasury(daoEnum, "total", numDays); + const { data: liquidTreasuryData } = useTreasury( + daoEnum, + "liquid", + days as TimeInterval, + ); + const { data: totalTreasuryData } = useTreasury( + daoEnum, + "total", + days as TimeInterval, + ); const { data: daoTokenPriceHistoricalData } = useDaoTokenHistoricalData({ daoId: daoEnum, diff --git a/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts index 964812c3f..e6aeceb80 100644 --- a/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts +++ b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts @@ -1,6 +1,7 @@ import useSWR, { SWRConfiguration } 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"; @@ -24,16 +25,16 @@ const QUERY_NAME_MAP: Record = { const fetchTreasury = async ({ daoId, type = "total", - days = 365, + days = TimeInterval.ONE_YEAR, order = "asc", }: { daoId: DaoIdEnum; type?: TreasuryType; - days?: number; + days?: TimeInterval; order?: "asc" | "desc"; }): Promise => { const queryName = QUERY_NAME_MAP[type]; - const daysParam = `_${days}d`; + const daysParam = `_${days}`; const query = `query GetTreasury { ${queryName}(days: ${daysParam}, order: ${order}) { @@ -59,7 +60,7 @@ const fetchTreasury = async ({ export const useTreasury = ( daoId: DaoIdEnum, type: TreasuryType = "total", - days: number = 365, + days: TimeInterval = TimeInterval.ONE_YEAR, options?: { order?: "asc" | "desc"; config?: Partial>; diff --git a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts index d32f3fe80..16cca7600 100644 --- a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts +++ b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts @@ -2,6 +2,7 @@ import { useMemo } from "react"; 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 = ({ daoId, @@ -11,15 +12,30 @@ export const useDaoTreasuryStats = ({ tokenData: { data?: TokenDataResponse | null }; }) => { // Use 7 days (minimum supported) with desc order to get most recent first - const { data: liquidTreasury } = useTreasury(daoId, "liquid", 7, { - order: "desc", - }); - const { data: tokenTreasury } = useTreasury(daoId, "dao-token", 7, { - order: "desc", - }); - const { data: allTreasury } = useTreasury(daoId, "total", 7, { - order: "desc", - }); + const { data: liquidTreasury } = useTreasury( + daoId, + "liquid", + TimeInterval.SEVEN_DAYS, + { + order: "desc", + }, + ); + const { data: tokenTreasury } = useTreasury( + daoId, + "dao-token", + TimeInterval.SEVEN_DAYS, + { + order: "desc", + }, + ); + const { data: allTreasury } = useTreasury( + daoId, + "total", + TimeInterval.SEVEN_DAYS, + { + order: "desc", + }, + ); return useMemo(() => { const lastPrice = Number(tokenData.data?.price) || 0; From f19a9aa208ba9e9459f7bf329e07a89eeca5110a Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 12:05:29 -0300 Subject: [PATCH 31/50] refactor: unnecessary Number(days.split("d")[0]) --- .../components/MultilineChartAttackProfitability.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx index 0799e4935..1d48ec962 100644 --- a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx +++ b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx @@ -58,8 +58,6 @@ export const MultilineChartAttackProfitability = ({ const { data: daoData } = useDaoData(daoEnum); const daoConfig = daoConfigByDaoId[daoEnum]; - const numDays = Number(days.split("d")[0]); - const { data: liquidTreasuryData } = useTreasury( daoEnum, "liquid", @@ -73,7 +71,7 @@ export const MultilineChartAttackProfitability = ({ const { data: daoTokenPriceHistoricalData } = useDaoTokenHistoricalData({ daoId: daoEnum, - limit: numDays, + limit: Number(days.split("d")[0]), }); const { data: timeSeriesData } = useTimeSeriesData( From f825c221d42322c861db6cd33beaba4ae59c2fa6 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 13:02:50 -0300 Subject: [PATCH 32/50] refactor: decouple treasury endpoint creation and provider --- apps/indexer/src/api/index.ts | 9 +++---- .../treasury/treasury-provider-factory.ts | 27 +++++++++++-------- .../api/services/treasury/treasury.service.ts | 6 ++++- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index aafadb7ea..2245d4564 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -58,7 +58,7 @@ import { import { CONTRACT_ADDRESSES } from "@/lib/constants"; import { DaoIdEnum } from "@/lib/enums"; import { - createLiquidTreasuryProvider, + createTreasuryService, createTokenPriceProvider, } from "./services/treasury/treasury-provider-factory"; @@ -133,11 +133,8 @@ const accountBalanceService = new BalanceVariationsService( ); const tokenPriceProvider = createTokenPriceProvider(); -const treasuryService = createLiquidTreasuryProvider(tokenPriceProvider!); - -if (treasuryService) { - treasury(app, treasuryService); -} +const treasuryService = createTreasuryService(tokenPriceProvider!); +treasury(app, treasuryService); const tokenPriceClient = env.DAO_ID === DaoIdEnum.NOUNS diff --git a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts index 060068fd3..06eaad20a 100644 --- a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -23,30 +23,35 @@ export function createTokenPriceProvider(): PriceProvider | undefined { } /** - * Creates a treasury provider - * Providers fetch data on-demand - * @returns TreasuryService instance or null if no provider is configured + * 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 createLiquidTreasuryProvider( +export function createTreasuryService( tokenPriceProvider: PriceProvider, -): TreasuryService | null { +): TreasuryService { + let liquidProvider: DefiLlamaProvider | DuneProvider | undefined; if (env.TREASURY_PROVIDER_PROTOCOL_ID && env.DEFILLAMA_API_URL) { const axiosClient = axios.create({ baseURL: env.DEFILLAMA_API_URL, }); - const defiLlamaProvider = new DefiLlamaProvider( + liquidProvider = new DefiLlamaProvider( axiosClient, env.TREASURY_PROVIDER_PROTOCOL_ID, ); - return new TreasuryService(defiLlamaProvider, tokenPriceProvider); } else if (env.DUNE_API_URL && env.DUNE_API_KEY) { const axiosClient = axios.create({ baseURL: env.DUNE_API_URL, }); - const duneProvider = new DuneProvider(axiosClient, env.DUNE_API_KEY); - return new TreasuryService(duneProvider, tokenPriceProvider); + liquidProvider = new DuneProvider(axiosClient, env.DUNE_API_KEY); } else { - console.warn("Treasury provider not configured."); - return null; + console.warn( + "Liquid treasury provider not configured. Only dao-token treasury will be available.", + ); } + + return new TreasuryService(liquidProvider, tokenPriceProvider); } diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index ff26d3eee..984b51458 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -17,7 +17,7 @@ export class TreasuryService { private repository: TreasuryRepository; constructor( - private provider: TreasuryProvider, + private provider?: TreasuryProvider, private priceProvider?: PriceProvider, ) { this.repository = new TreasuryRepository(); @@ -30,6 +30,10 @@ export class TreasuryService { days: number, order: "asc" | "desc", ): Promise { + if (!this.provider) { + return { items: [], totalCount: 0 }; + } + const cutoffTimestamp = calculateCutoffTimestamp(days); const data = await this.provider.fetchTreasury(cutoffTimestamp); From b07eb191d8cd072487d444d65753862fe8e11002 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 13:14:23 -0300 Subject: [PATCH 33/50] refactor: decouple env and treasury factory --- apps/indexer/src/api/index.ts | 14 ++++++- .../treasury/treasury-provider-factory.ts | 38 +++++++++---------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index 2245d4564..5b1804619 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -132,8 +132,18 @@ const accountBalanceService = new BalanceVariationsService( accountInteractionRepo, ); -const tokenPriceProvider = createTokenPriceProvider(); -const treasuryService = createTreasuryService(tokenPriceProvider!); +const tokenPriceProvider = createTokenPriceProvider( + env.COINGECKO_API_URL, + env.COINGECKO_API_KEY, + env.DAO_ID, +); +const treasuryService = createTreasuryService( + tokenPriceProvider, + env.DEFILLAMA_API_URL, + env.TREASURY_PROVIDER_PROTOCOL_ID, + env.DUNE_API_URL, + env.DUNE_API_KEY, +); treasury(app, treasuryService); const tokenPriceClient = diff --git a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts index 06eaad20a..027726221 100644 --- a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -1,4 +1,3 @@ -import { env } from "@/env"; import axios from "axios"; import { DefiLlamaProvider } from "./providers/defillama–provider"; import { DuneProvider } from "./providers/dune-provider"; @@ -6,20 +5,18 @@ import { CoingeckoPriceProvider } from "./providers/coingecko-price-provider"; import { TreasuryService } from "./treasury.service"; import { CoingeckoService } from "@/api/services/coingecko"; import { PriceProvider } from "./types"; +import { DaoIdEnum } from "@/lib/enums"; /** * Creates the price provider for token prices (CoinGecko) */ -export function createTokenPriceProvider(): PriceProvider | undefined { - if (env.COINGECKO_API_URL && env.COINGECKO_API_KEY) { - const coingeckoService = new CoingeckoService( - env.COINGECKO_API_URL, - env.COINGECKO_API_KEY, - env.DAO_ID, - ); - return new CoingeckoPriceProvider(coingeckoService); - } - return undefined; +export function createTokenPriceProvider( + apiUrl: string, + apiKey: string, + daoId: DaoIdEnum, +): PriceProvider { + const coingeckoService = new CoingeckoService(apiUrl, apiKey, daoId); + return new CoingeckoPriceProvider(coingeckoService); } /** @@ -32,21 +29,22 @@ export function createTokenPriceProvider(): PriceProvider | undefined { */ export function createTreasuryService( tokenPriceProvider: PriceProvider, + defiLlamaApiUrl?: string, + defiLlamaProtocolId?: string, + duneApiUrl?: string, + duneApiKey?: string, ): TreasuryService { let liquidProvider: DefiLlamaProvider | DuneProvider | undefined; - if (env.TREASURY_PROVIDER_PROTOCOL_ID && env.DEFILLAMA_API_URL) { + if (defiLlamaProtocolId && defiLlamaApiUrl) { const axiosClient = axios.create({ - baseURL: env.DEFILLAMA_API_URL, + baseURL: defiLlamaApiUrl, }); - liquidProvider = new DefiLlamaProvider( - axiosClient, - env.TREASURY_PROVIDER_PROTOCOL_ID, - ); - } else if (env.DUNE_API_URL && env.DUNE_API_KEY) { + liquidProvider = new DefiLlamaProvider(axiosClient, defiLlamaProtocolId); + } else if (duneApiUrl && duneApiKey) { const axiosClient = axios.create({ - baseURL: env.DUNE_API_URL, + baseURL: duneApiUrl, }); - liquidProvider = new DuneProvider(axiosClient, env.DUNE_API_KEY); + liquidProvider = new DuneProvider(axiosClient, duneApiKey); } else { console.warn( "Liquid treasury provider not configured. Only dao-token treasury will be available.", From 6b943f869e53456923f5d77ac41b9b4bb9a85e66 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 13:18:50 -0300 Subject: [PATCH 34/50] refactor: move const for other file --- apps/indexer/src/api/services/treasury/forward-fill.ts | 3 ++- apps/indexer/src/eventHandlers/shared.ts | 5 +---- apps/indexer/src/lib/enums.ts | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/indexer/src/api/services/treasury/forward-fill.ts b/apps/indexer/src/api/services/treasury/forward-fill.ts index 09c19b04e..fc46ecc0b 100644 --- a/apps/indexer/src/api/services/treasury/forward-fill.ts +++ b/apps/indexer/src/api/services/treasury/forward-fill.ts @@ -6,7 +6,8 @@ * */ -import { truncateTimestampTimeMs, ONE_DAY_MS } from "@/eventHandlers/shared"; +import { truncateTimestampTimeMs } from "@/eventHandlers/shared"; +import { ONE_DAY_MS } from "@/lib/enums"; /** * Forward-fill sparse data across a master timeline. diff --git a/apps/indexer/src/eventHandlers/shared.ts b/apps/indexer/src/eventHandlers/shared.ts index ecc995fcc..03c63c3f1 100644 --- a/apps/indexer/src/eventHandlers/shared.ts +++ b/apps/indexer/src/eventHandlers/shared.ts @@ -3,7 +3,7 @@ import { Context } from "ponder:registry"; import { account, daoMetricsDayBucket, transaction } from "ponder:schema"; import { MetricTypesEnum } from "@/lib/constants"; -import { SECONDS_IN_DAY } from "@/lib/enums"; +import { SECONDS_IN_DAY, ONE_DAY_MS } from "@/lib/enums"; import { delta, max, min } from "@/lib/utils"; export const ensureAccountExists = async ( @@ -145,9 +145,6 @@ export const handleTransaction = async ( ); }; -// Time constants -export const ONE_DAY_MS = SECONDS_IN_DAY * 1000; - /** * Truncate timestamp (seconds) to midnight UTC */ 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) From 97df5196138c133db8da44eec86edf5da4b74637 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 13:30:10 -0300 Subject: [PATCH 35/50] refactor: outdated interface --- apps/indexer/src/api/services/treasury/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts index 0c538d0e6..ba76d8188 100644 --- a/apps/indexer/src/api/services/treasury/types.ts +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -4,8 +4,6 @@ export interface TreasuryDataPoint { date: bigint; // Unix timestamp in seconds (start of day) liquidTreasury: number; - tokenTreasury?: number; - totalTreasury?: number; } /** From f4d4da156a802be966b60d1d7ba5eafe8666863e Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 13:31:45 -0300 Subject: [PATCH 36/50] refactor: function name --- .../treasury/providers/defillama\342\200\223provider.ts" | 8 +++++--- .../src/api/services/treasury/providers/dune-provider.ts | 8 +++++--- .../treasury/providers/treasury-provider.interface.ts | 4 ++-- apps/indexer/src/api/services/treasury/types.ts | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) 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" index f65953c42..46185ff73 100644 --- "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" @@ -1,6 +1,6 @@ import { AxiosInstance } from "axios"; import { TreasuryProvider } from "./treasury-provider.interface"; -import { TreasuryDataPoint } from "../types"; +import { LiquidTreasuryDataPoint } from "../types"; import { truncateTimestampTime } from "@/eventHandlers/shared"; interface RawDefiLlamaResponse { @@ -26,7 +26,9 @@ export class DefiLlamaProvider implements TreasuryProvider { this.providerDaoId = providerDaoId; } - async fetchTreasury(cutoffTimestamp: bigint): Promise { + async fetchTreasury( + cutoffTimestamp: bigint, + ): Promise { try { const response = await this.client.get( `/${this.providerDaoId}`, @@ -48,7 +50,7 @@ export class DefiLlamaProvider implements TreasuryProvider { private transformData( rawData: RawDefiLlamaResponse, cutoffTimestamp: bigint, - ): TreasuryDataPoint[] { + ): LiquidTreasuryDataPoint[] { const { chainTvls } = rawData; // Map: chainKey → Map(dayTimestamp → latest dataPoint) diff --git a/apps/indexer/src/api/services/treasury/providers/dune-provider.ts b/apps/indexer/src/api/services/treasury/providers/dune-provider.ts index 6660a0f12..4dd2fca77 100644 --- a/apps/indexer/src/api/services/treasury/providers/dune-provider.ts +++ b/apps/indexer/src/api/services/treasury/providers/dune-provider.ts @@ -1,5 +1,5 @@ import { HTTPException } from "hono/http-exception"; -import { TreasuryDataPoint } from "../types"; +import { LiquidTreasuryDataPoint } from "../types"; import { TreasuryProvider } from "./treasury-provider.interface"; import { AxiosInstance } from "axios"; @@ -28,7 +28,9 @@ export class DuneProvider implements TreasuryProvider { private readonly apiKey: string, ) {} - async fetchTreasury(cutoffTimestamp: bigint): Promise { + async fetchTreasury( + cutoffTimestamp: bigint, + ): Promise { try { const response = await this.client.get("/", { headers: { @@ -48,7 +50,7 @@ export class DuneProvider implements TreasuryProvider { private transformData( data: DuneResponse, cutoffTimestamp: bigint, - ): TreasuryDataPoint[] { + ): LiquidTreasuryDataPoint[] { return data.result.rows .map((row) => { // Parse date string "YYYY-MM-DD" and convert to Unix timestamp (seconds) 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 index cb0fa75d2..80ae26805 100644 --- a/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts +++ b/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts @@ -1,4 +1,4 @@ -import { TreasuryDataPoint } from "../types"; +import { LiquidTreasuryDataPoint } from "../types"; export interface TreasuryProvider { /** @@ -7,5 +7,5 @@ export interface TreasuryProvider { * @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: bigint): Promise; + fetchTreasury(cutoffTimestamp: bigint): Promise; } diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts index ba76d8188..6d461a4c0 100644 --- a/apps/indexer/src/api/services/treasury/types.ts +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -1,7 +1,7 @@ /** * Interface to represent a treasury's data point */ -export interface TreasuryDataPoint { +export interface LiquidTreasuryDataPoint { date: bigint; // Unix timestamp in seconds (start of day) liquidTreasury: number; } From 670fff42515f2abffa34881c5a5b5e57453c340e Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 13:40:32 -0300 Subject: [PATCH 37/50] refactor: improve comments --- apps/indexer/src/api/services/treasury/treasury.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index 984b51458..9a7c87ea3 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -134,11 +134,12 @@ export class TreasuryService { return { items: [], totalCount: 0 }; } - // If only one has data, use that timeline; otherwise both have same timeline + // Use the timeline with more data points (liquid or token could be empty) const baseItems = - liquidResult.items.length > 0 ? liquidResult.items : tokenResult.items; + liquidResult.items.length > tokenResult.items.length + ? liquidResult.items + : tokenResult.items; - // Sum values (same timeline guaranteed by forward-fill when both have data) const items = baseItems.map((item, i) => ({ date: item.date, value: From 08d7b1f57d3fc629f05aa79e2a9c37f2ce609d28 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 13:59:17 -0300 Subject: [PATCH 38/50] feat: add getLastTokenQuantityBeforeDate in the getTokenTreasury function --- .../src/api/services/treasury/treasury.service.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index 9a7c87ea3..ab5070981 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -99,8 +99,16 @@ export class TreasuryService { normalizedPrices, ); + // 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); + const filledQuantities = forwardFill( + timeline, + normalizedQuantities, + lastKnownQuantity ?? undefined, + ); const filledPrices = forwardFill(timeline, normalizedPrices); // Calculate token treasury values @@ -136,9 +144,7 @@ export class TreasuryService { // Use the timeline with more data points (liquid or token could be empty) const baseItems = - liquidResult.items.length > tokenResult.items.length - ? liquidResult.items - : tokenResult.items; + liquidResult.items.length > 0 ? liquidResult.items : tokenResult.items; const items = baseItems.map((item, i) => ({ date: item.date, From a37a26f4ec4627ad2ee7fedd9cfe6be52564116a Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 22 Dec 2025 14:06:40 -0300 Subject: [PATCH 39/50] refactor: remove old db query --- .../services/treasury/treasury.repository.ts | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/apps/indexer/src/api/services/treasury/treasury.repository.ts b/apps/indexer/src/api/services/treasury/treasury.repository.ts index 1556ddbe7..af8968e99 100644 --- a/apps/indexer/src/api/services/treasury/treasury.repository.ts +++ b/apps/indexer/src/api/services/treasury/treasury.repository.ts @@ -1,5 +1,5 @@ import { db } from "ponder:api"; -import { daoMetricsDayBucket, tokenPrice } from "ponder:schema"; +import { daoMetricsDayBucket } from "ponder:schema"; import { and, eq, gte, lte, desc } from "ponder"; import { MetricTypesEnum } from "@/lib/constants"; @@ -36,32 +36,6 @@ export class TreasuryRepository { return map; } - /** - * Fetch historical token prices from tokenPrice table - * @param cutoffTimestamp - The timestamp to filter the data - * @returns Map of timestamp (ms) to price (number) - */ - async getHistoricalPrices( - cutoffTimestamp: bigint, - ): Promise> { - const results = await db.query.tokenPrice.findMany({ - columns: { - timestamp: true, - price: true, - }, - where: gte(tokenPrice.timestamp, cutoffTimestamp), - orderBy: (fields, { asc }) => [asc(fields.timestamp)], - }); - - const map = new Map(); - results.forEach((item) => { - const timestampMs = Number(item.timestamp) * 1000; - map.set(timestampMs, Number(item.price)); - }); - - 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. From e6895033fce3eb9a24dfd6f23f04717c90479095 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Tue, 23 Dec 2025 11:05:48 -0300 Subject: [PATCH 40/50] refactor: timestamp to be number not bigint --- .../providers/defillama\342\200\223provider.ts" | 14 +++++++------- .../services/treasury/providers/dune-provider.ts | 8 +++----- .../providers/treasury-provider.interface.ts | 2 +- .../api/services/treasury/treasury.repository.ts | 8 ++++---- .../src/api/services/treasury/treasury.service.ts | 2 +- apps/indexer/src/api/services/treasury/types.ts | 2 +- apps/indexer/src/eventHandlers/shared.ts | 11 +++++------ apps/indexer/src/indexer/nouns/governor.ts | 2 +- 8 files changed, 23 insertions(+), 26 deletions(-) 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" index 46185ff73..d41fab178 100644 --- "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" @@ -27,7 +27,7 @@ export class DefiLlamaProvider implements TreasuryProvider { } async fetchTreasury( - cutoffTimestamp: bigint, + cutoffTimestamp: number, ): Promise { try { const response = await this.client.get( @@ -49,14 +49,14 @@ export class DefiLlamaProvider implements TreasuryProvider { */ private transformData( rawData: RawDefiLlamaResponse, - cutoffTimestamp: bigint, + cutoffTimestamp: number, ): LiquidTreasuryDataPoint[] { const { chainTvls } = rawData; // Map: chainKey → Map(dayTimestamp → latest dataPoint) const chainsByDate = new Map< string, - Map + Map >(); // For each chain, keep only the latest timestamp per date @@ -66,10 +66,10 @@ export class DefiLlamaProvider implements TreasuryProvider { continue; // Skip {Chain}-OwnTokens variants } - const dateMap = new Map(); + const dateMap = new Map(); for (const dataPoint of chainData.tvl || []) { - const dayTimestamp = truncateTimestampTime(BigInt(dataPoint.date)); + const dayTimestamp = truncateTimestampTime(dataPoint.date); const existing = dateMap.get(dayTimestamp); // Keep only the latest timestamp for each date @@ -86,7 +86,7 @@ export class DefiLlamaProvider implements TreasuryProvider { // Aggregate across chains const aggregatedByDate = new Map< - bigint, + number, { total: number; withoutOwnToken: number } >(); @@ -118,6 +118,6 @@ export class DefiLlamaProvider implements TreasuryProvider { date: dayTimestamp, liquidTreasury: values.withoutOwnToken, // Liquid Treasury })) - .sort((a, b) => Number(a.date - b.date)); // Sort by timestamp ascending + .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 index 4dd2fca77..f78f806cf 100644 --- a/apps/indexer/src/api/services/treasury/providers/dune-provider.ts +++ b/apps/indexer/src/api/services/treasury/providers/dune-provider.ts @@ -29,7 +29,7 @@ export class DuneProvider implements TreasuryProvider { ) {} async fetchTreasury( - cutoffTimestamp: bigint, + cutoffTimestamp: number, ): Promise { try { const response = await this.client.get("/", { @@ -49,7 +49,7 @@ export class DuneProvider implements TreasuryProvider { private transformData( data: DuneResponse, - cutoffTimestamp: bigint, + cutoffTimestamp: number, ): LiquidTreasuryDataPoint[] { return data.result.rows .map((row) => { @@ -58,9 +58,7 @@ export class DuneProvider implements TreasuryProvider { if (!year || !month || !day) { throw new Error(`Invalid date string: ${row.date}`); } - const timestamp = BigInt( - Math.floor(Date.UTC(year, month - 1, day) / 1000), - ); + const timestamp = Math.floor(Date.UTC(year, month - 1, day) / 1000); return { date: timestamp, liquidTreasury: row.totalAssets ?? 0, 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 index 80ae26805..ea342ee93 100644 --- a/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts +++ b/apps/indexer/src/api/services/treasury/providers/treasury-provider.interface.ts @@ -7,5 +7,5 @@ export interface TreasuryProvider { * @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: bigint): Promise; + fetchTreasury(cutoffTimestamp: number): Promise; } diff --git a/apps/indexer/src/api/services/treasury/treasury.repository.ts b/apps/indexer/src/api/services/treasury/treasury.repository.ts index af8968e99..d4bc4ae9f 100644 --- a/apps/indexer/src/api/services/treasury/treasury.repository.ts +++ b/apps/indexer/src/api/services/treasury/treasury.repository.ts @@ -13,7 +13,7 @@ export class TreasuryRepository { * @returns Map of timestamp (ms) to token quantity (bigint) */ async getTokenQuantities( - cutoffTimestamp: bigint, + cutoffTimestamp: number, ): Promise> { const results = await db.query.daoMetricsDayBucket.findMany({ columns: { @@ -22,7 +22,7 @@ export class TreasuryRepository { }, where: and( eq(daoMetricsDayBucket.metricType, MetricTypesEnum.TREASURY), - gte(daoMetricsDayBucket.date, cutoffTimestamp), + gte(daoMetricsDayBucket.date, BigInt(cutoffTimestamp)), ), orderBy: (fields, { asc }) => [asc(fields.date)], }); @@ -43,12 +43,12 @@ export class TreasuryRepository { * @returns The last known token quantity or null if not found */ async getLastTokenQuantityBeforeDate( - cutoffTimestamp: bigint, + cutoffTimestamp: number, ): Promise { const result = await db.query.daoMetricsDayBucket.findFirst({ where: and( eq(daoMetricsDayBucket.metricType, MetricTypesEnum.TREASURY), - lte(daoMetricsDayBucket.date, cutoffTimestamp), + lte(daoMetricsDayBucket.date, BigInt(cutoffTimestamp)), ), orderBy: desc(daoMetricsDayBucket.date), }); diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index ab5070981..00ad59e49 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -44,7 +44,7 @@ export class TreasuryService { // Convert to map with normalized timestamps (midnight UTC) const liquidMap = new Map(); data.forEach((item) => { - const timestampMs = truncateTimestampTimeMs(Number(item.date) * 1000); + const timestampMs = truncateTimestampTimeMs(item.date * 1000); liquidMap.set(timestampMs, item.liquidTreasury); }); diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts index 6d461a4c0..0ac0de638 100644 --- a/apps/indexer/src/api/services/treasury/types.ts +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -2,7 +2,7 @@ * Interface to represent a treasury's data point */ export interface LiquidTreasuryDataPoint { - date: bigint; // Unix timestamp in seconds (start of day) + date: number; // Unix timestamp in seconds (start of day) liquidTreasury: number; } diff --git a/apps/indexer/src/eventHandlers/shared.ts b/apps/indexer/src/eventHandlers/shared.ts index 03c63c3f1..df9197c9c 100644 --- a/apps/indexer/src/eventHandlers/shared.ts +++ b/apps/indexer/src/eventHandlers/shared.ts @@ -43,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, @@ -148,9 +148,8 @@ export const handleTransaction = async ( /** * Truncate timestamp (seconds) to midnight UTC */ -export const truncateTimestampTime = (timestampSeconds: bigint): bigint => { - const secondsInDay = BigInt(SECONDS_IN_DAY); - return (timestampSeconds / secondsInDay) * secondsInDay; +export const truncateTimestampTime = (timestampSeconds: number): number => { + return Math.floor(timestampSeconds / SECONDS_IN_DAY) * SECONDS_IN_DAY; }; /** @@ -163,8 +162,8 @@ export const truncateTimestampTimeMs = (timestampMs: number): number => { /** * Calculate cutoff timestamp for filtering data by days */ -export const calculateCutoffTimestamp = (days: number): bigint => { - return BigInt(Math.floor(Date.now() / 1000) - days * SECONDS_IN_DAY); +export const calculateCutoffTimestamp = (days: number): number => { + return Math.floor(Date.now() / 1000) - days * SECONDS_IN_DAY; }; /** 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))), }); }); } From aa23384c611fc90dc62ff3cd2b16e239edac4ae2 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Tue, 23 Dec 2025 11:07:18 -0300 Subject: [PATCH 41/50] refactor: remove duplicated interface TreasuryResponse --- apps/indexer/src/api/mappers/treasury/index.ts | 2 ++ .../src/api/services/treasury/treasury.service.ts | 3 ++- apps/indexer/src/api/services/treasury/types.ts | 11 ----------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/indexer/src/api/mappers/treasury/index.ts b/apps/indexer/src/api/mappers/treasury/index.ts index 7612c32f8..aeaeefec8 100644 --- a/apps/indexer/src/api/mappers/treasury/index.ts +++ b/apps/indexer/src/api/mappers/treasury/index.ts @@ -11,6 +11,8 @@ export const TreasuryResponseSchema = z.object({ totalCount: z.number().describe("Total number of items"), }); +export type TreasuryResponse = z.infer; + export const TreasuryQuerySchema = z.object({ days: z .enum(DaysOpts) diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index 00ad59e49..5ae429ad7 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -1,6 +1,7 @@ import { formatUnits } from "viem"; import { TreasuryProvider } from "./providers"; -import { PriceProvider, TreasuryResponse } from "./types"; +import { PriceProvider } from "./types"; +import { TreasuryResponse } from "@/api/mappers/treasury"; import { TreasuryRepository } from "./treasury.repository"; import { forwardFill, createDailyTimelineFromData } from "./forward-fill"; import { diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts index 0ac0de638..87f45e91e 100644 --- a/apps/indexer/src/api/services/treasury/types.ts +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -6,17 +6,6 @@ export interface LiquidTreasuryDataPoint { liquidTreasury: number; } -/** - * Treasury's response to the client - */ -export interface TreasuryResponse { - items: { - value: number; - date: number; - }[]; - totalCount: number; -} - /** * Interface for fetching historical token prices */ From 7d61c656f03bbcd559958eac1b4b36375f0cb384 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Tue, 23 Dec 2025 11:23:25 -0300 Subject: [PATCH 42/50] refactor: move file dir --- .../treasury/treasury.repository.ts | 0 apps/indexer/src/api/services/treasury/index.ts | 2 +- .../src/api/services/treasury/treasury.service.ts | 9 +++------ 3 files changed, 4 insertions(+), 7 deletions(-) rename apps/indexer/src/api/{services => repositories}/treasury/treasury.repository.ts (100%) diff --git a/apps/indexer/src/api/services/treasury/treasury.repository.ts b/apps/indexer/src/api/repositories/treasury/treasury.repository.ts similarity index 100% rename from apps/indexer/src/api/services/treasury/treasury.repository.ts rename to apps/indexer/src/api/repositories/treasury/treasury.repository.ts diff --git a/apps/indexer/src/api/services/treasury/index.ts b/apps/indexer/src/api/services/treasury/index.ts index 728a4b89b..bd4940246 100644 --- a/apps/indexer/src/api/services/treasury/index.ts +++ b/apps/indexer/src/api/services/treasury/index.ts @@ -1,5 +1,5 @@ export * from "./providers"; export * from "./types"; export * from "./treasury.service"; -export * from "./treasury.repository"; +export * from "../../repositories/treasury/treasury.repository"; export * from "./forward-fill"; diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index 5ae429ad7..b2bce7092 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -2,7 +2,7 @@ import { formatUnits } from "viem"; import { TreasuryProvider } from "./providers"; import { PriceProvider } from "./types"; import { TreasuryResponse } from "@/api/mappers/treasury"; -import { TreasuryRepository } from "./treasury.repository"; +import { TreasuryRepository } from "../../repositories/treasury/treasury.repository"; import { forwardFill, createDailyTimelineFromData } from "./forward-fill"; import { calculateCutoffTimestamp, @@ -15,14 +15,11 @@ import { * Responsibility: Coordinate between provider, repository, and business logic. */ export class TreasuryService { - private repository: TreasuryRepository; - constructor( private provider?: TreasuryProvider, private priceProvider?: PriceProvider, - ) { - this.repository = new TreasuryRepository(); - } + private repository: TreasuryRepository = new TreasuryRepository(), + ) {} /** * Get liquid treasury only (from external providers) From e7dbe3c775747a8ffabc0f8456ba3a3f561f24c8 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Tue, 23 Dec 2025 11:29:43 -0300 Subject: [PATCH 43/50] refactor: inject repo on service --- apps/indexer/src/api/index.ts | 2 ++ apps/indexer/src/api/repositories/index.ts | 1 + .../treasury/{treasury.repository.ts => index.ts} | 0 apps/indexer/src/api/services/treasury/index.ts | 2 +- .../src/api/services/treasury/treasury-provider-factory.ts | 4 +++- apps/indexer/src/api/services/treasury/treasury.service.ts | 4 ++-- 6 files changed, 9 insertions(+), 4 deletions(-) rename apps/indexer/src/api/repositories/treasury/{treasury.repository.ts => index.ts} (100%) diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index 5b1804619..f0f407489 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -38,6 +38,7 @@ import { DrizzleProposalsActivityRepository, NounsVotingPowerRepository, AccountInteractionsRepository, + TreasuryRepository, } from "@/api/repositories"; import { errorHandler } from "@/api/middlewares"; import { getClient } from "@/lib/client"; @@ -138,6 +139,7 @@ const tokenPriceProvider = createTokenPriceProvider( env.DAO_ID, ); const treasuryService = createTreasuryService( + new TreasuryRepository(), tokenPriceProvider, env.DEFILLAMA_API_URL, env.TREASURY_PROVIDER_PROTOCOL_ID, 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/treasury.repository.ts b/apps/indexer/src/api/repositories/treasury/index.ts similarity index 100% rename from apps/indexer/src/api/repositories/treasury/treasury.repository.ts rename to apps/indexer/src/api/repositories/treasury/index.ts diff --git a/apps/indexer/src/api/services/treasury/index.ts b/apps/indexer/src/api/services/treasury/index.ts index bd4940246..c9b31e527 100644 --- a/apps/indexer/src/api/services/treasury/index.ts +++ b/apps/indexer/src/api/services/treasury/index.ts @@ -1,5 +1,5 @@ export * from "./providers"; export * from "./types"; export * from "./treasury.service"; -export * from "../../repositories/treasury/treasury.repository"; +export * from "../../repositories/treasury"; export * from "./forward-fill"; diff --git a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts index 027726221..7460fd47c 100644 --- a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -3,6 +3,7 @@ import { DefiLlamaProvider } from "./providers/defillama–provider"; import { DuneProvider } from "./providers/dune-provider"; import { CoingeckoPriceProvider } from "./providers/coingecko-price-provider"; import { TreasuryService } from "./treasury.service"; +import { TreasuryRepository } from "@/api/repositories/treasury"; import { CoingeckoService } from "@/api/services/coingecko"; import { PriceProvider } from "./types"; import { DaoIdEnum } from "@/lib/enums"; @@ -28,6 +29,7 @@ export function createTokenPriceProvider( * @returns TreasuryService instance */ export function createTreasuryService( + repository: TreasuryRepository, tokenPriceProvider: PriceProvider, defiLlamaApiUrl?: string, defiLlamaProtocolId?: string, @@ -51,5 +53,5 @@ export function createTreasuryService( ); } - return new TreasuryService(liquidProvider, tokenPriceProvider); + 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 index b2bce7092..e0f6968c6 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -2,7 +2,7 @@ import { formatUnits } from "viem"; import { TreasuryProvider } from "./providers"; import { PriceProvider } from "./types"; import { TreasuryResponse } from "@/api/mappers/treasury"; -import { TreasuryRepository } from "../../repositories/treasury/treasury.repository"; +import { TreasuryRepository } from "../../repositories/treasury"; import { forwardFill, createDailyTimelineFromData } from "./forward-fill"; import { calculateCutoffTimestamp, @@ -16,9 +16,9 @@ import { */ export class TreasuryService { constructor( + private repository: TreasuryRepository, private provider?: TreasuryProvider, private priceProvider?: PriceProvider, - private repository: TreasuryRepository = new TreasuryRepository(), ) {} /** From ed851944b43bf8c1bf3320681fbd6d908052588b Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Tue, 23 Dec 2025 12:29:06 -0300 Subject: [PATCH 44/50] refactor: createDailyTimelineFromData to receive a vec --- .../components/AttackCostBarChart.tsx | 2 +- .../indexer/src/api/services/treasury/forward-fill.ts | 11 +++-------- .../src/api/services/treasury/treasury.service.ts | 10 +++++----- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx b/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx index 500ce2d3d..5b621c5e1 100644 --- a/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx +++ b/apps/dashboard/features/attack-profitability/components/AttackCostBarChart.tsx @@ -72,7 +72,7 @@ export const AttackCostBarChart = ({ const timeInterval = TimeInterval.NINETY_DAYS; const { data: liquidTreasuryData, loading: liquidTreasuryLoading } = - useTreasury(selectedDaoId, "liquid", timeInterval); + useTreasury(selectedDaoId, "liquid", TimeInterval.SEVEN_DAYS); const delegatedSupply = useDelegatedSupply(selectedDaoId, timeInterval); const activeSupply = useActiveSupply(selectedDaoId, timeInterval); const averageTurnout = useAverageTurnout(selectedDaoId, timeInterval); diff --git a/apps/indexer/src/api/services/treasury/forward-fill.ts b/apps/indexer/src/api/services/treasury/forward-fill.ts index fc46ecc0b..755cf535b 100644 --- a/apps/indexer/src/api/services/treasury/forward-fill.ts +++ b/apps/indexer/src/api/services/treasury/forward-fill.ts @@ -42,16 +42,11 @@ export function forwardFill( /** * Create daily timeline from first data point to today (midnight UTC) - * Accepts multiple maps and finds the earliest timestamp across all */ -export function createDailyTimelineFromData( - ...dataMaps: Map[] -): number[] { - const allTimestamps = dataMaps.flatMap((map) => [...map.keys()]); +export function createDailyTimelineFromData(timestamps: number[]): number[] { + if (timestamps.length === 0) return []; - if (allTimestamps.length === 0) return []; - - const firstTimestamp = Math.min(...allTimestamps); + const firstTimestamp = Math.min(...timestamps); const todayMidnight = truncateTimestampTimeMs(Date.now()); const totalDays = Math.floor((todayMidnight - firstTimestamp) / ONE_DAY_MS) + 1; diff --git a/apps/indexer/src/api/services/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index e0f6968c6..329b73b5c 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -47,7 +47,7 @@ export class TreasuryService { }); // Create timeline from first data point to today - const timeline = createDailyTimelineFromData(liquidMap); + const timeline = createDailyTimelineFromData([...liquidMap.keys()]); // Forward-fill to remove gaps const filledValues = forwardFill(timeline, liquidMap); @@ -92,10 +92,10 @@ export class TreasuryService { const normalizedPrices = normalizeMapTimestamps(historicalPrices); // Create timeline from first data point to today - const timeline = createDailyTimelineFromData( - normalizedQuantities, - normalizedPrices, - ); + const timeline = createDailyTimelineFromData([ + ...normalizedQuantities.keys(), + ...normalizedPrices.keys(), + ]); // Get last known quantity before cutoff to use as initial value for forward-fill const lastKnownQuantity = From b0e4b901aef13d26df8d2a2a7a7c65cf9996358b Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Tue, 23 Dec 2025 15:15:05 -0300 Subject: [PATCH 45/50] refactor: coingeckoPriceProvider class --- apps/dashboard/shared/dao-config/gtc.ts | 14 +++++------ apps/indexer/src/api/index.ts | 7 ++---- .../providers/coingecko-price-provider.ts | 24 ------------------- .../api/services/treasury/providers/index.ts | 1 - .../treasury/treasury-provider-factory.ts | 15 ------------ 5 files changed, 8 insertions(+), 53 deletions(-) delete mode 100644 apps/indexer/src/api/services/treasury/providers/coingecko-price-provider.ts diff --git a/apps/dashboard/shared/dao-config/gtc.ts b/apps/dashboard/shared/dao-config/gtc.ts index 0bccf4d1e..c68c25057 100644 --- a/apps/dashboard/shared/dao-config/gtc.ts +++ b/apps/dashboard/shared/dao-config/gtc.ts @@ -40,14 +40,12 @@ export const GTC: DaoConfiguration = { proposalThreshold: "150k GTC", }, }, - // attackProfitability: { - // riskLevel: RiskLevel.HIGH, - // 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/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index f0f407489..29a4d568a 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -58,10 +58,7 @@ import { } from "@/api/services"; import { CONTRACT_ADDRESSES } from "@/lib/constants"; import { DaoIdEnum } from "@/lib/enums"; -import { - createTreasuryService, - createTokenPriceProvider, -} from "./services/treasury/treasury-provider-factory"; +import { createTreasuryService } from "./services/treasury/treasury-provider-factory"; const app = new Hono({ defaultHook: (result, c) => { @@ -133,7 +130,7 @@ const accountBalanceService = new BalanceVariationsService( accountInteractionRepo, ); -const tokenPriceProvider = createTokenPriceProvider( +const tokenPriceProvider = new CoingeckoService( env.COINGECKO_API_URL, env.COINGECKO_API_KEY, env.DAO_ID, diff --git a/apps/indexer/src/api/services/treasury/providers/coingecko-price-provider.ts b/apps/indexer/src/api/services/treasury/providers/coingecko-price-provider.ts deleted file mode 100644 index da5b33435..000000000 --- a/apps/indexer/src/api/services/treasury/providers/coingecko-price-provider.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CoingeckoService } from "@/api/services/coingecko"; -import { PriceProvider } from "../types"; -import { truncateTimestampTimeMs } from "@/eventHandlers/shared"; - -/** - * Adapter that wraps CoingeckoService to implement PriceProvider interface. - * Fetches historical token prices from CoinGecko API. - */ -export class CoingeckoPriceProvider implements PriceProvider { - constructor(private coingeckoService: CoingeckoService) {} - - async getHistoricalPrices(days: number): Promise> { - const priceData = await this.coingeckoService.getHistoricalTokenData(days); - - const priceMap = new Map(); - priceData.forEach((item) => { - // Normalize timestamp to midnight UTC - const normalizedTimestamp = truncateTimestampTimeMs(item.timestamp); - priceMap.set(normalizedTimestamp, Number(item.price)); - }); - - return priceMap; - } -} diff --git a/apps/indexer/src/api/services/treasury/providers/index.ts b/apps/indexer/src/api/services/treasury/providers/index.ts index 2978472f1..73710af82 100644 --- a/apps/indexer/src/api/services/treasury/providers/index.ts +++ b/apps/indexer/src/api/services/treasury/providers/index.ts @@ -1,4 +1,3 @@ export * from "./treasury-provider.interface"; export * from "./defillama–provider"; export * from "./dune-provider"; -export * from "./coingecko-price-provider"; diff --git a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts index 7460fd47c..c5d746480 100644 --- a/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts +++ b/apps/indexer/src/api/services/treasury/treasury-provider-factory.ts @@ -1,24 +1,9 @@ import axios from "axios"; import { DefiLlamaProvider } from "./providers/defillama–provider"; import { DuneProvider } from "./providers/dune-provider"; -import { CoingeckoPriceProvider } from "./providers/coingecko-price-provider"; import { TreasuryService } from "./treasury.service"; import { TreasuryRepository } from "@/api/repositories/treasury"; -import { CoingeckoService } from "@/api/services/coingecko"; import { PriceProvider } from "./types"; -import { DaoIdEnum } from "@/lib/enums"; - -/** - * Creates the price provider for token prices (CoinGecko) - */ -export function createTokenPriceProvider( - apiUrl: string, - apiKey: string, - daoId: DaoIdEnum, -): PriceProvider { - const coingeckoService = new CoingeckoService(apiUrl, apiKey, daoId); - return new CoingeckoPriceProvider(coingeckoService); -} /** * Creates a treasury service with optional liquid treasury provider. From 2126cc2dc2ff9e4bbf507b8915d6536efc5a750f Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Tue, 23 Dec 2025 15:19:06 -0300 Subject: [PATCH 46/50] refactor: function name --- apps/indexer/src/api/services/coingecko/index.ts | 16 +++++++++++++++- .../api/services/treasury/treasury.service.ts | 2 +- apps/indexer/src/api/services/treasury/types.ts | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) 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/treasury/treasury.service.ts b/apps/indexer/src/api/services/treasury/treasury.service.ts index 329b73b5c..b328e2092 100644 --- a/apps/indexer/src/api/services/treasury/treasury.service.ts +++ b/apps/indexer/src/api/services/treasury/treasury.service.ts @@ -80,7 +80,7 @@ export class TreasuryService { // Fetch token quantities from DB and prices from CoinGecko const [tokenQuantities, historicalPrices] = await Promise.all([ this.repository.getTokenQuantities(cutoffTimestamp), - this.priceProvider.getHistoricalPrices(days), + this.priceProvider.getHistoricalPricesMap(days), ]); if (tokenQuantities.size === 0 && historicalPrices.size === 0) { diff --git a/apps/indexer/src/api/services/treasury/types.ts b/apps/indexer/src/api/services/treasury/types.ts index 87f45e91e..24640fe3b 100644 --- a/apps/indexer/src/api/services/treasury/types.ts +++ b/apps/indexer/src/api/services/treasury/types.ts @@ -10,5 +10,5 @@ export interface LiquidTreasuryDataPoint { * Interface for fetching historical token prices */ export interface PriceProvider { - getHistoricalPrices(days: number): Promise>; + getHistoricalPricesMap(days: number): Promise>; } From 025c63bb87ac15a705616b2213a4a95521b63de3 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Tue, 23 Dec 2025 15:40:31 -0300 Subject: [PATCH 47/50] refactor: useTreasury --- .../attack-profitability/hooks/useTreasury.ts | 16 ++++------------ .../dao-overview/hooks/useDaoTreasuryStats.ts | 12 +++--------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts index e6aeceb80..75bfe7dc0 100644 --- a/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts +++ b/apps/dashboard/features/attack-profitability/hooks/useTreasury.ts @@ -1,4 +1,4 @@ -import useSWR, { SWRConfiguration } from "swr"; +import useSWR from "swr"; import axios from "axios"; import { DaoIdEnum } from "@/shared/types/daos"; import { TimeInterval } from "@/shared/types/enums/TimeInterval"; @@ -61,21 +61,14 @@ export const useTreasury = ( daoId: DaoIdEnum, type: TreasuryType = "total", days: TimeInterval = TimeInterval.ONE_YEAR, - options?: { - order?: "asc" | "desc"; - config?: Partial>; - }, + order: "asc" | "desc" = "asc", ) => { - const { order = "asc", config } = options || {}; - const key = daoId ? ["treasury", daoId, type, days, order] : null; - - const { data, error, isValidating, mutate } = useSWR( - key, + const { data, error, isValidating } = useSWR( + ["treasury", daoId, type, days, order], () => fetchTreasury({ daoId, type, days, order }), { revalidateOnFocus: false, shouldRetryOnError: false, - ...config, }, ); @@ -84,6 +77,5 @@ export const useTreasury = ( totalCount: data?.totalCount ?? 0, loading: isValidating, error, - refetch: mutate, }; }; diff --git a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts index 16cca7600..8926d8d4e 100644 --- a/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts +++ b/apps/dashboard/features/dao-overview/hooks/useDaoTreasuryStats.ts @@ -16,25 +16,19 @@ export const useDaoTreasuryStats = ({ daoId, "liquid", TimeInterval.SEVEN_DAYS, - { - order: "desc", - }, + "desc", ); const { data: tokenTreasury } = useTreasury( daoId, "dao-token", TimeInterval.SEVEN_DAYS, - { - order: "desc", - }, + "desc", ); const { data: allTreasury } = useTreasury( daoId, "total", TimeInterval.SEVEN_DAYS, - { - order: "desc", - }, + "desc", ); return useMemo(() => { From 3b8db8a3b2b1aefdfcb39b9f92db0c020f150350 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Wed, 7 Jan 2026 17:59:36 -0300 Subject: [PATCH 48/50] refactor: remove unnecessary tokenPriceProvider const --- apps/indexer/src/api/index.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index 29a4d568a..0efffc93e 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -130,21 +130,6 @@ const accountBalanceService = new BalanceVariationsService( accountInteractionRepo, ); -const tokenPriceProvider = new CoingeckoService( - env.COINGECKO_API_URL, - env.COINGECKO_API_KEY, - env.DAO_ID, -); -const treasuryService = createTreasuryService( - new TreasuryRepository(), - tokenPriceProvider, - env.DEFILLAMA_API_URL, - env.TREASURY_PROVIDER_PROTOCOL_ID, - env.DUNE_API_URL, - env.DUNE_API_KEY, -); -treasury(app, treasuryService); - const tokenPriceClient = env.DAO_ID === DaoIdEnum.NOUNS ? new NFTPriceService( @@ -158,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, From 4f13f40c192709fe5a0c951e8fd40a7f724561da Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Wed, 7 Jan 2026 18:00:43 -0300 Subject: [PATCH 49/50] refactor: multilineChartDataSetPoint to possibly have an null key --- .../components/MultilineChartAttackProfitability.tsx | 8 +++++--- .../attack-profitability/utils/normalizeDataset.ts | 7 +++++-- apps/dashboard/shared/dao-config/types.ts | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx index 1d48ec962..0f83f8820 100644 --- a/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx +++ b/apps/dashboard/features/attack-profitability/components/MultilineChartAttackProfitability.tsx @@ -136,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( 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/shared/dao-config/types.ts b/apps/dashboard/shared/dao-config/types.ts index 3cdb34697..bdbd56252 100644 --- a/apps/dashboard/shared/dao-config/types.ts +++ b/apps/dashboard/shared/dao-config/types.ts @@ -23,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 { From 449ec993ffa799b07e0970b1dc3015e1056b2c62 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Wed, 7 Jan 2026 18:16:24 -0300 Subject: [PATCH 50/50] refactor: nFTPriceService to implement foward fill and PriceProvider class --- .../src/api/services/nft-price/index.ts | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) 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; + } }