import { keccak256, stringToHex } from "viem"; import { IndexerError, InvalidInputError } from "./errors.js"; import type { FillsScope, GetUserFillsPageOptions } from "./fills.js"; type Position = { timestamp: string; blockNumber: string; logIndex: number }; const isDecimal = (value: unknown): value is string => typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value); const isLogIndex = (value: unknown): value is number => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 2_147_483_647; const normalizeFilter = (value: string | undefined): string | undefined => { if (value === undefined) return undefined; if (typeof value !== "string" || !value.length) throw new InvalidInputError("Fill scope must be non-empty strings"); return value.toLowerCase(); }; export const prepare = ({ account, options, chainId, source, }: { account: string; options: GetUserFillsPageOptions; chainId: number; source: string; }) => { if (typeof account !== "string" || !account.length) throw new InvalidInputError("Fill account is required"); const limit = options.limit ?? 50; if (!Number.isInteger(limit) || limit < 1 || limit > 1_000) { throw new InvalidInputError("Fill page limit must be an integer from 1 to 1000"); } for (const time of [options.since, options.until]) { if (time !== undefined && (!Number.isSafeInteger(time) || time < 0)) { throw new InvalidInputError("Fill time bounds must be non-negative safe integer seconds"); } } if (options.since !== undefined && options.until !== undefined && options.since > options.until) { throw new InvalidInputError("Fill since must not exceed until"); } if (options.markets !== undefined && !Array.isArray(options.markets)) { throw new InvalidInputError("Fill markets must be an array"); } const markets = options.markets?.map((market) => { const normalized = normalizeFilter(market); if (normalized === undefined) throw new InvalidInputError("Fill market must be a non-empty string"); return normalized; }); const scope: FillsScope = { market: normalizeFilter(options.market), markets: markets === undefined ? undefined : [...new Set(markets)].sort(), pool: normalizeFilter(options.pool), since: options.since, until: options.until, }; const acct = account.toLowerCase(); // Bind scope without exposing endpoint credentials. This is not authentication or a dataset generation. const binding = keccak256(stringToHex(JSON.stringify([chainId, source, acct, scope, "timestamp/block/log:desc"]))); return { account: acct, scope, limit, binding }; }; export const decode = (cursor: string, binding: string): Position => { if (typeof cursor !== "string" || cursor.length > 1_024) throw new InvalidInputError("Invalid fill cursor"); let value: unknown; try { value = JSON.parse(cursor); } catch (cause) { throw new InvalidInputError("Invalid fill cursor", { cause }); } if (!Array.isArray(value) || value.length !== 5) throw new InvalidInputError("Invalid fill cursor"); const [version, scope, timestamp, blockNumber, logIndex] = value; if (version !== 1 || scope !== binding || !isDecimal(timestamp) || !isDecimal(blockNumber) || !isLogIndex(logIndex)) { throw new InvalidInputError("Invalid or foreign fill cursor"); } return { timestamp, blockNumber, logIndex }; }; export const encode = (position: Position, binding: string): string => { const cursor = JSON.stringify([1, binding, position.timestamp, position.blockNumber, position.logIndex]); if (cursor.length > 1_024) throw new IndexerError("UserFillsPage", "Fill position exceeds cursor bounds"); return cursor; }; export const validatePosition = (position: Position): void => { if ( typeof position !== "object" || position === null || !isDecimal(position.timestamp) || !isDecimal(position.blockNumber) || !isLogIndex(position.logIndex) ) { throw new IndexerError("UserFillsPage", "Invalid fill position scalars"); } }; export const before = ({ timestamp, blockNumber, logIndex }: Position) => ({ _or: [ { timestamp: { _lt: timestamp } }, { timestamp: { _eq: timestamp }, blockNumber: { _lt: blockNumber } }, { timestamp: { _eq: timestamp }, blockNumber: { _eq: blockNumber }, logIndex: { _lt: logIndex } }, ], });