import { createHash } from "node:crypto";
export const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
export const TOKEN_2022_PROGRAM = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
export const TOKEN_PROGRAMS = [TOKEN_PROGRAM, TOKEN_2022_PROGRAM] as const;
export const DEFAULT_TOP = 100;
/** One getProgramAccounts item in jsonParsed encoding. Only the fields we read are typed. */
export interface ParsedTokenAccountItem {
pubkey: string;
account: {
owner?: string;
data: {
program?: string;
parsed?: {
type?: string;
info?: {
mint?: string;
owner?: string;
state?: string;
tokenAmount?: { amount?: string; decimals?: number };
extensions?: unknown[];
};
};
} | unknown;
};
}
/** Owner and token account exclusions. Entries may be plain addresses or {address, label}. */
export interface Exclusions {
owners?: Array<string | { address: string; label?: string }>;
token_accounts?: Array<string | { address: string; label?: string }>;
}
export interface Holder {
rank: number;
owner: string;
amount: string;
}
export interface Snapshot {
mint: string;
slot: number;
total: string;
holders: Holder[];
hash: string;
}
export interface BuildSnapshotInput {
mint: string;
slot: number;
accounts: ParsedTokenAccountItem[];
exclusions?: Exclusions;
top?: number;
}
const DIGITS = /^[0-9]+$/;
function addressSet(list: Exclusions["owners"]): Set<string> {
const out = new Set<string>();
for (const e of list ?? []) {
if (typeof e === "string") out.add(e);
else if (e && typeof e === "object" && typeof e.address === "string") out.add(e.address);
}
return out;
}
/** Plain string comparison (UTF-16 code units), the same in every locale. */
function cmp(a: string, b: string): number {
return a < b ? -1 : a > b ? 1 : 0;
}
/**
* Group the counted token accounts of `mint` by owner (BigInt, base units), apply the exclusions,
* and return the totals per owner. Items that are not token accounts, other mints, zero or invalid
* amounts are ignored. Token 2022 extensions and the frozen state do not change the count.
*/
export function groupByOwner(mint: string, accounts: ParsedTokenAccountItem[], exclusions: Exclusions = {}): Map<string, bigint> {
const exOwners = addressSet(exclusions.owners);
const exAccounts = addressSet(exclusions.token_accounts);
const byOwner = new Map<string, bigint>();
const seen = new Set<string>();
for (const item of accounts ?? []) {
if (!item || typeof item !== "object" || typeof item.pubkey !== "string") continue;
const data = (item.account as { data?: unknown } | undefined)?.data as
| { program?: string; parsed?: { type?: string; info?: Record<string, any> } }
| undefined;
const parsed = data && typeof data === "object" && !Array.isArray(data) ? data.parsed : undefined;
if (!parsed || parsed.type !== "account") continue;
if (data!.program !== undefined && data!.program !== "spl-token" && data!.program !== "spl-token-2022") continue;
const info = parsed.info ?? {};
if (info.mint !== mint) continue;
if (typeof info.owner !== "string" || info.owner.length === 0) continue;
const raw = info.tokenAmount?.amount;
if (typeof raw !== "string" || !DIGITS.test(raw)) continue;
const amount = BigInt(raw);
if (amount <= 0n) continue;
if (seen.has(item.pubkey)) continue; // the same account listed twice counts once
seen.add(item.pubkey);
if (exAccounts.has(item.pubkey) || exOwners.has(info.owner)) continue;
byOwner.set(info.owner, (byOwner.get(info.owner) ?? 0n) + amount);
}
return byOwner;
}
/** Ranking: amount descending, ties by owner ascending (plain string comparison). */
export function rankHolders(byOwner: Map<string, bigint>): Array<{ owner: string; amount: bigint }> {
return [...byOwner.entries()]
.map(([owner, amount]) => ({ owner, amount }))
.sort((a, b) => (a.amount === b.amount ? cmp(a.owner, b.owner) : a.amount > b.amount ? -1 : 1));
}
/** sha256 (lowercase hex) of "rank,owner,amount" lines joined by "\n", no trailing newline. */
export function hashHolders(holders: Holder[]): string {
const text = holders.map((h) => `${h.rank},${h.owner},${h.amount}`).join("\n");
return createHash("sha256").update(text, "utf8").digest("hex");
}
function checkTop(top: unknown): number {
if (top === undefined) return DEFAULT_TOP;
if (typeof top !== "number" || !Number.isInteger(top) || top < 1) {
throw new RangeError("top must be a whole number of at least 1");
}
return top;
}
export function buildSnapshot(input: BuildSnapshotInput): Snapshot {
if (!input || typeof input.mint !== "string" || input.mint.length === 0) throw new TypeError("mint is required");
if (!Array.isArray(input.accounts)) throw new TypeError("accounts must be an array");
const top = checkTop(input.top);
const byOwner = groupByOwner(input.mint, input.accounts, input.exclusions ?? {});
const ranked = rankHolders(byOwner);
let total = 0n;
for (const r of ranked) total += r.amount;
const holders: Holder[] = ranked.slice(0, top).map((r, i) => ({ rank: i + 1, owner: r.owner, amount: r.amount.toString() }));
return { mint: input.mint, slot: input.slot, total: total.toString(), holders, hash: hashHolders(holders) };
}
/**
* Seat interface (KOG top holder snapshot): snapshot({accounts, exclusions, top}) -> {holders, hash}.
* The mint is optional here: when it is missing, the single mint present in the accounts is used.
*/
export function snapshot(input: { accounts: ParsedTokenAccountItem[]; exclusions?: Exclusions; top?: number; mint?: string; slot?: number }): Snapshot {
let mint = input.mint;
if (!mint) {
const mints = new Set<string>();
for (const a of input.accounts ?? []) {
const m = (a?.account?.data as any)?.parsed?.info?.mint;
if ((a?.account?.data as any)?.parsed?.type === "account" && typeof m === "string") mints.add(m);
}
if (mints.size !== 1) throw new TypeError("mint is required when the accounts hold more than one mint");
mint = [...mints][0];
}
return buildSnapshot({ mint, slot: input.slot ?? 0, accounts: input.accounts, exclusions: input.exclusions, top: input.top });
}
agentgate-source: code mirror on Cloudflare Pages. All repos