veralayer 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +83 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +82 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.js +78 -0
- package/dist/guard.d.ts +67 -0
- package/dist/guard.js +103 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/inspect.d.ts +23 -0
- package/dist/inspect.js +96 -0
- package/dist/policy.d.ts +76 -0
- package/dist/policy.js +70 -0
- package/dist/programs.d.ts +1 -0
- package/dist/programs.js +37 -0
- package/package.json +66 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VeraLayer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,84 @@
|
|
|
1
|
-
#
|
|
1
|
+
# veralayer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A payment guard for AI agents on Solana. It sits between your agent's wallet and the network and checks every outgoing transaction:
|
|
4
|
+
|
|
5
|
+
- **Decodes the transaction.** It finds where value goes: SOL transfers, SPL and Token-2022 transfers (each token account is resolved to the wallet that owns it), v0 lookup tables, and every program called.
|
|
6
|
+
- **Scores each recipient.** Every recipient is checked with [Vera](https://vera-agent-psi.vercel.app). The score is built from on-chain history, and every reason cites the fact behind it.
|
|
7
|
+
- **Verifies the signature.** Each result is signed by Vera. The guard verifies that signature locally against Vera's published ed25519 key, so a tampered score is rejected.
|
|
8
|
+
- **Applies your policy.** It returns `allow`, `review` or `block`, with a finding for each rule that fired. Only allowed transactions are signed and sent.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install veralayer @solana/web3.js
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Guard a wallet
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Connection } from '@solana/web3.js';
|
|
18
|
+
import { VeraGuard, VeraBlockedError } from 'veralayer';
|
|
19
|
+
|
|
20
|
+
const guard = new VeraGuard({
|
|
21
|
+
connection: new Connection(process.env.RPC_URL!),
|
|
22
|
+
policy: {
|
|
23
|
+
minTier: 'medium', // block low-trust recipients
|
|
24
|
+
tierLimitsSol: { high: 50, medium: 5 }, // max SOL per recipient per tx, by tier
|
|
25
|
+
dailyLimitSol: 20, // across all transactions today
|
|
26
|
+
tokenLimits: { EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: 500 }, // 500 USDC per tx
|
|
27
|
+
deny: ['<known bad address>'],
|
|
28
|
+
allow: ['<your treasury>'],
|
|
29
|
+
},
|
|
30
|
+
onReview: async (decision) => askHumanInTelegram(decision), // optional; without it, review = block
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { signature, decision } = await guard.sendTransaction(tx, [agentKeypair]);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (e instanceof VeraBlockedError) console.log(e.decision.findings); // why it was stopped
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Decide before you build a transaction
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const d = await guard.evaluatePayment({ to: seller, amountSol: 1.5 });
|
|
44
|
+
if (d.action !== 'allow') return refuse(d.findings[0].message);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Just score an address
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { Vera } from 'veralayer';
|
|
51
|
+
const vera = new Vera();
|
|
52
|
+
const r = await vera.check(address); // { attestation: { score, tier, reasons, ... }, signature, summary }
|
|
53
|
+
await vera.verify(r); // { valid: true, expired: false }, checked offline
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CLI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx veralayer check <address>
|
|
60
|
+
npx veralayer pay <address> 2.5 --min-tier medium --daily 20
|
|
61
|
+
npx veralayer inspect <base64 transaction> --rpc <url>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Policy reference
|
|
65
|
+
|
|
66
|
+
| Field | Default | Effect |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `minTier` | `'medium'` | Recipients below this tier are blocked. |
|
|
69
|
+
| `tierLimitsSol` | `{ high: ∞, medium: 5, low: 0, unknown: 0 }` | Max SOL to one recipient in one transaction. Over the limit means `review`; a limit of 0 means `block`. |
|
|
70
|
+
| `maxSolPerTx` | none | Caps the total SOL out per transaction. |
|
|
71
|
+
| `dailyLimitSol` | none | Caps the total SOL out per UTC day, tracked by `ledger`: `MemoryLedger` by default, or your own store. |
|
|
72
|
+
| `tokenLimits` | none | Per-mint cap in UI units. Over the cap means `review`. |
|
|
73
|
+
| `allow` / `deny` | `[]` | `allow` skips scoring; `deny` always blocks. |
|
|
74
|
+
| `onUnknown` | `'review'` | Recipients with no history to score. |
|
|
75
|
+
| `onCheckError` | `'block'` | When the check fails or the signature does not verify. |
|
|
76
|
+
| `unknownPrograms` | `'review'` | Transactions that call programs outside the well-known list. |
|
|
77
|
+
|
|
78
|
+
## What it does not do (yet)
|
|
79
|
+
|
|
80
|
+
- **Value inside other programs.** Value moved inside swaps and other CPIs is not decoded. Those programs are listed, and unknown ones trigger `review`.
|
|
81
|
+
- **On-chain anchoring.** Vera's results are signed but not yet anchored on-chain.
|
|
82
|
+
- **Guarantees.** A score reads history, not intent. It is a risk signal, not a guarantee.
|
|
83
|
+
|
|
84
|
+
MIT · by [VeraLayer](https://veralayer.io)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npx veralayer check <address> score one address
|
|
3
|
+
// npx veralayer pay <address> <sol> what the guard would decide for a SOL payment
|
|
4
|
+
// npx veralayer inspect <base64 tx> decode a transaction and decide on it
|
|
5
|
+
// options: --min-tier high|medium|low --daily <sol> --max-tx <sol> --json --api <url> --rpc <url>
|
|
6
|
+
import { Vera } from './client.js';
|
|
7
|
+
import { VeraGuard } from './guard.js';
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const flag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; };
|
|
10
|
+
const json = args.includes('--json');
|
|
11
|
+
const pos = args.filter((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--') && args[i - 1] !== '--json'));
|
|
12
|
+
const [cmd, a1, a2] = pos;
|
|
13
|
+
const policy = {
|
|
14
|
+
...(flag('min-tier') ? { minTier: flag('min-tier') } : {}),
|
|
15
|
+
...(flag('daily') ? { dailyLimitSol: Number(flag('daily')) } : {}),
|
|
16
|
+
...(flag('max-tx') ? { maxSolPerTx: Number(flag('max-tx')) } : {}),
|
|
17
|
+
};
|
|
18
|
+
const vera = new Vera({ apiUrl: flag('api') ?? process.env.VERA_API_URL, apiKey: process.env.VERA_API_KEY });
|
|
19
|
+
const c = process.stdout.isTTY ? { r: '\x1b[31m', g: '\x1b[32m', y: '\x1b[33m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' } : { r: '', g: '', y: '', d: '', b: '', x: '' };
|
|
20
|
+
const tierColor = (t) => (t === 'high' ? c.g : t === 'medium' ? c.y : t === 'low' ? c.r : c.d);
|
|
21
|
+
const glyph = (k) => (k === 'ok' ? `${c.g}✓${c.x}` : k === 'warn' ? `${c.y}!${c.x}` : k === 'bad' ? `${c.r}×${c.x}` : `${c.d}i${c.x}`);
|
|
22
|
+
function printDecision(d) {
|
|
23
|
+
const col = d.action === 'allow' ? c.g : d.action === 'review' ? c.y : c.r;
|
|
24
|
+
console.log(`\n${c.b}${col}${d.action.toUpperCase()}${c.x} ${d.totalSol ? `${d.totalSol} SOL out` : ''}`);
|
|
25
|
+
for (const o of d.outflows)
|
|
26
|
+
console.log(` ${c.d}→${c.x} ${o.amount} ${o.kind === 'sol' ? 'SOL' : (o.mint ?? 'token').slice(0, 6) + '…'} to ${o.to}`);
|
|
27
|
+
for (const r of d.recipients) {
|
|
28
|
+
if (!r.ok) {
|
|
29
|
+
console.log(` ${c.r}×${c.x} ${r.address}: ${r.error}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
console.log(` ${tierColor(r.tier)}${r.tier.toUpperCase()} ${r.score ?? '—'}${c.x} ${r.address} ${r.verified ? `${c.d}signature ✓${c.x}` : `${c.r}signature ✗${c.x}`}`);
|
|
33
|
+
}
|
|
34
|
+
for (const f of d.findings)
|
|
35
|
+
console.log(` ${f.action === 'block' ? c.r : c.y}${f.action}${c.x} ${f.message}`);
|
|
36
|
+
if (!d.findings.length)
|
|
37
|
+
console.log(` ${c.d}no policy findings${c.x}`);
|
|
38
|
+
}
|
|
39
|
+
async function main() {
|
|
40
|
+
if (cmd === 'check' && a1) {
|
|
41
|
+
const r = await vera.check(a1);
|
|
42
|
+
const v = await vera.verify(r);
|
|
43
|
+
if (json)
|
|
44
|
+
return console.log(JSON.stringify({ ...r, verified: v }, null, 2));
|
|
45
|
+
const a = r.attestation;
|
|
46
|
+
console.log(`\n${c.b}${tierColor(a.tier)}${a.score ?? '—'}/100 ${a.tier.toUpperCase()}${c.x} ${a.subject}`);
|
|
47
|
+
if (r.summary)
|
|
48
|
+
console.log(`${c.d}${r.summary}${c.x}`);
|
|
49
|
+
for (const x of a.reasons)
|
|
50
|
+
console.log(` ${glyph(x.kind)} ${x.text}`);
|
|
51
|
+
console.log(`\n${c.d}attestation ${r.id}\nsignature ${v.valid ? 'verified' : 'INVALID'} · ${a.method} · expires ${a.expires_at}${c.x}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (cmd === 'pay' && a1 && a2) {
|
|
55
|
+
const d = await new VeraGuard({ vera, policy }).evaluatePayment({ to: a1, amountSol: Number(a2) });
|
|
56
|
+
return json ? console.log(JSON.stringify(d, null, 2)) : printDecision(d);
|
|
57
|
+
}
|
|
58
|
+
if (cmd === 'inspect' && a1) {
|
|
59
|
+
const web3 = await import('@solana/web3.js').catch(() => { throw new Error('inspect needs @solana/web3.js: npm i @solana/web3.js'); });
|
|
60
|
+
const raw = Buffer.from(a1, 'base64');
|
|
61
|
+
let tx;
|
|
62
|
+
try {
|
|
63
|
+
tx = web3.VersionedTransaction.deserialize(raw);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
tx = web3.Transaction.from(raw);
|
|
67
|
+
}
|
|
68
|
+
const connection = new web3.Connection(flag('rpc') ?? process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com', 'confirmed');
|
|
69
|
+
const d = await new VeraGuard({ vera, policy, connection }).evaluate(tx);
|
|
70
|
+
return json ? console.log(JSON.stringify(d, null, 2)) : printDecision(d);
|
|
71
|
+
}
|
|
72
|
+
console.log(`veralayer: payment guard for AI agents on Solana
|
|
73
|
+
|
|
74
|
+
veralayer check <address> score an address (signed, verified locally)
|
|
75
|
+
veralayer pay <address> <sol> decide on a planned SOL payment
|
|
76
|
+
veralayer inspect <base64 tx> decode a transaction and decide on it
|
|
77
|
+
|
|
78
|
+
--min-tier high|medium|low --daily <sol> --max-tx <sol> --json
|
|
79
|
+
--api <url> (or VERA_API_URL) --rpc <url> (or SOLANA_RPC_URL)`);
|
|
80
|
+
process.exitCode = cmd ? 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
main().then(() => { }, (e) => { console.error(`${c.r}error${c.x} ${e.message}`); process.exitCode = 1; });
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type Tier = 'high' | 'medium' | 'low' | 'unknown';
|
|
2
|
+
export type Attestation = {
|
|
3
|
+
v: 1;
|
|
4
|
+
issuer: string;
|
|
5
|
+
subject: string;
|
|
6
|
+
score: number | null;
|
|
7
|
+
tier: Tier;
|
|
8
|
+
reasons: {
|
|
9
|
+
kind: 'ok' | 'warn' | 'bad' | 'info';
|
|
10
|
+
text: string;
|
|
11
|
+
}[];
|
|
12
|
+
features_sha256: string;
|
|
13
|
+
method: string;
|
|
14
|
+
issued_at: string;
|
|
15
|
+
expires_at: string;
|
|
16
|
+
};
|
|
17
|
+
export type CheckResult = {
|
|
18
|
+
id: string;
|
|
19
|
+
attestation: Attestation;
|
|
20
|
+
signature: string;
|
|
21
|
+
summary: string | null;
|
|
22
|
+
anchor?: {
|
|
23
|
+
cluster: string;
|
|
24
|
+
tx: string;
|
|
25
|
+
} | null;
|
|
26
|
+
features?: Record<string, unknown>;
|
|
27
|
+
};
|
|
28
|
+
export type VerifyResult = {
|
|
29
|
+
valid: boolean;
|
|
30
|
+
expired: boolean;
|
|
31
|
+
reason?: string;
|
|
32
|
+
};
|
|
33
|
+
export declare class VeraError extends Error {
|
|
34
|
+
status?: number | undefined;
|
|
35
|
+
constructor(message: string, status?: number | undefined);
|
|
36
|
+
}
|
|
37
|
+
export declare function canonical(v: unknown): string;
|
|
38
|
+
/** Verify an attestation offline. Pass the trusted Vera issuer key (from /.well-known/vera.json) to pin it. */
|
|
39
|
+
export declare function verifyAttestation(attestation: Attestation, signature: string, trustedIssuer?: string): VerifyResult;
|
|
40
|
+
export declare class Vera {
|
|
41
|
+
private apiUrl;
|
|
42
|
+
private apiKey?;
|
|
43
|
+
private issuer?;
|
|
44
|
+
constructor(opts?: {
|
|
45
|
+
apiUrl?: string;
|
|
46
|
+
apiKey?: string;
|
|
47
|
+
issuer?: string;
|
|
48
|
+
});
|
|
49
|
+
private req;
|
|
50
|
+
/** Score a counterparty. Returns score, tier, reasons and a signed attestation. */
|
|
51
|
+
check(address: string, opts?: {
|
|
52
|
+
fresh?: boolean;
|
|
53
|
+
}): Promise<CheckResult>;
|
|
54
|
+
/** The Vera issuer public key, fetched once from /.well-known/vera.json unless pinned in the constructor. */
|
|
55
|
+
getIssuer(): Promise<string>;
|
|
56
|
+
/** Verify a check result locally against the trusted issuer key. */
|
|
57
|
+
verify(result: Pick<CheckResult, 'attestation' | 'signature'>): Promise<VerifyResult>;
|
|
58
|
+
/** Convenience gate: throws unless the counterparty meets the minimum tier and the attestation verifies. */
|
|
59
|
+
require(address: string, minTier?: Exclude<Tier, 'unknown'>): Promise<CheckResult>;
|
|
60
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Vera API client: score an address and verify the signed result locally against Vera's published key.
|
|
2
|
+
//
|
|
3
|
+
// import { Vera } from 'veralayer';
|
|
4
|
+
// const vera = new Vera() // defaults to the hosted API; pass { apiKey } only if you were issued one;
|
|
5
|
+
// const check = await vera.check(counterparty);
|
|
6
|
+
// if (!(await vera.verify(check)).valid) throw new Error('bad attestation');
|
|
7
|
+
// if (check.attestation.tier === 'low') throw new Error(check.attestation.reasons[0].text);
|
|
8
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
9
|
+
import bs58 from 'bs58';
|
|
10
|
+
export class VeraError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(message, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// key-sorted JSON: must match the engine byte for byte
|
|
18
|
+
export function canonical(v) {
|
|
19
|
+
if (Array.isArray(v))
|
|
20
|
+
return `[${v.map(canonical).join(',')}]`;
|
|
21
|
+
if (v && typeof v === 'object')
|
|
22
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`;
|
|
23
|
+
return JSON.stringify(v);
|
|
24
|
+
}
|
|
25
|
+
/** Verify an attestation offline. Pass the trusted Vera issuer key (from /.well-known/vera.json) to pin it. */
|
|
26
|
+
export function verifyAttestation(attestation, signature, trustedIssuer) {
|
|
27
|
+
const expired = Date.parse(attestation.expires_at) < Date.now();
|
|
28
|
+
if (trustedIssuer && attestation.issuer !== trustedIssuer)
|
|
29
|
+
return { valid: false, expired, reason: 'issuer is not the trusted Vera key' };
|
|
30
|
+
try {
|
|
31
|
+
const ok = ed25519.verify(bs58.decode(signature), new TextEncoder().encode(canonical(attestation)), bs58.decode(attestation.issuer));
|
|
32
|
+
return ok ? { valid: true, expired } : { valid: false, expired, reason: 'signature does not match' };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { valid: false, expired, reason: 'malformed signature or issuer' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class Vera {
|
|
39
|
+
apiUrl;
|
|
40
|
+
apiKey;
|
|
41
|
+
issuer;
|
|
42
|
+
constructor(opts = {}) {
|
|
43
|
+
this.apiUrl = (opts.apiUrl ?? 'https://vera-agent-psi.vercel.app').replace(/\/$/, '');
|
|
44
|
+
this.apiKey = opts.apiKey;
|
|
45
|
+
this.issuer = opts.issuer;
|
|
46
|
+
}
|
|
47
|
+
async req(path, init) {
|
|
48
|
+
const r = await fetch(this.apiUrl + path, { ...init, headers: { 'content-type': 'application/json', ...(this.apiKey ? { 'x-api-key': this.apiKey } : {}), ...init?.headers } });
|
|
49
|
+
const j = (await r.json().catch(() => ({})));
|
|
50
|
+
if (!r.ok)
|
|
51
|
+
throw new VeraError(j.error || `Vera API error ${r.status}`, r.status);
|
|
52
|
+
return j;
|
|
53
|
+
}
|
|
54
|
+
/** Score a counterparty. Returns score, tier, reasons and a signed attestation. */
|
|
55
|
+
check(address, opts = {}) {
|
|
56
|
+
return this.req('/v1/check', { method: 'POST', body: JSON.stringify({ address, fresh: opts.fresh }) });
|
|
57
|
+
}
|
|
58
|
+
/** The Vera issuer public key, fetched once from /.well-known/vera.json unless pinned in the constructor. */
|
|
59
|
+
async getIssuer() {
|
|
60
|
+
this.issuer ??= (await this.req('/.well-known/vera.json')).issuer;
|
|
61
|
+
return this.issuer;
|
|
62
|
+
}
|
|
63
|
+
/** Verify a check result locally against the trusted issuer key. */
|
|
64
|
+
async verify(result) {
|
|
65
|
+
return verifyAttestation(result.attestation, result.signature, await this.getIssuer());
|
|
66
|
+
}
|
|
67
|
+
/** Convenience gate: throws unless the counterparty meets the minimum tier and the attestation verifies. */
|
|
68
|
+
async require(address, minTier = 'medium') {
|
|
69
|
+
const r = await this.check(address);
|
|
70
|
+
const v = await this.verify(r);
|
|
71
|
+
if (!v.valid)
|
|
72
|
+
throw new VeraError(`attestation invalid: ${v.reason}`);
|
|
73
|
+
const rank = { unknown: 0, low: 1, medium: 2, high: 3 };
|
|
74
|
+
if (rank[r.attestation.tier] < rank[minTier])
|
|
75
|
+
throw new VeraError(`counterparty tier ${r.attestation.tier} is below ${minTier}: ${r.attestation.reasons[0]?.text ?? ''}`);
|
|
76
|
+
return r;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Connection, Signer, Transaction, VersionedTransaction, SendOptions } from '@solana/web3.js';
|
|
2
|
+
import { Vera } from './client.js';
|
|
3
|
+
import { type Decision, type Policy, type RecipientCheck } from './policy.js';
|
|
4
|
+
/** Where the guard keeps today's spend. Swap in Redis/DB for multi-process agents. */
|
|
5
|
+
export interface SpendLedger {
|
|
6
|
+
spentToday(): Promise<number> | number;
|
|
7
|
+
add(sol: number): Promise<void> | void;
|
|
8
|
+
}
|
|
9
|
+
export declare class MemoryLedger implements SpendLedger {
|
|
10
|
+
private day;
|
|
11
|
+
private spent;
|
|
12
|
+
private roll;
|
|
13
|
+
spentToday(): number;
|
|
14
|
+
add(sol: number): void;
|
|
15
|
+
}
|
|
16
|
+
export declare class VeraBlockedError extends Error {
|
|
17
|
+
decision: Decision;
|
|
18
|
+
constructor(decision: Decision);
|
|
19
|
+
}
|
|
20
|
+
export declare class VeraReviewRequiredError extends Error {
|
|
21
|
+
decision: Decision;
|
|
22
|
+
constructor(decision: Decision);
|
|
23
|
+
}
|
|
24
|
+
export type GuardOptions = {
|
|
25
|
+
connection?: Connection;
|
|
26
|
+
policy?: Policy;
|
|
27
|
+
vera?: Vera | {
|
|
28
|
+
apiUrl?: string;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
issuer?: string;
|
|
31
|
+
};
|
|
32
|
+
ledger?: SpendLedger;
|
|
33
|
+
/** Called for 'review' decisions. Return true to send anyway (e.g. after a human approves in Slack/Telegram). Without it, review = block. */
|
|
34
|
+
onReview?: (decision: Decision) => Promise<boolean> | boolean;
|
|
35
|
+
/** Called after every decision, for logs and audit trails. */
|
|
36
|
+
onDecision?: (decision: Decision) => void;
|
|
37
|
+
};
|
|
38
|
+
export declare class VeraGuard {
|
|
39
|
+
readonly vera: Vera;
|
|
40
|
+
readonly policy: Policy;
|
|
41
|
+
private connection?;
|
|
42
|
+
private ledger;
|
|
43
|
+
private onReview?;
|
|
44
|
+
private onDecision?;
|
|
45
|
+
constructor(opts?: GuardOptions);
|
|
46
|
+
/** Score recipients (deduplicated, in parallel) and verify each signed result against Vera's published key. */
|
|
47
|
+
checkRecipients(addresses: string[]): Promise<RecipientCheck[]>;
|
|
48
|
+
/** Decide on a planned payment without building a transaction (e.g. before quoting a price to another agent). */
|
|
49
|
+
evaluatePayment(payment: {
|
|
50
|
+
to: string;
|
|
51
|
+
amountSol?: number;
|
|
52
|
+
token?: {
|
|
53
|
+
mint: string;
|
|
54
|
+
amount: number;
|
|
55
|
+
};
|
|
56
|
+
}): Promise<Decision>;
|
|
57
|
+
/** Decode a transaction and decide on it. Does not send anything. */
|
|
58
|
+
evaluate(tx: Transaction | VersionedTransaction): Promise<Decision>;
|
|
59
|
+
private decide;
|
|
60
|
+
/** Throws VeraBlockedError / VeraReviewRequiredError unless the transaction may be sent. */
|
|
61
|
+
assert(tx: Transaction | VersionedTransaction): Promise<Decision>;
|
|
62
|
+
/** Evaluate, then sign and send only if allowed. Records the SOL spent in the ledger. */
|
|
63
|
+
sendTransaction(tx: Transaction | VersionedTransaction, signers?: Signer[], options?: SendOptions): Promise<{
|
|
64
|
+
signature: string;
|
|
65
|
+
decision: Decision;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
package/dist/guard.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Vera } from './client.js';
|
|
2
|
+
import { inspectTransaction } from './inspect.js';
|
|
3
|
+
import { evaluatePolicy } 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
|
+
ledger;
|
|
33
|
+
onReview;
|
|
34
|
+
onDecision;
|
|
35
|
+
constructor(opts = {}) {
|
|
36
|
+
this.vera = opts.vera instanceof Vera ? opts.vera : new Vera(opts.vera);
|
|
37
|
+
this.policy = opts.policy ?? {};
|
|
38
|
+
this.connection = opts.connection;
|
|
39
|
+
this.ledger = opts.ledger ?? new MemoryLedger();
|
|
40
|
+
this.onReview = opts.onReview;
|
|
41
|
+
this.onDecision = opts.onDecision;
|
|
42
|
+
}
|
|
43
|
+
/** Score recipients (deduplicated, in parallel) and verify each signed result against Vera's published key. */
|
|
44
|
+
async checkRecipients(addresses) {
|
|
45
|
+
const skip = new Set([...(this.policy.allow ?? []), ...(this.policy.deny ?? [])]);
|
|
46
|
+
const uniq = [...new Set(addresses)].filter((a) => !skip.has(a));
|
|
47
|
+
return Promise.all(uniq.map(async (address) => {
|
|
48
|
+
try {
|
|
49
|
+
const r = await this.vera.check(address);
|
|
50
|
+
const v = await this.vera.verify(r);
|
|
51
|
+
const a = r.attestation;
|
|
52
|
+
return { address, ok: true, score: a.score, tier: a.tier, reasons: a.reasons, attestationId: r.id, verified: v.valid, expired: v.expired };
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
return { address, ok: false, error: e.message };
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
/** Decide on a planned payment without building a transaction (e.g. before quoting a price to another agent). */
|
|
60
|
+
async evaluatePayment(payment) {
|
|
61
|
+
const outflows = payment.token
|
|
62
|
+
? [{ kind: 'token', to: payment.to, toAccount: payment.to, amount: payment.token.amount, mint: payment.token.mint, instruction: 'planned' }]
|
|
63
|
+
: [{ kind: 'sol', to: payment.to, toAccount: payment.to, amount: payment.amountSol ?? 0, instruction: 'planned' }];
|
|
64
|
+
return this.decide(outflows, []);
|
|
65
|
+
}
|
|
66
|
+
/** Decode a transaction and decide on it. Does not send anything. */
|
|
67
|
+
async evaluate(tx) {
|
|
68
|
+
const ins = await inspectTransaction(tx, this.connection);
|
|
69
|
+
return this.decide(ins.outflows, ins.unknownPrograms);
|
|
70
|
+
}
|
|
71
|
+
async decide(outflows, unknownPrograms) {
|
|
72
|
+
const recipients = await this.checkRecipients(outflows.map((o) => o.to));
|
|
73
|
+
const decision = evaluatePolicy({ outflows, recipients, unknownPrograms, spentTodaySol: await this.ledger.spentToday(), policy: this.policy });
|
|
74
|
+
this.onDecision?.(decision);
|
|
75
|
+
return decision;
|
|
76
|
+
}
|
|
77
|
+
/** Throws VeraBlockedError / VeraReviewRequiredError unless the transaction may be sent. */
|
|
78
|
+
async assert(tx) {
|
|
79
|
+
const d = await this.evaluate(tx);
|
|
80
|
+
if (d.action === 'block')
|
|
81
|
+
throw new VeraBlockedError(d);
|
|
82
|
+
if (d.action === 'review' && !(this.onReview && (await this.onReview(d))))
|
|
83
|
+
throw new VeraReviewRequiredError(d);
|
|
84
|
+
return d;
|
|
85
|
+
}
|
|
86
|
+
/** Evaluate, then sign and send only if allowed. Records the SOL spent in the ledger. */
|
|
87
|
+
async sendTransaction(tx, signers = [], options) {
|
|
88
|
+
if (!this.connection)
|
|
89
|
+
throw new Error('VeraGuard needs a Connection to send transactions');
|
|
90
|
+
const decision = await this.assert(tx);
|
|
91
|
+
let signature;
|
|
92
|
+
if ('version' in tx) {
|
|
93
|
+
if (signers.length)
|
|
94
|
+
tx.sign(signers);
|
|
95
|
+
signature = await this.connection.sendTransaction(tx, options);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
signature = await this.connection.sendTransaction(tx, signers, options);
|
|
99
|
+
}
|
|
100
|
+
await this.ledger.add(decision.totalSol);
|
|
101
|
+
return { signature, decision };
|
|
102
|
+
}
|
|
103
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Vera, VeraError, 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, DEFAULT_POLICY, type Policy, 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, verifyAttestation, canonical } from './client.js';
|
|
3
|
+
export { VeraGuard, MemoryLedger, VeraBlockedError, VeraReviewRequiredError } from './guard.js';
|
|
4
|
+
export { inspectTransaction, instructionsOf } from './inspect.js';
|
|
5
|
+
export { evaluatePolicy, DEFAULT_POLICY } from './policy.js';
|
|
6
|
+
export { KNOWN_PROGRAMS } from './programs.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
2
|
+
import type { Outflow } from './policy.js';
|
|
3
|
+
export type Inspection = {
|
|
4
|
+
payer: string;
|
|
5
|
+
outflows: Outflow[];
|
|
6
|
+
programs: string[];
|
|
7
|
+
unknownPrograms: string[];
|
|
8
|
+
/** instructions we decoded vs. ones we could not attribute value to */
|
|
9
|
+
notes: string[];
|
|
10
|
+
};
|
|
11
|
+
type Ix = {
|
|
12
|
+
programId: string;
|
|
13
|
+
accounts: string[];
|
|
14
|
+
data: Uint8Array;
|
|
15
|
+
};
|
|
16
|
+
/** Flatten a legacy or versioned transaction into plain instructions, resolving address lookup tables. */
|
|
17
|
+
export declare function instructionsOf(tx: Transaction | VersionedTransaction, connection?: Connection): Promise<{
|
|
18
|
+
payer: string;
|
|
19
|
+
ixs: Ix[];
|
|
20
|
+
}>;
|
|
21
|
+
/** Decode outflows from the paying wallet. Token destinations are resolved to their owner via RPC when needed. */
|
|
22
|
+
export declare function inspectTransaction(tx: Transaction | VersionedTransaction, connection?: Connection): Promise<Inspection>;
|
|
23
|
+
export {};
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { KNOWN_PROGRAMS } from './programs.js';
|
|
2
|
+
const SYSTEM = '11111111111111111111111111111111';
|
|
3
|
+
const TOKEN = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
|
|
4
|
+
const TOKEN22 = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
|
|
5
|
+
const ATA = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
|
|
6
|
+
const SKIP_PROGRAMS = new Set([SYSTEM, TOKEN, TOKEN22, ATA, 'ComputeBudget111111111111111111111111111111']);
|
|
7
|
+
const u64 = (d, o) => Number(new DataView(d.buffer, d.byteOffset + o, 8).getBigUint64(0, true));
|
|
8
|
+
const u32 = (d, o) => new DataView(d.buffer, d.byteOffset + o, 4).getUint32(0, true);
|
|
9
|
+
function isVersioned(tx) {
|
|
10
|
+
return 'version' in tx;
|
|
11
|
+
}
|
|
12
|
+
/** Flatten a legacy or versioned transaction into plain instructions, resolving address lookup tables. */
|
|
13
|
+
export async function instructionsOf(tx, connection) {
|
|
14
|
+
if (!isVersioned(tx)) {
|
|
15
|
+
const payer = (tx.feePayer ?? tx.instructions[0]?.keys.find((k) => k.isSigner)?.pubkey)?.toBase58() ?? '';
|
|
16
|
+
return { payer, ixs: tx.instructions.map((ix) => ({ programId: ix.programId.toBase58(), accounts: ix.keys.map((k) => k.pubkey.toBase58()), data: Uint8Array.from(ix.data) })) };
|
|
17
|
+
}
|
|
18
|
+
const msg = tx.message;
|
|
19
|
+
const luts = [];
|
|
20
|
+
for (const l of msg.addressTableLookups ?? []) {
|
|
21
|
+
if (!connection)
|
|
22
|
+
throw new Error('a Connection is needed to resolve address lookup tables');
|
|
23
|
+
const res = await connection.getAddressLookupTable(l.accountKey);
|
|
24
|
+
if (!res.value)
|
|
25
|
+
throw new Error(`lookup table ${l.accountKey.toBase58()} not found`);
|
|
26
|
+
luts.push(res.value);
|
|
27
|
+
}
|
|
28
|
+
const keys = msg.getAccountKeys({ addressLookupTableAccounts: luts });
|
|
29
|
+
const k = (i) => keys.get(i).toBase58();
|
|
30
|
+
return {
|
|
31
|
+
payer: k(0),
|
|
32
|
+
ixs: msg.compiledInstructions.map((ix) => ({ programId: k(ix.programIdIndex), accounts: ix.accountKeyIndexes.map(k), data: ix.data })),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Decode outflows from the paying wallet. Token destinations are resolved to their owner via RPC when needed. */
|
|
36
|
+
export async function inspectTransaction(tx, connection) {
|
|
37
|
+
const { payer, ixs } = await instructionsOf(tx, connection);
|
|
38
|
+
const outflows = [];
|
|
39
|
+
const notes = [];
|
|
40
|
+
const programs = new Set();
|
|
41
|
+
// token accounts created in this transaction: ata -> { wallet, mint }
|
|
42
|
+
const created = new Map();
|
|
43
|
+
for (const ix of ixs)
|
|
44
|
+
if (ix.programId === ATA && ix.accounts.length >= 4)
|
|
45
|
+
created.set(ix.accounts[1], { owner: ix.accounts[2], mint: ix.accounts[3] });
|
|
46
|
+
const pendingTokens = [];
|
|
47
|
+
for (const ix of ixs) {
|
|
48
|
+
programs.add(ix.programId);
|
|
49
|
+
if (ix.programId === SYSTEM && ix.data.length >= 12) {
|
|
50
|
+
const op = u32(ix.data, 0);
|
|
51
|
+
if (op === 2)
|
|
52
|
+
outflows.push({ kind: 'sol', to: ix.accounts[1], toAccount: ix.accounts[1], amount: u64(ix.data, 4) / 1e9, instruction: 'system.transfer' });
|
|
53
|
+
else if (op === 11)
|
|
54
|
+
outflows.push({ kind: 'sol', to: ix.accounts[2], toAccount: ix.accounts[2], amount: u64(ix.data, 4) / 1e9, instruction: 'system.transferWithSeed' });
|
|
55
|
+
else if (op === 0 && ix.accounts[1])
|
|
56
|
+
outflows.push({ kind: 'sol', to: ix.accounts[1], toAccount: ix.accounts[1], amount: u64(ix.data, 4) / 1e9, instruction: 'system.createAccount' });
|
|
57
|
+
}
|
|
58
|
+
else if ((ix.programId === TOKEN || ix.programId === TOKEN22) && ix.data.length >= 9) {
|
|
59
|
+
const op = ix.data[0];
|
|
60
|
+
if (op === 3)
|
|
61
|
+
pendingTokens.push({ account: ix.accounts[1], raw: u64(ix.data, 1), instruction: 'token.transfer' });
|
|
62
|
+
else if (op === 12 && ix.data.length >= 10)
|
|
63
|
+
pendingTokens.push({ account: ix.accounts[2], raw: u64(ix.data, 1), decimals: ix.data[9], mint: ix.accounts[1], instruction: 'token.transferChecked' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// resolve token destinations to owners (and mint/decimals for plain `transfer`)
|
|
67
|
+
for (const t of pendingTokens) {
|
|
68
|
+
let owner, mint = t.mint, decimals = t.decimals;
|
|
69
|
+
const c = created.get(t.account);
|
|
70
|
+
if (c) {
|
|
71
|
+
owner = c.owner;
|
|
72
|
+
mint ??= c.mint;
|
|
73
|
+
}
|
|
74
|
+
if ((!owner || decimals === undefined) && connection) {
|
|
75
|
+
const info = await connection.getParsedAccountInfo(new (await import('@solana/web3.js')).PublicKey(t.account));
|
|
76
|
+
const parsed = info.value?.data?.parsed?.info;
|
|
77
|
+
owner ??= parsed?.owner;
|
|
78
|
+
mint ??= parsed?.mint;
|
|
79
|
+
decimals ??= parsed?.tokenAmount?.decimals;
|
|
80
|
+
if (decimals === undefined && mint) {
|
|
81
|
+
const m = await connection.getParsedAccountInfo(new (await import('@solana/web3.js')).PublicKey(mint));
|
|
82
|
+
decimals = m.value?.data?.parsed?.info?.decimals;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!owner)
|
|
86
|
+
notes.push(`could not resolve the owner of token account ${t.account}; scored as-is`);
|
|
87
|
+
outflows.push({ kind: 'token', to: owner ?? t.account, toAccount: t.account, amount: decimals === undefined ? t.raw : t.raw / 10 ** decimals, mint, instruction: t.instruction });
|
|
88
|
+
}
|
|
89
|
+
const all = [...programs];
|
|
90
|
+
const unknownPrograms = all.filter((p) => !SKIP_PROGRAMS.has(p) && !KNOWN_PROGRAMS[p]);
|
|
91
|
+
// value moved inside other programs (swaps, CPIs) is not decoded here; say so instead of pretending
|
|
92
|
+
for (const p of all)
|
|
93
|
+
if (!SKIP_PROGRAMS.has(p))
|
|
94
|
+
notes.push(`calls ${KNOWN_PROGRAMS[p] ?? p}: value moved inside this program is not decoded`);
|
|
95
|
+
return { payer, outflows: outflows.filter((o) => o.to !== payer && o.amount > 0), programs: all, unknownPrograms, notes };
|
|
96
|
+
}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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: 0. */
|
|
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
|
+
/** What to do with recipients Vera cannot score (no history). Default 'review'. */
|
|
19
|
+
onUnknown?: Exclude<Action, 'allow'> | 'allow';
|
|
20
|
+
/** What to do when the Vera check itself fails or its signature does not verify. Default 'block'. */
|
|
21
|
+
onCheckError?: Exclude<Action, 'allow'>;
|
|
22
|
+
/** Transactions that call programs outside the well-known list. Default 'review'. */
|
|
23
|
+
unknownPrograms?: Action;
|
|
24
|
+
};
|
|
25
|
+
export declare const DEFAULT_POLICY: Required<Omit<Policy, 'maxSolPerTx' | 'dailyLimitSol' | 'tokenLimits'>> & Pick<Policy, 'maxSolPerTx' | 'dailyLimitSol' | 'tokenLimits'>;
|
|
26
|
+
/** One movement of value out of the paying wallet. */
|
|
27
|
+
export type Outflow = {
|
|
28
|
+
kind: 'sol' | 'token';
|
|
29
|
+
/** The wallet that ends up owning the funds (token accounts are resolved to their owner). */
|
|
30
|
+
to: string;
|
|
31
|
+
/** The account that is credited: same as `to` for SOL, the token account for SPL. */
|
|
32
|
+
toAccount: string;
|
|
33
|
+
/** SOL for kind 'sol'; UI units for tokens. */
|
|
34
|
+
amount: number;
|
|
35
|
+
mint?: string;
|
|
36
|
+
instruction: string;
|
|
37
|
+
};
|
|
38
|
+
/** What the guard learned about one recipient. */
|
|
39
|
+
export type RecipientCheck = {
|
|
40
|
+
address: string;
|
|
41
|
+
ok: true;
|
|
42
|
+
score: number | null;
|
|
43
|
+
tier: Tier;
|
|
44
|
+
reasons: {
|
|
45
|
+
kind: string;
|
|
46
|
+
text: string;
|
|
47
|
+
}[];
|
|
48
|
+
attestationId: string;
|
|
49
|
+
verified: boolean;
|
|
50
|
+
expired: boolean;
|
|
51
|
+
} | {
|
|
52
|
+
address: string;
|
|
53
|
+
ok: false;
|
|
54
|
+
error: string;
|
|
55
|
+
};
|
|
56
|
+
export type Finding = {
|
|
57
|
+
action: Action;
|
|
58
|
+
code: string;
|
|
59
|
+
message: string;
|
|
60
|
+
address?: string;
|
|
61
|
+
};
|
|
62
|
+
export type Decision = {
|
|
63
|
+
action: Action;
|
|
64
|
+
findings: Finding[];
|
|
65
|
+
outflows: Outflow[];
|
|
66
|
+
recipients: RecipientCheck[];
|
|
67
|
+
totalSol: number;
|
|
68
|
+
unknownPrograms: string[];
|
|
69
|
+
};
|
|
70
|
+
export declare function evaluatePolicy(input: {
|
|
71
|
+
outflows: Outflow[];
|
|
72
|
+
recipients: RecipientCheck[];
|
|
73
|
+
unknownPrograms?: string[];
|
|
74
|
+
spentTodaySol?: number;
|
|
75
|
+
policy?: Policy;
|
|
76
|
+
}): Decision;
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const DEFAULT_POLICY = {
|
|
2
|
+
minTier: 'medium',
|
|
3
|
+
tierLimitsSol: { high: Infinity, medium: 5, low: 0, unknown: 0 },
|
|
4
|
+
allow: [],
|
|
5
|
+
deny: [],
|
|
6
|
+
onUnknown: 'review',
|
|
7
|
+
onCheckError: 'block',
|
|
8
|
+
unknownPrograms: 'review',
|
|
9
|
+
};
|
|
10
|
+
const RANK = { unknown: 0, low: 1, medium: 2, high: 3 };
|
|
11
|
+
const worst = (a, b) => (a === 'block' || b === 'block' ? 'block' : a === 'review' || b === 'review' ? 'review' : 'allow');
|
|
12
|
+
const fmt = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(4).replace(/0+$/, ''));
|
|
13
|
+
export function evaluatePolicy(input) {
|
|
14
|
+
const p = { ...DEFAULT_POLICY, ...input.policy, tierLimitsSol: { ...DEFAULT_POLICY.tierLimitsSol, ...input.policy?.tierLimitsSol } };
|
|
15
|
+
const allow = new Set(p.allow), deny = new Set(p.deny);
|
|
16
|
+
const findings = [];
|
|
17
|
+
const add = (action, code, message, address) => findings.push({ action, code, message, ...(address ? { address } : {}) });
|
|
18
|
+
const totalSol = input.outflows.filter((o) => o.kind === 'sol').reduce((s, o) => s + o.amount, 0);
|
|
19
|
+
const solTo = new Map();
|
|
20
|
+
for (const o of input.outflows)
|
|
21
|
+
if (o.kind === 'sol')
|
|
22
|
+
solTo.set(o.to, (solTo.get(o.to) || 0) + o.amount);
|
|
23
|
+
const byAddr = new Map(input.recipients.map((r) => [r.address, r]));
|
|
24
|
+
for (const to of new Set(input.outflows.map((o) => o.to))) {
|
|
25
|
+
const short = `${to.slice(0, 4)}…${to.slice(-4)}`;
|
|
26
|
+
if (deny.has(to)) {
|
|
27
|
+
add('block', 'denylisted', `${short} is on your deny list`, to);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (allow.has(to))
|
|
31
|
+
continue;
|
|
32
|
+
const r = byAddr.get(to);
|
|
33
|
+
if (!r || !r.ok) {
|
|
34
|
+
add(p.onCheckError, 'check_failed', `Could not check ${short}${r && !r.ok ? `: ${r.error}` : ''}`, to);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!r.verified) {
|
|
38
|
+
add(p.onCheckError, 'bad_signature', `Vera's signature for ${short} did not verify`, to);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (r.expired) {
|
|
42
|
+
add('review', 'stale_check', `Vera's result for ${short} has expired`, to);
|
|
43
|
+
}
|
|
44
|
+
if (r.tier === 'unknown') {
|
|
45
|
+
add(p.onUnknown, 'unknown_recipient', `${short} has no history Vera can score`, to);
|
|
46
|
+
}
|
|
47
|
+
else if (RANK[r.tier] < RANK[p.minTier]) {
|
|
48
|
+
add('block', 'tier_below_minimum', `${short} is ${r.tier} trust (${r.score}/100), below your minimum of ${p.minTier}: ${r.reasons[0]?.text ?? ''}`.trim(), to);
|
|
49
|
+
}
|
|
50
|
+
const cap = p.tierLimitsSol[r.tier] ?? Infinity;
|
|
51
|
+
const sent = solTo.get(to) || 0;
|
|
52
|
+
if (sent > cap && !(r.tier === 'unknown' && p.onUnknown === 'allow')) {
|
|
53
|
+
add(cap === 0 ? 'block' : 'review', 'tier_limit', `${fmt(sent)} SOL to ${short} exceeds the ${fmt(cap)} SOL limit for ${r.tier} trust`, to);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const o of input.outflows) {
|
|
57
|
+
if (o.kind !== 'token' || !o.mint || !p.tokenLimits || !(o.mint in p.tokenLimits))
|
|
58
|
+
continue;
|
|
59
|
+
if (o.amount > p.tokenLimits[o.mint])
|
|
60
|
+
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);
|
|
61
|
+
}
|
|
62
|
+
if (p.maxSolPerTx !== undefined && totalSol > p.maxSolPerTx)
|
|
63
|
+
add('block', 'tx_limit', `${fmt(totalSol)} SOL in one transaction exceeds your ${fmt(p.maxSolPerTx)} SOL cap`);
|
|
64
|
+
if (p.dailyLimitSol !== undefined && (input.spentTodaySol ?? 0) + totalSol > p.dailyLimitSol)
|
|
65
|
+
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`);
|
|
66
|
+
if (input.unknownPrograms?.length && p.unknownPrograms !== 'allow')
|
|
67
|
+
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(', ')}`);
|
|
68
|
+
const action = findings.reduce((a, f) => worst(a, f.action), 'allow');
|
|
69
|
+
return { action, findings, outflows: input.outflows, recipients: input.recipients, totalSol, unknownPrograms: input.unknownPrograms ?? [] };
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const KNOWN_PROGRAMS: Record<string, string>;
|
package/dist/programs.js
ADDED
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "veralayer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Payment guard for AI agents on Solana: inspects every outgoing transaction, scores each recipient with Vera, and enforces your spending policy before anything is signed.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./policy": {
|
|
14
|
+
"types": "./dist/policy.d.ts",
|
|
15
|
+
"import": "./dist/policy.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"types": "./dist/client.d.ts",
|
|
19
|
+
"import": "./dist/client.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"veralayer": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"test": "node --env-file-if-exists=../../../../checker/.env test/run.mjs",
|
|
34
|
+
"prepublishOnly": "tsc && node test/smoke.mjs"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"solana",
|
|
38
|
+
"ai-agent",
|
|
39
|
+
"agent-payments",
|
|
40
|
+
"trust-score",
|
|
41
|
+
"risk",
|
|
42
|
+
"guard",
|
|
43
|
+
"mcp",
|
|
44
|
+
"solana-agent-kit"
|
|
45
|
+
],
|
|
46
|
+
"homepage": "https://vera-agent-psi.vercel.app",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@solana/web3.js": "^1.95.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@solana/web3.js": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@noble/curves": "^2.4.0",
|
|
61
|
+
"bs58": "^6.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@solana/web3.js": "^1.99.0",
|
|
65
|
+
"@types/node": "^26.6.6",
|
|
66
|
+
"typescript": "^5.9.3"
|
|
67
|
+
}
|
|
68
|
+
}
|