From dcfc23603a6b6fcfbe040d0e4a6ca419fefd3a2c Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 5 Feb 2026 21:35:09 -0300 Subject: [PATCH 01/41] chore(api): migrate jest config to cjs format Co-Authored-By: Claude Opus 4.5 --- apps/api/{jest.config.js => jest.config.cjs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/api/{jest.config.js => jest.config.cjs} (100%) diff --git a/apps/api/jest.config.js b/apps/api/jest.config.cjs similarity index 100% rename from apps/api/jest.config.js rename to apps/api/jest.config.cjs From ec5e77c74a15a335ab6c6c9ad154dbee4389d91a Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 5 Feb 2026 21:35:16 -0300 Subject: [PATCH 02/41] test(api): refactor delegation-percentage tests Co-Authored-By: Claude Opus 4.5 --- .../delegation-percentage.service.test.ts | 870 ------------------ .../delegation-percentage.test.ts | 72 -- 2 files changed, 942 deletions(-) delete mode 100644 apps/api/src/services/delegation-percentage/delegation-percentage.service.test.ts diff --git a/apps/api/src/services/delegation-percentage/delegation-percentage.service.test.ts b/apps/api/src/services/delegation-percentage/delegation-percentage.service.test.ts deleted file mode 100644 index cecf85adaa..0000000000 --- a/apps/api/src/services/delegation-percentage/delegation-percentage.service.test.ts +++ /dev/null @@ -1,870 +0,0 @@ -import { DelegationPercentageService } from "./delegation-percentage"; -import { DaoMetricsDayBucketRepository } from "@/repositories/"; -import { MetricTypesEnum } from "@/lib/constants"; -import { DBTokenMetric } from "@/mappers/delegation-percentage"; - -/** - * Mock Factory Pattern for type-safe test data - * Creates complete DaoMetricRow objects with sensible defaults - * Only requires specifying fields relevant to each test case - */ -const createMockRow = ( - overwrites: Partial = {}, -): DBTokenMetric => ({ - date: 0n, - daoId: "uniswap", - tokenId: "uni", - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - open: 0n, - close: 0n, - low: 0n, - high: 0n, - average: 0n, - volume: 0n, - count: 0, - lastUpdate: 0n, - ...overwrites, -}); - -describe("DelegationPercentageService", () => { - let service: DelegationPercentageService; - let mockRepository: jest.Mocked< - Pick< - DaoMetricsDayBucketRepository, - "getMetricsByDateRange" | "getLastMetricBeforeDate" - > - >; - - beforeEach(() => { - mockRepository = { - getMetricsByDateRange: jest.fn(), - getLastMetricBeforeDate: jest.fn(), - } as jest.Mocked< - Pick< - DaoMetricsDayBucketRepository, - "getMetricsByDateRange" | "getLastMetricBeforeDate" - > - >; - - service = new DelegationPercentageService(mockRepository); - }); - - describe("delegationPercentageByDay", () => { - it("should return empty response when no data is available", async () => { - mockRepository.getMetricsByDateRange.mockResolvedValue([]); - - const result = await service.delegationPercentageByDay({ - limit: 365, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(0); - expect(result.totalCount).toBe(0); - expect(result.hasNextPage).toBe(false); - expect(result.startDate).toBeNull(); - expect(result.endDate).toBeNull(); - }); - - it("should calculate delegation percentage correctly", async () => { - const mockRows = [ - createMockRow({ - date: 1600041600n, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, // 50 tokens delegated - }), - createMockRow({ - date: 1600041600n, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, // 100 tokens total - }), - ]; - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: "1600041600", - endDate: "1600041600", - limit: 365, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(1); - // 50/100 = 0.5 = 50% - expect(result.items[0]?.high).toBe("50.00"); - expect(result.items[0]?.date).toBe("1600041600"); - }); - - it("should apply forward-fill for missing dates", async () => { - const ONE_DAY = 86400; - const day1 = 1600041600n; - const day2 = day1 + BigInt(ONE_DAY); - const day3 = day2 + BigInt(ONE_DAY); - - const mockRows = [ - // Day 1: 40% delegation - createMockRow({ - date: day1, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 40000000000000000000n, - }), - createMockRow({ - date: day1, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - // Day 3: 60% delegation (day 2 is missing) - createMockRow({ - date: day3, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 60000000000000000000n, - }), - createMockRow({ - date: day3, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]; - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: day1.toString(), - endDate: day3.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(3); - // Day 1: 40% - expect(result.items[0]?.high).toBe("40.00"); - // Day 2: forward-filled from day 1 = 40% - expect(result.items[1]?.high).toBe("40.00"); - expect(result.items[1]?.date).toBe(day2.toString()); - // Day 3: 60% - expect(result.items[2]?.high).toBe("60.00"); - }); - - it("should handle division by zero when total supply is zero", async () => { - const mockRows = [ - createMockRow({ - date: 1600041600n, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date: 1600041600n, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 0n, // zero total supply - }), - ]; - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: "1600041600", - endDate: "1600041600", - limit: 365, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(1); - expect(result.items[0]?.high).toBe("0.00"); // Should be 0 instead of throwing error - }); - - it("should apply pagination with limit", async () => { - const ONE_DAY = 86400; - const mockRows = []; - - // Create 5 days of data - for (let i = 0; i < 5; i++) { - const date = BigInt(1600041600 + i * ONE_DAY); - mockRows.push( - createMockRow({ - date, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ); - } - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: "1600041600", - endDate: "1600387200", // 5 days - limit: 3, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(3); - expect(result.hasNextPage).toBe(true); - expect(result.startDate).toBe("1600041600"); - expect(result.endDate).toBe("1600214400"); - }); - - it("should apply cursor-based pagination with after", async () => { - const ONE_DAY = 86400; - const mockRows = []; - - // Create 5 days of data - for (let i = 0; i < 5; i++) { - const date = BigInt(1600041600 + i * ONE_DAY); - mockRows.push( - createMockRow({ - date, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ); - } - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: "1600041600", - endDate: "1600387200", - after: "1600128000", // After day 2 - limit: 2, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(2); - expect(result.items[0]?.date).toBe("1600214400"); // Day 3 - expect(result.items[1]?.date).toBe("1600300800"); // Day 4 - }); - - it("should apply cursor-based pagination with before", async () => { - const ONE_DAY = 86400; - const mockRows = []; - - // Create 5 days of data - for (let i = 0; i < 5; i++) { - const date = BigInt(1600041600 + i * ONE_DAY); - mockRows.push( - createMockRow({ - date, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ); - } - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: "1600041600", - endDate: "1600387200", - before: "1600214400", // Before day 3 - limit: 2, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(2); - expect(result.items[0]?.date).toBe("1600041600"); // Day 1 - expect(result.items[1]?.date).toBe("1600128000"); // Day 2 - }); - - it("should sort data in descending order when specified", async () => { - const ONE_DAY = 86400; - const mockRows = []; - - // Create 3 days of data - for (let i = 0; i < 3; i++) { - const date = BigInt(1600041600 + i * ONE_DAY); - mockRows.push( - createMockRow({ - date, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: BigInt(30 + i * 10) * BigInt(1e18), - }), - createMockRow({ - date, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ); - } - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: "1600041600", - endDate: "1600214400", - orderDirection: "desc", - limit: 365, - }); - - expect(result.items).toHaveLength(3); - // Should be in descending order - expect(result.items[0]?.date).toBe("1600214400"); // Day 3 - expect(result.items[1]?.date).toBe("1600128000"); // Day 2 - expect(result.items[2]?.date).toBe("1600041600"); // Day 1 - }); - - it("should use default values when optional parameters are not provided", async () => { - mockRepository.getMetricsByDateRange.mockResolvedValue([]); - - const result = await service.delegationPercentageByDay({ - limit: 365, - orderDirection: "asc" as const, - }); - - expect(mockRepository.getMetricsByDateRange).toHaveBeenCalledWith({ - metricTypes: [ - MetricTypesEnum.DELEGATED_SUPPLY, - MetricTypesEnum.TOTAL_SUPPLY, - ], - startDate: undefined, - endDate: undefined, - orderDirection: "asc", - limit: 732, - }); - expect(result.items).toHaveLength(0); - }); - - it("should handle complex scenario with multiple days and changing values", async () => { - const ONE_DAY = 86400; - const day1 = 1600041600n; - const day2 = day1 + BigInt(ONE_DAY); - const day3 = day2 + BigInt(ONE_DAY); - const day4 = day3 + BigInt(ONE_DAY); - - const mockRows = [ - // Day 1: 25% (25/100) - createMockRow({ - date: day1, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 25000000000000000000n, - }), - createMockRow({ - date: day1, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - // Day 2: only total supply changes to 200 -> 25/200 = 12.5% - createMockRow({ - date: day2, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 200000000000000000000n, - }), - // Day 3: delegated changes to 50 -> 50/200 = 25% - createMockRow({ - date: day3, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - // Day 4: no changes -> forward fill 50/200 = 25% - ]; - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: day1.toString(), - endDate: day4.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(4); - // Day 1: 25% - expect(result.items[0]?.high).toBe("25.00"); - // Day 2: 12.5% (25/200) - expect(result.items[1]?.high).toBe("12.50"); - // Day 3: 25% (50/200) - expect(result.items[2]?.high).toBe("25.00"); - // Day 4: 25% (forward-filled) - expect(result.items[3]?.high).toBe("25.00"); - }); - - it("should use last known values before startDate for forward-fill", async () => { - const ONE_DAY = 86400; - const day1 = 1599955200n; - const day100 = day1 + BigInt(ONE_DAY * 100); - const day105 = day100 + BigInt(ONE_DAY * 5); - - mockRepository.getLastMetricBeforeDate - .mockResolvedValueOnce( - createMockRow({ - date: day1, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 40000000000000000000n, - }), - ) - .mockResolvedValueOnce( - createMockRow({ - date: day1, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ); - - mockRepository.getMetricsByDateRange.mockResolvedValue([ - createMockRow({ - date: day105, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 60000000000000000000n, - }), - createMockRow({ - date: day105, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]); - - const result = await service.delegationPercentageByDay({ - startDate: day100.toString(), - endDate: day105.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(6); - // Days 100-104: should use values from day 1 (40/100 = 40%) - expect(result.items[0]?.high).toBe("40.00"); // day 100 - expect(result.items[4]?.high).toBe("40.00"); // day 104 - // Day 105: new value (60/100 = 60%) - expect(result.items[5]?.high).toBe("60.00"); - }); - - it("should handle when only one metric has previous value", async () => { - const ONE_DAY = 86400; - const day50 = 1599955200n; - const day100 = day50 + BigInt(ONE_DAY * 50); - - // Mock: only DELEGATED has previous value - mockRepository.getLastMetricBeforeDate - .mockResolvedValueOnce( - createMockRow({ - date: day50, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 30000000000000000000n, - }), - ) - .mockResolvedValueOnce(undefined); - - // Main data: TOTAL_SUPPLY appears on day 100 - mockRepository.getMetricsByDateRange.mockResolvedValue([ - createMockRow({ - date: day100, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]); - - const result = await service.delegationPercentageByDay({ - startDate: day100.toString(), - endDate: day100.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - // With total = 100 and delegated = 30 (from past) = 30% - expect(result.items[0]?.high).toBe("30.00"); - }); - - it("should start with 0% when no previous values exist", async () => { - const day100 = 1599955200n; - - // Mock: no previous values - mockRepository.getLastMetricBeforeDate - .mockResolvedValueOnce(undefined) - .mockResolvedValueOnce(undefined); - - // Main data: appears only on day 100 - mockRepository.getMetricsByDateRange.mockResolvedValue([ - createMockRow({ - date: day100, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date: day100, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]); - - const result = await service.delegationPercentageByDay({ - startDate: day100.toString(), - endDate: day100.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - // Should use values from day 100 directly (50/100 = 50%) - expect(result.items[0]?.high).toBe("50.00"); - }); - - it("should not fetch previous values when neither startDate nor after is provided", async () => { - mockRepository.getMetricsByDateRange.mockResolvedValue([]); - - await service.delegationPercentageByDay({ - limit: 365, - orderDirection: "asc" as const, - }); - - // Should not call getLastMetricBeforeDate when no reference date - expect(mockRepository.getLastMetricBeforeDate).not.toHaveBeenCalled(); - }); - - it("should fallback to 0 when fetching previous values fails", async () => { - const day100 = 1599955200n; - - // Mock console.error to suppress test output - const consoleErrorSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - - // Mock: error fetching previous values - mockRepository.getLastMetricBeforeDate.mockRejectedValue( - new Error("Database error"), - ); - - // Main data - mockRepository.getMetricsByDateRange.mockResolvedValue([ - createMockRow({ - date: day100, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date: day100, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]); - - const result = await service.delegationPercentageByDay({ - startDate: day100.toString(), - endDate: day100.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - // Should work normally with fallback to 0 (50/100 = 50%) - expect(result.items[0]?.high).toBe("50.00"); - expect(result.items).toHaveLength(1); - - // Verify error was logged - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error fetching initial values:", - expect.any(Error), - ); - - consoleErrorSpy.mockRestore(); - }); - - it("should adjust startDate when requested startDate is before first real data", async () => { - const ONE_DAY = 86400; - const day5 = 1599955200n; - const day10 = day5 + BigInt(ONE_DAY * 5); - const day15 = day10 + BigInt(ONE_DAY * 5); - - // Mock: no values before day 5 - mockRepository.getLastMetricBeforeDate - .mockResolvedValueOnce(undefined) - .mockResolvedValueOnce(undefined); - - // Real data starts on day 10 - mockRepository.getMetricsByDateRange.mockResolvedValue([ - createMockRow({ - date: day10, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 40000000000000000000n, - }), - createMockRow({ - date: day10, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - createMockRow({ - date: day15, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date: day15, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]); - - const result = await service.delegationPercentageByDay({ - startDate: day5.toString(), - endDate: day15.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - // Should start from day 10 (first real data), not day 5 - expect(result.items.length).toBeGreaterThan(0); - expect(result.items[0]?.date).toBe(day10.toString()); - expect(result.items[0]?.high).toBe("40.00"); - - // Should not have data from day 5-9 (before first real data) - const hasDayBefore10 = result.items.some( - (item) => BigInt(item.date) < day10, - ); - expect(hasDayBefore10).toBe(false); - }); - - it("should return empty when startDate is after all available data", async () => { - const day5 = 1599955200n; - const day100 = day5 + BigInt(86400 * 100); - - // Mock: no values before day 100 - mockRepository.getLastMetricBeforeDate - .mockResolvedValueOnce(undefined) - .mockResolvedValueOnce(undefined); - - // Mock: no data >= day 100 - mockRepository.getMetricsByDateRange.mockResolvedValue([]); - - const result = await service.delegationPercentageByDay({ - startDate: day100.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - // Should return empty - expect(result.items).toHaveLength(0); - expect(result.totalCount).toBe(0); - expect(result.hasNextPage).toBe(false); - }); - - it("should fetch previous values and optimize query when only after is provided", async () => { - const ONE_DAY = 86400; - const day1 = 1599955200n; - const day50 = day1 + BigInt(ONE_DAY * 50); - const day100 = day50 + BigInt(ONE_DAY * 50); - - // Mock: values before day50 - mockRepository.getLastMetricBeforeDate - .mockResolvedValueOnce( - createMockRow({ - date: day1, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 30000000000000000000n, // 30% - }), - ) - .mockResolvedValueOnce( - createMockRow({ - date: day1, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ); - - // Mock: data from day50 onwards (query should be optimized) - mockRepository.getMetricsByDateRange.mockResolvedValue([ - createMockRow({ - date: day100, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date: day100, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]); - - const result = await service.delegationPercentageByDay({ - after: day50.toString(), - limit: 365, - orderDirection: "asc" as const, - }); - - // Verify query was optimized (used after as startDate) - expect(mockRepository.getMetricsByDateRange).toHaveBeenCalledWith({ - metricTypes: [ - MetricTypesEnum.DELEGATED_SUPPLY, - MetricTypesEnum.TOTAL_SUPPLY, - ], - startDate: day50.toString(), - endDate: undefined, - orderDirection: "asc", - limit: 732, - }); - - // Verify previous values were fetched - expect(mockRepository.getLastMetricBeforeDate).toHaveBeenCalledTimes(2); - - // Results should have correct forward-fill from previous values - expect(result.items.length).toBeGreaterThan(0); - }); - - it("should optimize query when only before is provided", async () => { - const day1 = 1599955200n; - const day50 = day1 + BigInt(86400 * 50); - - mockRepository.getMetricsByDateRange.mockResolvedValue([ - createMockRow({ - date: day1, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 30000000000000000000n, - }), - createMockRow({ - date: day1, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]); - - const result = await service.delegationPercentageByDay({ - before: day50.toString(), - endDate: day50.toString(), // Add explicit endDate to prevent forward-fill to today - limit: 365, - orderDirection: "asc" as const, - }); - - // Verify query was optimized (used before as endDate) - expect(mockRepository.getMetricsByDateRange).toHaveBeenCalledWith({ - metricTypes: [ - MetricTypesEnum.DELEGATED_SUPPLY, - MetricTypesEnum.TOTAL_SUPPLY, - ], - startDate: undefined, - endDate: day50.toString(), - orderDirection: "asc", - limit: 732, - }); - - // Should not fetch previous values (no startDate or after) - expect(mockRepository.getLastMetricBeforeDate).not.toHaveBeenCalled(); - - // With forward-fill, should generate from day1 to day50 (50 days) - expect(result.items.length).toBeGreaterThan(1); - // First day should have 30% - expect(result.items[0]?.high).toBe("30.00"); - // All days should have forward-filled value of 30% - result.items.forEach((item) => { - expect(item.high).toBe("30.00"); - }); - }); - - it("should forward-fill to today when endDate is not provided", async () => { - const ONE_DAY = 86400; - const threeDaysAgo = Date.now() / 1000 - 3 * ONE_DAY; - const threeDaysAgoMidnight = Math.floor(threeDaysAgo / ONE_DAY) * ONE_DAY; - - // Mock data from 3 days ago (only last data point) - const mockRows = [ - createMockRow({ - date: BigInt(threeDaysAgoMidnight), - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date: BigInt(threeDaysAgoMidnight), - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]; - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: threeDaysAgoMidnight.toString(), - // No endDate - should forward-fill to today - limit: 10, - orderDirection: "asc" as const, - }); - - // Should have data from 3 days ago until today (4 days total) - expect(result.items.length).toBeGreaterThanOrEqual(4); - - // All items should have the same percentage (forward-filled) - // 50/100 = 0.5 = 50% - result.items.forEach((item) => { - expect(item.high).toBe("50.00"); - }); - - // Last item should be today - const todayMidnight = Math.floor(Date.now() / 1000 / ONE_DAY) * ONE_DAY; - expect(result.items[result.items.length - 1]?.date).toBe( - todayMidnight.toString(), - ); - }); - - it("should set hasNextPage to false when reaching today without endDate", async () => { - const ONE_DAY = 86400; - const twoDaysAgo = Date.now() / 1000 - 2 * ONE_DAY; - const twoDaysAgoMidnight = Math.floor(twoDaysAgo / ONE_DAY) * ONE_DAY; - - const mockRows = [ - createMockRow({ - date: BigInt(twoDaysAgoMidnight), - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date: BigInt(twoDaysAgoMidnight), - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]; - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: twoDaysAgoMidnight.toString(), - // No endDate, limit covers all days to today - limit: 10, - orderDirection: "asc" as const, - }); - - // Should have hasNextPage = false because we reached today - expect(result.hasNextPage).toBe(false); - }); - - it("should set hasNextPage to true when limit cuts before today without endDate", async () => { - const ONE_DAY = 86400; - const tenDaysAgo = Date.now() / 1000 - 10 * ONE_DAY; - const tenDaysAgoMidnight = Math.floor(tenDaysAgo / ONE_DAY) * ONE_DAY; - - const mockRows = [ - createMockRow({ - date: BigInt(tenDaysAgoMidnight), - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date: BigInt(tenDaysAgoMidnight), - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ]; - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: tenDaysAgoMidnight.toString(), - // No endDate, but limit only returns 3 items (not reaching today) - limit: 3, - orderDirection: "asc" as const, - }); - - // Should have exactly 3 items - expect(result.items).toHaveLength(3); - - // Should have hasNextPage = true because we didn't reach today - expect(result.hasNextPage).toBe(true); - }); - }); -}); diff --git a/apps/api/src/services/delegation-percentage/delegation-percentage.test.ts b/apps/api/src/services/delegation-percentage/delegation-percentage.test.ts index cecf85adaa..3956019c75 100644 --- a/apps/api/src/services/delegation-percentage/delegation-percentage.test.ts +++ b/apps/api/src/services/delegation-percentage/delegation-percentage.test.ts @@ -207,78 +207,6 @@ describe("DelegationPercentageService", () => { expect(result.endDate).toBe("1600214400"); }); - it("should apply cursor-based pagination with after", async () => { - const ONE_DAY = 86400; - const mockRows = []; - - // Create 5 days of data - for (let i = 0; i < 5; i++) { - const date = BigInt(1600041600 + i * ONE_DAY); - mockRows.push( - createMockRow({ - date, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ); - } - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: "1600041600", - endDate: "1600387200", - after: "1600128000", // After day 2 - limit: 2, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(2); - expect(result.items[0]?.date).toBe("1600214400"); // Day 3 - expect(result.items[1]?.date).toBe("1600300800"); // Day 4 - }); - - it("should apply cursor-based pagination with before", async () => { - const ONE_DAY = 86400; - const mockRows = []; - - // Create 5 days of data - for (let i = 0; i < 5; i++) { - const date = BigInt(1600041600 + i * ONE_DAY); - mockRows.push( - createMockRow({ - date, - metricType: MetricTypesEnum.DELEGATED_SUPPLY, - high: 50000000000000000000n, - }), - createMockRow({ - date, - metricType: MetricTypesEnum.TOTAL_SUPPLY, - high: 100000000000000000000n, - }), - ); - } - - mockRepository.getMetricsByDateRange.mockResolvedValue(mockRows); - - const result = await service.delegationPercentageByDay({ - startDate: "1600041600", - endDate: "1600387200", - before: "1600214400", // Before day 3 - limit: 2, - orderDirection: "asc" as const, - }); - - expect(result.items).toHaveLength(2); - expect(result.items[0]?.date).toBe("1600041600"); // Day 1 - expect(result.items[1]?.date).toBe("1600128000"); // Day 2 - }); - it("should sort data in descending order when specified", async () => { const ONE_DAY = 86400; const mockRows = []; From 0b36cda6b08a26c6c67a63b009d35335ebc87187 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 5 Feb 2026 21:35:23 -0300 Subject: [PATCH 03/41] test(api): add treasury service and controller tests Co-Authored-By: Claude Opus 4.5 --- .../treasury/treasury.integration.test.ts | 371 ++++++++++++++++++ .../treasury/providers/provider-cache.test.ts | 147 +++++++ .../treasury/treasury.service.test.ts | 366 +++++++++++++++++ 3 files changed, 884 insertions(+) create mode 100644 apps/api/src/controllers/treasury/treasury.integration.test.ts create mode 100644 apps/api/src/services/treasury/providers/provider-cache.test.ts create mode 100644 apps/api/src/services/treasury/treasury.service.test.ts diff --git a/apps/api/src/controllers/treasury/treasury.integration.test.ts b/apps/api/src/controllers/treasury/treasury.integration.test.ts new file mode 100644 index 0000000000..6e5721c03e --- /dev/null +++ b/apps/api/src/controllers/treasury/treasury.integration.test.ts @@ -0,0 +1,371 @@ +import { OpenAPIHono as Hono } from "@hono/zod-openapi"; +import { testClient } from "hono/testing"; +import { treasury } from "./index"; +import { TreasuryService } from "@/services/treasury"; +import { TreasuryProvider, LiquidTreasuryDataPoint, PriceProvider } from "@/services/treasury"; +import { TreasuryRepository } from "@/repositories/treasury"; + +/** + * Fakes for dependency injection + */ +class FakeTreasuryProvider implements TreasuryProvider { + private data: LiquidTreasuryDataPoint[] = []; + + setData(data: LiquidTreasuryDataPoint[]) { + this.data = data; + } + + async fetchTreasury(_cutoffTimestamp: number): Promise { + return this.data; + } +} + +class FakePriceProvider implements PriceProvider { + private prices: Map = new Map(); + + setPrices(prices: Map) { + this.prices = prices; + } + + async getHistoricalPricesMap(_days: number): Promise> { + return this.prices; + } +} + +class FakeTreasuryRepository { + private tokenQuantities: Map = new Map(); + private lastKnownQuantity: bigint | null = null; + + setTokenQuantities(quantities: Map) { + this.tokenQuantities = quantities; + } + + setLastKnownQuantity(quantity: bigint | null) { + this.lastKnownQuantity = quantity; + } + + async getTokenQuantities(_cutoffTimestamp: number): Promise> { + return this.tokenQuantities; + } + + async getLastTokenQuantityBeforeDate(_cutoffTimestamp: number): Promise { + return this.lastKnownQuantity; + } +} + +/** + * Creates a test app with treasury routes + */ +function createTestApp( + treasuryService: TreasuryService, + decimals: number = 18, +) { + const app = new Hono(); + treasury(app, treasuryService, decimals); + return app; +} + +describe("Treasury Controller - Integration Tests", () => { + const FIXED_DATE = new Date("2026-01-15T00:00:00Z"); + const FIXED_TIMESTAMP = Math.floor(FIXED_DATE.getTime() / 1000); + const ONE_DAY = 86400; + + let fakeProvider: FakeTreasuryProvider; + let fakePriceProvider: FakePriceProvider; + let fakeRepository: FakeTreasuryRepository; + + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(FIXED_DATE); + + fakeProvider = new FakeTreasuryProvider(); + fakePriceProvider = new FakePriceProvider(); + fakeRepository = new FakeTreasuryRepository(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe("GET /treasury/liquid", () => { + it("should return 200 with valid response structure", async () => { + fakeProvider.setData([ + { date: FIXED_TIMESTAMP - ONE_DAY, liquidTreasury: 1000000 }, + { date: FIXED_TIMESTAMP, liquidTreasury: 1100000 }, + ]); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + const app = createTestApp(service); + const client = testClient(app); + + const res = await client["treasury"]["liquid"].$get({ + query: { days: "7d", order: "asc" }, + }); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data).toHaveProperty("items"); + expect(data).toHaveProperty("totalCount"); + expect(Array.isArray(data.items)).toBe(true); + expect(data.totalCount).toBe(data.items.length); + }); + + it("should return items with correct shape (date and value)", async () => { + fakeProvider.setData([ + { date: FIXED_TIMESTAMP, liquidTreasury: 5000000 }, + ]); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + const app = createTestApp(service); + const client = testClient(app); + + const res = await client["treasury"]["liquid"].$get({ + query: { days: "7d" }, + }); + + const data = await res.json(); + const item = data.items[0]; + + expect(item).toHaveProperty("date"); + expect(item).toHaveProperty("value"); + expect(typeof item?.date).toBe("number"); + expect(typeof item?.value).toBe("number"); + }); + + it("should use default values when query params are omitted", async () => { + fakeProvider.setData([ + { date: FIXED_TIMESTAMP, liquidTreasury: 1000000 }, + ]); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + const app = createTestApp(service); + const client = testClient(app); + + const res = await client["treasury"]["liquid"].$get({ + query: {}, + }); + + expect(res.status).toBe(200); + }); + + it("should return empty items when no data available", async () => { + fakeProvider.setData([]); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + const app = createTestApp(service); + const client = testClient(app); + + const res = await client["treasury"]["liquid"].$get({ + query: { days: "7d" }, + }); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.items).toHaveLength(0); + expect(data.totalCount).toBe(0); + }); + + it("should respect order parameter", async () => { + const day1 = FIXED_TIMESTAMP - ONE_DAY * 2; + const day2 = FIXED_TIMESTAMP - ONE_DAY; + const day3 = FIXED_TIMESTAMP; + + fakeProvider.setData([ + { date: day1, liquidTreasury: 1000 }, + { date: day2, liquidTreasury: 2000 }, + { date: day3, liquidTreasury: 3000 }, + ]); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + const app = createTestApp(service); + const client = testClient(app); + + const resAsc = await client["treasury"]["liquid"].$get({ + query: { days: "7d", order: "asc" }, + }); + const dataAsc = await resAsc.json(); + + const resDesc = await client["treasury"]["liquid"].$get({ + query: { days: "7d", order: "desc" }, + }); + const dataDesc = await resDesc.json(); + + expect(dataAsc.items[0]?.date).toBeLessThan(dataAsc.items[dataAsc.items.length - 1]?.date ?? 0); + expect(dataDesc.items[0]?.date).toBeGreaterThan(dataDesc.items[dataDesc.items.length - 1]?.date ?? 0); + }); + }); + + describe("GET /treasury/dao-token", () => { + it("should return 200 with calculated token treasury", async () => { + const quantity = 100n * 10n ** 18n; // 100 tokens + const price = 50; // $50 per token + + fakeRepository.setTokenQuantities(new Map([[FIXED_TIMESTAMP, quantity]])); + fakePriceProvider.setPrices(new Map([[FIXED_TIMESTAMP, price]])); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + fakePriceProvider, + ); + const app = createTestApp(service, 18); + const client = testClient(app); + + const res = await client["treasury"]["dao-token"].$get({ + query: { days: "7d" }, + }); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data).toHaveProperty("items"); + expect(data).toHaveProperty("totalCount"); + + const itemWithValue = data.items.find((item: { value: number }) => item.value > 0); + expect(itemWithValue?.value).toBe(5000); // 100 * $50 + }); + + it("should return empty when price provider is not configured", async () => { + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + undefined, + ); + const app = createTestApp(service); + const client = testClient(app); + + const res = await client["treasury"]["dao-token"].$get({ + query: { days: "7d" }, + }); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.items).toHaveLength(0); + }); + }); + + describe("GET /treasury/total", () => { + it("should return sum of liquid and token treasury", async () => { + // Liquid: $5,000,000 + fakeProvider.setData([ + { date: FIXED_TIMESTAMP, liquidTreasury: 5000000 }, + ]); + + // Token: 1000 tokens * $100 = $100,000 + const quantity = 1000n * 10n ** 18n; + fakeRepository.setTokenQuantities(new Map([[FIXED_TIMESTAMP, quantity]])); + fakePriceProvider.setPrices(new Map([[FIXED_TIMESTAMP, 100]])); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + fakePriceProvider, + ); + const app = createTestApp(service, 18); + const client = testClient(app); + + const res = await client["treasury"]["total"].$get({ + query: { days: "7d" }, + }); + + expect(res.status).toBe(200); + + const data = await res.json(); + const todayItem = data.items.find((item: { date: number }) => item.date === FIXED_TIMESTAMP); + + // Total = $5,000,000 + $100,000 = $5,100,000 + expect(todayItem?.value).toBe(5100000); + }); + + it("should work when only liquid treasury has data", async () => { + fakeProvider.setData([ + { date: FIXED_TIMESTAMP, liquidTreasury: 1000000 }, + ]); + fakeRepository.setTokenQuantities(new Map()); + fakePriceProvider.setPrices(new Map()); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + fakePriceProvider, + ); + const app = createTestApp(service); + const client = testClient(app); + + const res = await client["treasury"]["total"].$get({ + query: { days: "7d" }, + }); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.items.length).toBeGreaterThan(0); + }); + }); + + describe("Query Parameter Validation", () => { + it("should accept all valid days options", async () => { + fakeProvider.setData([]); + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + const app = createTestApp(service); + const client = testClient(app); + + const validDays = ["7d", "30d", "90d", "180d", "365d"] as const; + + for (const days of validDays) { + const res = await client["treasury"]["liquid"].$get({ + query: { days }, + }); + expect(res.status).toBe(200); + } + }); + + it("should accept both asc and desc order", async () => { + fakeProvider.setData([]); + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + const app = createTestApp(service); + const client = testClient(app); + + const resAsc = await client["treasury"]["liquid"].$get({ + query: { days: "7d", order: "asc" }, + }); + const resDesc = await client["treasury"]["liquid"].$get({ + query: { days: "7d", order: "desc" }, + }); + + expect(resAsc.status).toBe(200); + expect(resDesc.status).toBe(200); + }); + }); +}); diff --git a/apps/api/src/services/treasury/providers/provider-cache.test.ts b/apps/api/src/services/treasury/providers/provider-cache.test.ts new file mode 100644 index 0000000000..4c637d68c0 --- /dev/null +++ b/apps/api/src/services/treasury/providers/provider-cache.test.ts @@ -0,0 +1,147 @@ +import { TreasuryProviderCache } from "./provider-cache"; +import { LiquidTreasuryDataPoint } from "../types"; + +function createDataPoint( + overrides?: Partial, +): LiquidTreasuryDataPoint { + return { + date: 1700000000, + liquidTreasury: 1000000, + ...overrides, + }; +} + +describe("TreasuryProviderCache", () => { + let cache: TreasuryProviderCache; + + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2025-01-15T12:00:00Z")); + cache = new TreasuryProviderCache(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe("get", () => { + it("should return null when cache is empty", () => { + const result = cache.get(); + + expect(result).toBeNull(); + }); + + it("should return cached data when not expired", () => { + const data = [createDataPoint(), createDataPoint({ date: 1700086400 })]; + cache.set(data); + + const result = cache.get(); + + expect(result).toEqual(data); + }); + + it("should return null when cache is expired", () => { + const data = [createDataPoint()]; + cache.set(data); + jest.advanceTimersByTime(25 * 60 * 60 * 1000); + + const result = cache.get(); + + expect(result).toBeNull(); + }); + + it("should clear cache when expired", () => { + const data = [createDataPoint()]; + cache.set(data); + jest.advanceTimersByTime(25 * 60 * 60 * 1000); + + cache.get(); + + jest.setSystemTime(new Date("2025-01-16T13:00:00Z")); + const newData = [createDataPoint({ liquidTreasury: 2000000 })]; + cache.set(newData); + + expect(cache.get()).toEqual(newData); + }); + + it("should return data at exactly 24 hours (boundary)", () => { + const data = [createDataPoint()]; + cache.set(data); + jest.advanceTimersByTime(24 * 60 * 60 * 1000); + + const result = cache.get(); + + expect(result).toEqual(data); + }); + + it("should return null at 24 hours + 1ms (just expired)", () => { + const data = [createDataPoint()]; + cache.set(data); + jest.advanceTimersByTime(24 * 60 * 60 * 1000 + 1); + + const result = cache.get(); + + expect(result).toBeNull(); + }); + }); + + describe("set", () => { + it("should store data that can be retrieved", () => { + const data = [ + createDataPoint({ date: 1700000000, liquidTreasury: 500000 }), + ]; + + cache.set(data); + + expect(cache.get()).toEqual(data); + }); + + it("should overwrite previous data", () => { + const oldData = [createDataPoint({ liquidTreasury: 100 })]; + const newData = [createDataPoint({ liquidTreasury: 200 })]; + cache.set(oldData); + + cache.set(newData); + + expect(cache.get()).toEqual(newData); + }); + + it("should reset TTL when setting new data", () => { + const oldData = [createDataPoint({ liquidTreasury: 100 })]; + cache.set(oldData); + jest.advanceTimersByTime(23 * 60 * 60 * 1000); + + const newData = [createDataPoint({ liquidTreasury: 200 })]; + cache.set(newData); + jest.advanceTimersByTime(23 * 60 * 60 * 1000); + + const result = cache.get(); + + expect(result).toEqual(newData); + }); + + it("should handle empty array", () => { + const data: LiquidTreasuryDataPoint[] = []; + + cache.set(data); + + expect(cache.get()).toEqual([]); + }); + }); + + describe("clear", () => { + it("should remove all cached data", () => { + const data = [createDataPoint()]; + cache.set(data); + + cache.clear(); + + expect(cache.get()).toBeNull(); + }); + + it("should be safe to call on empty cache", () => { + expect(() => cache.clear()).not.toThrow(); + expect(cache.get()).toBeNull(); + }); + }); +}); diff --git a/apps/api/src/services/treasury/treasury.service.test.ts b/apps/api/src/services/treasury/treasury.service.test.ts new file mode 100644 index 0000000000..ece7a19385 --- /dev/null +++ b/apps/api/src/services/treasury/treasury.service.test.ts @@ -0,0 +1,366 @@ +import { TreasuryService } from "./treasury.service"; +import { TreasuryProvider } from "./providers"; +import { PriceProvider, LiquidTreasuryDataPoint } from "./types"; +import { TreasuryRepository } from "@/repositories/treasury"; + +/** + * Fakes for dependency injection + */ +class FakeTreasuryProvider implements TreasuryProvider { + private data: LiquidTreasuryDataPoint[] = []; + + setData(data: LiquidTreasuryDataPoint[]) { + this.data = data; + } + + async fetchTreasury(_cutoffTimestamp: number): Promise { + return this.data; + } +} + +class FakePriceProvider implements PriceProvider { + private prices: Map = new Map(); + + setPrices(prices: Map) { + this.prices = prices; + } + + async getHistoricalPricesMap(_days: number): Promise> { + return this.prices; + } +} + +class FakeTreasuryRepository { + private tokenQuantities: Map = new Map(); + private lastKnownQuantity: bigint | null = null; + + setTokenQuantities(quantities: Map) { + this.tokenQuantities = quantities; + } + + setLastKnownQuantity(quantity: bigint | null) { + this.lastKnownQuantity = quantity; + } + + async getTokenQuantities(_cutoffTimestamp: number): Promise> { + return this.tokenQuantities; + } + + async getLastTokenQuantityBeforeDate(_cutoffTimestamp: number): Promise { + return this.lastKnownQuantity; + } +} + +describe("TreasuryService", () => { + const FIXED_DATE = new Date("2026-01-15T00:00:00Z"); + const FIXED_TIMESTAMP = Math.floor(FIXED_DATE.getTime() / 1000); // 1736937600 + const ONE_DAY = 86400; + + let fakeProvider: FakeTreasuryProvider; + let fakePriceProvider: FakePriceProvider; + let fakeRepository: FakeTreasuryRepository; + + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(FIXED_DATE); + + fakeProvider = new FakeTreasuryProvider(); + fakePriceProvider = new FakePriceProvider(); + fakeRepository = new FakeTreasuryRepository(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe("getLiquidTreasury", () => { + it("should return empty when provider is undefined", async () => { + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + undefined, + ); + + const result = await service.getLiquidTreasury(7, "asc"); + + expect(result.items).toHaveLength(0); + expect(result.totalCount).toBe(0); + }); + + it("should return empty when provider returns empty array", async () => { + fakeProvider.setData([]); + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + + const result = await service.getLiquidTreasury(7, "asc"); + + expect(result.items).toHaveLength(0); + expect(result.totalCount).toBe(0); + }); + + it("should return items sorted ascending", async () => { + const day1 = FIXED_TIMESTAMP - ONE_DAY * 2; + const day2 = FIXED_TIMESTAMP - ONE_DAY; + const day3 = FIXED_TIMESTAMP; + + fakeProvider.setData([ + { date: day1, liquidTreasury: 1000 }, + { date: day2, liquidTreasury: 2000 }, + { date: day3, liquidTreasury: 3000 }, + ]); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + + const result = await service.getLiquidTreasury(7, "asc"); + + expect(result.items.length).toBeGreaterThanOrEqual(3); + expect(result.items[0]?.date).toBeLessThan(result.items[1]?.date ?? 0); + expect(result.totalCount).toBe(result.items.length); + }); + + it("should return items sorted descending", async () => { + const day1 = FIXED_TIMESTAMP - ONE_DAY * 2; + const day2 = FIXED_TIMESTAMP - ONE_DAY; + const day3 = FIXED_TIMESTAMP; + + fakeProvider.setData([ + { date: day1, liquidTreasury: 1000 }, + { date: day2, liquidTreasury: 2000 }, + { date: day3, liquidTreasury: 3000 }, + ]); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + undefined, + ); + + const result = await service.getLiquidTreasury(7, "desc"); + + expect(result.items.length).toBeGreaterThanOrEqual(3); + expect(result.items[0]?.date).toBeGreaterThan(result.items[1]?.date ?? 0); + }); + }); + + describe("getTokenTreasury", () => { + it("should return empty when priceProvider is undefined", async () => { + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + undefined, + ); + + const result = await service.getTokenTreasury(7, "asc", 18); + + expect(result.items).toHaveLength(0); + expect(result.totalCount).toBe(0); + }); + + it("should return empty when repository and priceProvider return empty", async () => { + fakeRepository.setTokenQuantities(new Map()); + fakePriceProvider.setPrices(new Map()); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + fakePriceProvider, + ); + + const result = await service.getTokenTreasury(7, "asc", 18); + + expect(result.items).toHaveLength(0); + expect(result.totalCount).toBe(0); + }); + + it("should calculate value correctly with decimals", async () => { + // Both repository and priceProvider return timestamps in seconds (normalized to midnight) + const dayTimestamp = FIXED_TIMESTAMP; + + // 100 tokens with 18 decimals = 100 * 10^18 + const quantity = 100n * 10n ** 18n; + const price = 10; // $10 per token + + fakeRepository.setTokenQuantities(new Map([[dayTimestamp, quantity]])); + fakePriceProvider.setPrices(new Map([[dayTimestamp, price]])); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + fakePriceProvider, + ); + + const result = await service.getTokenTreasury(7, "asc", 18); + + // 100 tokens * $10 = $1000 + const todayItem = result.items.find((item) => item.value > 0); + expect(todayItem?.value).toBe(1000); + }); + + it("should return items sorted ascending", async () => { + const day1 = FIXED_TIMESTAMP - ONE_DAY * 2; + const day2 = FIXED_TIMESTAMP - ONE_DAY; + + fakeRepository.setTokenQuantities( + new Map([ + [day1, 100n * 10n ** 18n], + [day2, 200n * 10n ** 18n], + ]), + ); + fakePriceProvider.setPrices( + new Map([ + [day1, 10], + [day2, 10], + ]), + ); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + fakePriceProvider, + ); + + const result = await service.getTokenTreasury(7, "asc", 18); + + expect(result.items.length).toBeGreaterThanOrEqual(2); + expect(result.items[0]?.date).toBeLessThan(result.items[1]?.date ?? 0); + }); + + it("should return items sorted descending", async () => { + const day1 = FIXED_TIMESTAMP - ONE_DAY * 2; + const day2 = FIXED_TIMESTAMP - ONE_DAY; + + fakeRepository.setTokenQuantities( + new Map([ + [day1, 100n * 10n ** 18n], + [day2, 200n * 10n ** 18n], + ]), + ); + fakePriceProvider.setPrices( + new Map([ + [day1, 10], + [day2, 10], + ]), + ); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + fakePriceProvider, + ); + + const result = await service.getTokenTreasury(7, "desc", 18); + + expect(result.items.length).toBeGreaterThanOrEqual(2); + expect(result.items[0]?.date).toBeGreaterThan(result.items[1]?.date ?? 0); + }); + + it("should use lastKnownQuantity from repository", async () => { + const dayTimestamp = FIXED_TIMESTAMP; + + // No quantities in range, but has last known value + fakeRepository.setTokenQuantities(new Map()); + fakeRepository.setLastKnownQuantity(50n * 10n ** 18n); // 50 tokens + fakePriceProvider.setPrices(new Map([[dayTimestamp, 20]])); // $20 per token + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, + fakePriceProvider, + ); + + const result = await service.getTokenTreasury(7, "asc", 18); + + // Should use last known quantity: 50 * $20 = $1000 + const itemWithValue = result.items.find((item) => item.value > 0); + expect(itemWithValue?.value).toBe(1000); + }); + }); + + describe("getTotalTreasury", () => { + it("should return empty when both liquid and token are empty", async () => { + fakeProvider.setData([]); + fakeRepository.setTokenQuantities(new Map()); + fakePriceProvider.setPrices(new Map()); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + fakePriceProvider, + ); + + const result = await service.getTotalTreasury(7, "asc", 18); + + expect(result.items).toHaveLength(0); + expect(result.totalCount).toBe(0); + }); + + it("should sum liquid and token treasury correctly", async () => { + const dayTimestamp = FIXED_TIMESTAMP; + + // Liquid: $5000 + fakeProvider.setData([{ date: dayTimestamp, liquidTreasury: 5000 }]); + + // Token: 100 tokens * $30 = $3000 + fakeRepository.setTokenQuantities(new Map([[dayTimestamp, 100n * 10n ** 18n]])); + fakePriceProvider.setPrices(new Map([[dayTimestamp, 30]])); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + fakePriceProvider, + ); + + const result = await service.getTotalTreasury(7, "asc", 18); + + // Total = $5000 + $3000 = $8000 + const todayItem = result.items.find((item) => item.date === dayTimestamp); + expect(todayItem?.value).toBe(8000); + }); + + it("should work when only liquid has data", async () => { + fakeProvider.setData([{ date: FIXED_TIMESTAMP, liquidTreasury: 5000 }]); + fakeRepository.setTokenQuantities(new Map()); + fakePriceProvider.setPrices(new Map()); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + fakeProvider, + fakePriceProvider, + ); + + const result = await service.getTotalTreasury(7, "asc", 18); + + expect(result.items.length).toBeGreaterThan(0); + const todayItem = result.items.find((item) => item.date === FIXED_TIMESTAMP); + expect(todayItem?.value).toBe(5000); + }); + + it("should work when only token has data", async () => { + const dayTimestamp = FIXED_TIMESTAMP; + + fakeProvider.setData([]); + fakeRepository.setTokenQuantities(new Map([[dayTimestamp, 100n * 10n ** 18n]])); + fakePriceProvider.setPrices(new Map([[dayTimestamp, 25]])); + + const service = new TreasuryService( + fakeRepository as unknown as TreasuryRepository, + undefined, // no liquid provider + fakePriceProvider, + ); + + const result = await service.getTotalTreasury(7, "asc", 18); + + expect(result.items.length).toBeGreaterThan(0); + // 100 tokens * $25 = $2500 + const itemWithValue = result.items.find((item) => item.value > 0); + expect(itemWithValue?.value).toBe(2500); + }); + }); +}); From a7e5ea866999fc69249e90742e69810f10f6f55f Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 5 Feb 2026 21:35:28 -0300 Subject: [PATCH 04/41] docs: update CLAUDE.md instructions Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2b2767d67f..e3e58838e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,7 +173,7 @@ docker-compose up -d # Start PostgreSQL and local blockchain - Functions: camelCase - **Git**: Conventional Commits with commitlint - **Type Safety**: Full TypeScript coverage -- **Testing**: Jest, Vitest, Playwright for different layers +- **Testing**: Jest, Vitest, Playwright for different layers (see @.claude/rules/testing.md for guidelines) ## Configuration Files From 1b0fdb6bfc5aec2386eb995dd99df01030a2b5b4 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Thu, 5 Feb 2026 21:35:33 -0300 Subject: [PATCH 05/41] chore: add .claude configuration directory Co-Authored-By: Claude Opus 4.5 --- .claude/rules/testing.md | 289 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 .claude/rules/testing.md diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000000..42ef062f29 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,289 @@ +# Testing Guidelines + +Follow these rules when writing tests for the Anticapture project. + +--- + +## Rule 1: Test Pyramid + +Follow the test pyramid strategy with three test types. +**Target ratio:** ~70% unit, ~20% integration, ~10% E2E + +1. **Unit Tests** (majority) + - Fast, cheap, isolated + - Test business logic in services and utilities + - Mock external dependencies (database, APIs) + +2. **Integration Tests** (moderate) + - Test component interactions + - Verify API endpoints with real HTTP calls + - May use test database + +3. **E2E Tests** (few) + - Test critical user flows end-to-end + - Slow and expensive to maintain + - Reserve for high-value scenarios only + +--- + +## Rule 2: Test Doubles Strategy + +Prefer **stubs** and **fakes** over mocks to avoid brittle tests. + +| Type | Use When | Example | +| -------- | ---------------------------------------------- | ----------------------------------- | +| **Stub** | Need fixed return values | `{ findAll: () => [mockData] }` | +| **Fake** | Need working simplified implementation | `InMemoryRepository` | +| **Mock** | Need to verify a call was made (use sparingly) | `jest.fn()` with `toHaveBeenCalled` | + +### Why? + +- **Mocks verify implementation** → tests break on refactor +- **Stubs/Fakes verify behavior** → tests survive refactor + +### Example + +```typescript +// ❌ Avoid: mock that verifies implementation +expect(repository.findById).toHaveBeenCalledWith("123"); + +// ✅ Prefer: stub that enables behavior testing +const stub = { findById: () => mockAccount }; +const result = await service.getAccount("123"); +expect(result.name).toBe("vitalik.eth"); +``` + +Exception: Use mocks when the call itself IS the behavior (e.g., verifying an event was emitted, an email was sent). + +--- + +## Rule 3: Arrange-Act-Assert (AAA) Pattern + +Structure every test using AAA for readability and consistency. **Do not write `// Arrange`, `// Act`, `// Assert` comments** — use blank lines to separate sections visually. + +### Structure + +```typescript +it("should calculate delegation percentage", async () => { + const mockData = [createMockRow({ delegated: 100n, total: 1000n })]; + stubRepository.getDelegationPercentage.mockResolvedValue(mockData); + + const result = await service.execute({ dao: "uni" }); + + expect(result[0].percentage).toBe(10); +}); +``` + +--- + +## Rule 4: Test Coverage Policy + +Write tests for all new business logic. There is no minimum coverage enforced in CI. + +### What must have tests: + +- Services and their business rules +- Utility functions (`lib/`) +- Calculations (voting power, percentages, etc.) +- Data transformations (mappers) + +### What may not have tests: + +- Infrastructure/boilerplate code +- UI components (nice-to-have, not required) +- Configuration and wiring + +> **Philosophy:** High coverage doesn't mean well-tested code. 100% coverage with bad tests is worse than 60% coverage with meaningful tests. + +--- + +## Rule 5: Test Happy Paths and Break Your Code + +Cover the **happy path** first, then actively try to break your code. + +### Mindset + +Don't just prove the code works — **try to break it**. Think like a malicious user or an unstable system. + +### Common edge cases to consider: + +| Category | Examples | +| ------------------------ | ---------------------------------------------------- | +| **Empty values** | `null`, `undefined`, `[]`, `""`, `{}` | +| **Zeros and boundaries** | `0`, `1`, `-1`, `MAX_INT`, overflow | +| **Divisions** | Division by zero, percentages > 100% | +| **Dates** | Future timestamps, distant past, midnight edge cases | +| **Strings** | Unicode, whitespace, special characters | +| **Concurrency** | Missing data, unexpected order | + +### Example + +```typescript +// ❌ Happy path only +it("should calculate percentage", () => { + expect(calcPercentage(50, 100)).toBe(50); +}); + +// ✅ Happy path + trying to break it +it("should calculate percentage", () => { + expect(calcPercentage(50, 100)).toBe(50); +}); + +it("should handle division by zero", () => { + expect(calcPercentage(50, 0)).toBe(0); // or throw? +}); + +it("should handle when value exceeds total", () => { + expect(calcPercentage(150, 100)).toBe(100); // cap? or 150? +}); +``` + +--- + +## Rule 6: Deterministic Tests + +Write tests that produce the **same result every time**, in any environment, in any order. + +### Never depend on: + +- Execution order between tests +- System date/time (`Date.now()`) +- Data generated by other tests +- Randomness without a fixed seed +- Shared global state + +### How to handle dates + +```typescript +// ❌ Non-deterministic +const result = service.getRecentItems(); // uses Date.now() internally + +// ✅ Deterministic - mock the time +beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2025-01-15T00:00:00Z")); +}); + +afterEach(() => { + jest.useRealTimers(); +}); +``` + +### How to ensure isolation + +```typescript +// ✅ Each test creates its own data +it("should find account by id", () => { + const account = createAccount({ id: "test-123" }); + // ... +}); + +// ❌ Depends on data created in another test +it("should find the account", () => { + const account = repository.findById("test-123"); // created where? +}); +``` + +--- + +## Useful Tools + +### 1. Testing HTTP (Hono) + +Use Hono's native test client: + +```ts +import { testClient } from "hono/testing"; +import { app } from "../app"; +const client = testClient(app); + +it("should return proposals", async () => { + const res = await client.proposals.$get({ + query: { dao: "uni", limit: "10" }, + }); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.proposals).toHaveLength(10); +}); +``` + +### 2. Test Database + +Use Testcontainers for real PostgreSQL in Docker: + +```ts +import { PostgreSqlContainer } from "@testcontainers/postgresql"; + +let container: PostgreSqlContainer; + +beforeAll(async () => { + container = await new PostgreSqlContainer().start(); + process.env.DATABASE_URL = container.getConnectionUri(); + await runMigrations(); +}); + +afterAll(async () => { + await container.stop(); +}); +``` + +### 3. External APIs + +Use MSW (Mock Service Worker) to intercept HTTP requests: + +```ts +import { setupServer } from "msw/node"; +import { http, HttpResponse } from "msw"; + +const server = setupServer( + http.get("https://api.coingecko.com/api/v3/simple/price", () => { + return HttpResponse.json({ + uniswap: { usd: 7.5 }, + }); + }), +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); +``` + +--- + +## Important Patterns + +### 1. Test Data Builders / Factories + +```ts +// factories/proposal.ts +export function createProposal(overrides?: Partial): Proposal { + return { + id: randomId(), + dao: "uni", + title: "Test Proposal", + status: "active", + createdAt: new Date(), + ...overrides, + }; +} + +// In the test +const proposal = createProposal({ status: "executed" }); +await db.insert(proposals).values(proposal); +``` + +### 2. Database Seeding + +```ts +async function seedTestData() { + const account = createAccount({ address: "0x123" }); + const proposal = createProposal({ dao: "uni" }); + const vote = createVote({ proposalId: proposal.id, voter: account.address }); + + await db.insert(accounts).values(account); + await db.insert(proposals).values(proposal); + await db.insert(votes).values(vote); + + return { account, proposal, vote }; +} +``` From ef551256dbad764b7629c50388aa00bdb088e8fd Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Fri, 6 Feb 2026 11:03:07 -0300 Subject: [PATCH 06/41] feat: any cast --- .../treasury/treasury.integration.test.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/api/src/controllers/treasury/treasury.integration.test.ts b/apps/api/src/controllers/treasury/treasury.integration.test.ts index 6e5721c03e..987ebdb9bc 100644 --- a/apps/api/src/controllers/treasury/treasury.integration.test.ts +++ b/apps/api/src/controllers/treasury/treasury.integration.test.ts @@ -55,6 +55,10 @@ class FakeTreasuryRepository { /** * Creates a test app with treasury routes + * + * Note: testClient(app) returns 'unknown' because Hono can't infer types + * when routes are added dynamically. We use 'as any' as a workaround. + * See: https://github.com/honojs/hono/issues/3148 */ function createTestApp( treasuryService: TreasuryService, @@ -100,7 +104,7 @@ describe("Treasury Controller - Integration Tests", () => { undefined, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const res = await client["treasury"]["liquid"].$get({ query: { days: "7d", order: "asc" }, @@ -126,7 +130,7 @@ describe("Treasury Controller - Integration Tests", () => { undefined, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const res = await client["treasury"]["liquid"].$get({ query: { days: "7d" }, @@ -152,7 +156,7 @@ describe("Treasury Controller - Integration Tests", () => { undefined, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const res = await client["treasury"]["liquid"].$get({ query: {}, @@ -170,7 +174,7 @@ describe("Treasury Controller - Integration Tests", () => { undefined, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const res = await client["treasury"]["liquid"].$get({ query: { days: "7d" }, @@ -200,7 +204,7 @@ describe("Treasury Controller - Integration Tests", () => { undefined, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const resAsc = await client["treasury"]["liquid"].$get({ query: { days: "7d", order: "asc" }, @@ -231,7 +235,7 @@ describe("Treasury Controller - Integration Tests", () => { fakePriceProvider, ); const app = createTestApp(service, 18); - const client = testClient(app); + const client = testClient(app) as any; const res = await client["treasury"]["dao-token"].$get({ query: { days: "7d" }, @@ -254,7 +258,7 @@ describe("Treasury Controller - Integration Tests", () => { undefined, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const res = await client["treasury"]["dao-token"].$get({ query: { days: "7d" }, @@ -285,7 +289,7 @@ describe("Treasury Controller - Integration Tests", () => { fakePriceProvider, ); const app = createTestApp(service, 18); - const client = testClient(app); + const client = testClient(app) as any; const res = await client["treasury"]["total"].$get({ query: { days: "7d" }, @@ -313,7 +317,7 @@ describe("Treasury Controller - Integration Tests", () => { fakePriceProvider, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const res = await client["treasury"]["total"].$get({ query: { days: "7d" }, @@ -335,7 +339,7 @@ describe("Treasury Controller - Integration Tests", () => { undefined, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const validDays = ["7d", "30d", "90d", "180d", "365d"] as const; @@ -355,7 +359,7 @@ describe("Treasury Controller - Integration Tests", () => { undefined, ); const app = createTestApp(service); - const client = testClient(app); + const client = testClient(app) as any; const resAsc = await client["treasury"]["liquid"].$get({ query: { days: "7d", order: "asc" }, From 7d7a09e3889d8e0bf1ac9b0a9c21b49feee5eea1 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 9 Feb 2026 19:45:29 -0300 Subject: [PATCH 07/41] feat: first repo test --- apps/api/package.json | 5 +- .../treasury.repository.integration.test.ts | 214 ++++++++++++++++++ pnpm-lock.yaml | 25 +- 3 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 apps/api/src/repositories/treasury/treasury.repository.integration.test.ts diff --git a/apps/api/package.json b/apps/api/package.json index 36af10504e..d3308479dc 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "typecheck": "tsc --noEmit", - "test": "jest", - "test:watch": "jest --watch", + "test": "NODE_OPTIONS='--experimental-vm-modules' jest", + "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch", "clean": "rm -rf node_modules *.tsbuildinfo dist", "start": "node dist/index.js", "build": "tsup", @@ -31,6 +31,7 @@ "zod-validation-error": "^3.4.1" }, "devDependencies": { + "@electric-sql/pglite": "^0.3.15", "@types/jest": "^29.5.14", "@types/node": "^20.16.5", "@types/pg": "^8.15.6", diff --git a/apps/api/src/repositories/treasury/treasury.repository.integration.test.ts b/apps/api/src/repositories/treasury/treasury.repository.integration.test.ts new file mode 100644 index 0000000000..9c473cf722 --- /dev/null +++ b/apps/api/src/repositories/treasury/treasury.repository.integration.test.ts @@ -0,0 +1,214 @@ +import { PGlite } from "@electric-sql/pglite"; +import { drizzle } from "drizzle-orm/pglite"; +import { sql } from "drizzle-orm"; +import * as schema from "@/database/schema"; +import { daoMetricsDayBucket } from "@/database/schema"; +import { MetricTypesEnum } from "@/lib/constants"; +import { TreasuryRepository } from "."; + +type DaoMetricInsert = typeof daoMetricsDayBucket.$inferInsert; + +const createMetricRow = ( + overrides: Partial = {}, +): DaoMetricInsert => ({ + date: 1600041600n, + daoId: "ENS", + tokenId: "ENS-token", + metricType: MetricTypesEnum.TREASURY, + open: 0n, + close: 1000n, + low: 0n, + high: 1000n, + average: 500n, + volume: 100n, + count: 1, + lastUpdate: 1600041600n, + ...overrides, +}); + +describe("TreasuryRepository - Integration", () => { + let client: PGlite; + let db: ReturnType>; + let repository: TreasuryRepository; + + beforeAll(async () => { + client = new PGlite(); + db = drizzle(client as any, { schema }); + + await db.execute(sql` + CREATE TYPE "metricType" AS ENUM ( + 'TOTAL_SUPPLY', 'DELEGATED_SUPPLY', 'CEX_SUPPLY', + 'DEX_SUPPLY', 'LENDING_SUPPLY', 'CIRCULATING_SUPPLY', 'TREASURY' + ); + `); + + await db.execute(sql` + CREATE TABLE "dao_metrics_day_buckets" ( + "date" BIGINT NOT NULL, + "dao_id" TEXT NOT NULL, + "token_id" TEXT NOT NULL, + "metricType" "metricType" NOT NULL, + "open" BIGINT NOT NULL, + "close" BIGINT NOT NULL, + "low" BIGINT NOT NULL, + "high" BIGINT NOT NULL, + "average" BIGINT NOT NULL, + "volume" BIGINT NOT NULL, + "count" INTEGER NOT NULL, + "lastUpdate" BIGINT NOT NULL, + PRIMARY KEY ("date", "token_id", "metricType") + ); + `); + }); + + afterAll(async () => { + await client.close(); + }); + + beforeEach(async () => { + await db.delete(daoMetricsDayBucket); + repository = new TreasuryRepository(db); + }); + + describe("getTokenQuantities", () => { + it("returns correct Map with timestamp keys and close values", async () => { + await db.insert(daoMetricsDayBucket).values([ + createMetricRow({ date: 1000n, close: 500n }), + createMetricRow({ date: 2000n, close: 700n, tokenId: "ENS-token-2" }), + ]); + + const result = await repository.getTokenQuantities(0); + + expect(result.size).toBe(2); + expect(result.get(1000 * 1000)).toBe(500n); // Timestamp in milliseconds + expect(result.get(2000 * 1000)).toBe(700n); // Timestamp in milliseconds + }); + + it("filters by cutoff timestamp (date >= cutoff)", async () => { + await db.insert(daoMetricsDayBucket).values([ + createMetricRow({ date: 100n, close: 10n }), + createMetricRow({ date: 200n, close: 20n, tokenId: "ENS-token-2" }), + createMetricRow({ date: 300n, close: 30n, tokenId: "ENS-token-3" }), + ]); + + const result = await repository.getTokenQuantities(200); + + expect(result.size).toBe(2); + expect(result.has(100 * 1000)).toBe(false); + expect(result.get(200 * 1000)).toBe(20n); + expect(result.get(300 * 1000)).toBe(30n); + }); + + it("filters by metricType (only TREASURY rows)", async () => { + await db.insert(daoMetricsDayBucket).values([ + createMetricRow({ date: 100n, close: 10n }), + createMetricRow({ + date: 100n, + close: 99n, + metricType: MetricTypesEnum.TOTAL_SUPPLY, + }), + ]); + + const result = await repository.getTokenQuantities(0); + + expect(result.size).toBe(1); + expect(result.get(100 * 1000)).toBe(10n); + }); + + it("returns results in ascending date order regardless of insertion order", async () => { + await db.insert(daoMetricsDayBucket).values([ + createMetricRow({ date: 300n, close: 30n }), + createMetricRow({ date: 100n, close: 10n, tokenId: "ENS-token-2" }), + createMetricRow({ date: 200n, close: 20n, tokenId: "ENS-token-3" }), + ]); + + const result = await repository.getTokenQuantities(0); + const keys = [...result.keys()]; + + expect(keys).toEqual([100 * 1000, 200 * 1000, 300 * 1000]); + }); + + it("returns empty Map when no rows exist", async () => { + const result = await repository.getTokenQuantities(0); + + expect(result.size).toBe(0); + }); + + it("returns empty Map when no rows match the cutoff", async () => { + await db + .insert(daoMetricsDayBucket) + .values([createMetricRow({ date: 100n, close: 10n })]); + + const result = await repository.getTokenQuantities(999); + + expect(result.size).toBe(0); + }); + + it("boundary: cutoff equals exact date (inclusive, gte)", async () => { + await db + .insert(daoMetricsDayBucket) + .values([createMetricRow({ date: 500n, close: 50n })]); + + const result = await repository.getTokenQuantities(500); + + expect(result.size).toBe(1); + expect(result.get(500 * 1000)).toBe(50n); + }); + }); + + describe("getLastTokenQuantityBeforeDate", () => { + + it("returns null when no rows exist", async () => { + const result = await repository.getLastTokenQuantityBeforeDate(999); + + expect(result).toBeNull(); + }); + + it("returns null when no rows are before cutoff", async () => { + await db + .insert(daoMetricsDayBucket) + .values([createMetricRow({ date: 500n, close: 50n })]); + + const result = await repository.getLastTokenQuantityBeforeDate(100); + + expect(result).toBeNull(); + }); + + it("boundary: cutoff equals exact date (inclusive, lte)", async () => { + await db + .insert(daoMetricsDayBucket) + .values([createMetricRow({ date: 500n, close: 50n })]); + + const result = await repository.getLastTokenQuantityBeforeDate(500); + + expect(result).toBe(50n); + }); + + it("returns most recent when multiple rows exist before cutoff", async () => { + await db.insert(daoMetricsDayBucket).values([ + createMetricRow({ date: 100n, close: 10n }), + createMetricRow({ date: 200n, close: 20n, tokenId: "ENS-token-2" }), + createMetricRow({ date: 300n, close: 30n, tokenId: "ENS-token-3" }), + ]); + + const result = await repository.getLastTokenQuantityBeforeDate(400); + + expect(result).toBe(30n); + }); + + it("filters by metricType (only TREASURY)", async () => { + await db.insert(daoMetricsDayBucket).values([ + createMetricRow({ + date: 200n, + close: 99n, + metricType: MetricTypesEnum.TOTAL_SUPPLY, + }), + createMetricRow({ date: 100n, close: 10n }), + ]); + + const result = await repository.getLastTokenQuantityBeforeDate(300); + + expect(result).toBe(10n); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd1c6a9b9e..b3a2503ad6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,7 +72,7 @@ importers: version: 0.31.7 drizzle-orm: specifier: ~0.41.0 - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.26.3)(pg@8.17.2) + version: 0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.26.3)(pg@8.17.2) hono: specifier: ^4.7.10 version: 4.10.7 @@ -89,6 +89,9 @@ importers: specifier: ^3.4.1 version: 3.5.4(zod@3.25.76) devDependencies: + "@electric-sql/pglite": + specifier: ^0.3.15 + version: 0.3.15 "@types/jest": specifier: ^29.5.14 version: 29.5.14 @@ -496,7 +499,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/8e7bbe65b08aa71a0c93b50dc707da716a2ac1ff + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/aeb45e9f32ef8ca78f0aeda17596e9c46374da41 packages: "@adobe/css-tools@4.4.3": @@ -2068,6 +2071,12 @@ packages: integrity: sha512-YRY806NnScVqa21/1L1vaysSQ+0/cAva50z7vlwzaGiBOTS9JhdzIRHN0KfgMhobFAphbznZJ7urMso4RtMBIQ==, } + "@electric-sql/pglite@0.3.15": + resolution: + { + integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==, + } + "@emnapi/core@1.4.5": resolution: { @@ -11455,10 +11464,10 @@ packages: } engines: { node: ">=14" } - forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/8e7bbe65b08aa71a0c93b50dc707da716a2ac1ff: + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/aeb45e9f32ef8ca78f0aeda17596e9c46374da41: resolution: { - tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/8e7bbe65b08aa71a0c93b50dc707da716a2ac1ff, + tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/aeb45e9f32ef8ca78f0aeda17596e9c46374da41, } version: 1.14.0 @@ -20070,6 +20079,8 @@ snapshots: "@electric-sql/pglite@0.2.13": {} + "@electric-sql/pglite@0.3.15": {} + "@emnapi/core@1.4.5": dependencies: "@emnapi/wasi-threads": 1.0.4 @@ -27113,9 +27124,9 @@ snapshots: kysely: 0.26.3 pg: 8.16.3 - drizzle-orm@0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.26.3)(pg@8.17.2): + drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.26.3)(pg@8.17.2): optionalDependencies: - "@electric-sql/pglite": 0.2.13 + "@electric-sql/pglite": 0.3.15 "@opentelemetry/api": 1.9.0 "@types/pg": 8.15.6 kysely: 0.26.3 @@ -28043,7 +28054,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/8e7bbe65b08aa71a0c93b50dc707da716a2ac1ff: + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/aeb45e9f32ef8ca78f0aeda17596e9c46374da41: {} fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.103.0): From e6218c62430e6860a0c3eae38457938e78017c7d Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 9 Feb 2026 20:00:12 -0300 Subject: [PATCH 08/41] refactor: api to use vitest instead of jest --- apps/api/jest.config.cjs | 8 - apps/api/package.json | 8 +- .../treasury/treasury.integration.test.ts | 7 +- .../delegation-percentage.test.ts | 25 +- .../treasury/providers/provider-cache.test.ts | 21 +- .../treasury/treasury.service.test.ts | 7 +- apps/api/vitest.config.ts | 14 + pnpm-lock.yaml | 295 ++++-------------- 8 files changed, 98 insertions(+), 287 deletions(-) delete mode 100644 apps/api/jest.config.cjs create mode 100644 apps/api/vitest.config.ts diff --git a/apps/api/jest.config.cjs b/apps/api/jest.config.cjs deleted file mode 100644 index ba20b9b7c3..0000000000 --- a/apps/api/jest.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - preset: "ts-jest", - testEnvironment: "node", - testMatch: ["**/*.test.ts"], - moduleNameMapper: { - "^@/(.*)$": "/src/$1", - }, -}; diff --git a/apps/api/package.json b/apps/api/package.json index d3308479dc..1be8376675 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "typecheck": "tsc --noEmit", - "test": "NODE_OPTIONS='--experimental-vm-modules' jest", - "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch", + "test": "vitest run", + "test:watch": "vitest", "clean": "rm -rf node_modules *.tsbuildinfo dist", "start": "node dist/index.js", "build": "tsup", @@ -32,16 +32,14 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.3.15", - "@types/jest": "^29.5.14", "@types/node": "^20.16.5", "@types/pg": "^8.15.6", "dotenv": "^16.5.0", "eslint": "^8.53.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", - "jest": "^29.7.0", "prettier": "^3.5.3", - "ts-jest": "^29.4.1", + "vitest": "^3.0.5", "tsup": "^8.5.1", "tsx": "^4.21.0", "typescript": "^5.8.3" diff --git a/apps/api/src/controllers/treasury/treasury.integration.test.ts b/apps/api/src/controllers/treasury/treasury.integration.test.ts index 987ebdb9bc..ac20145d7a 100644 --- a/apps/api/src/controllers/treasury/treasury.integration.test.ts +++ b/apps/api/src/controllers/treasury/treasury.integration.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { OpenAPIHono as Hono } from "@hono/zod-openapi"; import { testClient } from "hono/testing"; import { treasury } from "./index"; @@ -79,8 +80,8 @@ describe("Treasury Controller - Integration Tests", () => { let fakeRepository: FakeTreasuryRepository; beforeEach(() => { - jest.useFakeTimers(); - jest.setSystemTime(FIXED_DATE); + vi.useFakeTimers(); + vi.setSystemTime(FIXED_DATE); fakeProvider = new FakeTreasuryProvider(); fakePriceProvider = new FakePriceProvider(); @@ -88,7 +89,7 @@ describe("Treasury Controller - Integration Tests", () => { }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe("GET /treasury/liquid", () => { diff --git a/apps/api/src/services/delegation-percentage/delegation-percentage.test.ts b/apps/api/src/services/delegation-percentage/delegation-percentage.test.ts index 3956019c75..312a9d7982 100644 --- a/apps/api/src/services/delegation-percentage/delegation-percentage.test.ts +++ b/apps/api/src/services/delegation-percentage/delegation-percentage.test.ts @@ -1,5 +1,5 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; import { DelegationPercentageService } from "./delegation-percentage"; -import { DaoMetricsDayBucketRepository } from "@/repositories/"; import { MetricTypesEnum } from "@/lib/constants"; import { DBTokenMetric } from "@/mappers/delegation-percentage"; @@ -28,23 +28,16 @@ const createMockRow = ( describe("DelegationPercentageService", () => { let service: DelegationPercentageService; - let mockRepository: jest.Mocked< - Pick< - DaoMetricsDayBucketRepository, - "getMetricsByDateRange" | "getLastMetricBeforeDate" - > - >; + let mockRepository: { + getMetricsByDateRange: ReturnType; + getLastMetricBeforeDate: ReturnType; + }; beforeEach(() => { mockRepository = { - getMetricsByDateRange: jest.fn(), - getLastMetricBeforeDate: jest.fn(), - } as jest.Mocked< - Pick< - DaoMetricsDayBucketRepository, - "getMetricsByDateRange" | "getLastMetricBeforeDate" - > - >; + getMetricsByDateRange: vi.fn(), + getLastMetricBeforeDate: vi.fn(), + }; service = new DelegationPercentageService(mockRepository); }); @@ -454,7 +447,7 @@ describe("DelegationPercentageService", () => { const day100 = 1599955200n; // Mock console.error to suppress test output - const consoleErrorSpy = jest + const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementation(() => {}); diff --git a/apps/api/src/services/treasury/providers/provider-cache.test.ts b/apps/api/src/services/treasury/providers/provider-cache.test.ts index 4c637d68c0..b6d67bf463 100644 --- a/apps/api/src/services/treasury/providers/provider-cache.test.ts +++ b/apps/api/src/services/treasury/providers/provider-cache.test.ts @@ -1,3 +1,4 @@ +import { expect, afterEach, beforeEach, vi, describe, it } from "vitest"; import { TreasuryProviderCache } from "./provider-cache"; import { LiquidTreasuryDataPoint } from "../types"; @@ -15,13 +16,13 @@ describe("TreasuryProviderCache", () => { let cache: TreasuryProviderCache; beforeEach(() => { - jest.useFakeTimers(); - jest.setSystemTime(new Date("2025-01-15T12:00:00Z")); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2025-01-15T12:00:00Z")); cache = new TreasuryProviderCache(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe("get", () => { @@ -43,7 +44,7 @@ describe("TreasuryProviderCache", () => { it("should return null when cache is expired", () => { const data = [createDataPoint()]; cache.set(data); - jest.advanceTimersByTime(25 * 60 * 60 * 1000); + vi.advanceTimersByTime(25 * 60 * 60 * 1000); const result = cache.get(); @@ -53,11 +54,11 @@ describe("TreasuryProviderCache", () => { it("should clear cache when expired", () => { const data = [createDataPoint()]; cache.set(data); - jest.advanceTimersByTime(25 * 60 * 60 * 1000); + vi.advanceTimersByTime(25 * 60 * 60 * 1000); cache.get(); - jest.setSystemTime(new Date("2025-01-16T13:00:00Z")); + vi.setSystemTime(new Date("2025-01-16T13:00:00Z")); const newData = [createDataPoint({ liquidTreasury: 2000000 })]; cache.set(newData); @@ -67,7 +68,7 @@ describe("TreasuryProviderCache", () => { it("should return data at exactly 24 hours (boundary)", () => { const data = [createDataPoint()]; cache.set(data); - jest.advanceTimersByTime(24 * 60 * 60 * 1000); + vi.advanceTimersByTime(24 * 60 * 60 * 1000); const result = cache.get(); @@ -77,7 +78,7 @@ describe("TreasuryProviderCache", () => { it("should return null at 24 hours + 1ms (just expired)", () => { const data = [createDataPoint()]; cache.set(data); - jest.advanceTimersByTime(24 * 60 * 60 * 1000 + 1); + vi.advanceTimersByTime(24 * 60 * 60 * 1000 + 1); const result = cache.get(); @@ -109,11 +110,11 @@ describe("TreasuryProviderCache", () => { it("should reset TTL when setting new data", () => { const oldData = [createDataPoint({ liquidTreasury: 100 })]; cache.set(oldData); - jest.advanceTimersByTime(23 * 60 * 60 * 1000); + vi.advanceTimersByTime(23 * 60 * 60 * 1000); const newData = [createDataPoint({ liquidTreasury: 200 })]; cache.set(newData); - jest.advanceTimersByTime(23 * 60 * 60 * 1000); + vi.advanceTimersByTime(23 * 60 * 60 * 1000); const result = cache.get(); diff --git a/apps/api/src/services/treasury/treasury.service.test.ts b/apps/api/src/services/treasury/treasury.service.test.ts index ece7a19385..c6f1049a3b 100644 --- a/apps/api/src/services/treasury/treasury.service.test.ts +++ b/apps/api/src/services/treasury/treasury.service.test.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, vi, describe, it, expect } from "vitest"; import { TreasuryService } from "./treasury.service"; import { TreasuryProvider } from "./providers"; import { PriceProvider, LiquidTreasuryDataPoint } from "./types"; @@ -61,8 +62,8 @@ describe("TreasuryService", () => { let fakeRepository: FakeTreasuryRepository; beforeEach(() => { - jest.useFakeTimers(); - jest.setSystemTime(FIXED_DATE); + vi.useFakeTimers(); + vi.setSystemTime(FIXED_DATE); fakeProvider = new FakeTreasuryProvider(); fakePriceProvider = new FakePriceProvider(); @@ -70,7 +71,7 @@ describe("TreasuryService", () => { }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe("getLiquidTreasury", () => { diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts new file mode 100644 index 0000000000..7d198199f7 --- /dev/null +++ b/apps/api/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + }, + resolve: { + alias: { + "@": path.resolve(import.meta.dirname, "./src"), // Ensure we can import from the src directory + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3a2503ad6..990267cadc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,9 +92,6 @@ importers: "@electric-sql/pglite": specifier: ^0.3.15 version: 0.3.15 - "@types/jest": - specifier: ^29.5.14 - version: 29.5.14 "@types/node": specifier: ^20.16.5 version: 20.19.25 @@ -113,15 +110,9 @@ importers: eslint-plugin-prettier: specifier: ^5.2.1 version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.7.4) - 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)) prettier: specifier: ^3.5.3 version: 3.7.4 - 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))(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) tsup: specifier: ^8.5.1 version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) @@ -131,6 +122,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.9.3 + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@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) apps/api-gateway: dependencies: @@ -139,7 +133,7 @@ importers: version: 0.108.20(graphql@16.12.0) "@graphql-mesh/graphql": specifier: ^0.104.3 - version: 0.104.18(@types/node@22.7.5)(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10) + version: 0.104.18(@types/node@20.19.25)(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10) "@graphql-mesh/http": specifier: ^0.106.3 version: 0.106.18(graphql@16.12.0) @@ -173,10 +167,10 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)) + version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) 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@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(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@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(typescript@5.9.3) tsx: specifier: ^4.19.4 version: 4.21.0 @@ -191,7 +185,7 @@ importers: version: link:../../packages/graphql-client "@apollo/client": specifier: ^3.13.8 - version: 3.14.0(@types/react@19.2.8)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 3.14.0(@types/react@19.2.8)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) "@ethersproject/providers": specifier: ^5.8.0 version: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -227,7 +221,7 @@ importers: version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) "@rainbow-me/rainbowkit": specifier: ^2.2.0 - version: 2.2.9(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + version: 2.2.9(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) "@snapshot-labs/snapshot.js": specifier: ^0.12.62 version: 0.12.65(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -311,7 +305,7 @@ importers: version: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.12.25 - version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) zod: specifier: ^3.25.76 version: 3.25.76 @@ -18571,7 +18565,7 @@ snapshots: "@jridgewell/gen-mapping": 0.3.13 "@jridgewell/trace-mapping": 0.3.31 - "@apollo/client@3.14.0(@types/react@19.2.8)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)": + "@apollo/client@3.14.0(@types/react@19.2.8)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)": dependencies: "@graphql-typed-document-node/core": 3.2.0(graphql@16.12.0) "@wry/caches": 1.0.1 @@ -18588,7 +18582,7 @@ snapshots: tslib: 2.8.1 zen-observable-ts: 1.2.5 optionalDependencies: - graphql-ws: 6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + graphql-ws: 6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: @@ -19857,9 +19851,9 @@ snapshots: "@babel/helper-string-parser": 7.27.1 "@babel/helper-validator-identifier": 7.28.5 - "@base-org/account@2.4.0(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)": + "@base-org/account@2.4.0(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)": dependencies: - "@coinbase/cdp-sdk": 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@coinbase/cdp-sdk": 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@noble/hashes": 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 @@ -19896,11 +19890,11 @@ snapshots: - "@chromatic-com/cypress" - "@chromatic-com/playwright" - "@coinbase/cdp-sdk@1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@coinbase/cdp-sdk@1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: - "@solana-program/system": 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - "@solana-program/token": 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana-program/system": 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + "@solana-program/token": 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/web3.js": 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) axios: 1.13.2 @@ -21293,7 +21287,7 @@ snapshots: - ioredis - supports-color - "@graphql-mesh/graphql@0.104.18(@types/node@22.7.5)(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10)": + "@graphql-mesh/graphql@0.104.18(@types/node@20.19.25)(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10)": dependencies: "@graphql-mesh/cross-helpers": 0.4.10(graphql@16.12.0) "@graphql-mesh/store": 0.104.18(graphql@16.12.0) @@ -21301,9 +21295,9 @@ snapshots: "@graphql-mesh/types": 0.104.18(graphql@16.12.0) "@graphql-mesh/utils": 0.104.17(graphql@16.12.0) "@graphql-tools/delegate": 12.0.2(graphql@16.12.0) - "@graphql-tools/federation": 4.2.6(@types/node@22.7.5)(graphql@16.12.0) + "@graphql-tools/federation": 4.2.6(@types/node@20.19.25)(graphql@16.12.0) "@graphql-tools/merge": 9.1.1(graphql@16.12.0) - "@graphql-tools/url-loader": 9.0.5(@types/node@22.7.5)(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10) + "@graphql-tools/url-loader": 9.0.5(@types/node@20.19.25)(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10) "@graphql-tools/utils": 10.9.1(graphql@16.12.0) "@whatwg-node/promise-helpers": 1.3.2 graphql: 16.12.0 @@ -21712,7 +21706,7 @@ snapshots: transitivePeerDependencies: - "@types/node" - "@graphql-tools/executor-http@3.0.7(@types/node@22.7.5)(graphql@16.12.0)": + "@graphql-tools/executor-http@3.0.7(@types/node@20.19.25)(graphql@16.12.0)": dependencies: "@graphql-hive/signal": 2.0.0 "@graphql-tools/executor-common": 1.0.5(graphql@16.12.0) @@ -21722,7 +21716,7 @@ snapshots: "@whatwg-node/fetch": 0.10.13 "@whatwg-node/promise-helpers": 1.3.2 graphql: 16.12.0 - meros: 1.3.2(@types/node@22.7.5) + meros: 1.3.2(@types/node@20.19.25) tslib: 2.8.1 transitivePeerDependencies: - "@types/node" @@ -21781,11 +21775,11 @@ snapshots: graphql: 16.8.2 tslib: 2.8.1 - "@graphql-tools/federation@4.2.6(@types/node@22.7.5)(graphql@16.12.0)": + "@graphql-tools/federation@4.2.6(@types/node@20.19.25)(graphql@16.12.0)": dependencies: "@graphql-tools/delegate": 12.0.2(graphql@16.12.0) "@graphql-tools/executor": 1.5.0(graphql@16.12.0) - "@graphql-tools/executor-http": 3.0.7(@types/node@22.7.5)(graphql@16.12.0) + "@graphql-tools/executor-http": 3.0.7(@types/node@20.19.25)(graphql@16.12.0) "@graphql-tools/merge": 9.1.6(graphql@16.12.0) "@graphql-tools/schema": 10.0.30(graphql@16.12.0) "@graphql-tools/stitch": 10.1.6(graphql@16.12.0) @@ -22026,10 +22020,10 @@ snapshots: - uWebSockets.js - utf-8-validate - "@graphql-tools/url-loader@9.0.5(@types/node@22.7.5)(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10)": + "@graphql-tools/url-loader@9.0.5(@types/node@20.19.25)(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10)": dependencies: "@graphql-tools/executor-graphql-ws": 3.1.3(bufferutil@4.0.9)(crossws@0.3.5)(graphql@16.12.0)(utf-8-validate@5.0.10) - "@graphql-tools/executor-http": 3.0.7(@types/node@22.7.5)(graphql@16.12.0) + "@graphql-tools/executor-http": 3.0.7(@types/node@20.19.25)(graphql@16.12.0) "@graphql-tools/executor-legacy-ws": 1.1.24(bufferutil@4.0.9)(graphql@16.12.0)(utf-8-validate@5.0.10) "@graphql-tools/utils": 10.11.0(graphql@16.12.0) "@graphql-tools/wrap": 11.1.2(graphql@16.12.0) @@ -22341,41 +22335,6 @@ snapshots: - supports-color - ts-node - "@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.7.5)(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@22.7.5)(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 @@ -23354,7 +23313,7 @@ snapshots: "@radix-ui/rect@1.1.1": {} - "@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))": + "@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))": dependencies: "@tanstack/react-query": 5.90.11(react@19.2.3) "@vanilla-extract/css": 1.17.3 @@ -23367,7 +23326,7 @@ snapshots: react-remove-scroll: 2.6.2(@types/react@19.2.8)(react@19.2.3) ua-parser-js: 1.0.40 viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - "@types/react" - babel-plugin-macros @@ -23800,13 +23759,13 @@ snapshots: "@socket.io/component-emitter@3.1.2": {} - "@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))": + "@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))": dependencies: - "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - "@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))": + "@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))": dependencies: - "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/accounts@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)": dependencies: @@ -23936,7 +23895,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - "@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: "@solana/accounts": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/addresses": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -23950,11 +23909,11 @@ snapshots: "@solana/rpc": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/rpc-parsed-types": 3.0.3(typescript@5.9.3) "@solana/rpc-spec-types": 3.0.3(typescript@5.9.3) - "@solana/rpc-subscriptions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/rpc-subscriptions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/rpc-types": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/signers": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/sysvars": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - "@solana/transaction-confirmation": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/transaction-confirmation": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/transaction-messages": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/transactions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 @@ -24033,14 +23992,14 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - "@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: "@solana/errors": 3.0.3(typescript@5.9.3) "@solana/functional": 3.0.3(typescript@5.9.3) "@solana/rpc-subscriptions-spec": 3.0.3(typescript@5.9.3) "@solana/subscribable": 3.0.3(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) "@solana/rpc-subscriptions-spec@3.0.3(typescript@5.9.3)": dependencies: @@ -24050,7 +24009,7 @@ snapshots: "@solana/subscribable": 3.0.3(typescript@5.9.3) typescript: 5.9.3 - "@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: "@solana/errors": 3.0.3(typescript@5.9.3) "@solana/fast-stable-stringify": 3.0.3(typescript@5.9.3) @@ -24058,7 +24017,7 @@ snapshots: "@solana/promises": 3.0.3(typescript@5.9.3) "@solana/rpc-spec-types": 3.0.3(typescript@5.9.3) "@solana/rpc-subscriptions-api": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - "@solana/rpc-subscriptions-channel-websocket": 3.0.3(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/rpc-subscriptions-channel-websocket": 3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/rpc-subscriptions-spec": 3.0.3(typescript@5.9.3) "@solana/rpc-transformers": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/rpc-types": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24143,7 +24102,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - "@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: "@solana/addresses": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/codecs-strings": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24151,7 +24110,7 @@ snapshots: "@solana/keys": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/promises": 3.0.3(typescript@5.9.3) "@solana/rpc": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - "@solana/rpc-subscriptions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/rpc-subscriptions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/rpc-types": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/transaction-messages": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/transactions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24997,9 +24956,9 @@ snapshots: loupe: 3.1.4 tinyrainbow: 2.0.0 - "@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)": + "@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)": dependencies: - "@base-org/account": 2.4.0(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + "@base-org/account": 2.4.0(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) "@coinbase/wallet-sdk": 4.3.6(@types/react@19.2.8)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@3.25.76) "@gemini-wallet/core": 0.3.2(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) "@metamask/sdk": 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -25008,7 +24967,7 @@ snapshots: "@wagmi/core": 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) "@walletconnect/ethereum-provider": 2.21.1(@types/react@19.2.8)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: "@coinbase/wallet-sdk@3.9.3" - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 @@ -26734,21 +26693,6 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)): - 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@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - "@types/node" - - babel-plugin-macros - - supports-color - - ts-node - create-require@1.1.1: {} cross-fetch@3.2.0: @@ -28313,12 +28257,12 @@ snapshots: dependencies: graphql: 16.12.0 - graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: graphql: 16.12.0 optionalDependencies: crossws: 0.3.5 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) optional: true graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): @@ -28938,25 +28882,6 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)): - dependencies: - "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@22.7.5)(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@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - 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@20.19.25)(typescript@5.9.3)): dependencies: "@babel/core": 7.28.0 @@ -28988,68 +28913,6 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)): - dependencies: - "@babel/core": 7.28.0 - "@jest/test-sequencer": 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 - 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@22.7.5)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)): - dependencies: - "@babel/core": 7.28.0 - "@jest/test-sequencer": 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 - 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": 22.7.5 - ts-node: 10.9.2(@types/node@22.7.5)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -29283,18 +29146,6 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)): - dependencies: - "@jest/core": 29.7.0(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)) - "@jest/types": 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)) - transitivePeerDependencies: - - "@types/node" - - babel-plugin-macros - - supports-color - - ts-node - jiti@1.21.7: {} jiti@2.4.2: {} @@ -29724,9 +29575,9 @@ snapshots: optionalDependencies: "@types/node": 20.19.25 - meros@1.3.2(@types/node@22.7.5): + meros@1.3.2(@types/node@20.19.25): optionalDependencies: - "@types/node": 22.7.5 + "@types/node": 20.19.25 mersenne-twister@1.1.0: {} @@ -30503,7 +30354,7 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: "@wagmi/core": 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.10.7 @@ -30517,7 +30368,7 @@ snapshots: "@tanstack/react-query": 5.90.11(react@19.2.3) react: 19.2.3 typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - "@types/react" - immer @@ -31853,7 +31704,7 @@ snapshots: dependencies: tslib: 2.8.1 - 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)(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@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -31866,27 +31717,6 @@ snapshots: type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 - optionalDependencies: - "@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 - - 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@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)))(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@22.7.5)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.3 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 optionalDependencies: "@babel/core": 7.28.0 "@jest/transform": 29.7.0 @@ -31936,25 +31766,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@22.7.5)(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": 22.7.5 - 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 @@ -32514,10 +32325,10 @@ snapshots: vm-browserify@1.1.2: {} - wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): dependencies: "@tanstack/react-query": 5.90.11(react@19.2.3) - "@wagmi/connectors": 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + "@wagmi/connectors": 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) "@wagmi/core": 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.2.3 use-sync-external-store: 1.4.0(react@19.2.3) From 3f69bb6d9a0a6253d0c6dd016f35473f42d47a63 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 9 Feb 2026 20:04:08 -0300 Subject: [PATCH 09/41] feat: vitest coverage --- apps/api/package.json | 8 +- pnpm-lock.yaml | 261 +++++++++++++++++++++++++++++------------- 2 files changed, 187 insertions(+), 82 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 1be8376675..905995eea4 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "typecheck": "tsc --noEmit", - "test": "vitest run", + "test": "vitest", + "test:coverage": "vitest run --coverage", "test:watch": "vitest", "clean": "rm -rf node_modules *.tsbuildinfo dist", "start": "node dist/index.js", @@ -34,14 +35,15 @@ "@electric-sql/pglite": "^0.3.15", "@types/node": "^20.16.5", "@types/pg": "^8.15.6", + "@vitest/coverage-v8": "3.2.4", "dotenv": "^16.5.0", "eslint": "^8.53.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "prettier": "^3.5.3", - "vitest": "^3.0.5", "tsup": "^8.5.1", "tsx": "^4.21.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^3.0.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 990267cadc..1037b55602 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: "@types/pg": specifier: ^8.15.6 version: 8.15.6 + "@vitest/coverage-v8": + specifier: 3.2.4 + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@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)) dotenv: specifier: ^16.5.0 version: 16.6.1 @@ -185,7 +188,7 @@ importers: version: link:../../packages/graphql-client "@apollo/client": specifier: ^3.13.8 - version: 3.14.0(@types/react@19.2.8)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 3.14.0(@types/react@19.2.8)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) "@ethersproject/providers": specifier: ^5.8.0 version: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -221,7 +224,7 @@ importers: version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) "@rainbow-me/rainbowkit": specifier: ^2.2.0 - version: 2.2.9(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + version: 2.2.9(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) "@snapshot-labs/snapshot.js": specifier: ^0.12.62 version: 0.12.65(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -305,7 +308,7 @@ importers: version: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.12.25 - version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) zod: specifier: ^3.25.76 version: 3.25.76 @@ -1882,6 +1885,13 @@ packages: integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==, } + "@bcoe/v8-coverage@1.0.2": + resolution: + { + integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==, + } + engines: { node: ">=18" } + "@chromatic-com/storybook@4.1.3": resolution: { @@ -7726,6 +7736,18 @@ packages: peerDependencies: "@vanilla-extract/css": ^1.0.0 + "@vitest/coverage-v8@3.2.4": + resolution: + { + integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==, + } + peerDependencies: + "@vitest/browser": 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + "@vitest/browser": + optional: true + "@vitest/expect@3.2.4": resolution: { @@ -8628,6 +8650,12 @@ packages: } engines: { node: ">=4" } + ast-v8-to-istanbul@0.3.11: + resolution: + { + integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==, + } + astral-regex@2.0.0: resolution: { @@ -11671,7 +11699,7 @@ packages: { integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, } - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: @@ -12725,6 +12753,13 @@ packages: } engines: { node: ">=10" } + istanbul-lib-source-maps@5.0.6: + resolution: + { + integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==, + } + engines: { node: ">=10" } + istanbul-reports@3.1.7: resolution: { @@ -13013,6 +13048,12 @@ packages: integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==, } + js-tokens@10.0.0: + resolution: + { + integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==, + } + js-tokens@4.0.0: resolution: { @@ -13723,6 +13764,12 @@ packages: integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, } + magicast@0.3.5: + resolution: + { + integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==, + } + make-dir@3.1.0: resolution: { @@ -16939,6 +16986,13 @@ packages: } engines: { node: ">=8" } + test-exclude@7.0.1: + resolution: + { + integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==, + } + engines: { node: ">=18" } + text-encoding-utf-8@1.0.2: resolution: { @@ -18565,7 +18619,7 @@ snapshots: "@jridgewell/gen-mapping": 0.3.13 "@jridgewell/trace-mapping": 0.3.31 - "@apollo/client@3.14.0(@types/react@19.2.8)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)": + "@apollo/client@3.14.0(@types/react@19.2.8)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)": dependencies: "@graphql-typed-document-node/core": 3.2.0(graphql@16.12.0) "@wry/caches": 1.0.1 @@ -18582,7 +18636,7 @@ snapshots: tslib: 2.8.1 zen-observable-ts: 1.2.5 optionalDependencies: - graphql-ws: 6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + graphql-ws: 6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: @@ -18615,10 +18669,10 @@ snapshots: dependencies: "@babel/core": 7.28.5 "@babel/generator": 7.28.0 - "@babel/parser": 7.28.0 + "@babel/parser": 7.28.6 "@babel/runtime": 7.27.6 "@babel/traverse": 7.28.5 - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 babel-preset-fbjs: 3.4.0(@babel/core@7.28.5) chalk: 4.1.2 fb-watchman: 2.0.2 @@ -18638,7 +18692,7 @@ snapshots: "@ardatan/relay-compiler@12.0.3(graphql@16.12.0)": dependencies: "@babel/generator": 7.28.0 - "@babel/parser": 7.28.0 + "@babel/parser": 7.28.6 "@babel/runtime": 7.27.6 chalk: 4.1.2 fb-watchman: 2.0.2 @@ -18680,10 +18734,10 @@ snapshots: "@babel/helper-compilation-targets": 7.27.2 "@babel/helper-module-transforms": 7.27.3(@babel/core@7.28.0) "@babel/helpers": 7.27.6 - "@babel/parser": 7.28.0 + "@babel/parser": 7.28.6 "@babel/template": 7.27.2 "@babel/traverse": 7.28.0 - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -18722,8 +18776,8 @@ snapshots: "@babel/generator@7.28.5": dependencies: - "@babel/parser": 7.28.5 - "@babel/types": 7.28.5 + "@babel/parser": 7.28.6 + "@babel/types": 7.28.6 "@jridgewell/gen-mapping": 0.3.13 "@jridgewell/trace-mapping": 0.3.31 jsesc: 3.1.0 @@ -18738,7 +18792,7 @@ snapshots: "@babel/helper-annotate-as-pure@7.27.3": dependencies: - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 "@babel/helper-compilation-targets@7.27.2": dependencies: @@ -18812,21 +18866,21 @@ snapshots: "@babel/helper-member-expression-to-functions@7.27.1": dependencies: "@babel/traverse": 7.28.0 - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 transitivePeerDependencies: - supports-color "@babel/helper-member-expression-to-functions@7.28.5": dependencies: "@babel/traverse": 7.28.6 - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 transitivePeerDependencies: - supports-color "@babel/helper-module-imports@7.27.1": dependencies: "@babel/traverse": 7.28.0 - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 transitivePeerDependencies: - supports-color @@ -18841,7 +18895,7 @@ snapshots: dependencies: "@babel/core": 7.28.0 "@babel/helper-module-imports": 7.27.1 - "@babel/helper-validator-identifier": 7.27.1 + "@babel/helper-validator-identifier": 7.28.5 "@babel/traverse": 7.28.0 transitivePeerDependencies: - supports-color @@ -18850,7 +18904,7 @@ snapshots: dependencies: "@babel/core": 7.28.5 "@babel/helper-module-imports": 7.27.1 - "@babel/helper-validator-identifier": 7.27.1 + "@babel/helper-validator-identifier": 7.28.5 "@babel/traverse": 7.28.0 transitivePeerDependencies: - supports-color @@ -18875,7 +18929,7 @@ snapshots: "@babel/helper-optimise-call-expression@7.27.1": dependencies: - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 "@babel/helper-plugin-utils@7.27.1": {} @@ -18911,7 +18965,7 @@ snapshots: "@babel/helper-skip-transparent-expression-wrappers@7.27.1": dependencies: "@babel/traverse": 7.28.0 - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 transitivePeerDependencies: - supports-color @@ -18927,27 +18981,27 @@ snapshots: dependencies: "@babel/template": 7.27.2 "@babel/traverse": 7.28.6 - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 transitivePeerDependencies: - supports-color "@babel/helpers@7.27.6": dependencies: "@babel/template": 7.27.2 - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 "@babel/helpers@7.28.4": dependencies: "@babel/template": 7.27.2 - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 "@babel/parser@7.28.0": dependencies: - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 "@babel/parser@7.28.5": dependencies: - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 "@babel/parser@7.28.6": dependencies: @@ -19570,7 +19624,7 @@ snapshots: "@babel/helper-module-imports": 7.27.1 "@babel/helper-plugin-utils": 7.27.1 "@babel/plugin-syntax-jsx": 7.27.1(@babel/core@7.28.5) - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 transitivePeerDependencies: - supports-color @@ -19758,7 +19812,7 @@ snapshots: dependencies: "@babel/core": 7.28.5 "@babel/helper-plugin-utils": 7.28.6 - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 esutils: 2.0.3 "@babel/preset-react@7.28.5(@babel/core@7.28.5)": @@ -19805,9 +19859,9 @@ snapshots: "@babel/code-frame": 7.27.1 "@babel/generator": 7.28.0 "@babel/helper-globals": 7.28.0 - "@babel/parser": 7.28.0 + "@babel/parser": 7.28.6 "@babel/template": 7.27.2 - "@babel/types": 7.28.1 + "@babel/types": 7.28.6 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -19817,9 +19871,9 @@ snapshots: "@babel/code-frame": 7.27.1 "@babel/generator": 7.28.5 "@babel/helper-globals": 7.28.0 - "@babel/parser": 7.28.5 + "@babel/parser": 7.28.6 "@babel/template": 7.27.2 - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -19851,9 +19905,9 @@ snapshots: "@babel/helper-string-parser": 7.27.1 "@babel/helper-validator-identifier": 7.28.5 - "@base-org/account@2.4.0(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)": + "@base-org/account@2.4.0(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)": dependencies: - "@coinbase/cdp-sdk": 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@coinbase/cdp-sdk": 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@noble/hashes": 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 @@ -19878,6 +19932,8 @@ snapshots: "@bcoe/v8-coverage@0.2.3": {} + "@bcoe/v8-coverage@1.0.2": {} + "@chromatic-com/storybook@4.1.3(storybook@10.2.0(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(utf-8-validate@5.0.10))": dependencies: "@neoconfetti/react": 1.0.0 @@ -19890,11 +19946,11 @@ snapshots: - "@chromatic-com/cypress" - "@chromatic-com/playwright" - "@coinbase/cdp-sdk@1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@coinbase/cdp-sdk@1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: - "@solana-program/system": 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - "@solana-program/token": 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana-program/system": 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + "@solana-program/token": 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/web3.js": 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) axios: 1.13.2 @@ -22378,7 +22434,7 @@ snapshots: "@jest/test-result": 29.7.0 "@jest/transform": 29.7.0 "@jest/types": 29.6.3 - "@jridgewell/trace-mapping": 0.3.29 + "@jridgewell/trace-mapping": 0.3.31 "@types/node": 20.19.25 chalk: 4.1.2 collect-v8-coverage: 1.0.2 @@ -22428,7 +22484,7 @@ snapshots: dependencies: "@babel/core": 7.28.0 "@jest/types": 29.6.3 - "@jridgewell/trace-mapping": 0.3.29 + "@jridgewell/trace-mapping": 0.3.31 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -22455,8 +22511,8 @@ snapshots: "@jridgewell/gen-mapping@0.3.12": dependencies: - "@jridgewell/sourcemap-codec": 1.5.4 - "@jridgewell/trace-mapping": 0.3.29 + "@jridgewell/sourcemap-codec": 1.5.5 + "@jridgewell/trace-mapping": 0.3.31 "@jridgewell/gen-mapping@0.3.13": dependencies: @@ -22482,7 +22538,7 @@ snapshots: "@jridgewell/trace-mapping@0.3.29": dependencies: "@jridgewell/resolve-uri": 3.1.2 - "@jridgewell/sourcemap-codec": 1.5.4 + "@jridgewell/sourcemap-codec": 1.5.5 "@jridgewell/trace-mapping@0.3.31": dependencies: @@ -22492,7 +22548,7 @@ snapshots: "@jridgewell/trace-mapping@0.3.9": dependencies: "@jridgewell/resolve-uri": 3.1.2 - "@jridgewell/sourcemap-codec": 1.5.4 + "@jridgewell/sourcemap-codec": 1.5.5 "@json-schema-tools/meta-schema@1.8.0": {} @@ -23313,7 +23369,7 @@ snapshots: "@radix-ui/rect@1.1.1": {} - "@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))": + "@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))": dependencies: "@tanstack/react-query": 5.90.11(react@19.2.3) "@vanilla-extract/css": 1.17.3 @@ -23326,7 +23382,7 @@ snapshots: react-remove-scroll: 2.6.2(@types/react@19.2.8)(react@19.2.3) ua-parser-js: 1.0.40 viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - "@types/react" - babel-plugin-macros @@ -23759,13 +23815,13 @@ snapshots: "@socket.io/component-emitter@3.1.2": {} - "@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))": + "@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))": dependencies: - "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - "@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))": + "@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))": dependencies: - "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/kit": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/accounts@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)": dependencies: @@ -23895,7 +23951,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - "@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: "@solana/accounts": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/addresses": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -23909,11 +23965,11 @@ snapshots: "@solana/rpc": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/rpc-parsed-types": 3.0.3(typescript@5.9.3) "@solana/rpc-spec-types": 3.0.3(typescript@5.9.3) - "@solana/rpc-subscriptions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/rpc-subscriptions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/rpc-types": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/signers": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/sysvars": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - "@solana/transaction-confirmation": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/transaction-confirmation": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/transaction-messages": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/transactions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 @@ -23992,14 +24048,14 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - "@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: "@solana/errors": 3.0.3(typescript@5.9.3) "@solana/functional": 3.0.3(typescript@5.9.3) "@solana/rpc-subscriptions-spec": 3.0.3(typescript@5.9.3) "@solana/subscribable": 3.0.3(typescript@5.9.3) typescript: 5.9.3 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) "@solana/rpc-subscriptions-spec@3.0.3(typescript@5.9.3)": dependencies: @@ -24009,7 +24065,7 @@ snapshots: "@solana/subscribable": 3.0.3(typescript@5.9.3) typescript: 5.9.3 - "@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: "@solana/errors": 3.0.3(typescript@5.9.3) "@solana/fast-stable-stringify": 3.0.3(typescript@5.9.3) @@ -24017,7 +24073,7 @@ snapshots: "@solana/promises": 3.0.3(typescript@5.9.3) "@solana/rpc-spec-types": 3.0.3(typescript@5.9.3) "@solana/rpc-subscriptions-api": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - "@solana/rpc-subscriptions-channel-websocket": 3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/rpc-subscriptions-channel-websocket": 3.0.3(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/rpc-subscriptions-spec": 3.0.3(typescript@5.9.3) "@solana/rpc-transformers": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/rpc-types": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24102,7 +24158,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - "@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))": + "@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))": dependencies: "@solana/addresses": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/codecs-strings": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24110,7 +24166,7 @@ snapshots: "@solana/keys": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/promises": 3.0.3(typescript@5.9.3) "@solana/rpc": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - "@solana/rpc-subscriptions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + "@solana/rpc-subscriptions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) "@solana/rpc-types": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/transaction-messages": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) "@solana/transactions": 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24522,24 +24578,24 @@ snapshots: "@types/babel__core@7.20.5": dependencies: - "@babel/parser": 7.28.5 - "@babel/types": 7.28.5 + "@babel/parser": 7.28.6 + "@babel/types": 7.28.6 "@types/babel__generator": 7.27.0 "@types/babel__template": 7.4.4 "@types/babel__traverse": 7.28.0 "@types/babel__generator@7.27.0": dependencies: - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 "@types/babel__template@7.4.4": dependencies: - "@babel/parser": 7.28.5 - "@babel/types": 7.28.5 + "@babel/parser": 7.28.6 + "@babel/types": 7.28.6 "@types/babel__traverse@7.28.0": dependencies: - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 "@types/chai@5.2.2": dependencies: @@ -24914,6 +24970,25 @@ snapshots: dependencies: "@vanilla-extract/css": 1.17.3 + "@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@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))": + dependencies: + "@ampproject/remapping": 2.3.0 + "@bcoe/v8-coverage": 1.0.2 + ast-v8-to-istanbul: 0.3.11 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@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) + transitivePeerDependencies: + - supports-color + "@vitest/expect@3.2.4": dependencies: "@types/chai": 5.2.2 @@ -24956,9 +25031,9 @@ snapshots: loupe: 3.1.4 tinyrainbow: 2.0.0 - "@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)": + "@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)": dependencies: - "@base-org/account": 2.4.0(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + "@base-org/account": 2.4.0(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) "@coinbase/wallet-sdk": 4.3.6(@types/react@19.2.8)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@3.25.76) "@gemini-wallet/core": 0.3.2(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) "@metamask/sdk": 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -24967,7 +25042,7 @@ snapshots: "@wagmi/core": 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) "@walletconnect/ethereum-provider": 2.21.1(@types/react@19.2.8)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: "@coinbase/wallet-sdk@3.9.3" - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 @@ -25984,6 +26059,12 @@ snapshots: dependencies: tslib: 2.8.1 + ast-v8-to-istanbul@0.3.11: + dependencies: + "@jridgewell/trace-mapping": 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + astral-regex@2.0.0: {} async-function@1.0.0: {} @@ -26071,7 +26152,7 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: "@babel/template": 7.27.2 - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 "@types/babel__core": 7.20.5 "@types/babel__traverse": 7.28.0 @@ -28257,12 +28338,12 @@ snapshots: dependencies: graphql: 16.12.0 - graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: graphql: 16.12.0 optionalDependencies: crossws: 0.3.5 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) optional: true graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): @@ -28762,7 +28843,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: "@babel/core": 7.28.0 - "@babel/parser": 7.28.0 + "@babel/parser": 7.28.6 "@istanbuljs/schema": 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -28772,7 +28853,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: "@babel/core": 7.28.0 - "@babel/parser": 7.28.0 + "@babel/parser": 7.28.6 "@istanbuljs/schema": 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.7.3 @@ -28793,6 +28874,14 @@ snapshots: transitivePeerDependencies: - supports-color + istanbul-lib-source-maps@5.0.6: + dependencies: + "@jridgewell/trace-mapping": 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + istanbul-reports@3.1.7: dependencies: html-escaper: 2.0.2 @@ -29160,6 +29249,8 @@ snapshots: js-sha3@0.8.0: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -29523,6 +29614,12 @@ snapshots: dependencies: "@jridgewell/sourcemap-codec": 1.5.5 + magicast@0.3.5: + dependencies: + "@babel/parser": 7.28.6 + "@babel/types": 7.28.6 + source-map-js: 1.2.1 + make-dir@3.1.0: dependencies: semver: 6.3.1 @@ -30354,7 +30451,7 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: "@wagmi/core": 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.10.7 @@ -30368,7 +30465,7 @@ snapshots: "@tanstack/react-query": 5.90.11(react@19.2.3) react: 19.2.3 typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - "@types/react" - immer @@ -30591,7 +30688,7 @@ snapshots: dependencies: "@babel/core": 7.28.5 "@babel/traverse": 7.28.5 - "@babel/types": 7.28.5 + "@babel/types": 7.28.6 "@types/babel__core": 7.20.5 "@types/babel__traverse": 7.28.0 "@types/doctrine": 0.0.9 @@ -31596,6 +31693,12 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + test-exclude@7.0.1: + dependencies: + "@istanbuljs/schema": 0.1.3 + glob: 10.5.0 + minimatch: 9.0.5 + text-encoding-utf-8@1.0.2: {} text-extensions@2.4.0: {} @@ -32113,7 +32216,7 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - "@jridgewell/trace-mapping": 0.3.29 + "@jridgewell/trace-mapping": 0.3.31 "@types/istanbul-lib-coverage": 2.0.6 convert-source-map: 2.0.0 @@ -32325,10 +32428,10 @@ snapshots: vm-browserify@1.1.2: {} - wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): dependencies: "@tanstack/react-query": 5.90.11(react@19.2.3) - "@wagmi/connectors": 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + "@wagmi/connectors": 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.8)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) "@wagmi/core": 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.8)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.2.3 use-sync-external-store: 1.4.0(react@19.2.3) From da397fc94745b470a74686811743da124ce12a62 Mon Sep 17 00:00:00 2001 From: Leonardo Vieira Date: Mon, 9 Feb 2026 22:24:06 -0300 Subject: [PATCH 10/41] feat: types on config file --- apps/api/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 9306ab311f..285863b4fb 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -15,7 +15,8 @@ "@/*": ["./src/*"] }, "outDir": "./dist", - "moduleResolution": "node" + "moduleResolution": "node", + "types": ["vitest/globals"] }, "include": ["./**/*.ts"], "exclude": ["node_modules", "test"] From 8ebf980a13580e8bce856294f871b1d73a92cbc1 Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Tue, 10 Feb 2026 14:37:36 -0300 Subject: [PATCH 11/41] fix: account balance variations query --- apps/api-gateway/schema.graphql | 101 ++++++++---------- .../controllers/account-balance/variations.ts | 2 +- .../account-balance/variations.ts | 24 ++--- .../services/account-balance/variations.ts | 2 +- 4 files changed, 54 insertions(+), 75 deletions(-) diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index ae321a6392..998fb0f4cf 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -11,83 +11,73 @@ directive @httpOperation(subgraph: String, path: String, operationSpecificHeader directive @transport(subgraph: String, kind: String, location: String, headers: [[String]], queryStringOptions: ObjMap, queryParams: [[String]]) repeatable on SCHEMA type Query { - """ - Get historical delegations for an account, with optional filtering and sorting - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/{args.address}/delegations/historical`\nGet historical delegations for an account, with optional filtering and sorting\n" historicalDelegations(address: String!, delegateAddressIn: JSON, skip: NonNegativeInt, limit: PositiveInt = 10, fromValue: String, toValue: String, orderDirection: queryInput_historicalDelegations_orderDirection = desc): historicalDelegations_200_response - """Get current delegators of an account""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/{args.address}/delegations`\nGet current delegators of an account\n" delegations(address: String!): delegations_200_response - """ - Get historical Liquid Treasury (treasury without DAO tokens) from external providers (DefiLlama/Dune) - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/treasury/liquid`\nGet historical Liquid Treasury (treasury without DAO tokens) from external providers (DefiLlama/Dune)\n" 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) - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/treasury/dao-token`\nGet historical DAO Token Treasury value (governance token quantity × token price)\n" getDaoTokenTreasury(days: queryInput_getDaoTokenTreasury_days = _365d, order: queryInput_getDaoTokenTreasury_order = asc): getDaoTokenTreasury_200_response - """Get historical Total Treasury (liquid treasury + DAO token treasury)""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/treasury/total`\nGet historical Total Treasury (liquid treasury + DAO token treasury)\n" getTotalTreasury(days: queryInput_getTotalTreasury_days = _365d, order: queryInput_getTotalTreasury_order = asc): getTotalTreasury_200_response - """Get historical market data for a specific token""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/token/historical-data`\nGet historical market data for a specific token\n" historicalTokenData(skip: NonNegativeInt, limit: Float = 365): [query_historicalTokenData_items] - """Get property data for a specific token""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/token`\nGet property data for a specific token\n" token(currency: queryInput_token_currency = usd): token_200_response - """Compare total supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/total-supply/compare`\nCompare total supply between periods\n" compareTotalSupply(days: queryInput_compareTotalSupply_days = _90d): compareTotalSupply_200_response - """Compare delegated supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/delegated-supply/compare`\nCompare delegated supply between periods\n" compareDelegatedSupply(days: queryInput_compareDelegatedSupply_days = _90d): compareDelegatedSupply_200_response - """Compare circulating supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/circulating-supply/compare`\nCompare circulating supply between periods\n" compareCirculatingSupply(days: queryInput_compareCirculatingSupply_days = _90d): compareCirculatingSupply_200_response - """Compare treasury between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/treasury/compare`\nCompare treasury between periods\n" compareTreasury(days: queryInput_compareTreasury_days = _90d): compareTreasury_200_response - """Compare cex supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/cex-supply/compare`\nCompare cex supply between periods\n" compareCexSupply(days: queryInput_compareCexSupply_days = _90d): compareCexSupply_200_response - """Compare dex supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/dex-supply/compare`\nCompare dex supply between periods\n" compareDexSupply(days: queryInput_compareDexSupply_days = _90d): compareDexSupply_200_response - """Compare lending supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/lending-supply/compare`\nCompare lending supply between periods\n" compareLendingSupply(days: queryInput_compareLendingSupply_days = _90d): compareLendingSupply_200_response - """Get active token supply for DAO""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/active-supply/compare`\nGet active token supply for DAO\n" compareActiveSupply(days: queryInput_compareActiveSupply_days = _90d): compareActiveSupply_200_response - """Compare number of proposals between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals/compare`\nCompare number of proposals between time periods\n" compareProposals(days: queryInput_compareProposals_days = _90d): compareProposals_200_response - """Compare number of votes between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/votes/compare`\nCompare number of votes between time periods\n" compareVotes(days: queryInput_compareVotes_days = _90d): compareVotes_200_response - """Compare average turnout between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/average-turnout/compare`\nCompare average turnout between time periods\n" compareAverageTurnout(days: queryInput_compareAverageTurnout_days = _90d): compareAverageTurnout_200_response - """ - Returns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals-activity`\nReturns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window\n" proposalsActivity(address: String!, fromDate: String, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_proposalsActivity_orderBy = timestamp, orderDirection: queryInput_proposalsActivity_orderDirection = desc, userVoteFilter: queryInput_proposalsActivity_userVoteFilter): proposalsActivity_200_response - """Returns a list of proposal""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals`\nReturns a list of proposal\n" proposals(skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposals_orderDirection = desc, status: JSON, fromDate: Float, fromEndDate: Float, includeOptimisticProposals: queryInput_proposals_includeOptimisticProposals = TRUE): proposals_200_response - """Returns a single proposal by its ID""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals/{args.id}`\nReturns a single proposal by its ID\n" proposal(id: String!): proposal_200_response - """TODO""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/{args.address}/balances/historical`\nTODO\n" historicalBalances(address: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_historicalBalances_orderBy = timestamp, orderDirection: queryInput_historicalBalances_orderDirection = desc, fromDate: String, toDate: String, fromValue: String, toValue: String): historicalBalances_200_response - """ - Get transactions with their associated transfers and delegations, with optional filtering and sorting - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/transactions`\nGet transactions with their associated transfers and delegations, with optional filtering and sorting\n" transactions( limit: PositiveInt = 50 offset: NonNegativeInt @@ -109,68 +99,61 @@ type Query { includes: JSON ): transactions_200_response - """Get the last update time""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/last-update`\nGet the last update time\n" lastUpdate(chart: queryInput_lastUpdate_chart!): lastUpdate_200_response - """Get delegation percentage day buckets with forward-fill""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/delegation-percentage`\nGet delegation percentage day buckets with forward-fill\n" delegationPercentageByDay(startDate: String, endDate: String, orderDirection: queryInput_delegationPercentageByDay_orderDirection = asc, limit: NonNegativeInt = 365, after: String, before: String): delegationPercentageByDay_200_response - """Returns a list of voting power changes for a specific account""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/{args.address}/voting-powers/historical`\nReturns a list of voting power changes for a specific account\n" historicalVotingPowerByAccountId(address: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_historicalVotingPowerByAccountId_orderBy = timestamp, orderDirection: queryInput_historicalVotingPowerByAccountId_orderDirection = desc, fromDate: String, toDate: String, fromValue: String, toValue: String): historicalVotingPowerByAccountId_200_response - """Returns a list of voting power changes.""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/voting-powers/historical`\nReturns a list of voting power changes.\n" historicalVotingPower(skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_historicalVotingPower_orderBy = timestamp, orderDirection: queryInput_historicalVotingPower_orderDirection = desc, fromDate: String, toDate: String, fromValue: String, toValue: String, address: String): historicalVotingPower_200_response - """ - Returns a mapping of the voting power changes within a time frame for the given addresses - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/voting-powers/variations`\nReturns a mapping of the voting power changes within a time frame for the given addresses\n" votingPowerVariations(limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_votingPowerVariations_orderDirection = desc, addresses: JSON, fromDate: String, toDate: String): votingPowerVariations_200_response - """Returns a the changes to voting power by period and accountId""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/{args.address}/voting-powers/variations`\nReturns a the changes to voting power by period and accountId\n" votingPowerVariationsByAccountId(address: String!, fromDate: String, toDate: String): votingPowerVariationsByAccountId_200_response - """Returns sorted and paginated account voting power records""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/voting-powers`\nReturns sorted and paginated account voting power records\n" votingPowers(limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_votingPowers_orderDirection = desc, orderBy: queryInput_votingPowers_orderBy = votingPower, addresses: JSON, fromValue: String, toValue: String): votingPowers_200_response - """Returns voting power information for a specific address (account)""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/voting-powers/{args.accountId}`\nReturns voting power information for a specific address (account)\n" votingPowerByAccountId(accountId: String!): votingPowerByAccountId_200_response - """ - Returns a mapping of the biggest variations to account balances associated by account address - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/balances/variations`\nReturns a mapping of the biggest variations to account balances associated by account address\n" accountBalanceVariations(fromDate: String, toDate: String, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountBalanceVariations_orderDirection = desc, addresses: JSON): accountBalanceVariations_200_response - """Returns a the changes to balance by period and accountId""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/{args.address}/balances/variations`\nReturns a the changes to balance by period and accountId\n" accountBalanceVariationsByAccountId(address: String!, fromDate: String, toDate: String): accountBalanceVariationsByAccountId_200_response - """Returns sorted and paginated account balance records""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/balances`\nReturns sorted and paginated account balance records\n" accountBalances(limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountBalances_orderDirection = desc, addresses: JSON, delegates: JSON, fromValue: String, toValue: String): accountBalances_200_response - """Returns account balance information for a specific address""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/{args.address}/balances`\nReturns account balance information for a specific address\n" accountBalanceByAccountId(address: String!): accountBalanceByAccountId_200_response - """ - Returns a mapping of the largest interactions between accounts. - Positive amounts signify net token transfers FROM
, whilst negative amounts refer to net transfers TO
- """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/balances/{args.address}/interactions`\nReturns a mapping of the largest interactions between accounts. \nPositive amounts signify net token transfers FROM
, whilst negative amounts refer to net transfers TO
\n" accountInteractions(address: String!, fromDate: String, toDate: String, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountInteractions_orderDirection = desc, minAmount: String, maxAmount: String, orderBy: queryInput_accountInteractions_orderBy = count, filterAddress: String): accountInteractions_200_response - """Get transfers of a given address""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/accounts/{args.address}/transfers`\nGet transfers of a given address\n" transfers(address: String!, limit: Float = 10, offset: Float, sortBy: queryInput_transfers_sortBy = timestamp, sortOrder: queryInput_transfers_sortOrder = asc, from: String, to: String, fromDate: Float, toDate: Float, fromValue: String, toValue: String): transfers_200_response - """Returns a paginated list of votes cast on a specific proposal""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals/{args.id}/votes`\nReturns a paginated list of votes cast on a specific proposal\n" votesByProposalId(id: String!, skip: NonNegativeInt, limit: Float = 10, voterAddressIn: JSON, orderBy: queryInput_votesByProposalId_orderBy = timestamp, orderDirection: queryInput_votesByProposalId_orderDirection = desc, support: Float, fromDate: Float, toDate: Float): votesByProposalId_200_response - """Get all votes ordered by timestamp or voting power""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/votes`\nGet all votes ordered by timestamp or voting power\n" votes(skip: NonNegativeInt, limit: Float = 10, voterAddressIn: JSON, orderBy: queryInput_votes_orderBy = timestamp, orderDirection: queryInput_votes_orderDirection = desc, support: Float, fromDate: Float, toDate: Float): votes_200_response - """Returns the active delegates that did not vote on a given proposal""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals/{args.id}/non-voters`\nReturns the active delegates that did not vote on a given proposal\n" proposalNonVoters(id: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposalNonVoters_orderDirection = desc, addresses: JSON): proposalNonVoters_200_response - """Returns current governance parameters for this DAO""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/dao`\nReturns current governance parameters for this DAO\n" dao: dao_200_response - """Returns token related metrics for a single metric type.""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/token-metrics`\nReturns token related metrics for a single metric type.\n" tokenMetrics(metricType: queryInput_tokenMetrics_metricType!, startDate: Float, endDate: Float, orderDirection: queryInput_tokenMetrics_orderDirection = asc, limit: NonNegativeInt = 365, skip: NonNegativeInt): tokenMetrics_200_response """ diff --git a/apps/api/src/controllers/account-balance/variations.ts b/apps/api/src/controllers/account-balance/variations.ts index b64e13f91d..4f516e2604 100644 --- a/apps/api/src/controllers/account-balance/variations.ts +++ b/apps/api/src/controllers/account-balance/variations.ts @@ -51,7 +51,7 @@ export function accountBalanceVariations( ); return context.json( - AccountBalanceVariationsResponseMapper(result, fromDate, toDate), + AccountBalanceVariationsResponseMapper(result, fromDate, toDate) ); }, ); diff --git a/apps/api/src/repositories/account-balance/variations.ts b/apps/api/src/repositories/account-balance/variations.ts index f6416759a7..35b24dc8c6 100644 --- a/apps/api/src/repositories/account-balance/variations.ts +++ b/apps/api/src/repositories/account-balance/variations.ts @@ -1,11 +1,11 @@ -import { asc, desc, gte, sql, and, inArray, lte, or } from "drizzle-orm"; +import { asc, desc, gte, sql, and, inArray, lte } from "drizzle-orm"; import { Drizzle } from "@/database"; import { accountBalance, transfer } from "@/database"; import { DBAccountBalanceVariation } from "@/mappers"; import { Address } from "viem"; export class BalanceVariationsRepository { - constructor(private readonly db: Drizzle) {} + constructor(private readonly db: Drizzle) { } async getAccountBalanceVariations( fromTimestamp: number | undefined, @@ -67,12 +67,6 @@ export class BalanceVariationsRepository { toTimestamp ? lte(transfer.timestamp, BigInt(toTimestamp)) : undefined, - addresses - ? or( - inArray(transfer.fromAccountId, addresses), - inArray(transfer.toAccountId, addresses), - ) - : undefined, ), ) .as("scoped_transfers"); @@ -80,7 +74,7 @@ export class BalanceVariationsRepository { const transfersFrom = this.db .select({ accountId: scopedTransfers.fromAccountId, - fromAmount: sql`-SUM(${transfer.amount})`.as("from_amount"), + fromAmount: sql`-SUM(${scopedTransfers.amount})`.as("from_amount"), }) .from(scopedTransfers) .groupBy(scopedTransfers.fromAccountId) @@ -89,7 +83,7 @@ export class BalanceVariationsRepository { const transfersTo = this.db .select({ accountId: scopedTransfers.toAccountId, - toAmount: sql`SUM(${transfer.amount})`.as("to_amount"), + toAmount: sql`SUM(${scopedTransfers.amount})`.as("to_amount"), }) .from(scopedTransfers) .groupBy(scopedTransfers.toAccountId) @@ -116,7 +110,9 @@ export class BalanceVariationsRepository { sql`${accountBalance.accountId} = ${transfersTo.accountId}`, ) .where( - sql`${transfersFrom.accountId} IS NOT NULL OR ${transfersTo.accountId} IS NOT NULL`, + addresses + ? inArray(accountBalance.accountId, addresses) + : undefined, ) .as("combined"); @@ -146,9 +142,9 @@ export class BalanceVariationsRepository { absoluteChange: BigInt(absoluteChange), percentageChange: (currentBalance - BigInt(absoluteChange) ? Number( - (BigInt(absoluteChange) * 10000n) / - (currentBalance - BigInt(absoluteChange)), - ) / 100 + (BigInt(absoluteChange) * 10000n) / + (currentBalance - BigInt(absoluteChange)), + ) / 100 : 0 ).toString(), })); diff --git a/apps/api/src/services/account-balance/variations.ts b/apps/api/src/services/account-balance/variations.ts index d2b510134d..81b4f84277 100644 --- a/apps/api/src/services/account-balance/variations.ts +++ b/apps/api/src/services/account-balance/variations.ts @@ -39,7 +39,7 @@ export class BalanceVariationsService { constructor( private readonly balanceRepository: AccountBalanceRepository, private readonly interactionRepository: AccountInteractionsRepository, - ) {} + ) { } async getAccountBalanceVariations( fromTimestamp: number | undefined, From 1f6923f4163c6a72e770156bf78ab8b7a6056a0e Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Tue, 10 Feb 2026 14:51:35 -0300 Subject: [PATCH 12/41] fix: performance adjustment --- apps/api/src/repositories/account-balance/variations.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/api/src/repositories/account-balance/variations.ts b/apps/api/src/repositories/account-balance/variations.ts index 3ed847fc55..dd09398462 100644 --- a/apps/api/src/repositories/account-balance/variations.ts +++ b/apps/api/src/repositories/account-balance/variations.ts @@ -104,9 +104,12 @@ export class BalanceVariationsRepository { sql`${accountBalance.accountId} = ${transfersTo.accountId}`, ) .where( - addresses - ? inArray(accountBalance.accountId, addresses) - : undefined, + and( + addresses + ? inArray(accountBalance.accountId, addresses) + : undefined, + sql`${transfersFrom.accountId} IS NOT NULL OR ${transfersTo.accountId} IS NOT NULL`, + ) ) .as("combined"); From 6933130671f42c20792add6b211d6ef40984ba5c Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Tue, 10 Feb 2026 15:14:50 -0300 Subject: [PATCH 13/41] refactor: separated common query into two distinct operations --- .../account-balance/variations.ts | 127 +++++++++++++----- 1 file changed, 91 insertions(+), 36 deletions(-) diff --git a/apps/api/src/repositories/account-balance/variations.ts b/apps/api/src/repositories/account-balance/variations.ts index dd09398462..e0c23b62b3 100644 --- a/apps/api/src/repositories/account-balance/variations.ts +++ b/apps/api/src/repositories/account-balance/variations.ts @@ -1,4 +1,4 @@ -import { asc, desc, gte, sql, and, inArray, lte } from "drizzle-orm"; +import { asc, desc, gte, sql, and, inArray, lte, eq } from "drizzle-orm"; import { Drizzle } from "@/database"; import { accountBalance, transfer } from "@/database"; import { DBAccountBalanceVariation } from "@/mappers"; @@ -14,41 +14,6 @@ export class BalanceVariationsRepository { skip: number, orderDirection: "asc" | "desc", addresses?: Address[], - ): Promise { - return this.commonQuery( - fromTimestamp, - toTimestamp, - limit, - skip, - orderDirection, - addresses, - ); - } - - async getAccountBalanceVariationsByAccountId( - address: Address, - fromTimestamp: number | undefined, - toTimestamp: number | undefined, - ): Promise { - const [result] = await this.commonQuery( - fromTimestamp, - toTimestamp, - 1, - 0, - "desc", - [address], - ); - - return result; - } - - private async commonQuery( - fromTimestamp: number | undefined, - toTimestamp: number | undefined, - limit: number, - skip: number, - orderDirection: "asc" | "desc", - addresses?: Address[], ): Promise { const scopedTransfers = this.db .select() @@ -146,4 +111,94 @@ export class BalanceVariationsRepository { ).toString(), })); } + + async getAccountBalanceVariationsByAccountId( + address: Address, + fromTimestamp: number | undefined, + toTimestamp: number | undefined, + ): Promise { + const scopedTransfers = this.db + .select() + .from(transfer) + .where( + and( + fromTimestamp + ? gte(transfer.timestamp, BigInt(fromTimestamp)) + : undefined, + toTimestamp + ? lte(transfer.timestamp, BigInt(toTimestamp)) + : undefined, + ), + ) + .as("scoped_transfers"); + + const transfersFrom = this.db + .select({ + accountId: scopedTransfers.fromAccountId, + fromAmount: sql`-SUM(${scopedTransfers.amount})`.as("from_amount"), + }) + .from(scopedTransfers) + .groupBy(scopedTransfers.fromAccountId) + .as("transfers_from"); + + const transfersTo = this.db + .select({ + accountId: scopedTransfers.toAccountId, + toAmount: sql`SUM(${scopedTransfers.amount})`.as("to_amount"), + }) + .from(scopedTransfers) + .groupBy(scopedTransfers.toAccountId) + .as("transfers_to"); + + const combined = this.db + .select({ + accountId: accountBalance.accountId, + currentBalance: accountBalance.balance, + fromChange: sql`COALESCE(${transfersFrom.fromAmount}, 0)`.as( + "from_change", + ), + toChange: sql`COALESCE(${transfersTo.toAmount}, 0)`.as( + "to_change", + ), + }) + .from(accountBalance) + .leftJoin( + transfersFrom, + sql`${accountBalance.accountId} = ${transfersFrom.accountId}`, + ) + .leftJoin( + transfersTo, + sql`${accountBalance.accountId} = ${transfersTo.accountId}`, + ) + .where(eq(accountBalance.accountId, address)) + .as("combined"); + + const [result] = await this.db + .select({ + accountId: combined.accountId, + currentBalance: combined.currentBalance, + absoluteChange: + sql`${combined.fromChange} + ${combined.toChange}`.as( + "absolute_change", + ), + }) + .from(combined) + .where(sql`(${combined.fromChange} + ${combined.toChange}) != 0`); + + if (!result) return undefined + + return { + accountId: result.accountId, + previousBalance: result.currentBalance - BigInt(result.absoluteChange), + currentBalance: result.currentBalance, + absoluteChange: BigInt(result.absoluteChange), + percentageChange: (result.currentBalance - BigInt(result.absoluteChange) + ? Number( + (BigInt(result.absoluteChange) * 10000n) / + (result.currentBalance - BigInt(result.absoluteChange)), + ) / 100 + : 0 + ).toString(), + }; + } } From b3e4e7e6c0f6ebf89636c975efd3c7afb31e74e8 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 10 Feb 2026 16:42:24 -0300 Subject: [PATCH 14/41] chore: new dashboard-focused skills --- .cursor/skills/implement-design/SKILL.md | 252 ++ .cursor/skills/next-best-practices/SKILL.md | 171 + .../next-best-practices/async-patterns.md | 87 + .../skills/next-best-practices/bundling.md | 182 ++ .../next-best-practices/data-patterns.md | 306 ++ .../next-best-practices/debug-tricks.md | 125 + .../skills/next-best-practices/directives.md | 74 + .../next-best-practices/error-handling.md | 232 ++ .../next-best-practices/file-conventions.md | 141 + .cursor/skills/next-best-practices/font.md | 257 ++ .../skills/next-best-practices/functions.md | 108 + .../next-best-practices/hydration-error.md | 88 + .cursor/skills/next-best-practices/image.md | 179 + .../skills/next-best-practices/metadata.md | 296 ++ .../next-best-practices/parallel-routes.md | 298 ++ .../next-best-practices/route-handlers.md | 146 + .../next-best-practices/rsc-boundaries.md | 164 + .../next-best-practices/runtime-selection.md | 40 + .cursor/skills/next-best-practices/scripts.md | 141 + .../next-best-practices/self-hosting.md | 384 +++ .../suspense-boundaries.md | 67 + .cursor/skills/next-cache-components/SKILL.md | 363 ++ .../skills/storybook-story-writing/SKILL.md | 364 +++ .../systematic-debugging/CREATION-LOG.md | 133 + .cursor/skills/systematic-debugging/SKILL.md | 305 ++ .../condition-based-waiting-example.ts | 158 + .../condition-based-waiting.md | 122 + .../systematic-debugging/defense-in-depth.md | 130 + .../systematic-debugging/find-polluter.sh | 63 + .../root-cause-tracing.md | 183 ++ .../systematic-debugging/test-academic.md | 14 + .../systematic-debugging/test-pressure-1.md | 65 + .../systematic-debugging/test-pressure-2.md | 75 + .../systematic-debugging/test-pressure-3.md | 75 + .../skills/tailwind-design-system/SKILL.md | 874 +++++ .../tanstack-query-best-practices/SKILL.md | 115 + .../rules/cache-gc-time.md | 93 + .../rules/cache-invalidation.md | 117 + .../rules/cache-placeholder-vs-initial.md | 156 + .../rules/cache-stale-time.md | 80 + .../rules/err-error-boundaries.md | 148 + .../rules/inf-page-params.md | 127 + .../rules/mut-invalidate-queries.md | 123 + .../rules/mut-mutation-state.md | 168 + .../rules/mut-optimistic-updates.md | 138 + .../rules/network-mode.md | 174 + .../rules/parallel-use-queries.md | 154 + .../rules/perf-select-transform.md | 149 + .../rules/persist-queries.md | 192 ++ .../rules/pf-intent-prefetch.md | 141 + .../rules/qk-array-structure.md | 50 + .../rules/qk-factory-pattern.md | 104 + .../rules/qk-hierarchical-organization.md | 76 + .../rules/qk-include-dependencies.md | 62 + .../rules/qk-serializable.md | 97 + .../rules/query-cancellation.md | 171 + .../rules/ssr-dehydration.md | 162 + .../skills/typescript-advanced-types/SKILL.md | 724 ++++ .../vercel-composition-patterns/AGENTS.md | 951 ++++++ .../vercel-composition-patterns/SKILL.md | 89 + .../rules/architecture-avoid-boolean-props.md | 100 + .../rules/architecture-compound-components.md | 112 + .../patterns-children-over-render-props.md | 87 + .../rules/patterns-explicit-variants.md | 100 + .../rules/react19-no-forwardref.md | 45 + .../rules/state-context-interface.md | 191 ++ .../rules/state-decouple-implementation.md | 113 + .../rules/state-lift-state.md | 127 + .../vercel-react-best-practices/AGENTS.md | 2906 +++++++++++++++++ .../vercel-react-best-practices/SKILL.md | 138 + .../rules/advanced-event-handler-refs.md | 55 + .../rules/advanced-init-once.md | 42 + .../rules/advanced-use-latest.md | 39 + .../rules/async-api-routes.md | 38 + .../rules/async-defer-await.md | 80 + .../rules/async-dependencies.md | 52 + .../rules/async-parallel.md | 28 + .../rules/async-suspense-boundaries.md | 99 + .../rules/bundle-barrel-imports.md | 59 + .../rules/bundle-conditional.md | 37 + .../rules/bundle-defer-third-party.md | 49 + .../rules/bundle-dynamic-imports.md | 35 + .../rules/bundle-preload.md | 46 + .../rules/client-event-listeners.md | 78 + .../rules/client-localstorage-schema.md | 77 + .../rules/client-passive-event-listeners.md | 48 + .../rules/client-swr-dedup.md | 56 + .../rules/js-batch-dom-css.md | 110 + .../rules/js-cache-function-results.md | 80 + .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 70 + .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 + .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 50 + .../rules/js-min-max-loop.md | 82 + .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 + .../rules/rendering-activity.md | 26 + .../rules/rendering-animate-svg-wrapper.md | 38 + .../rules/rendering-conditional-render.md | 32 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 36 + .../rules/rendering-hydration-no-flicker.md | 72 + .../rendering-hydration-suppress-warning.md | 26 + .../rules/rendering-svg-precision.md | 28 + .../rules/rendering-usetransition-loading.md | 75 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state-no-effect.md | 40 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 77 + .../rules/rerender-lazy-state-init.md | 58 + .../rules/rerender-memo-with-default-value.md | 36 + .../rules/rerender-memo.md | 44 + .../rules/rerender-move-effect-to-event.md | 45 + .../rerender-simple-expression-in-memo.md | 35 + .../rules/rerender-transitions.md | 40 + .../rerender-use-ref-transient-values.md | 73 + .../rules/server-after-nonblocking.md | 74 + .../rules/server-auth-actions.md | 96 + .../rules/server-cache-lru.md | 41 + .../rules/server-cache-react.md | 76 + .../rules/server-dedup-props.md | 65 + .../rules/server-parallel-fetching.md | 83 + .../rules/server-serialization.md | 38 + .cursor/skills/web-design-guidelines/SKILL.md | 40 + .gitignore | 4 + 129 files changed, 18174 insertions(+) create mode 100644 .cursor/skills/implement-design/SKILL.md create mode 100644 .cursor/skills/next-best-practices/SKILL.md create mode 100644 .cursor/skills/next-best-practices/async-patterns.md create mode 100644 .cursor/skills/next-best-practices/bundling.md create mode 100644 .cursor/skills/next-best-practices/data-patterns.md create mode 100644 .cursor/skills/next-best-practices/debug-tricks.md create mode 100644 .cursor/skills/next-best-practices/directives.md create mode 100644 .cursor/skills/next-best-practices/error-handling.md create mode 100644 .cursor/skills/next-best-practices/file-conventions.md create mode 100644 .cursor/skills/next-best-practices/font.md create mode 100644 .cursor/skills/next-best-practices/functions.md create mode 100644 .cursor/skills/next-best-practices/hydration-error.md create mode 100644 .cursor/skills/next-best-practices/image.md create mode 100644 .cursor/skills/next-best-practices/metadata.md create mode 100644 .cursor/skills/next-best-practices/parallel-routes.md create mode 100644 .cursor/skills/next-best-practices/route-handlers.md create mode 100644 .cursor/skills/next-best-practices/rsc-boundaries.md create mode 100644 .cursor/skills/next-best-practices/runtime-selection.md create mode 100644 .cursor/skills/next-best-practices/scripts.md create mode 100644 .cursor/skills/next-best-practices/self-hosting.md create mode 100644 .cursor/skills/next-best-practices/suspense-boundaries.md create mode 100644 .cursor/skills/next-cache-components/SKILL.md create mode 100644 .cursor/skills/storybook-story-writing/SKILL.md create mode 100644 .cursor/skills/systematic-debugging/CREATION-LOG.md create mode 100644 .cursor/skills/systematic-debugging/SKILL.md create mode 100644 .cursor/skills/systematic-debugging/condition-based-waiting-example.ts create mode 100644 .cursor/skills/systematic-debugging/condition-based-waiting.md create mode 100644 .cursor/skills/systematic-debugging/defense-in-depth.md create mode 100644 .cursor/skills/systematic-debugging/find-polluter.sh create mode 100644 .cursor/skills/systematic-debugging/root-cause-tracing.md create mode 100644 .cursor/skills/systematic-debugging/test-academic.md create mode 100644 .cursor/skills/systematic-debugging/test-pressure-1.md create mode 100644 .cursor/skills/systematic-debugging/test-pressure-2.md create mode 100644 .cursor/skills/systematic-debugging/test-pressure-3.md create mode 100644 .cursor/skills/tailwind-design-system/SKILL.md create mode 100644 .cursor/skills/tanstack-query-best-practices/SKILL.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/cache-gc-time.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/cache-invalidation.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/cache-placeholder-vs-initial.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/cache-stale-time.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/err-error-boundaries.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/inf-page-params.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/mut-invalidate-queries.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/mut-mutation-state.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/mut-optimistic-updates.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/network-mode.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/parallel-use-queries.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/perf-select-transform.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/persist-queries.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/pf-intent-prefetch.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/qk-array-structure.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/qk-factory-pattern.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/qk-hierarchical-organization.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/qk-include-dependencies.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/qk-serializable.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/query-cancellation.md create mode 100644 .cursor/skills/tanstack-query-best-practices/rules/ssr-dehydration.md create mode 100644 .cursor/skills/typescript-advanced-types/SKILL.md create mode 100644 .cursor/skills/vercel-composition-patterns/AGENTS.md create mode 100644 .cursor/skills/vercel-composition-patterns/SKILL.md create mode 100644 .cursor/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md create mode 100644 .cursor/skills/vercel-composition-patterns/rules/architecture-compound-components.md create mode 100644 .cursor/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md create mode 100644 .cursor/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md create mode 100644 .cursor/skills/vercel-composition-patterns/rules/react19-no-forwardref.md create mode 100644 .cursor/skills/vercel-composition-patterns/rules/state-context-interface.md create mode 100644 .cursor/skills/vercel-composition-patterns/rules/state-decouple-implementation.md create mode 100644 .cursor/skills/vercel-composition-patterns/rules/state-lift-state.md create mode 100644 .cursor/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .cursor/skills/vercel-react-best-practices/SKILL.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/advanced-init-once.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/async-api-routes.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-activity.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/server-after-nonblocking.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/server-auth-actions.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/server-cache-lru.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/server-cache-react.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/server-dedup-props.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/server-parallel-fetching.md create mode 100644 .cursor/skills/vercel-react-best-practices/rules/server-serialization.md create mode 100644 .cursor/skills/web-design-guidelines/SKILL.md diff --git a/.cursor/skills/implement-design/SKILL.md b/.cursor/skills/implement-design/SKILL.md new file mode 100644 index 0000000000..1170eaea3c --- /dev/null +++ b/.cursor/skills/implement-design/SKILL.md @@ -0,0 +1,252 @@ +--- +name: implement-design +description: Translates Figma designs into production-ready code with 1:1 visual fidelity. Use when implementing UI from Figma files, when user mentions "implement design", "generate code", "implement component", "build Figma design", provides Figma URLs, or asks to build components matching Figma specs. Requires Figma MCP server connection. +metadata: + mcp-server: figma, figma-desktop +--- + +# Implement Design + +## Overview + +This skill provides a structured workflow for translating Figma designs into production-ready code with pixel-perfect accuracy. It ensures consistent integration with the Figma MCP server, proper use of design tokens, and 1:1 visual parity with designs. + +## Prerequisites + +- Figma MCP server must be connected and accessible +- User must provide a Figma URL in the format: `https://figma.com/design/:fileKey/:fileName?node-id=1-2` + - `:fileKey` is the file key + - `1-2` is the node ID (the specific component or frame to implement) +- **OR** when using `figma-desktop` MCP: User can select a node directly in the Figma desktop app (no URL required) +- Project should have an established design system or component library (preferred) + +## Required Workflow + +**Follow these steps in order. Do not skip steps.** + +### Step 1: Get Node ID + +#### Option A: Parse from Figma URL + +When the user provides a Figma URL, extract the file key and node ID to pass as arguments to MCP tools. + +**URL format:** `https://figma.com/design/:fileKey/:fileName?node-id=1-2` + +**Extract:** + +- **File key:** `:fileKey` (the segment after `/design/`) +- **Node ID:** `1-2` (the value of the `node-id` query parameter) + +**Note:** When using the local desktop MCP (`figma-desktop`), `fileKey` is not passed as a parameter to tool calls. The server automatically uses the currently open file, so only `nodeId` is needed. + +**Example:** + +- URL: `https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15` +- File key: `kL9xQn2VwM8pYrTb4ZcHjF` +- Node ID: `42-15` + +#### Option B: Use Current Selection from Figma Desktop App (figma-desktop MCP only) + +When using the `figma-desktop` MCP and the user has NOT provided a URL, the tools automatically use the currently selected node from the open Figma file in the desktop app. + +**Note:** Selection-based prompting only works with the `figma-desktop` MCP server. The remote server requires a link to a frame or layer to extract context. The user must have the Figma desktop app open with a node selected. + +### Step 2: Fetch Design Context + +Run `get_design_context` with the extracted file key and node ID. + +``` +get_design_context(fileKey=":fileKey", nodeId="1-2") +``` + +This provides the structured data including: + +- Layout properties (Auto Layout, constraints, sizing) +- Typography specifications +- Color values and design tokens +- Component structure and variants +- Spacing and padding values + +**If the response is too large or truncated:** + +1. Run `get_metadata(fileKey=":fileKey", nodeId="1-2")` to get the high-level node map +2. Identify the specific child nodes needed from the metadata +3. Fetch individual child nodes with `get_design_context(fileKey=":fileKey", nodeId=":childNodeId")` + +### Step 3: Capture Visual Reference + +Run `get_screenshot` with the same file key and node ID for a visual reference. + +``` +get_screenshot(fileKey=":fileKey", nodeId="1-2") +``` + +This screenshot serves as the source of truth for visual validation. Keep it accessible throughout implementation. + +### Step 4: Download Required Assets + +Download any assets (images, icons, SVGs) returned by the Figma MCP server. + +**IMPORTANT:** Follow these asset rules: + +- If the Figma MCP server returns a `localhost` source for an image or SVG, use that source directly +- DO NOT import or add new icon packages - all assets should come from the Figma payload +- DO NOT use or create placeholders if a `localhost` source is provided +- Assets are served through the Figma MCP server's built-in assets endpoint + +### Step 5: Translate to Project Conventions + +Translate the Figma output into this project's framework, styles, and conventions. + +**Key principles:** + +- Treat the Figma MCP output (typically React + Tailwind) as a representation of design and behavior, not as final code style +- Replace Tailwind utility classes with the project's preferred utilities or design system tokens +- Reuse existing components (buttons, inputs, typography, icon wrappers) instead of duplicating functionality +- Use the project's color system, typography scale, and spacing tokens consistently +- Respect existing routing, state management, and data-fetch patterns + +### Step 6: Achieve 1:1 Visual Parity + +Strive for pixel-perfect visual parity with the Figma design. + +**Guidelines:** + +- Prioritize Figma fidelity to match designs exactly +- Avoid hardcoded values - use design tokens from Figma where available +- When conflicts arise between design system tokens and Figma specs, prefer design system tokens but adjust spacing or sizes minimally to match visuals +- Follow WCAG requirements for accessibility +- Add component documentation as needed + +### Step 7: Validate Against Figma + +Before marking complete, validate the final UI against the Figma screenshot. + +**Validation checklist:** + +- [ ] Layout matches (spacing, alignment, sizing) +- [ ] Typography matches (font, size, weight, line height) +- [ ] Colors match exactly +- [ ] Interactive states work as designed (hover, active, disabled) +- [ ] Responsive behavior follows Figma constraints +- [ ] Assets render correctly +- [ ] Accessibility standards met + +## Implementation Rules + +### Component Organization + +- Place UI components in the project's designated design system directory +- Follow the project's component naming conventions +- Avoid inline styles unless truly necessary for dynamic values + +### Design System Integration + +- ALWAYS use components from the project's design system when possible +- Map Figma design tokens to project design tokens +- When a matching component exists, extend it rather than creating a new one +- Document any new components added to the design system + +### Code Quality + +- Avoid hardcoded values - extract to constants or design tokens +- Keep components composable and reusable +- Add TypeScript types for component props +- Include JSDoc comments for exported components + +## Examples + +### Example 1: Implementing a Button Component + +User says: "Implement this Figma button component: https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15" + +**Actions:** + +1. Parse URL to extract fileKey=`kL9xQn2VwM8pYrTb4ZcHjF` and nodeId=`42-15` +2. Run `get_design_context(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42-15")` +3. Run `get_screenshot(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42-15")` for visual reference +4. Download any button icons from the assets endpoint +5. Check if project has existing button component +6. If yes, extend it with new variant; if no, create new component using project conventions +7. Map Figma colors to project design tokens (e.g., `primary-500`, `primary-hover`) +8. Validate against screenshot for padding, border radius, typography + +**Result:** Button component matching Figma design, integrated with project design system. + +### Example 2: Building a Dashboard Layout + +User says: "Build this dashboard: https://figma.com/design/pR8mNv5KqXzGwY2JtCfL4D/Dashboard?node-id=10-5" + +**Actions:** + +1. Parse URL to extract fileKey=`pR8mNv5KqXzGwY2JtCfL4D` and nodeId=`10-5` +2. Run `get_metadata(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10-5")` to understand the page structure +3. Identify main sections from metadata (header, sidebar, content area, cards) and their child node IDs +4. Run `get_design_context(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId=":childNodeId")` for each major section +5. Run `get_screenshot(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10-5")` for the full page +6. Download all assets (logos, icons, charts) +7. Build layout using project's layout primitives +8. Implement each section using existing components where possible +9. Validate responsive behavior against Figma constraints + +**Result:** Complete dashboard matching Figma design with responsive layout. + +## Best Practices + +### Always Start with Context + +Never implement based on assumptions. Always fetch `get_design_context` and `get_screenshot` first. + +### Incremental Validation + +Validate frequently during implementation, not just at the end. This catches issues early. + +### Document Deviations + +If you must deviate from the Figma design (e.g., for accessibility or technical constraints), document why in code comments. + +### Reuse Over Recreation + +Always check for existing components before creating new ones. Consistency across the codebase is more important than exact Figma replication. + +### Design System First + +When in doubt, prefer the project's design system patterns over literal Figma translation. + +## Common Issues and Solutions + +### Issue: Figma output is truncated + +**Cause:** The design is too complex or has too many nested layers to return in a single response. +**Solution:** Use `get_metadata` to get the node structure, then fetch specific nodes individually with `get_design_context`. + +### Issue: Design doesn't match after implementation + +**Cause:** Visual discrepancies between the implemented code and the original Figma design. +**Solution:** Compare side-by-side with the screenshot from Step 3. Check spacing, colors, and typography values in the design context data. + +### Issue: Assets not loading + +**Cause:** The Figma MCP server's assets endpoint is not accessible or the URLs are being modified. +**Solution:** Verify the Figma MCP server's assets endpoint is accessible. The server serves assets at `localhost` URLs. Use these directly without modification. + +### Issue: Design token values differ from Figma + +**Cause:** The project's design system tokens have different values than those specified in the Figma design. +**Solution:** When project tokens differ from Figma values, prefer project tokens for consistency but adjust spacing/sizing to maintain visual fidelity. + +## Understanding Design Implementation + +The Figma implementation workflow establishes a reliable process for translating designs to code: + +**For designers:** Confidence that implementations will match their designs with pixel-perfect accuracy. +**For developers:** A structured approach that eliminates guesswork and reduces back-and-forth revisions. +**For teams:** Consistent, high-quality implementations that maintain design system integrity. + +By following this workflow, you ensure that every Figma design is implemented with the same level of care and attention to detail. + +## Additional Resources + +- [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server/) +- [Figma MCP Server Tools and Prompts](https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/) +- [Figma Variables and Design Tokens](https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma) diff --git a/.cursor/skills/next-best-practices/SKILL.md b/.cursor/skills/next-best-practices/SKILL.md new file mode 100644 index 0000000000..3d5e686936 --- /dev/null +++ b/.cursor/skills/next-best-practices/SKILL.md @@ -0,0 +1,171 @@ +--- +name: next-best-practices +description: Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling +user-invocable: false +--- + +# Next.js Best Practices + +Apply these rules when writing or reviewing Next.js code. + +## File Conventions + +See [file-conventions.md](./file-conventions.md) for: + +- Project structure and special files +- Route segments (dynamic, catch-all, groups) +- Parallel and intercepting routes +- Middleware rename in v16 (middleware → proxy) + +## RSC Boundaries + +Detect invalid React Server Component patterns. + +See [rsc-boundaries.md](./rsc-boundaries.md) for: + +- Async client component detection (invalid) +- Non-serializable props detection +- Server Action exceptions + +## Async Patterns + +Next.js 15+ async API changes. + +See [async-patterns.md](./async-patterns.md) for: + +- Async `params` and `searchParams` +- Async `cookies()` and `headers()` +- Migration codemod + +## Runtime Selection + +See [runtime-selection.md](./runtime-selection.md) for: + +- Default to Node.js runtime +- When Edge runtime is appropriate + +## Directives + +See [directives.md](./directives.md) for: + +- `'use client'`, `'use server'` (React) +- `'use cache'` (Next.js) + +## Functions + +See [functions.md](./functions.md) for: + +- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams` +- Server functions: `cookies`, `headers`, `draftMode`, `after` +- Generate functions: `generateStaticParams`, `generateMetadata` + +## Error Handling + +See [error-handling.md](./error-handling.md) for: + +- `error.tsx`, `global-error.tsx`, `not-found.tsx` +- `redirect`, `permanentRedirect`, `notFound` +- `forbidden`, `unauthorized` (auth errors) +- `unstable_rethrow` for catch blocks + +## Data Patterns + +See [data-patterns.md](./data-patterns.md) for: + +- Server Components vs Server Actions vs Route Handlers +- Avoiding data waterfalls (`Promise.all`, Suspense, preload) +- Client component data fetching + +## Route Handlers + +See [route-handlers.md](./route-handlers.md) for: + +- `route.ts` basics +- GET handler conflicts with `page.tsx` +- Environment behavior (no React DOM) +- When to use vs Server Actions + +## Metadata & OG Images + +See [metadata.md](./metadata.md) for: + +- Static and dynamic metadata +- `generateMetadata` function +- OG image generation with `next/og` +- File-based metadata conventions + +## Image Optimization + +See [image.md](./image.md) for: + +- Always use `next/image` over `` +- Remote images configuration +- Responsive `sizes` attribute +- Blur placeholders +- Priority loading for LCP + +## Font Optimization + +See [font.md](./font.md) for: + +- `next/font` setup +- Google Fonts, local fonts +- Tailwind CSS integration +- Preloading subsets + +## Bundling + +See [bundling.md](./bundling.md) for: + +- Server-incompatible packages +- CSS imports (not link tags) +- Polyfills (already included) +- ESM/CommonJS issues +- Bundle analysis + +## Scripts + +See [scripts.md](./scripts.md) for: + +- `next/script` vs native script tags +- Inline scripts need `id` +- Loading strategies +- Google Analytics with `@next/third-parties` + +## Hydration Errors + +See [hydration-error.md](./hydration-error.md) for: + +- Common causes (browser APIs, dates, invalid HTML) +- Debugging with error overlay +- Fixes for each cause + +## Suspense Boundaries + +See [suspense-boundaries.md](./suspense-boundaries.md) for: + +- CSR bailout with `useSearchParams` and `usePathname` +- Which hooks require Suspense boundaries + +## Parallel & Intercepting Routes + +See [parallel-routes.md](./parallel-routes.md) for: + +- Modal patterns with `@slot` and `(.)` interceptors +- `default.tsx` for fallbacks +- Closing modals correctly with `router.back()` + +## Self-Hosting + +See [self-hosting.md](./self-hosting.md) for: + +- `output: 'standalone'` for Docker +- Cache handlers for multi-instance ISR +- What works vs needs extra setup + +## Debug Tricks + +See [debug-tricks.md](./debug-tricks.md) for: + +- MCP endpoint for AI-assisted debugging +- Rebuild specific routes with `--debug-build-paths` diff --git a/.cursor/skills/next-best-practices/async-patterns.md b/.cursor/skills/next-best-practices/async-patterns.md new file mode 100644 index 0000000000..0d5f91494b --- /dev/null +++ b/.cursor/skills/next-best-practices/async-patterns.md @@ -0,0 +1,87 @@ +# Async Patterns + +In Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous. + +## Async Params and SearchParams + +Always type them as `Promise<...>` and await them. + +### Pages and Layouts + +```tsx +type Props = { params: Promise<{ slug: string }> }; + +export default async function Page({ params }: Props) { + const { slug } = await params; +} +``` + +### Route Handlers + +```tsx +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; +} +``` + +### SearchParams + +```tsx +type Props = { + params: Promise<{ slug: string }>; + searchParams: Promise<{ query?: string }>; +}; + +export default async function Page({ params, searchParams }: Props) { + const { slug } = await params; + const { query } = await searchParams; +} +``` + +### Synchronous Components + +Use `React.use()` for non-async components: + +```tsx +import { use } from "react"; + +type Props = { params: Promise<{ slug: string }> }; + +export default function Page({ params }: Props) { + const { slug } = use(params); +} +``` + +### generateMetadata + +```tsx +type Props = { params: Promise<{ slug: string }> }; + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params; + return { title: slug }; +} +``` + +## Async Cookies and Headers + +```tsx +import { cookies, headers } from "next/headers"; + +export default async function Page() { + const cookieStore = await cookies(); + const headersList = await headers(); + + const theme = cookieStore.get("theme"); + const userAgent = headersList.get("user-agent"); +} +``` + +## Migration Codemod + +```bash +npx @next/codemod@latest next-async-request-api . +``` diff --git a/.cursor/skills/next-best-practices/bundling.md b/.cursor/skills/next-best-practices/bundling.md new file mode 100644 index 0000000000..4d09f56bea --- /dev/null +++ b/.cursor/skills/next-best-practices/bundling.md @@ -0,0 +1,182 @@ +# Bundling + +Fix common bundling issues with third-party packages. + +## Server-Incompatible Packages + +Some packages use browser APIs (`window`, `document`, `localStorage`) and fail in Server Components. + +### Error Signs + +``` +ReferenceError: window is not defined +ReferenceError: document is not defined +ReferenceError: localStorage is not defined +Module not found: Can't resolve 'fs' +``` + +### Solution 1: Mark as Client-Only + +If the package is only needed on client: + +```tsx +// Bad: Fails - package uses window +import SomeChart from "some-chart-library"; + +export default function Page() { + return ; +} + +// Good: Use dynamic import with ssr: false +import dynamic from "next/dynamic"; + +const SomeChart = dynamic(() => import("some-chart-library"), { + ssr: false, +}); + +export default function Page() { + return ; +} +``` + +### Solution 2: Externalize from Server Bundle + +For packages that should run on server but have bundling issues: + +```js +// next.config.js +module.exports = { + serverExternalPackages: ["problematic-package"], +}; +``` + +Use this for: + +- Packages with native bindings (sharp, bcrypt) +- Packages that don't bundle well (some ORMs) +- Packages with circular dependencies + +### Solution 3: Client Component Wrapper + +Wrap the entire usage in a client component: + +```tsx +// components/ChartWrapper.tsx +"use client"; + +import { Chart } from "chart-library"; + +export function ChartWrapper(props) { + return ; +} + +// app/page.tsx (server component) +import { ChartWrapper } from "@/components/ChartWrapper"; + +export default function Page() { + return ; +} +``` + +## CSS Imports + +Import CSS files instead of using `` tags. Next.js handles bundling and optimization. + +```tsx +// Bad: Manual link tag +; + +// Good: Import CSS +import "./styles.css"; + +// Good: CSS Modules +import styles from "./Button.module.css"; +``` + +## Polyfills + +Next.js includes common polyfills automatically. Don't load redundant ones from polyfill.io or similar CDNs. + +Already included: `Array.from`, `Object.assign`, `Promise`, `fetch`, `Map`, `Set`, `Symbol`, `URLSearchParams`, and 50+ others. + +```tsx +// Bad: Redundant polyfills +; + +// Good: Next.js Script component +import Script from "next/script"; + + +``` + +## Don't Put Script in Head + +`next/script` should not be placed inside `next/head`. It handles its own positioning. + +```tsx +// Bad: Script inside Head +import Head from 'next/head' +import Script from 'next/script' + + + + +// Good: Next.js component +import { GoogleAnalytics } from '@next/third-parties/google' + +export default function Layout({ children }) { + return ( + + {children} + + + ) +} +``` + +## Google Tag Manager + +```tsx +import { GoogleTagManager } from "@next/third-parties/google"; + +export default function Layout({ children }) { + return ( + + + {children} + + ); +} +``` + +## Other Third-Party Scripts + +```tsx +// YouTube embed +import { YouTubeEmbed } from "@next/third-parties/google"; + +; + +// Google Maps +import { GoogleMapsEmbed } from "@next/third-parties/google"; + +; +``` + +## Quick Reference + +| Pattern | Issue | Fix | +| --------------------------------------------- | -------------------------- | ------------------------- | +| ` + + + `; +} + +// client.tsx +import { + hydrate, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; + +const queryClient = new QueryClient(); +hydrate(queryClient, window.__DEHYDRATED_STATE__); + +hydrateRoot( + document.getElementById("app"), + + + , +); +``` + +## Context + +- Create new QueryClient per request to prevent data sharing between users +- Set `staleTime > 0` on server to prevent immediate client refetch +- Use a safe serializer (not JSON.stringify) to prevent XSS +- Failed queries aren't dehydrated by default; use `shouldDehydrateQuery` to override +- `HydrationBoundary` can be nested for route-level prefetching diff --git a/.cursor/skills/typescript-advanced-types/SKILL.md b/.cursor/skills/typescript-advanced-types/SKILL.md new file mode 100644 index 0000000000..8d64396f79 --- /dev/null +++ b/.cursor/skills/typescript-advanced-types/SKILL.md @@ -0,0 +1,724 @@ +--- +name: typescript-advanced-types +description: Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects. +--- + +# TypeScript Advanced Types + +Comprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications. + +## When to Use This Skill + +- Building type-safe libraries or frameworks +- Creating reusable generic components +- Implementing complex type inference logic +- Designing type-safe API clients +- Building form validation systems +- Creating strongly-typed configuration objects +- Implementing type-safe state management +- Migrating JavaScript codebases to TypeScript + +## Core Concepts + +### 1. Generics + +**Purpose:** Create reusable, type-flexible components while maintaining type safety. + +**Basic Generic Function:** + +```typescript +function identity(value: T): T { + return value; +} + +const num = identity(42); // Type: number +const str = identity("hello"); // Type: string +const auto = identity(true); // Type inferred: boolean +``` + +**Generic Constraints:** + +```typescript +interface HasLength { + length: number; +} + +function logLength(item: T): T { + console.log(item.length); + return item; +} + +logLength("hello"); // OK: string has length +logLength([1, 2, 3]); // OK: array has length +logLength({ length: 10 }); // OK: object has length +// logLength(42); // Error: number has no length +``` + +**Multiple Type Parameters:** + +```typescript +function merge(obj1: T, obj2: U): T & U { + return { ...obj1, ...obj2 }; +} + +const merged = merge({ name: "John" }, { age: 30 }); +// Type: { name: string } & { age: number } +``` + +### 2. Conditional Types + +**Purpose:** Create types that depend on conditions, enabling sophisticated type logic. + +**Basic Conditional Type:** + +```typescript +type IsString = T extends string ? true : false; + +type A = IsString; // true +type B = IsString; // false +``` + +**Extracting Return Types:** + +```typescript +type ReturnType = T extends (...args: any[]) => infer R ? R : never; + +function getUser() { + return { id: 1, name: "John" }; +} + +type User = ReturnType; +// Type: { id: number; name: string; } +``` + +**Distributive Conditional Types:** + +```typescript +type ToArray = T extends any ? T[] : never; + +type StrOrNumArray = ToArray; +// Type: string[] | number[] +``` + +**Nested Conditions:** + +```typescript +type TypeName = T extends string + ? "string" + : T extends number + ? "number" + : T extends boolean + ? "boolean" + : T extends undefined + ? "undefined" + : T extends Function + ? "function" + : "object"; + +type T1 = TypeName; // "string" +type T2 = TypeName<() => void>; // "function" +``` + +### 3. Mapped Types + +**Purpose:** Transform existing types by iterating over their properties. + +**Basic Mapped Type:** + +```typescript +type Readonly = { + readonly [P in keyof T]: T[P]; +}; + +interface User { + id: number; + name: string; +} + +type ReadonlyUser = Readonly; +// Type: { readonly id: number; readonly name: string; } +``` + +**Optional Properties:** + +```typescript +type Partial = { + [P in keyof T]?: T[P]; +}; + +type PartialUser = Partial; +// Type: { id?: number; name?: string; } +``` + +**Key Remapping:** + +```typescript +type Getters = { + [K in keyof T as `get${Capitalize}`]: () => T[K]; +}; + +interface Person { + name: string; + age: number; +} + +type PersonGetters = Getters; +// Type: { getName: () => string; getAge: () => number; } +``` + +**Filtering Properties:** + +```typescript +type PickByType = { + [K in keyof T as T[K] extends U ? K : never]: T[K]; +}; + +interface Mixed { + id: number; + name: string; + age: number; + active: boolean; +} + +type OnlyNumbers = PickByType; +// Type: { id: number; age: number; } +``` + +### 4. Template Literal Types + +**Purpose:** Create string-based types with pattern matching and transformation. + +**Basic Template Literal:** + +```typescript +type EventName = "click" | "focus" | "blur"; +type EventHandler = `on${Capitalize}`; +// Type: "onClick" | "onFocus" | "onBlur" +``` + +**String Manipulation:** + +```typescript +type UppercaseGreeting = Uppercase<"hello">; // "HELLO" +type LowercaseGreeting = Lowercase<"HELLO">; // "hello" +type CapitalizedName = Capitalize<"john">; // "John" +type UncapitalizedName = Uncapitalize<"John">; // "john" +``` + +**Path Building:** + +```typescript +type Path = T extends object + ? { + [K in keyof T]: K extends string ? `${K}` | `${K}.${Path}` : never; + }[keyof T] + : never; + +interface Config { + server: { + host: string; + port: number; + }; + database: { + url: string; + }; +} + +type ConfigPath = Path; +// Type: "server" | "database" | "server.host" | "server.port" | "database.url" +``` + +### 5. Utility Types + +**Built-in Utility Types:** + +```typescript +// Partial - Make all properties optional +type PartialUser = Partial; + +// Required - Make all properties required +type RequiredUser = Required; + +// Readonly - Make all properties readonly +type ReadonlyUser = Readonly; + +// Pick - Select specific properties +type UserName = Pick; + +// Omit - Remove specific properties +type UserWithoutPassword = Omit; + +// Exclude - Exclude types from union +type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" + +// Extract - Extract types from union +type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b" + +// NonNullable - Exclude null and undefined +type T3 = NonNullable; // string + +// Record - Create object type with keys K and values T +type PageInfo = Record<"home" | "about", { title: string }>; +``` + +## Advanced Patterns + +### Pattern 1: Type-Safe Event Emitter + +```typescript +type EventMap = { + "user:created": { id: string; name: string }; + "user:updated": { id: string }; + "user:deleted": { id: string }; +}; + +class TypedEventEmitter> { + private listeners: { + [K in keyof T]?: Array<(data: T[K]) => void>; + } = {}; + + on(event: K, callback: (data: T[K]) => void): void { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event]!.push(callback); + } + + emit(event: K, data: T[K]): void { + const callbacks = this.listeners[event]; + if (callbacks) { + callbacks.forEach((callback) => callback(data)); + } + } +} + +const emitter = new TypedEventEmitter(); + +emitter.on("user:created", (data) => { + console.log(data.id, data.name); // Type-safe! +}); + +emitter.emit("user:created", { id: "1", name: "John" }); +// emitter.emit("user:created", { id: "1" }); // Error: missing 'name' +``` + +### Pattern 2: Type-Safe API Client + +```typescript +type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE"; + +type EndpointConfig = { + "/users": { + GET: { response: User[] }; + POST: { body: { name: string; email: string }; response: User }; + }; + "/users/:id": { + GET: { params: { id: string }; response: User }; + PUT: { params: { id: string }; body: Partial; response: User }; + DELETE: { params: { id: string }; response: void }; + }; +}; + +type ExtractParams = T extends { params: infer P } ? P : never; +type ExtractBody = T extends { body: infer B } ? B : never; +type ExtractResponse = T extends { response: infer R } ? R : never; + +class APIClient>> { + async request( + path: Path, + method: Method, + ...[options]: ExtractParams extends never + ? ExtractBody extends never + ? [] + : [{ body: ExtractBody }] + : [ + { + params: ExtractParams; + body?: ExtractBody; + }, + ] + ): Promise> { + // Implementation here + return {} as any; + } +} + +const api = new APIClient(); + +// Type-safe API calls +const users = await api.request("/users", "GET"); +// Type: User[] + +const newUser = await api.request("/users", "POST", { + body: { name: "John", email: "john@example.com" }, +}); +// Type: User + +const user = await api.request("/users/:id", "GET", { + params: { id: "123" }, +}); +// Type: User +``` + +### Pattern 3: Builder Pattern with Type Safety + +```typescript +type BuilderState = { + [K in keyof T]: T[K] | undefined; +}; + +type RequiredKeys = { + [K in keyof T]-?: {} extends Pick ? never : K; +}[keyof T]; + +type OptionalKeys = { + [K in keyof T]-?: {} extends Pick ? K : never; +}[keyof T]; + +type IsComplete = + RequiredKeys extends keyof S + ? S[RequiredKeys] extends undefined + ? false + : true + : false; + +class Builder = {}> { + private state: S = {} as S; + + set(key: K, value: T[K]): Builder> { + this.state[key] = value; + return this as any; + } + + build(this: IsComplete extends true ? this : never): T { + return this.state as T; + } +} + +interface User { + id: string; + name: string; + email: string; + age?: number; +} + +const builder = new Builder(); + +const user = builder + .set("id", "1") + .set("name", "John") + .set("email", "john@example.com") + .build(); // OK: all required fields set + +// const incomplete = builder +// .set("id", "1") +// .build(); // Error: missing required fields +``` + +### Pattern 4: Deep Readonly/Partial + +```typescript +type DeepReadonly = { + readonly [P in keyof T]: T[P] extends object + ? T[P] extends Function + ? T[P] + : DeepReadonly + : T[P]; +}; + +type DeepPartial = { + [P in keyof T]?: T[P] extends object + ? T[P] extends Array + ? Array> + : DeepPartial + : T[P]; +}; + +interface Config { + server: { + host: string; + port: number; + ssl: { + enabled: boolean; + cert: string; + }; + }; + database: { + url: string; + pool: { + min: number; + max: number; + }; + }; +} + +type ReadonlyConfig = DeepReadonly; +// All nested properties are readonly + +type PartialConfig = DeepPartial; +// All nested properties are optional +``` + +### Pattern 5: Type-Safe Form Validation + +```typescript +type ValidationRule = { + validate: (value: T) => boolean; + message: string; +}; + +type FieldValidation = { + [K in keyof T]?: ValidationRule[]; +}; + +type ValidationErrors = { + [K in keyof T]?: string[]; +}; + +class FormValidator> { + constructor(private rules: FieldValidation) {} + + validate(data: T): ValidationErrors | null { + const errors: ValidationErrors = {}; + let hasErrors = false; + + for (const key in this.rules) { + const fieldRules = this.rules[key]; + const value = data[key]; + + if (fieldRules) { + const fieldErrors: string[] = []; + + for (const rule of fieldRules) { + if (!rule.validate(value)) { + fieldErrors.push(rule.message); + } + } + + if (fieldErrors.length > 0) { + errors[key] = fieldErrors; + hasErrors = true; + } + } + } + + return hasErrors ? errors : null; + } +} + +interface LoginForm { + email: string; + password: string; +} + +const validator = new FormValidator({ + email: [ + { + validate: (v) => v.includes("@"), + message: "Email must contain @", + }, + { + validate: (v) => v.length > 0, + message: "Email is required", + }, + ], + password: [ + { + validate: (v) => v.length >= 8, + message: "Password must be at least 8 characters", + }, + ], +}); + +const errors = validator.validate({ + email: "invalid", + password: "short", +}); +// Type: { email?: string[]; password?: string[]; } | null +``` + +### Pattern 6: Discriminated Unions + +```typescript +type Success = { + status: "success"; + data: T; +}; + +type Error = { + status: "error"; + error: string; +}; + +type Loading = { + status: "loading"; +}; + +type AsyncState = Success | Error | Loading; + +function handleState(state: AsyncState): void { + switch (state.status) { + case "success": + console.log(state.data); // Type: T + break; + case "error": + console.log(state.error); // Type: string + break; + case "loading": + console.log("Loading..."); + break; + } +} + +// Type-safe state machine +type State = + | { type: "idle" } + | { type: "fetching"; requestId: string } + | { type: "success"; data: any } + | { type: "error"; error: Error }; + +type Event = + | { type: "FETCH"; requestId: string } + | { type: "SUCCESS"; data: any } + | { type: "ERROR"; error: Error } + | { type: "RESET" }; + +function reducer(state: State, event: Event): State { + switch (state.type) { + case "idle": + return event.type === "FETCH" + ? { type: "fetching", requestId: event.requestId } + : state; + case "fetching": + if (event.type === "SUCCESS") { + return { type: "success", data: event.data }; + } + if (event.type === "ERROR") { + return { type: "error", error: event.error }; + } + return state; + case "success": + case "error": + return event.type === "RESET" ? { type: "idle" } : state; + } +} +``` + +## Type Inference Techniques + +### 1. Infer Keyword + +```typescript +// Extract array element type +type ElementType = T extends (infer U)[] ? U : never; + +type NumArray = number[]; +type Num = ElementType; // number + +// Extract promise type +type PromiseType = T extends Promise ? U : never; + +type AsyncNum = PromiseType>; // number + +// Extract function parameters +type Parameters = T extends (...args: infer P) => any ? P : never; + +function foo(a: string, b: number) {} +type FooParams = Parameters; // [string, number] +``` + +### 2. Type Guards + +```typescript +function isString(value: unknown): value is string { + return typeof value === "string"; +} + +function isArrayOf( + value: unknown, + guard: (item: unknown) => item is T, +): value is T[] { + return Array.isArray(value) && value.every(guard); +} + +const data: unknown = ["a", "b", "c"]; + +if (isArrayOf(data, isString)) { + data.forEach((s) => s.toUpperCase()); // Type: string[] +} +``` + +### 3. Assertion Functions + +```typescript +function assertIsString(value: unknown): asserts value is string { + if (typeof value !== "string") { + throw new Error("Not a string"); + } +} + +function processValue(value: unknown) { + assertIsString(value); + // value is now typed as string + console.log(value.toUpperCase()); +} +``` + +## Best Practices + +1. **Use `unknown` over `any`**: Enforce type checking +2. **Prefer `interface` for object shapes**: Better error messages +3. **Use `type` for unions and complex types**: More flexible +4. **Leverage type inference**: Let TypeScript infer when possible +5. **Create helper types**: Build reusable type utilities +6. **Use const assertions**: Preserve literal types +7. **Avoid type assertions**: Use type guards instead +8. **Document complex types**: Add JSDoc comments +9. **Use strict mode**: Enable all strict compiler options +10. **Test your types**: Use type tests to verify type behavior + +## Type Testing + +```typescript +// Type assertion tests +type AssertEqual = [T] extends [U] + ? [U] extends [T] + ? true + : false + : false; + +type Test1 = AssertEqual; // true +type Test2 = AssertEqual; // false +type Test3 = AssertEqual; // false + +// Expect error helper +type ExpectError = T; + +// Example usage +type ShouldError = ExpectError>; +``` + +## Common Pitfalls + +1. **Over-using `any`**: Defeats the purpose of TypeScript +2. **Ignoring strict null checks**: Can lead to runtime errors +3. **Too complex types**: Can slow down compilation +4. **Not using discriminated unions**: Misses type narrowing opportunities +5. **Forgetting readonly modifiers**: Allows unintended mutations +6. **Circular type references**: Can cause compiler errors +7. **Not handling edge cases**: Like empty arrays or null values + +## Performance Considerations + +- Avoid deeply nested conditional types +- Use simple types when possible +- Cache complex type computations +- Limit recursion depth in recursive types +- Use build tools to skip type checking in production + +## Resources + +- **TypeScript Handbook**: https://www.typescriptlang.org/docs/handbook/ +- **Type Challenges**: https://github.com/type-challenges/type-challenges +- **TypeScript Deep Dive**: https://basarat.gitbook.io/typescript/ +- **Effective TypeScript**: Book by Dan Vanderkam diff --git a/.cursor/skills/vercel-composition-patterns/AGENTS.md b/.cursor/skills/vercel-composition-patterns/AGENTS.md new file mode 100644 index 0000000000..7c3202d0e2 --- /dev/null +++ b/.cursor/skills/vercel-composition-patterns/AGENTS.md @@ -0,0 +1,951 @@ +# React Composition Patterns + +**Version 1.0.0** +Engineering +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring React codebases using composition. Humans +> may also find it useful, but guidance here is optimized for automation +> and consistency by AI-assisted workflows. + +--- + +## Abstract + +Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale. + +--- + +## Table of Contents + +1. [Component Architecture](#1-component-architecture) — **HIGH** + - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation) + - 1.2 [Use Compound Components](#12-use-compound-components) +2. [State Management](#2-state-management) — **MEDIUM** + - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui) + - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection) + - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components) +3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM** + - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants) + - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props) +4. [React 19 APIs](#4-react-19-apis) — **MEDIUM** + - 4.1 [React 19 API Changes](#41-react-19-api-changes) + +--- + +## 1. Component Architecture + +**Impact: HIGH** + +Fundamental patterns for structuring components to avoid prop +proliferation and enable flexible composition. + +### 1.1 Avoid Boolean Prop Proliferation + +**Impact: CRITICAL (prevents unmaintainable component variants)** + +Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize + +component behavior. Each boolean doubles possible states and creates + +unmaintainable conditional logic. Use composition instead. + +**Incorrect: boolean props create exponential complexity** + +```tsx +function Composer({ + onSubmit, + isThread, + channelId, + isDMThread, + dmId, + isEditing, + isForwarding, +}: Props) { + return ( +
+
+ + {isDMThread ? ( + + ) : isThread ? ( + + ) : null} + {isEditing ? ( + + ) : isForwarding ? ( + + ) : ( + + )} +