kog-holder-snapshot / tree / src/snapshot.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
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