Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ export function normalizeDataset(
const multipliersByTs = multiplier.reduce(
(acc, item) => ({
...acc,
[Number(item.date) * 1000]: Number(
formatUnits(BigInt(item.high), decimals),
),
[Number(item.date)]: Number(formatUnits(BigInt(item.high), decimals)),
}),
{} as Record<number, number>,
);
Expand All @@ -50,7 +48,7 @@ export function normalizeDataset(
*/
export const getOnlyClosedData = (data: PriceEntry[]): PriceEntry[] => {
return data.filter((entry) => {
const dateStr = new Date(entry.timestamp).toISOString();
const dateStr = new Date(entry.timestamp * 1000).toISOString();
return dateStr.endsWith("T00:00:00.000Z");
});
};
9 changes: 5 additions & 4 deletions apps/indexer/src/api/services/coingecko/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { DAYS_IN_YEAR } from "@/lib/constants";
import { DaoIdEnum } from "@/lib/enums";
import { TokenHistoricalPriceResponse } from "@/api/mappers";
import { PriceProvider } from "@/api/services/treasury/types";
import { truncateTimestampTimeMs } from "@/eventHandlers/shared";
import { truncateTimestampToMidnight } from "@/lib/time-series";

const createCoingeckoTokenPriceDataSchema = (
tokenContractAddress: string,
Expand Down Expand Up @@ -45,7 +45,7 @@ export class CoingeckoService implements PriceProvider {

const priceMap = new Map<number, number>();
priceData.forEach((item) => {
const normalizedTimestamp = truncateTimestampTimeMs(item.timestamp);
const normalizedTimestamp = truncateTimestampToMidnight(item.timestamp);
priceMap.set(normalizedTimestamp, Number(item.price));
});

Expand Down Expand Up @@ -78,9 +78,10 @@ export class CoingeckoService implements PriceProvider {
});
}

return data.prices.map(([timestamp, price]) => ({
// CoinGecko returns timestamps in milliseconds, convert to seconds
return data.prices.map(([timestampMs, price]) => ({
price: price.toFixed(2),
timestamp: timestamp,
timestamp: Math.floor(timestampMs / 1000),
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
*/

import { MetricTypesEnum } from "@/lib/constants";
import { SECONDS_IN_DAY, getCurrentDayTimestamp } from "@/lib/enums";
import { getCurrentDayTimestamp } from "@/lib/enums";
import { createDailyTimeline, forwardFill } from "@/lib/time-series";
import { DelegationPercentageRepository } from "@/api/repositories/";
import {
DelegationPercentageItem,
Expand Down Expand Up @@ -77,7 +78,7 @@ export class DelegationPercentageService {
// 1. Get initial values for proper forward-fill
const referenceDate = normalizedAfter || normalizedStartDate;
const initialValues = referenceDate
? await this.getInitialValuesBeforeDate(referenceDate)
? await this.fetchLastDelegationValues(referenceDate)
: { delegated: 0n, total: 0n };

// 2. Fetch data from repository
Expand Down Expand Up @@ -117,15 +118,19 @@ export class DelegationPercentageService {
);

// 6. Generate complete date range
const allDates = this.generateDateRange(
const allDates = this.getOrderedTimeline(
dateMap,
effectiveStartDate,
normalizedEndDate,
orderDirection,
);

// 7. Apply forward-fill and calculate percentage
const allItems = this.applyForwardFill(allDates, dateMap, initialValues);
// 7. Calculate delegation percentage
const allItems = this.calculateDelegationPercentage(
allDates,
dateMap,
initialValues,
);

// 8. Apply cursor-based pagination
const { items, hasNextPage } = this.applyCursorPagination(
Expand All @@ -148,7 +153,7 @@ export class DelegationPercentageService {
* Gets the last known values at or before a given date for proper forward-fill
* Returns { delegated: 0n, total: 0n } if no previous values exist
*/
private async getInitialValuesBeforeDate(
private async fetchLastDelegationValues(
beforeDate: string,
): Promise<{ delegated: bigint; total: bigint }> {
try {
Expand Down Expand Up @@ -195,11 +200,11 @@ export class DelegationPercentageService {
}

const datesFromDb = Array.from(dateMap.keys())
.map((d) => BigInt(d))
.sort((a, b) => Number(a - b));
.map((d) => Number(d))
.sort((a, b) => a - b);
const firstRealDate = datesFromDb[0];

if (firstRealDate && BigInt(referenceDate) < firstRealDate) {
if (firstRealDate && Number(referenceDate) < firstRealDate) {
return firstRealDate.toString();
}

Expand Down Expand Up @@ -241,37 +246,30 @@ export class DelegationPercentageService {
* Fills gaps between first and last date with all days
* If endDate is not provided, uses current day (today) for forward-fill
*/
private generateDateRange(
private getOrderedTimeline(
dateMap: Map<string, DateData>,
startDate?: string,
endDate?: string,
orderDirection?: "asc" | "desc",
): bigint[] {
const allDates: bigint[] = [];

): number[] {
if (dateMap.size === 0) {
return allDates;
return [];
}

const datesFromDb = Array.from(dateMap.keys())
.map((d) => BigInt(d))
.sort((a, b) => Number(a - b));
.map((d) => Number(d))
.sort((a, b) => a - b);

const firstDate = startDate ? BigInt(startDate) : datesFromDb[0];
const lastDate = endDate ? BigInt(endDate) : getCurrentDayTimestamp();
const firstDate = startDate ? Number(startDate) : datesFromDb[0];
const lastDate = endDate
? Number(endDate)
: Number(getCurrentDayTimestamp());

if (!firstDate || !lastDate) {
return allDates;
return [];
}

// Generate all days in range
for (
let date = firstDate;
date <= lastDate;
date += BigInt(SECONDS_IN_DAY)
) {
allDates.push(date);
}
const allDates = createDailyTimeline(firstDate, lastDate);

if (orderDirection === "desc") {
allDates.reverse();
Expand All @@ -281,35 +279,39 @@ export class DelegationPercentageService {
}

/**
* Applies forward-fill logic and calculates delegation percentage
* Forward-fill: carries forward the last known value for missing dates
* Calculates delegation percentage
*/
private applyForwardFill(
allDates: bigint[],
private calculateDelegationPercentage(
allDates: number[],
dateMap: Map<string, DateData>,
initialValues: { delegated: bigint; total: bigint } = {
delegated: 0n,
total: 0n,
},
): DelegationPercentageItem[] {
let lastDelegated = initialValues.delegated;
let lastTotal = initialValues.total;
const delegatedMap = new Map<number, bigint>();
const totalMap = new Map<number, bigint>();
for (const [dateStr, data] of dateMap) {
const date = Number(dateStr);
if (data.delegated !== undefined) delegatedMap.set(date, data.delegated);
if (data.total !== undefined) totalMap.set(date, data.total);
}
const filledDelegated = forwardFill(
allDates,
delegatedMap,
initialValues.delegated,
);
const filledTotal = forwardFill(allDates, totalMap, initialValues.total);

// Calculate percentage for each date
return allDates.map((date) => {
const dateStr = date.toString();
const data = dateMap.get(dateStr);

// Update known values (forward-fill)
if (data?.delegated !== undefined) lastDelegated = data.delegated;
if (data?.total !== undefined) lastTotal = data.total;

// Calculate percentage (avoid division by zero)
// Returns as string with 2 decimal places (e.g., "11.74" for 11.74%)
const delegated = filledDelegated.get(date) ?? 0n;
const total = filledTotal.get(date) ?? 0n;
const percentage =
lastTotal > 0n ? Number((lastDelegated * 10000n) / lastTotal) / 100 : 0;
total > 0n ? Number((delegated * 10000n) / total) / 100 : 0;

return {
date: dateStr,
date: date.toString(),
high: percentage.toFixed(2),
};
});
Expand Down
20 changes: 9 additions & 11 deletions apps/indexer/src/api/services/nft-price/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import axios, { AxiosInstance } from "axios";
import { TokenHistoricalPriceResponse } from "@/api/mappers";
import { PriceProvider } from "@/api/services/treasury/types";
import {
truncateTimestampTimeMs,
truncateTimestampToMidnight,
calculateCutoffTimestamp,
} from "@/eventHandlers/shared";
import {
createDailyTimeline,
forwardFill,
createDailyTimelineFromData,
} from "@/api/services/treasury/forward-fill"; // TODO: move to shared folder
} from "@/lib/time-series";

interface Repository {
getHistoricalNFTPrice(
Expand Down Expand Up @@ -63,23 +61,23 @@ export class NFTPriceService implements PriceProvider {
price: (
Number(formatEther(BigInt(price))) * ethPriceResponse[index]![1]
).toFixed(2),
timestamp: timestamp * 1000,
timestamp,
}));

// Create map with normalized timestamps (midnight UTC)
const priceMap = new Map<number, string>();
rawPrices.forEach((item) => {
const normalizedTs = truncateTimestampTimeMs(item.timestamp);
const normalizedTs = truncateTimestampToMidnight(item.timestamp);
priceMap.set(normalizedTs, item.price);
});

// Create timeline and forward-fill gaps
const timeline = createDailyTimelineFromData([...priceMap.keys()]);
const timeline = createDailyTimeline(Math.min(...priceMap.keys()));
const filledPrices = forwardFill(timeline, priceMap);

// Filter to only include last `limit` days
const cutoffMs = calculateCutoffTimestamp(limit) * 1000;
const filteredTimeline = timeline.filter((ts) => ts >= cutoffMs);
const cutoff = calculateCutoffTimestamp(limit);
const filteredTimeline = timeline.filter((ts) => ts >= cutoff);

return filteredTimeline.map((timestamp) => ({
price: filledPrices.get(timestamp) ?? "0",
Expand All @@ -104,7 +102,7 @@ export class NFTPriceService implements PriceProvider {

const priceMap = new Map<number, number>();
priceData.forEach((item) => {
const normalizedTimestamp = truncateTimestampTimeMs(item.timestamp);
const normalizedTimestamp = truncateTimestampToMidnight(item.timestamp);
priceMap.set(normalizedTimestamp, Number(item.price));
});

Expand Down
58 changes: 0 additions & 58 deletions apps/indexer/src/api/services/treasury/forward-fill.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/indexer/src/api/services/treasury/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ export * from "./providers";
export * from "./types";
export * from "./treasury.service";
export * from "../../repositories/treasury";
export * from "./forward-fill";
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { AxiosInstance } from "axios";
import { TreasuryProvider } from "./treasury-provider.interface";
import { LiquidTreasuryDataPoint } from "../types";
import { truncateTimestampTime } from "@/eventHandlers/shared";
import {
truncateTimestampToMidnight,
filterWithFallback,
} from "@/lib/time-series";
import { TreasuryProviderCache } from "./provider-cache";

interface RawDefiLlamaResponse {
Expand Down Expand Up @@ -31,14 +34,14 @@ export class DefiLlamaProvider implements TreasuryProvider {
): Promise<LiquidTreasuryDataPoint[]> {
const cached = this.cache.get();

if (cached !== null) return this.filterData(cached, cutoffTimestamp);
if (cached !== null) return filterWithFallback(cached, cutoffTimestamp);

try {
const response = await this.client.get<RawDefiLlamaResponse>("");
const data = this.transformData(response.data);
this.cache.set(data);

return this.filterData(data, cutoffTimestamp);
return filterWithFallback(data, cutoffTimestamp);
} catch (error) {
console.error(
`[DefiLlamaProvider] Failed to fetch treasury data:`,
Expand Down Expand Up @@ -72,7 +75,7 @@ export class DefiLlamaProvider implements TreasuryProvider {
const dateMap = new Map<number, { timestamp: number; value: number }>();

for (const dataPoint of chainData.tvl || []) {
const dayTimestamp = truncateTimestampTime(dataPoint.date);
const dayTimestamp = truncateTimestampToMidnight(dataPoint.date);
const existing = dateMap.get(dayTimestamp);

// Keep only the latest timestamp for each date
Expand Down Expand Up @@ -122,17 +125,4 @@ export class DefiLlamaProvider implements TreasuryProvider {
}))
.sort((a, b) => a.date - b.date); // Sort by timestamp ascending
}

private filterData(
data: LiquidTreasuryDataPoint[],
cutoffTimestamp: number,
): LiquidTreasuryDataPoint[] {
const filteredData = data.filter((item) => item.date >= cutoffTimestamp);
if (filteredData.length === 0 && data.length > 0) {
const lastAvailable = data.at(-1)!;
return [lastAvailable];
}

return filteredData;
}
}
Loading