kog-fee-verify / tree / src/verify.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
/**
 * Pure verifier for a Kognai fee paid over x402 in an SPL Token 2022 (or SPL Token) mint on Solana.
 * The parsed transaction (getTransaction, encoding jsonParsed) is passed in; there is no RPC inside,
 * no clock unless `now` is given, and no logging.
 */

export type Reason =
  | "ok"
  | "bad_input"
  | "bad_quote"
  | "tx_failed"
  | "replay"
  | "expired"
  | "wrong_mint"
  | "wrong_payee"
  | "short_fee";

export interface Quote {
  /** Fee in base units of the mint, as a decimal string (or bigint). */
  amount: string | bigint;
  /** Quote expiry: unix seconds, or an ISO 8601 date. */
  expires_at: number | string;
  /** Optional: when present it must equal the mint argument. */
  mint?: string;
  /** Optional: when present it must equal the payee argument. */
  payee?: string;
}

export interface SeenLike {
  has(signature: string): boolean;
}

export interface VerifyInput {
  tx: unknown;
  quote: Quote;
  /** The payee wallet (owner of the receiving token account), or the receiving token account itself. */
  payee: string;
  mint: string;
  /** Signatures already used for a fee: a Set, a Map, an array or anything with has(). */
  seen?: SeenLike | readonly string[];
  /** Unix seconds, used only when the transaction carries no blockTime. */
  now?: number;
}

export interface VerifyResult {
  ok: boolean;
  reason: Reason;
  /** The first transaction signature, when it could be read. */
  signature?: string;
  /** Net base units of the mint received by the payee, when computed. */
  received?: string;
}

const TRANSFER_TYPES = new Set(["transfer", "transferChecked", "transferCheckedWithFee"]);
const TOKEN_PROGRAMS = new Set(["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"]);
const DIGITS = /^[0-9]+$/;

function fail(reason: Reason, signature?: string, received?: string): VerifyResult {
  return { ok: false, reason, ...(signature ? { signature } : {}), ...(received !== undefined ? { received } : {}) };
}

function toBig(v: unknown): bigint | null {
  if (typeof v === "bigint") return v >= 0n ? v : null;
  if (typeof v === "string" && DIGITS.test(v)) return BigInt(v);
  return null;
}

function expirySeconds(v: unknown): number | null {
  if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
  if (typeof v === "string" && v.length > 0) {
    if (DIGITS.test(v)) return Number(v);
    const ms = Date.parse(v);
    return Number.isNaN(ms) ? null : ms / 1000;
  }
  return null;
}

function hasSeen(seen: VerifyInput["seen"], sig: string): boolean {
  if (!seen) return false;
  if (Array.isArray(seen)) return seen.includes(sig);
  return typeof (seen as SeenLike).has === "function" ? (seen as SeenLike).has(sig) : false;
}

function keyOf(k: unknown): string | undefined {
  if (typeof k === "string") return k;
  if (k && typeof k === "object" && typeof (k as any).pubkey === "string") return (k as any).pubkey;
  return undefined;
}

interface Balance {
  accountIndex: number;
  mint: string;
  owner?: string;
  amount: bigint;
}

function balances(list: unknown): Balance[] {
  const out: Balance[] = [];
  if (!Array.isArray(list)) return out;
  for (const b of list) {
    const amount = toBig(b?.uiTokenAmount?.amount);
    if (typeof b?.accountIndex !== "number" || typeof b?.mint !== "string" || amount === null) continue;
    out.push({ accountIndex: b.accountIndex, mint: b.mint, owner: typeof b.owner === "string" ? b.owner : undefined, amount });
  }
  return out;
}

/** All parsed token transfer instructions, top level and inner (CPI). */
function transfers(tx: any): Array<{ type: string; info: any }> {
  const list: any[] = [...(tx?.transaction?.message?.instructions ?? [])];
  for (const inner of tx?.meta?.innerInstructions ?? []) list.push(...(inner?.instructions ?? []));
  return list
    .filter((i) => i && TOKEN_PROGRAMS.has(i.programId) && i.parsed && typeof i.parsed === "object" && TRANSFER_TYPES.has(i.parsed.type))
    .map((i) => ({ type: i.parsed.type, info: i.parsed.info ?? {} }));
}

export function verifyKogFee(input: VerifyInput): VerifyResult {
  if (!input || typeof input !== "object") return fail("bad_input");
  const { tx, quote, payee, mint, seen, now } = input as VerifyInput & { tx: any };
  if (typeof payee !== "string" || payee.length === 0 || typeof mint !== "string" || mint.length === 0) return fail("bad_input");
  const sigs = (tx as any)?.transaction?.signatures;
  const signature = Array.isArray(sigs) && typeof sigs[0] === "string" ? sigs[0] : undefined;
  if (!signature || !(tx as any)?.meta || !Array.isArray((tx as any)?.transaction?.message?.accountKeys)) return fail("bad_input");

  const quoted = toBig(quote?.amount);
  const expires = expirySeconds(quote?.expires_at);
  if (quoted === null || quoted === 0n || expires === null) return fail("bad_quote", signature);
  if (quote.mint !== undefined && quote.mint !== mint) return fail("bad_quote", signature);
  if (quote.payee !== undefined && quote.payee !== payee) return fail("bad_quote", signature);

  const t = tx as any;
  if (t.meta.err !== null && t.meta.err !== undefined) return fail("tx_failed", signature);
  if (hasSeen(seen, signature)) return fail("replay", signature);

  const paidAt = typeof t.blockTime === "number" ? t.blockTime : typeof now === "number" ? now : null;
  if (paidAt === null) return fail("bad_input", signature);
  if (paidAt > expires) return fail("expired", signature);

  const keys: string[] = t.transaction.message.accountKeys.map(keyOf);
  const pre = balances(t.meta.preTokenBalances);
  const post = balances(t.meta.postTokenBalances);

  // Token accounts that belong to the payee (by owner) or are the payee (a token account passed directly).
  const payeeAccounts = new Set<string>();
  for (const b of [...pre, ...post]) {
    const pk = keys[b.accountIndex];
    if (!pk) continue;
    if (b.owner === payee || pk === payee) payeeAccounts.add(pk);
  }

  const moves = transfers(t);
  const ofMint = moves.filter((m) => m.info.mint === mint || (m.type === "transfer" && post.some((b) => keys[b.accountIndex] === m.info.destination && b.mint === mint)));
  if (ofMint.length === 0) return fail("wrong_mint", signature);
  const toPayee = ofMint.filter((m) => payeeAccounts.has(m.info.destination) || m.info.destination === payee);
  if (toPayee.length === 0) return fail("wrong_payee", signature);

  // Net amount of the mint the payee received in this transaction (after any Token 2022 transfer fee).
  let received = 0n;
  const preBy = new Map(pre.filter((b) => b.mint === mint).map((b) => [b.accountIndex, b.amount]));
  const seenIdx = new Set<number>();
  for (const b of post) {
    if (b.mint !== mint) continue;
    const pk = keys[b.accountIndex];
    if (!pk || !payeeAccounts.has(pk)) continue;
    seenIdx.add(b.accountIndex);
    received += b.amount - (preBy.get(b.accountIndex) ?? 0n);
  }
  for (const [idx, amt] of preBy) {
    const pk = keys[idx];
    if (!seenIdx.has(idx) && pk && payeeAccounts.has(pk)) received -= amt; // account closed in the transaction
  }
  if (received < quoted) return fail("short_fee", signature, received.toString());
  return { ok: true, reason: "ok", signature, received: received.toString() };
}

agentgate-source: code mirror on Cloudflare Pages. All repos