/**
* 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