veralayer 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/guard.js ADDED
@@ -0,0 +1,115 @@
1
+ import { Vera } from './client.js';
2
+ import { inspectTransaction } from './inspect.js';
3
+ import { evaluatePolicy, validatePolicy } from './policy.js';
4
+ export class MemoryLedger {
5
+ day = '';
6
+ spent = 0;
7
+ roll() { const d = new Date().toISOString().slice(0, 10); if (d !== this.day) {
8
+ this.day = d;
9
+ this.spent = 0;
10
+ } }
11
+ spentToday() { this.roll(); return this.spent; }
12
+ add(sol) { this.roll(); this.spent += sol; }
13
+ }
14
+ export class VeraBlockedError extends Error {
15
+ decision;
16
+ constructor(decision) {
17
+ super(`Vera blocked this transaction: ${decision.findings.filter((f) => f.action === 'block').map((f) => f.message).join('; ')}`);
18
+ this.decision = decision;
19
+ }
20
+ }
21
+ export class VeraReviewRequiredError extends Error {
22
+ decision;
23
+ constructor(decision) {
24
+ super(`Vera needs a human review: ${decision.findings.filter((f) => f.action === 'review').map((f) => f.message).join('; ')}`);
25
+ this.decision = decision;
26
+ }
27
+ }
28
+ export class VeraGuard {
29
+ vera;
30
+ policy;
31
+ connection;
32
+ wallet;
33
+ ledger;
34
+ onReview;
35
+ onDecision;
36
+ // evaluate + reserve run one at a time, so two concurrent sends can't both pass a daily limit
37
+ lock = Promise.resolve();
38
+ constructor(opts = {}) {
39
+ this.vera = opts.vera instanceof Vera ? opts.vera : new Vera(opts.vera);
40
+ this.policy = opts.policy ?? {};
41
+ validatePolicy(this.policy); // fail at startup on typos, not silently at payment time
42
+ this.connection = opts.connection;
43
+ this.wallet = opts.wallet;
44
+ this.ledger = opts.ledger ?? new MemoryLedger();
45
+ this.onReview = opts.onReview;
46
+ this.onDecision = opts.onDecision;
47
+ }
48
+ /** Score recipients (deduplicated, in parallel) and verify each signed result against Vera's published key. */
49
+ async checkRecipients(addresses) {
50
+ const skip = new Set([...(this.policy.allow ?? []), ...(this.policy.deny ?? [])]);
51
+ const uniq = [...new Set(addresses)].filter((a) => !skip.has(a));
52
+ return Promise.all(uniq.map(async (address) => {
53
+ try {
54
+ const r = await this.vera.check(address); // throws if the result is for another address
55
+ const v = await this.vera.verify(r);
56
+ const a = r.attestation;
57
+ return { address, ok: true, score: a.score, tier: a.tier, reasons: a.reasons, attestationId: r.id, verified: v.valid, expired: v.expired };
58
+ }
59
+ catch (e) {
60
+ return { address, ok: false, error: e.message };
61
+ }
62
+ }));
63
+ }
64
+ /** Decide on a planned payment without building a transaction (e.g. before quoting a price to another agent). */
65
+ async evaluatePayment(payment) {
66
+ const outflows = payment.token
67
+ ? [{ kind: 'token', to: payment.to, toAccount: payment.to, amount: payment.token.amount, mint: payment.token.mint, instruction: 'planned' }]
68
+ : [{ kind: 'sol', to: payment.to, toAccount: payment.to, amount: payment.amountSol ?? 0, instruction: 'planned' }];
69
+ return this.decide(outflows, {});
70
+ }
71
+ /** Decode a transaction and decide on it. Does not send anything. */
72
+ async evaluate(tx) {
73
+ const ins = await inspectTransaction(tx, this.connection, { wallet: this.wallet });
74
+ return this.decide(ins.outflows, { unknownPrograms: ins.unknownPrograms, valuePrograms: ins.valuePrograms, risky: ins.risky });
75
+ }
76
+ async decide(outflows, extra) {
77
+ const recipients = await this.checkRecipients(outflows.map((o) => o.to));
78
+ const decision = evaluatePolicy({ outflows, recipients, ...extra, spentTodaySol: await this.ledger.spentToday(), policy: this.policy });
79
+ this.onDecision?.(decision);
80
+ return decision;
81
+ }
82
+ /** Throws VeraBlockedError / VeraReviewRequiredError unless the transaction may be sent. */
83
+ async assert(tx) {
84
+ const d = await this.evaluate(tx);
85
+ if (d.action === 'block')
86
+ throw new VeraBlockedError(d);
87
+ if (d.action === 'review' && !(this.onReview && (await this.onReview(d))))
88
+ throw new VeraReviewRequiredError(d);
89
+ return d;
90
+ }
91
+ /** Evaluate, then sign and send only if allowed. The SOL is reserved in the ledger before sending and released if sending fails. */
92
+ async sendTransaction(tx, signers = [], options) {
93
+ if (!this.connection)
94
+ throw new Error('VeraGuard needs a Connection to send transactions');
95
+ const run = this.lock.then(async () => { const d = await this.assert(tx); await this.ledger.add(d.totalSol); return d; });
96
+ this.lock = run.catch(() => { });
97
+ const decision = await run;
98
+ try {
99
+ let signature;
100
+ if ('version' in tx) {
101
+ if (signers.length)
102
+ tx.sign(signers);
103
+ signature = await this.connection.sendTransaction(tx, options);
104
+ }
105
+ else {
106
+ signature = await this.connection.sendTransaction(tx, signers, options);
107
+ }
108
+ return { signature, decision };
109
+ }
110
+ catch (e) {
111
+ await this.ledger.add(-decision.totalSol);
112
+ throw e;
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,5 @@
1
+ export { Vera, VeraError, VERA_ISSUER, DEFAULT_API_URL, verifyAttestation, canonical, type Attestation, type CheckResult, type Tier, type VerifyResult } from './client.js';
2
+ export { VeraGuard, MemoryLedger, VeraBlockedError, VeraReviewRequiredError, type GuardOptions, type SpendLedger } from './guard.js';
3
+ export { inspectTransaction, instructionsOf, type Inspection } from './inspect.js';
4
+ export { evaluatePolicy, validatePolicy, DEFAULT_POLICY, type Policy, type RiskyInstruction, type Decision, type Finding, type Outflow, type RecipientCheck, type Action } from './policy.js';
5
+ export { KNOWN_PROGRAMS } from './programs.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // veralayer: payment guard for AI agents on Solana.
2
+ export { Vera, VeraError, VERA_ISSUER, DEFAULT_API_URL, verifyAttestation, canonical } from './client.js';
3
+ export { VeraGuard, MemoryLedger, VeraBlockedError, VeraReviewRequiredError } from './guard.js';
4
+ export { inspectTransaction, instructionsOf } from './inspect.js';
5
+ export { evaluatePolicy, validatePolicy, DEFAULT_POLICY } from './policy.js';
6
+ export { KNOWN_PROGRAMS } from './programs.js';
@@ -0,0 +1,29 @@
1
+ import type { Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
2
+ import type { Outflow, RiskyInstruction } from './policy.js';
3
+ export type Inspection = {
4
+ payer: string;
5
+ /** the wallet being guarded (option `wallet`, default the fee payer) */
6
+ wallet: string;
7
+ outflows: Outflow[];
8
+ programs: string[];
9
+ unknownPrograms: string[];
10
+ /** names of well-known programs called that can move value the guard does not decode */
11
+ valuePrograms: string[];
12
+ risky: RiskyInstruction[];
13
+ notes: string[];
14
+ };
15
+ type Ix = {
16
+ programId: string;
17
+ accounts: string[];
18
+ data: Uint8Array;
19
+ };
20
+ /** Flatten a legacy or versioned transaction into plain instructions, resolving address lookup tables. */
21
+ export declare function instructionsOf(tx: Transaction | VersionedTransaction, connection?: Connection): Promise<{
22
+ payer: string;
23
+ ixs: Ix[];
24
+ }>;
25
+ /** Decode what leaves `wallet` (default: the fee payer). Token destinations are resolved to their owner via RPC when needed. */
26
+ export declare function inspectTransaction(tx: Transaction | VersionedTransaction, connection?: Connection, opts?: {
27
+ wallet?: string;
28
+ }): Promise<Inspection>;
29
+ export {};
@@ -0,0 +1,125 @@
1
+ import { KNOWN_PROGRAMS } from './programs.js';
2
+ const SYSTEM = '11111111111111111111111111111111';
3
+ const TOKEN = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
4
+ const TOKEN22 = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
5
+ const ATA = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
6
+ // programs that never move value on their own
7
+ const INERT = new Set([SYSTEM, TOKEN, TOKEN22, ATA, 'ComputeBudget111111111111111111111111111111', 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr',
8
+ 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo', 'noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV', 'AddressLookupTab1e1111111111111111111111111']);
9
+ const u64 = (d, o) => Number(new DataView(d.buffer, d.byteOffset + o, 8).getBigUint64(0, true));
10
+ const u32 = (d, o) => new DataView(d.buffer, d.byteOffset + o, 4).getUint32(0, true);
11
+ const short = (a) => `${a.slice(0, 4)}…${a.slice(-4)}`;
12
+ function isVersioned(tx) {
13
+ return 'version' in tx;
14
+ }
15
+ /** Flatten a legacy or versioned transaction into plain instructions, resolving address lookup tables. */
16
+ export async function instructionsOf(tx, connection) {
17
+ if (!isVersioned(tx)) {
18
+ const payer = (tx.feePayer ?? tx.instructions[0]?.keys.find((k) => k.isSigner)?.pubkey)?.toBase58() ?? '';
19
+ return { payer, ixs: tx.instructions.map((ix) => ({ programId: ix.programId.toBase58(), accounts: ix.keys.map((k) => k.pubkey.toBase58()), data: Uint8Array.from(ix.data) })) };
20
+ }
21
+ const msg = tx.message;
22
+ const luts = [];
23
+ for (const l of msg.addressTableLookups ?? []) {
24
+ if (!connection)
25
+ throw new Error('a Connection is needed to resolve address lookup tables');
26
+ const res = await connection.getAddressLookupTable(l.accountKey);
27
+ if (!res.value)
28
+ throw new Error(`lookup table ${l.accountKey.toBase58()} not found`);
29
+ luts.push(res.value);
30
+ }
31
+ const keys = msg.getAccountKeys({ addressLookupTableAccounts: luts });
32
+ const k = (i) => keys.get(i).toBase58();
33
+ return {
34
+ payer: k(0),
35
+ ixs: msg.compiledInstructions.map((ix) => ({ programId: k(ix.programIdIndex), accounts: ix.accountKeyIndexes.map(k), data: ix.data })),
36
+ };
37
+ }
38
+ /** Decode what leaves `wallet` (default: the fee payer). Token destinations are resolved to their owner via RPC when needed. */
39
+ export async function inspectTransaction(tx, connection, opts = {}) {
40
+ const { payer, ixs } = await instructionsOf(tx, connection);
41
+ const wallet = opts.wallet ?? payer;
42
+ const outflows = [];
43
+ const risky = [];
44
+ const notes = [];
45
+ const programs = new Set();
46
+ // token accounts created in this transaction: ata -> { owner, mint } (Create = 0 / empty data, CreateIdempotent = 1)
47
+ const created = new Map();
48
+ for (const ix of ixs)
49
+ if (ix.programId === ATA && (ix.data.length === 0 || ix.data[0] <= 1) && ix.accounts.length >= 4)
50
+ created.set(ix.accounts[1], { owner: ix.accounts[2], mint: ix.accounts[3] });
51
+ const pendingTokens = [];
52
+ const sol = (from, to, lamports, instruction) => outflows.push({ kind: 'sol', from, to, toAccount: to, amount: lamports / 1e9, instruction });
53
+ for (const ix of ixs) {
54
+ programs.add(ix.programId);
55
+ const a = ix.accounts, d = ix.data;
56
+ if (ix.programId === SYSTEM && d.length >= 4) {
57
+ const op = u32(d, 0);
58
+ if (op === 0 && d.length >= 12)
59
+ sol(a[0], a[1], u64(d, 4), 'system.createAccount');
60
+ else if (op === 2 && d.length >= 12)
61
+ sol(a[0], a[1], u64(d, 4), 'system.transfer');
62
+ else if (op === 11 && d.length >= 12)
63
+ sol(a[1], a[2], u64(d, 4), 'system.transferWithSeed');
64
+ else if (op === 3 && d.length >= 44) {
65
+ const seedLen = u64(d, 36);
66
+ if (d.length >= 44 + seedLen + 8)
67
+ sol(a[0], a[1], u64(d, 44 + seedLen), 'system.createAccountWithSeed');
68
+ }
69
+ else if (op === 5 && d.length >= 12)
70
+ sol(a[4], a[1], u64(d, 4), 'system.withdrawNonceAccount');
71
+ else if ((op === 1 || op === 10) && a[0] === wallet)
72
+ risky.push({ instruction: 'system.assign', message: `Reassigns the wallet ${short(wallet)} to another program` });
73
+ }
74
+ else if ((ix.programId === TOKEN || ix.programId === TOKEN22) && d.length >= 1) {
75
+ const op = d[0];
76
+ if (op === 3 && d.length >= 9)
77
+ pendingTokens.push({ account: a[1], raw: u64(d, 1), authority: a[2], instruction: 'token.transfer' });
78
+ else if (op === 12 && d.length >= 10)
79
+ pendingTokens.push({ account: a[2], raw: u64(d, 1), decimals: d[9], mint: a[1], authority: a[3], instruction: 'token.transferChecked' });
80
+ else if (op === 26 && d[1] === 1 && d.length >= 11)
81
+ pendingTokens.push({ account: a[2], raw: u64(d, 2), decimals: d[10], mint: a[1], authority: a[3], instruction: 'token.transferCheckedWithFee' });
82
+ else if ((op === 4 && a[2] === wallet) || (op === 13 && a[3] === wallet))
83
+ risky.push({ instruction: 'token.approve', message: `Lets ${short(op === 4 ? a[1] : a[2])} spend tokens from ${short(a[0])}` });
84
+ else if (op === 6 && a[1] === wallet)
85
+ risky.push({ instruction: 'token.setAuthority', message: `Changes an authority on ${short(a[0])}, which the wallet controls` });
86
+ else if (op === 9 && a[2] === wallet && a[1] !== wallet)
87
+ risky.push({ instruction: 'token.closeAccount', message: `Closes ${short(a[0])} and sends its lamports (all wrapped SOL) to ${short(a[1])}` });
88
+ }
89
+ }
90
+ // resolve token destinations to owners (and mint/decimals for plain `transfer`)
91
+ const { PublicKey: PK } = connection ? await import('@solana/web3.js') : { PublicKey: undefined };
92
+ for (const t of pendingTokens) {
93
+ let owner, mint = t.mint, decimals = t.decimals;
94
+ const c = created.get(t.account);
95
+ if (c) {
96
+ owner = c.owner;
97
+ mint ??= c.mint;
98
+ }
99
+ if ((!owner || decimals === undefined) && connection && PK) {
100
+ const info = await connection.getParsedAccountInfo(new PK(t.account));
101
+ const parsed = info.value?.data?.parsed?.info;
102
+ owner ??= parsed?.owner;
103
+ mint ??= parsed?.mint;
104
+ decimals ??= parsed?.tokenAmount?.decimals;
105
+ if (decimals === undefined && mint) {
106
+ const m = await connection.getParsedAccountInfo(new PK(mint));
107
+ decimals = m.value?.data?.parsed?.info?.decimals;
108
+ }
109
+ }
110
+ if (!owner)
111
+ notes.push(`could not resolve the owner of token account ${t.account}; scored as-is`);
112
+ outflows.push({ kind: 'token', from: t.authority, to: owner ?? t.account, toAccount: t.account, amount: decimals === undefined ? t.raw : t.raw / 10 ** decimals, mint, decimalsKnown: decimals !== undefined, instruction: t.instruction });
113
+ }
114
+ const all = [...programs];
115
+ const unknownPrograms = all.filter((p) => !INERT.has(p) && !KNOWN_PROGRAMS[p]);
116
+ const valuePrograms = all.filter((p) => !INERT.has(p) && KNOWN_PROGRAMS[p]).map((p) => KNOWN_PROGRAMS[p]);
117
+ for (const p of all)
118
+ if (!INERT.has(p))
119
+ notes.push(`calls ${KNOWN_PROGRAMS[p] ?? p}: value moved inside this program is not decoded`);
120
+ // only value that leaves the guarded wallet counts; transfers between other parties in the same transaction do not
121
+ const mine = outflows.filter((o) => o.from === wallet && o.to !== wallet && o.amount > 0);
122
+ if (mine.length < outflows.filter((o) => o.amount > 0).length)
123
+ notes.push('some transfers in this transaction are not from the guarded wallet and were not counted');
124
+ return { payer, wallet, outflows: mine, programs: all, unknownPrograms, valuePrograms, risky, notes };
125
+ }
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function runMcp(opts?: {
2
+ apiUrl?: string;
3
+ apiKey?: string;
4
+ }): Promise<void>;
package/dist/mcp.js ADDED
@@ -0,0 +1,103 @@
1
+ // MCP server over stdio (newline-delimited JSON-RPC 2.0), no extra dependencies.
2
+ // Start with `npx -y veralayer mcp`; add to Claude Desktop / Cursor as a stdio server.
3
+ // Tools: vera_check (score an address) and vera_guard_payment (full guard decision for a planned SOL payment).
4
+ import { createInterface } from 'node:readline';
5
+ import { Vera } from './client.js';
6
+ import { VeraGuard } from './guard.js';
7
+ const VERSION = '0.2.0';
8
+ const PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05'];
9
+ const TOOLS = [
10
+ {
11
+ name: 'vera_check',
12
+ title: 'Vera trust check',
13
+ description: 'Score a Solana address before paying it. Returns a 0-100 trust score, a tier (high/medium/low/unknown), the reasons, and whether Vera\'s signed result verified. A score is a risk signal, not a guarantee.',
14
+ inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Solana address (base58)' } }, required: ['address'] },
15
+ },
16
+ {
17
+ name: 'vera_guard_payment',
18
+ title: 'Vera payment guard',
19
+ description: 'Decide whether a planned SOL payment should go ahead under a spending policy. Returns allow, review or block with the rule that fired. Call this before sending funds.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ to: { type: 'string', description: 'Recipient Solana address' },
24
+ amount_sol: { type: 'number', description: 'Amount in SOL' },
25
+ min_tier: { type: 'string', enum: ['high', 'medium', 'low'], description: 'Minimum recipient tier (default medium)' },
26
+ max_sol_per_tx: { type: 'number', description: 'Optional cap for this payment' },
27
+ },
28
+ required: ['to', 'amount_sol'],
29
+ },
30
+ },
31
+ ];
32
+ export async function runMcp(opts = {}) {
33
+ const vera = new Vera({ ...opts, issuer: process.env.VERA_ISSUER });
34
+ const send = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
35
+ const text = (t, structured, isError = false) => ({ content: [{ type: 'text', text: t }], ...(structured ? { structuredContent: structured } : {}), ...(isError ? { isError } : {}) });
36
+ async function call(name, args) {
37
+ try {
38
+ if (name === 'vera_check') {
39
+ const address = String(args.address ?? '').trim();
40
+ const r = await vera.check(address);
41
+ const v = await vera.verify(r);
42
+ const a = r.attestation;
43
+ const lines = [
44
+ `${a.score ?? 'n/a'}/100 · ${a.tier.toUpperCase()} · ${address}`,
45
+ ...(r.summary ? [r.summary] : []),
46
+ ...a.reasons.map((x) => `- [${x.kind}] ${x.text}`),
47
+ `Signature ${v.valid ? 'verified' : 'INVALID'} · attestation ${r.id}`,
48
+ ];
49
+ return text(lines.join('\n'), { score: a.score, tier: a.tier, reasons: a.reasons, verified: v.valid, attestation_id: r.id });
50
+ }
51
+ if (name === 'vera_guard_payment') {
52
+ const policy = {
53
+ ...(args.min_tier ? { minTier: args.min_tier } : {}),
54
+ ...(typeof args.max_sol_per_tx === 'number' ? { maxSolPerTx: args.max_sol_per_tx } : {}),
55
+ };
56
+ const amount = Number(args.amount_sol);
57
+ if (!Number.isFinite(amount) || amount < 0)
58
+ return text(`Invalid amount_sol: ${String(args.amount_sol)}`, undefined, true);
59
+ const d = await new VeraGuard({ vera, policy }).evaluatePayment({ to: String(args.to), amountSol: amount });
60
+ const lines = [`${d.action.toUpperCase()}: ${args.amount_sol} SOL to ${args.to}`, ...d.findings.map((f) => `- ${f.action}: ${f.message}`)];
61
+ if (!d.findings.length)
62
+ lines.push('- no policy rule fired');
63
+ return text(lines.join('\n'), { action: d.action, findings: d.findings });
64
+ }
65
+ return null;
66
+ }
67
+ catch (e) {
68
+ return text(`Vera failed: ${e.message}`, undefined, true);
69
+ }
70
+ }
71
+ const rl = createInterface({ input: process.stdin });
72
+ rl.on('line', async (line) => {
73
+ if (!line.trim())
74
+ return;
75
+ let msg;
76
+ try {
77
+ msg = JSON.parse(line);
78
+ }
79
+ catch {
80
+ return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
81
+ }
82
+ const { id, method, params } = msg;
83
+ if (id === undefined)
84
+ return; // notifications (e.g. notifications/initialized) need no reply
85
+ if (typeof method !== 'string')
86
+ return send({ jsonrpc: '2.0', id, error: { code: -32600, message: 'Invalid request' } });
87
+ const reply = (result) => send({ jsonrpc: '2.0', id, result });
88
+ switch (method) {
89
+ case 'initialize':
90
+ return reply({ protocolVersion: PROTOCOLS.includes(params?.protocolVersion) ? params?.protocolVersion : PROTOCOLS[0], capabilities: { tools: {} }, serverInfo: { name: 'veralayer', version: VERSION } });
91
+ case 'ping':
92
+ return reply({});
93
+ case 'tools/list':
94
+ return reply({ tools: TOOLS });
95
+ case 'tools/call': {
96
+ const res = await call(String(params?.name), params?.arguments ?? {});
97
+ return res ? reply(res) : send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${String(params?.name)}` } });
98
+ }
99
+ default:
100
+ return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } });
101
+ }
102
+ });
103
+ }
@@ -0,0 +1,104 @@
1
+ import type { Tier } from './client.js';
2
+ export type Action = 'allow' | 'review' | 'block';
3
+ export type Policy = {
4
+ /** Recipients below this tier are blocked. Default 'medium'. */
5
+ minTier?: Exclude<Tier, 'unknown'>;
6
+ /** Max SOL one recipient may receive in one transaction, per tier. Default high: no cap, medium: 5, low: 0. Unknown recipients are governed by onUnknown unless you set a cap here. */
7
+ tierLimitsSol?: Partial<Record<Tier, number>>;
8
+ /** Max total SOL leaving the wallet in one transaction. */
9
+ maxSolPerTx?: number;
10
+ /** Max total SOL per UTC day, tracked by the guard's ledger. */
11
+ dailyLimitSol?: number;
12
+ /** Per-mint caps for SPL tokens, in UI units (e.g. { EPjF…Dt1v: 500 } for 500 USDC per transaction). */
13
+ tokenLimits?: Record<string, number>;
14
+ /** Addresses that skip scoring (your own treasury, known vendors). */
15
+ allow?: string[];
16
+ /** Addresses that are always blocked. */
17
+ deny?: string[];
18
+ /** Recipients Vera cannot score (no history). Default 'review'. */
19
+ onUnknown?: Action;
20
+ /** The check failed, its signature did not verify, or it was for another address. Default 'block'. */
21
+ onCheckError?: Exclude<Action, 'allow'>;
22
+ /** Transactions that call programs outside the well-known list. Default 'review'. */
23
+ unknownPrograms?: Action;
24
+ /** Well-known programs that can move value the guard does not decode (swaps, lending). Default 'review'. */
25
+ knownPrograms?: Action;
26
+ /** Instructions that hand over control of the wallet or its tokens (approve, setAuthority, closeAccount to someone else, assign). Default 'block'. */
27
+ riskyInstructions?: Action;
28
+ };
29
+ export declare const DEFAULT_POLICY: {
30
+ minTier: "medium";
31
+ tierLimitsSol: Partial<Record<Tier, number>>;
32
+ allow: string[];
33
+ deny: string[];
34
+ onUnknown: Action;
35
+ onCheckError: Exclude<Action, "allow">;
36
+ unknownPrograms: Action;
37
+ knownPrograms: Action;
38
+ riskyInstructions: Action;
39
+ };
40
+ /** One movement of value out of the guarded wallet. */
41
+ export type Outflow = {
42
+ kind: 'sol' | 'token';
43
+ /** The wallet that ends up owning the funds (token accounts are resolved to their owner). */
44
+ to: string;
45
+ /** The account that is credited: same as `to` for SOL, the token account for SPL. */
46
+ toAccount: string;
47
+ /** SOL for kind 'sol'; UI units for tokens (raw units when decimals are unknown). */
48
+ amount: number;
49
+ mint?: string;
50
+ /** false when token decimals could not be resolved, so `amount` is in raw units. */
51
+ decimalsKnown?: boolean;
52
+ /** The wallet the value leaves (the signer of the transfer). */
53
+ from?: string;
54
+ instruction: string;
55
+ };
56
+ /** An instruction that changes who controls the wallet or its tokens. */
57
+ export type RiskyInstruction = {
58
+ instruction: string;
59
+ message: string;
60
+ };
61
+ /** What the guard learned about one recipient. */
62
+ export type RecipientCheck = {
63
+ address: string;
64
+ ok: true;
65
+ score: number | null;
66
+ tier: Tier;
67
+ reasons: {
68
+ kind: string;
69
+ text: string;
70
+ }[];
71
+ attestationId: string;
72
+ verified: boolean;
73
+ expired: boolean;
74
+ } | {
75
+ address: string;
76
+ ok: false;
77
+ error: string;
78
+ };
79
+ export type Finding = {
80
+ action: Action;
81
+ code: string;
82
+ message: string;
83
+ address?: string;
84
+ };
85
+ export type Decision = {
86
+ action: Action;
87
+ findings: Finding[];
88
+ outflows: Outflow[];
89
+ recipients: RecipientCheck[];
90
+ totalSol: number;
91
+ unknownPrograms: string[];
92
+ };
93
+ /** Throws on a policy that would silently weaken checks (typos in tiers or actions, non-numeric limits). */
94
+ export declare function validatePolicy(p?: Policy): void;
95
+ export declare function evaluatePolicy(input: {
96
+ outflows: Outflow[];
97
+ recipients: RecipientCheck[];
98
+ unknownPrograms?: string[];
99
+ /** names of well-known programs that may move value the guard does not decode */
100
+ valuePrograms?: string[];
101
+ risky?: RiskyInstruction[];
102
+ spentTodaySol?: number;
103
+ policy?: Policy;
104
+ }): Decision;
package/dist/policy.js ADDED
@@ -0,0 +1,112 @@
1
+ export const DEFAULT_POLICY = {
2
+ minTier: 'medium',
3
+ tierLimitsSol: { high: Infinity, medium: 5, low: 0 },
4
+ allow: [],
5
+ deny: [],
6
+ onUnknown: 'review',
7
+ onCheckError: 'block',
8
+ unknownPrograms: 'review',
9
+ knownPrograms: 'review',
10
+ riskyInstructions: 'block',
11
+ };
12
+ const RANK = { unknown: 0, low: 1, medium: 2, high: 3 };
13
+ const ACTIONS = ['allow', 'review', 'block'];
14
+ const worst = (a, b) => (a === 'block' || b === 'block' ? 'block' : a === 'review' || b === 'review' ? 'review' : 'allow');
15
+ const fmt = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(4).replace(/0+$/, ''));
16
+ const short = (a) => `${a.slice(0, 4)}…${a.slice(-4)}`;
17
+ /** Throws on a policy that would silently weaken checks (typos in tiers or actions, non-numeric limits). */
18
+ export function validatePolicy(p = {}) {
19
+ if (p.minTier !== undefined && !['high', 'medium', 'low'].includes(p.minTier))
20
+ throw new Error(`invalid minTier: ${String(p.minTier)}`);
21
+ for (const k of ['onUnknown', 'onCheckError', 'unknownPrograms', 'knownPrograms', 'riskyInstructions']) {
22
+ if (p[k] !== undefined && !ACTIONS.includes(p[k]))
23
+ throw new Error(`invalid ${k}: ${String(p[k])}`);
24
+ }
25
+ if (p.onCheckError === 'allow')
26
+ throw new Error('onCheckError cannot be allow');
27
+ const nums = [['maxSolPerTx', p.maxSolPerTx], ['dailyLimitSol', p.dailyLimitSol],
28
+ ...Object.entries(p.tierLimitsSol ?? {}).map(([k, v]) => [`tierLimitsSol.${k}`, v]),
29
+ ...Object.entries(p.tokenLimits ?? {}).map(([k, v]) => [`tokenLimits.${k}`, v])];
30
+ for (const [k, v] of nums)
31
+ if (v !== undefined && (typeof v !== 'number' || Number.isNaN(v) || v < 0))
32
+ throw new Error(`invalid ${k}: ${String(v)}`);
33
+ for (const k of Object.keys(p.tierLimitsSol ?? {}))
34
+ if (!(k in RANK))
35
+ throw new Error(`invalid tier in tierLimitsSol: ${k}`);
36
+ }
37
+ export function evaluatePolicy(input) {
38
+ validatePolicy(input.policy);
39
+ const p = { ...DEFAULT_POLICY, ...input.policy, tierLimitsSol: { ...DEFAULT_POLICY.tierLimitsSol, ...input.policy?.tierLimitsSol } };
40
+ const allow = new Set(p.allow), deny = new Set(p.deny);
41
+ const findings = [];
42
+ const add = (action, code, message, address) => findings.push({ action, code, message, ...(address ? { address } : {}) });
43
+ // malformed amounts never pass and never count toward limits
44
+ const good = (o) => typeof o.amount === 'number' && Number.isFinite(o.amount) && o.amount >= 0;
45
+ for (const o of input.outflows)
46
+ if (!good(o))
47
+ add('block', 'bad_amount', `Invalid amount ${String(o.amount)} to ${short(String(o.to))}`, o.to);
48
+ const outflows = input.outflows.filter(good);
49
+ const totalSol = outflows.filter((o) => o.kind === 'sol').reduce((s, o) => s + o.amount, 0);
50
+ const solTo = new Map();
51
+ for (const o of outflows)
52
+ if (o.kind === 'sol')
53
+ solTo.set(o.to, (solTo.get(o.to) || 0) + o.amount);
54
+ const byAddr = new Map(input.recipients.map((r) => [r.address, r]));
55
+ for (const to of new Set(outflows.map((o) => o.to))) {
56
+ const s = short(to);
57
+ if (deny.has(to)) {
58
+ add('block', 'denylisted', `${s} is on your deny list`, to);
59
+ continue;
60
+ }
61
+ if (allow.has(to))
62
+ continue;
63
+ const r = byAddr.get(to);
64
+ if (!r || !r.ok) {
65
+ add(p.onCheckError, 'check_failed', `Could not check ${s}${r && !r.ok ? `: ${r.error}` : ''}`, to);
66
+ continue;
67
+ }
68
+ if (!(r.tier in RANK)) {
69
+ add(p.onCheckError, 'bad_tier', `Vera returned an unexpected tier for ${s}`, to);
70
+ continue;
71
+ }
72
+ if (!r.verified) {
73
+ add(p.onCheckError, 'bad_signature', `Vera's signature for ${s} did not verify`, to);
74
+ continue;
75
+ }
76
+ if (r.expired)
77
+ add('review', 'stale_check', `Vera's result for ${s} has expired`, to);
78
+ if (r.tier === 'unknown') {
79
+ add(p.onUnknown, 'unknown_recipient', `${s} has no history Vera can score`, to);
80
+ }
81
+ else if (RANK[r.tier] < RANK[p.minTier]) {
82
+ add('block', 'tier_below_minimum', `${s} is ${r.tier} trust (${r.score}/100), below your minimum of ${p.minTier}: ${r.reasons[0]?.text ?? ''}`.trim(), to);
83
+ }
84
+ const cap = p.tierLimitsSol[r.tier] ?? Infinity;
85
+ const sent = solTo.get(to) || 0;
86
+ if (sent > cap)
87
+ add(cap === 0 ? 'block' : 'review', 'tier_limit', `${fmt(sent)} SOL to ${s} exceeds the ${fmt(cap)} SOL limit for ${r.tier} trust`, to);
88
+ }
89
+ for (const o of outflows) {
90
+ if (o.kind !== 'token')
91
+ continue;
92
+ if (o.decimalsKnown === false) {
93
+ add('review', 'unknown_decimals', `Could not read token decimals for ${short(o.toAccount)}; amount not compared to limits`, o.to);
94
+ continue;
95
+ }
96
+ if (o.mint && p.tokenLimits && o.mint in p.tokenLimits && o.amount > p.tokenLimits[o.mint])
97
+ add('review', 'token_limit', `${fmt(o.amount)} of ${o.mint.slice(0, 4)}… exceeds your ${fmt(p.tokenLimits[o.mint])} per-transaction cap`, o.to);
98
+ }
99
+ if (p.maxSolPerTx !== undefined && totalSol > p.maxSolPerTx)
100
+ add('block', 'tx_limit', `${fmt(totalSol)} SOL in one transaction exceeds your ${fmt(p.maxSolPerTx)} SOL cap`);
101
+ if (p.dailyLimitSol !== undefined && (input.spentTodaySol ?? 0) + totalSol > p.dailyLimitSol)
102
+ add('block', 'daily_limit', `This would bring today's spend to ${fmt((input.spentTodaySol ?? 0) + totalSol)} SOL, over your ${fmt(p.dailyLimitSol)} SOL daily limit`);
103
+ for (const r of input.risky ?? [])
104
+ if (p.riskyInstructions !== 'allow')
105
+ add(p.riskyInstructions, 'risky_instruction', r.message);
106
+ if (input.unknownPrograms?.length && p.unknownPrograms !== 'allow')
107
+ add(p.unknownPrograms, 'unknown_program', `Calls ${input.unknownPrograms.length} program${input.unknownPrograms.length > 1 ? 's' : ''} outside the well-known list: ${input.unknownPrograms.map((x) => x.slice(0, 4) + '…').join(', ')}`);
108
+ if (input.valuePrograms?.length && p.knownPrograms !== 'allow')
109
+ add(p.knownPrograms, 'opaque_value', `Value may move inside ${input.valuePrograms.join(', ')}, which the guard does not decode`);
110
+ const action = findings.reduce((a, f) => worst(a, f.action), 'allow');
111
+ return { action, findings, outflows, recipients: input.recipients, totalSol, unknownPrograms: input.unknownPrograms ?? [] };
112
+ }
@@ -0,0 +1 @@
1
+ export declare const KNOWN_PROGRAMS: Record<string, string>;
@@ -0,0 +1,37 @@
1
+ // Well-known, widely used Solana programs. Calls to anything else count as "unverified" in the score.
2
+ // This is an allowlist of popularity, not an audit: a known program can still be used in a harmful way.
3
+ export const KNOWN_PROGRAMS = {
4
+ '11111111111111111111111111111111': 'System',
5
+ TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: 'SPL Token',
6
+ TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb: 'Token-2022',
7
+ ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL: 'Associated Token Account',
8
+ ComputeBudget111111111111111111111111111111: 'Compute Budget',
9
+ MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr: 'Memo',
10
+ Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo: 'Memo v1',
11
+ Stake11111111111111111111111111111111111111: 'Stake',
12
+ Vote111111111111111111111111111111111111111: 'Vote',
13
+ AddressLookupTab1e1111111111111111111111111: 'Address Lookup Table',
14
+ metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s: 'Metaplex Token Metadata',
15
+ BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY: 'Metaplex Bubblegum',
16
+ cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK: 'Account Compression',
17
+ noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV: 'Noop',
18
+ JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: 'Jupiter',
19
+ '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P': 'pump.fun',
20
+ pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA: 'PumpSwap',
21
+ '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8': 'Raydium AMM',
22
+ CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK: 'Raydium CLMM',
23
+ CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C: 'Raydium CPMM',
24
+ whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc: 'Orca Whirlpools',
25
+ LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo: 'Meteora DLMM',
26
+ Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB: 'Meteora Pools',
27
+ dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN: 'Meteora DBC',
28
+ MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD: 'Marinade',
29
+ SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy: 'Stake Pool',
30
+ PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY: 'Phoenix',
31
+ dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH: 'Drift',
32
+ KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD: 'Kamino Lend',
33
+ So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo: 'Solend',
34
+ TSWAPaqyCSx2KABk68Shruf4rp7CxcNi8hAsbdwmHbN: 'Tensor',
35
+ M2mx93ekt1fmXSVkTrUL9xVFHkmME8HTUi5Cyc5aF7K: 'Magic Eden',
36
+ SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf: 'Squads',
37
+ };