agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet
The agent does not hold the payment key: the wallet holds it for the owner and signs an EIP-3009 authorization only for a payment the agent wrote into its signed ledger, verified without trusting the agent under the policy the owner pinned and against the ledger heads the wallet itself saw (a branch is refused with a proof of equivocation, a backdated entry is refused), naming exactly this purchase, written now, and within the limit judged also against what the wallet itself has signed. The authorization nonce is sha256(entry hash), so the on-chain payment names the ledger entry. The policy gains an optional `wallet` field. Also: an x402 v2 client, a minimal resource server, a local facilitator for tests, and verifica-plati.mjs, which proves from outside that a wallet's on-chain payments were allowed by the agent's policy (with --all-transfers, that no payment left the wallet without a ledger entry). Tests: wallet 25/25 and payment verifier 10/10 without a network (the verifier on chain responses recorded on testnet 28001), negative control 21/21; policy 27/27, agents control 26/26. On the public testnet 28001 through its x402 facilitator: 9/9, with the evidence in agents/x402/dovezi-28001/. Needs ethers (npm install in agents/x402).
This commit is contained in:
parent
35d711fa55
commit
2293c1da86
10
README.md
10
README.md
@ -1,8 +1,8 @@
|
||||
# Aere Quantum
|
||||
|
||||
Self-hosted post-quantum infrastructure from Aere Network. Each component is a few files with **no dependencies**: Node.js 24
|
||||
and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry. The one exception is the notarization
|
||||
command of the verification layer, which needs `ethers`.
|
||||
and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry. The two exceptions need `ethers`: the
|
||||
notarization command of the verification layer, and the agents' x402 wallet (EIP-712 and secp256k1).
|
||||
|
||||
| component | what it does |
|
||||
|---|---|
|
||||
@ -14,7 +14,7 @@ command of the verification layer, which needs `ethers`.
|
||||
| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content |
|
||||
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
|
||||
| [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again |
|
||||
| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check |
|
||||
| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet with its evidence |
|
||||
|
||||
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
|
||||
|
||||
@ -34,7 +34,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
|
||||
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
|
||||
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
|
||||
| control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8 | remediation 7/7, compliance report 3/3 in this repository |
|
||||
| agents | policy 23/23 with the AIP-23 verifier (without it 21 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here) | 25/25 (`node control-negativ-aprobare.mjs`) |
|
||||
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25 and payment verifier 10/10 without a network; on the public testnet 28001, 9/9 (`x402/proba-x402-testnet.mjs`, needs a funded testnet key) | 26/26 (`node control-negativ-aprobare.mjs`); x402 21/21 (`node x402/control-negativ-wallet.mjs`) |
|
||||
|
||||
Code comments, most function and variable names (also many exported between the files of a component), test names and control
|
||||
messages are in Romanian, and so are the two command words of the KMS HSM tool (explained in its README). Error codes, error
|
||||
@ -43,4 +43,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ...
|
||||
|
||||
## Licence
|
||||
|
||||
MIT, see [LICENSE](LICENSE). Files: 108 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 17, agents 10, readiness 4).
|
||||
MIT, see [LICENSE](LICENSE). Files: 122 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 17, agents 24, readiness 4).
|
||||
|
||||
@ -25,7 +25,8 @@ node agent-cli.mjs verify-equivocation --proof proof.json
|
||||
```
|
||||
|
||||
A spec is `{ agentId, spend: { amount, windowSeconds, asset? }, tools: [...], recipients: [...], approval: { approvers: [humanId...],
|
||||
threshold, above?, tools? }, owner: humanId }`; every field but `agentId` is optional. An action is `{ kind: "payment", to, amount,
|
||||
threshold, above?, tools? }, owner: humanId, wallet: 0x<EVM address> }`; every field but `agentId` is optional. When `wallet` is
|
||||
named, a payment must say `from` and be from it. An action is `{ kind: "payment", to, amount,
|
||||
asset? }` or `{ kind: "tool", tool, args? }`; the ledger sets its time. Exit codes: 0 yes (allowed, valid, found), 1 no (invalid,
|
||||
not found, refused by a check), 3 an action refused by the policy, 2 wrong usage. Private keys stay in their files and are never
|
||||
printed; the tool creates them with mode 0600, which has no effect on Windows (protect the folder there).
|
||||
@ -51,6 +52,12 @@ printed; the tool creates them with mode 0600, which has no effect on Windows (p
|
||||
a validity window of at most seven days and a nonce that counts once per ledger. Only approvers named in the policy count, each
|
||||
once. A revocation is signed by the owner named in the policy; from its time on every action is refused.
|
||||
|
||||
## Paying over x402
|
||||
|
||||
`x402/` holds the agent wallet: a service that keeps the payment key for the owner and signs an x402 payment only when the agent's
|
||||
ledger records it and the policy allows it, with the wallet as the witness of the ledger's heads and time. It was run end to end
|
||||
on the public testnet 28001 on 2026-09-29, and the evidence is there with a verifier anyone can run. See `x402/README.md`.
|
||||
|
||||
## What the verifier can and cannot see
|
||||
|
||||
- **Time.** An entry's time is the statement of whoever holds the agent key. The verifier bounds it from above with its clock. It
|
||||
@ -72,11 +79,11 @@ Measured on 2026-09-29 (Node.js 24.14.1, Windows):
|
||||
|
||||
| test | result |
|
||||
|---|---|
|
||||
| `node proba-agent-policy.mjs` | 23/23 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere-node>`); without it 21 run, 2 are reported as skipped and the exit code is 2 |
|
||||
| `node proba-agent-policy.mjs` | 27/27 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere-node>`); without it 25 run, 2 are reported as skipped and the exit code is 2 |
|
||||
| `node proba-agent-ledger.mjs` | 51/51: limits over the ledger, identity, tampering, a re-signed false decision, a looser policy under the real hash, backdating (refused when written; caught with anchors or `notBefore`), a branch, equivocation proofs, resuming |
|
||||
| `node proba-agent-aprobare.mjs` | 39/39: threshold, unnamed or repeated approvers, classical keys, another action or other tool arguments, reused nonces (also after a restart), expiry, approvals across branches, revocation, an omitted revocation |
|
||||
| `node proba-agent-cli.mjs` | 23/23 on Windows, through files and processes only; on Linux and macOS one more test checks the 0600 mode of the key (not measured here) |
|
||||
| `node control-negativ-aprobare.mjs` | 25/25: each guard is removed in a copy, one at a time, and the named test must fail for that reason; a test that stops before its summary counts as a failure of the control |
|
||||
| `node control-negativ-aprobare.mjs` | 26/26: each guard is removed in a copy, one at a time, and the named test must fail for that reason; a test that stops before its summary counts as a failure of the control |
|
||||
|
||||
The data format is version 2 (2026-09-29). Code comments, test names and most internal names are in Romanian; messages, data and
|
||||
the exported interface are in English. No third party has reviewed this component.
|
||||
|
||||
@ -20,7 +20,7 @@ export function canonical(v) {
|
||||
return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}';
|
||||
}
|
||||
|
||||
const CHEI_POLITICA = new Set(['v', 'kind', 'agentId', 'spend', 'tools', 'recipients', 'approval', 'owner']);
|
||||
const CHEI_POLITICA = new Set(['v', 'kind', 'agentId', 'spend', 'tools', 'recipients', 'approval', 'owner', 'wallet']);
|
||||
const HUMAN_ID = /^aere-human:[0-9a-f]{40}$/;
|
||||
const INTREG = /^[0-9]+$/;
|
||||
|
||||
@ -57,6 +57,12 @@ function formaNormala(p) {
|
||||
if (!HUMAN_ID.test(String(p.owner))) throw new Error('agent-policy: owner must be an aere-human: id');
|
||||
policy.owner = String(p.owner);
|
||||
}
|
||||
// 2026-09-29 (punctul 23/25): portofelul din care agentul are voie sa plateasca (o adresa EVM). Cand e numit, o plata trebuie sa
|
||||
// spuna `from` si sa fie chiar el; x402/wallet.mjs refuza sa semneze pentru o politica ce numeste alt portofel.
|
||||
if (p.wallet != null) {
|
||||
if (!/^0x[0-9a-fA-F]{40}$/.test(String(p.wallet))) throw new Error('agent-policy: wallet must be an EVM address (0x + 40 hex)');
|
||||
policy.wallet = String(p.wallet).toLowerCase();
|
||||
}
|
||||
return policy;
|
||||
}
|
||||
const hashFormei = (policy) => sha256(Buffer.from(JSON.stringify(policy), 'utf8'));
|
||||
@ -72,6 +78,7 @@ const hashFormei = (policy) => sha256(Buffer.from(JSON.stringify(policy), 'utf8'
|
||||
* o plata cu suma STRICT peste `above`, sau o unealta din `tools`, cere `threshold` aprobari de la aprobatori distincti
|
||||
* din `approvers` (id-uri 'aere-human:' derivate din cheile lor ML-DSA-65; vezi agent-aprobare.mjs)
|
||||
* @param {string} [p.owner] proprietarul ('aere-human:'), singurul care poate revoca agentul
|
||||
* @param {string} [p.wallet] (2026-09-29) adresa EVM din care agentul are voie sa plateasca; o plata trebuie sa o numeasca in `from`
|
||||
*/
|
||||
export function definePolicy(p) {
|
||||
const policy = formaNormala(p);
|
||||
@ -133,6 +140,7 @@ function faraAprobare(policy, action, spentInWindow) {
|
||||
// si un moment finit; altfel refuz, cu motivul numit.
|
||||
if (!INTREG.test(String(action.amount)) || BigInt(action.amount) === 0n) return { allowed: false, reason: `invalid amount (${String(action.amount).slice(0, 24)}): a strictly positive decimal integer is required` };
|
||||
if (!Number.isFinite(Number(action.at))) return { allowed: false, reason: 'invalid time: action.at is not a finite number' };
|
||||
if (policy.wallet && String(action.from || '').toLowerCase() !== policy.wallet) return { allowed: false, reason: `payment from ${String(action.from || 'an unnamed wallet').slice(0, 42)} is not from the wallet the policy names` };
|
||||
if (policy.recipients && !policy.recipients.includes(String(action.to).toLowerCase())) return { allowed: false, reason: `recipient ${action.to} is not allowed` };
|
||||
if (policy.spend) {
|
||||
// 2026-09-29: o plata in alt activ decat al limitei nu se aduna la ea si nu trece pe langa ea: e refuzata
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
||||
@ -41,33 +41,48 @@ const PLANTARI = [
|
||||
['linia de comanda lasa pe oricine sa aprobe', 'agent-cli.mjs', 'if (!policy.approval || !policy.approval.approvers.includes(human.humanId)) throw', 'if (false) throw', P.cli],
|
||||
['verify din linia de comanda uita hash-ul numit in fisierul politicii', 'agent-cli.mjs', "const pinned = get('--policy-hash') ?? j.policyHash;", "const pinned = get('--policy-hash');", P.cli],
|
||||
['record deschide mereu un registru nou in loc sa il reia', 'agent-cli.mjs', 'const L = fs.existsSync(lf) ? resumeLedger(', 'const L = false ? resumeLedger(', P.cli],
|
||||
['o plata din alt portofel decat cel numit trece', 'agent-policy.mjs', 'if (policy.wallet && String(action.from', 'if (false && String(action.from', P.pol],
|
||||
['politica cu un camp necunoscut e primita', 'agent-policy.mjs', 'if (necunoscute.length) throw', 'if (false) throw', P.pol],
|
||||
['o plata in alt activ trece pe langa limita', 'agent-policy.mjs', 'if (action.asset != null && String(action.asset) !== policy.spend.asset) return', 'if (false) return', P.pol],
|
||||
['actionHash-ul plicului nu mai e canonic', 'agent-policy.mjs', "actionHash: sha256(Buffer.from(canonical(action), 'utf8')),", "actionHash: sha256(Buffer.from(JSON.stringify(action), 'utf8')),", P.pol],
|
||||
];
|
||||
const FISIERE = ['agent-policy.mjs', 'agent-ledger.mjs', 'agent-aprobare.mjs', 'agent-cli.mjs', P.pol, P.reg, P.apr, P.cli];
|
||||
function copie() { const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-b2-ctl-')); for (const f of FISIERE) fs.copyFileSync(path.join(AICI, f), path.join(t, f)); return t; }
|
||||
// 2026-09-29: probele ruleaza cate PARALEL deodata (asincron), ca tot controlul sa incapa in termenul rulatorului comun
|
||||
const PARALEL = 4;
|
||||
function ruleaza(t, proba) {
|
||||
const env = { ...process.env }; if (VERIFY) env.AERE_VERIFY_PROOF = VERIFY; else delete env.AERE_VERIFY_PROOF;
|
||||
const r = spawnSync(process.execPath, [path.join(t, proba)], { encoding: 'utf8', timeout: 240000, env });
|
||||
const out = (r.stdout || '') + (r.stderr || '');
|
||||
const m = /agent-(policy|ledger|aprobare|cli): (\d+)\/(\d+)/.exec(out);
|
||||
return { cod: r.status, rulat: !!m, rele: (out.match(/^\s*(RAU\s|\[RAU)/gm) || []).length };
|
||||
return new Promise((resolve) => {
|
||||
const c = spawn(process.execPath, [path.join(t, proba)], { env }); let out = '';
|
||||
const ceas = setTimeout(() => c.kill(), 240000);
|
||||
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; });
|
||||
c.on('close', (cod) => { clearTimeout(ceas); const m = /agent-(policy|ledger|aprobare|cli): (\d+)\/(\d+)/.exec(out);
|
||||
resolve({ cod, rulat: !!m, rele: (out.match(/^\s*(RAU\s|\[RAU)/gm) || []).length }); });
|
||||
});
|
||||
}
|
||||
async function inGrup(lucrari) { const rez = new Array(lucrari.length); let i = 0;
|
||||
await Promise.all(Array.from({ length: PARALEL }, async () => { while (i < lucrari.length) { const k = i++; rez[k] = await lucrari[k](); } })); return rez; }
|
||||
let esecuri = 0;
|
||||
for (const proba of Object.values(P)) {
|
||||
const t0 = copie(); const m0 = ruleaza(t0, proba); fs.rmSync(t0, { recursive: true, force: true });
|
||||
const martori = await inGrup(Object.values(P).map((proba) => async () => { const t0 = copie(); try { return [proba, await ruleaza(t0, proba)]; } finally { fs.rmSync(t0, { recursive: true, force: true }); } }));
|
||||
for (const [proba, m0] of martori) {
|
||||
const verde = m0.rulat && m0.rele === 0 && (m0.cod === 0 || (m0.cod === 2 && proba === P.pol && !VERIFY));
|
||||
if (verde) console.log(` OK martorul ${proba}: copia neatinsa verde${m0.cod === 2 ? ' (fara verificatorul AIP-23: partea lui NEMASURATA)' : ''}`);
|
||||
else { esecuri++; console.log(` RAU martorul ${proba} nu e verde (cod ${m0.cod}, ${m0.rulat ? m0.rele + ' RAU' : 'nu a ajuns la rezumat'})`); }
|
||||
}
|
||||
for (const [nume, f, din, inl, proba] of PLANTARI) {
|
||||
const t = copie(); const fp = path.join(t, f); const src = fs.readFileSync(fp, 'utf8');
|
||||
if (src.split(din).length !== 2) { esecuri++; console.log(` RAU ${nume}: tiparul nu apare exact o data in ${f}`); fs.rmSync(t, { recursive: true, force: true }); continue; }
|
||||
fs.writeFileSync(fp, src.replace(din, inl));
|
||||
const r = ruleaza(t, proba); fs.rmSync(t, { recursive: true, force: true });
|
||||
if (r.rulat && r.cod !== 0 && r.rele > 0) console.log(` OK ${nume}: ${proba} ROSIE (${r.rele} RAU)`);
|
||||
else { esecuri++; console.log(` RAU ${nume}: ${r.rulat ? `${proba} a ramas verde` : `${proba} nu a ajuns la rezumat (STRICAT)`} (cod ${r.cod})`); }
|
||||
}
|
||||
const linii = await inGrup(PLANTARI.map((pl) => async () => {
|
||||
const [nume, f, din, inl, proba] = pl; let t = null;
|
||||
try {
|
||||
// o plantare scrisa gresit (fisier, tipar) e un esec al controlului, nu o cadere a lui
|
||||
if (!FISIERE.includes(f) || typeof din !== 'string' || !proba) return [false, ` RAU ${nume}: plantarea e scrisa gresit (fisier ${f}, proba ${proba})`];
|
||||
t = copie(); const fp = path.join(t, f); const src = fs.readFileSync(fp, 'utf8');
|
||||
if (src.split(din).length !== 2) return [false, ` RAU ${nume}: tiparul nu apare exact o data in ${f}`];
|
||||
fs.writeFileSync(fp, src.replace(din, inl));
|
||||
const r = await ruleaza(t, proba);
|
||||
if (r.rulat && r.cod !== 0 && r.rele > 0) return [true, ` OK ${nume}: ${proba} ROSIE (${r.rele} RAU)`];
|
||||
return [false, ` RAU ${nume}: ${r.rulat ? `${proba} a ramas verde` : `${proba} nu a ajuns la rezumat (STRICAT)`} (cod ${r.cod})`];
|
||||
} catch (e) { return [false, ` RAU ${nume}: controlul a cazut pe ea (${String(e.message).slice(0, 80)})`]; }
|
||||
finally { if (t) fs.rmSync(t, { recursive: true, force: true }); }
|
||||
}));
|
||||
for (const [bun, l] of linii) { console.log(l); if (!bun) esecuri++; }
|
||||
console.log(esecuri ? `RAU: ${esecuri} esecuri ale controlului` : `DOVEDIT: martorii verzi, ${PLANTARI.length} din ${PLANTARI.length} plantari rosii`);
|
||||
process.exitCode = esecuri ? 1 : 0;
|
||||
|
||||
@ -38,6 +38,15 @@ try {
|
||||
const altActiv = checkAction(policy, { kind: 'payment', to: '0xbbbb', amount: '10', asset: 'USDC', at: 1 });
|
||||
cer(`CONTROL: plata in alt activ decat al limitei -> denied (${altActiv.reason})`, !altActiv.allowed && /not the policy's asset/.test(altActiv.reason));
|
||||
|
||||
// portofelul numit (2026-09-29): o plata trebuie sa spuna `from` si sa fie chiar el
|
||||
const cuPortofel = definePolicy({ agentId: 'agent-1', spend: { amount: '100', windowSeconds: 3600 }, wallet: '0xAbCd00000000000000000000000000000000Ef01' }).policy;
|
||||
cer('portofelul politicii e scris cu litere mici', cuPortofel.wallet === '0xabcd00000000000000000000000000000000ef01');
|
||||
cer('plata din portofelul numit (orice litere) -> allowed', checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', from: '0xABCD00000000000000000000000000000000EF01', at: 1 }).allowed === true);
|
||||
const fara = checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', at: 1 });
|
||||
const dinAlt = checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', from: '0x1111111111111111111111111111111111111111', at: 1 });
|
||||
cer(`CONTROL: plata fara from, sau din alt portofel, sub o politica ce numeste portofelul -> denied (${dinAlt.reason})`, !fara.allowed && !dinAlt.allowed && /not from the wallet/.test(dinAlt.reason));
|
||||
cer('CONTROL: un portofel care nu e adresa EVM -> politica refuzata', /wallet must be an EVM address/.test(arunca(() => definePolicy({ agentId: 'a', wallet: '0x12' })) || ''));
|
||||
|
||||
// validarea politicii
|
||||
cer('CONTROL: spend.amount care nu e intreg -> politica refuzata', /spend.amount/.test(arunca(() => definePolicy({ agentId: 'a', spend: { amount: '1e9', windowSeconds: 60 } })) || ''));
|
||||
cer('CONTROL: fereastra zero -> politica refuzata', /windowSeconds/.test(arunca(() => definePolicy({ agentId: 'a', spend: { amount: '1', windowSeconds: 0 } })) || ''));
|
||||
|
||||
76
agents/x402/README.md
Normal file
76
agents/x402/README.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Agent payments over x402: a wallet that pays only what the agent's policy allows
|
||||
|
||||
An AI agent that holds a payment key can spend without limit, whatever its policy says: the policy is only a promise. Here the
|
||||
agent does **not** hold the payment key. A small service, the **agent wallet**, holds it for the owner, and signs an x402 payment only
|
||||
when the agent has written that payment into its signed ledger (`../agent-ledger.mjs`) and the owner's policy allows it. The agent
|
||||
authenticates to the wallet with the ledger entry itself, which only its ML-DSA-65 key can sign.
|
||||
|
||||
Node.js 24 and `ethers` (for EIP-712 and secp256k1: `npm install` here). Payments follow x402 version 2 (`exact` scheme, EIP-3009
|
||||
`transferWithAuthorization`), with the HTTP headers `PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE` and `PAYMENT-RESPONSE`.
|
||||
|
||||
## The flow
|
||||
|
||||
1. The agent asks for a resource and gets `402` with the payment requirements.
|
||||
2. The agent writes the payment into its ledger: `{ kind: "payment", from: <wallet>, to: payTo, amount, asset: "eip155:<chain>/erc20:<token>",
|
||||
ref: <hash of the resource and the requirement> }`. If its policy refuses (over the limit, a recipient not allowed, approval
|
||||
missing, revoked), it stops here.
|
||||
3. The agent sends the requirement and its ledger to the wallet (`POST /authorize`). The wallet:
|
||||
- checks the requirement (its network and asset, a positive integer amount, a timeout it caps at 300 seconds);
|
||||
- verifies the whole ledger without trusting the agent, under the policy the owner pinned when starting the wallet: identity,
|
||||
every signature, the chain of hashes, every decision re-run, the revocations the owner gave the wallet, and the **heads the
|
||||
wallet itself saw before**, with its own clock. A ledger that does not continue what the wallet saw is a branch: it is refused
|
||||
and, when the wallet kept the entry, answered with a proof of equivocation anyone can check;
|
||||
- requires the last entry to be a new, allowed payment from this wallet, naming exactly this purchase, written now by the
|
||||
wallet's clock (within 120 seconds);
|
||||
- checks the limit a second time against what the **wallet itself has signed**, so a rewritten ledger, a branch, or a new ledger
|
||||
under a changed policy cannot take out more than the limit allows;
|
||||
- signs an EIP-3009 authorization whose nonce is `sha256(entry hash)`: the on-chain payment names the ledger entry, and one entry
|
||||
can pay once, at the wallet and on chain.
|
||||
4. The agent retries the resource with `PAYMENT-SIGNATURE`; the resource server settles through its facilitator and serves.
|
||||
|
||||
`verifica-plati.mjs` then proves from the outside that a wallet's on-chain payments were allowed by the agent's policy: the ledger
|
||||
verifies, each payment names an allowed entry, its transaction emits `AuthorizationUsed(wallet, sha256(entry hash))` and the
|
||||
`Transfer` of that amount, and with `--all-transfers` every transfer out of the wallet since a block is one of those payments.
|
||||
|
||||
## Use
|
||||
|
||||
```
|
||||
node wallet.mjs serve --config wallet.json [--port 8793] # POST /authorize, POST /revocations, GET /status on 127.0.0.1
|
||||
```
|
||||
|
||||
`wallet.json`: `{ policy, policyHash, network: "eip155:28001", token: { address, name, version }, evmKeyFile, stateFile }`. The
|
||||
policy must name the wallet (`wallet: <its address>`, see `definePolicy`) and a spending limit in its asset, or the wallet does not
|
||||
start. The EVM key is read from its file and never printed; the state file keeps the heads seen and what was signed, and is written
|
||||
before a signature is returned. The agent side is `payWithAgent({ url, ledger, wallet })` in `client.mjs`; `walletOverHttp(url)`
|
||||
talks to the service. `resource-server.mjs` is a minimal x402 seller for tests; `facilitator-local.mjs` makes the same checks as an
|
||||
x402 facilitator, in memory, for tests without a chain.
|
||||
|
||||
## What it does not do
|
||||
|
||||
- It is not trustless custody: whoever holds the wallet key (the owner) can pay without the agent. A 2-of-2 contract wallet
|
||||
(the agent and the policy service, ERC-1271, which the testnet token accepts) would remove that trust; it is not built.
|
||||
- An authorization that is signed but never settled still counts against the limit until the window passes (conservative).
|
||||
- The wallet API must be reached over a trusted channel: an entry intercepted on the way could be submitted by someone else, who
|
||||
would then receive the payment payload (the money still goes to the seller the entry names).
|
||||
- It does not check that the seller delivered what was paid for.
|
||||
- The wallet keeps every signed payment in its state; nothing prunes it yet.
|
||||
|
||||
## Tests
|
||||
|
||||
Measured on 2026-09-29 (Node.js 24.14.1, ethers 6.16.0):
|
||||
|
||||
| test | result |
|
||||
|---|---|
|
||||
| `node proba-wallet.mjs` | 25/25 without a network, real EIP-712 signatures and ML-DSA-65 keys: five paid purchases through a local facilitator, the sixth refused by the policy; attacks by the holder of the agent key (a forged "allowed" entry over the limit, an entry for another amount, the same entry twice, a branch with its equivocation proof, a new ledger under a changed policy, a backdated entry, two branches sent at the same time), the owner's revocation, human approval, a payment for another requirement and a replayed payment at the seller |
|
||||
| `node proba-verifica-plati.mjs` | 10/10 without a network, on the chain responses recorded for the testnet run below |
|
||||
| `node control-negativ-wallet.mjs` | 21/21: each guard of the wallet, the client, the seller and the verifier removed in a copy, and the named check must fail |
|
||||
| `AERE_TESTNET_KEY_FILE=<funded testnet key> node proba-x402-testnet.mjs` | 9/9 on the public testnet 28001 through its x402 facilitator: a fresh wallet funded with 0.05 tUSD, three purchases of 0.01 paid and settled on chain, the fourth refused by the policy, a replayed payment refused, the balances and the used authorization nonces read on chain, and the evidence file verified `VALID` (a changed amount, or a payment removed from the file: `INVALID`) |
|
||||
|
||||
The testnet run of 2026-09-29 is in `dovezi-28001/`: the evidence file and the recorded chain responses. Anyone can check it:
|
||||
|
||||
```
|
||||
node verifica-plati.mjs dovezi-28001/plati-agent-2026-09-29-20-23-16.json --rpc https://testnet-rpc.aere.network --all-transfers
|
||||
```
|
||||
|
||||
tUSD is the testnet's EIP-3009 token, minted by a faucet and worth nothing. Nothing here has been run on the Aere Network mainnet.
|
||||
Code comments and test names are in Romanian; messages, data and the documentation are in English. No third party has reviewed it.
|
||||
48
agents/x402/client.mjs
Normal file
48
agents/x402/client.mjs
Normal file
@ -0,0 +1,48 @@
|
||||
// Clientul x402 al unui agent AI (2026-09-29, punctele 23, 25): cumpara o resursa HTTP care cere plata (402), scriind INTAI plata in
|
||||
// registrul agentului, apoi cerand portofelului (wallet.mjs) semnatura, apoi reluand cererea cu antetul PAYMENT-SIGNATURE. Numele
|
||||
// antetelor si forma lor sunt ale specificatiei x402 v2 (transportul HTTP): PAYMENT-REQUIRED, PAYMENT-SIGNATURE, PAYMENT-RESPONSE, toate
|
||||
// JSON in base64. Daca politica refuza plata, clientul se opreste inainte de portofel (si portofelul ar refuza oricum).
|
||||
import { x402Action } from './wallet.mjs';
|
||||
|
||||
export const b64json = (o) => Buffer.from(JSON.stringify(o), 'utf8').toString('base64');
|
||||
export function dinB64json(s) { try { return JSON.parse(Buffer.from(String(s), 'base64').toString('utf8')); } catch { return null; } }
|
||||
|
||||
/** portofelul prin HTTP (serviciul wallet.mjs serve), cu aceeasi forma ca obiectul din createWallet */
|
||||
export function walletOverHttp(baseUrl, fetchImpl = fetch) {
|
||||
const b = String(baseUrl).replace(/\/+$/, '');
|
||||
return {
|
||||
async status() { const r = await fetchImpl(b + '/status'); return r.json(); },
|
||||
async authorize(body) { const r = await fetchImpl(b + '/authorize', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); return r.json(); },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} o
|
||||
* @param {string} o.url resursa
|
||||
* @param {object} o.ledger registrul agentului (openLedger / resumeLedger)
|
||||
* @param {object} o.wallet portofelul (createWallet sau walletOverHttp)
|
||||
* @param {Array} [o.approvals] aprobari umane pentru aceasta plata, cand politica le cere
|
||||
* @param {Function} [o.fetchImpl]
|
||||
* @returns {Promise<{paid:boolean, status:number, reason?:string, body?:string, settlement?:object, entry?:object, receipt?:object, decision?:object}>}
|
||||
*/
|
||||
export async function payWithAgent({ url, ledger, wallet, approvals = [], fetchImpl = fetch }) {
|
||||
const r0 = await fetchImpl(url);
|
||||
if (r0.status !== 402) return { paid: false, status: r0.status, body: await r0.text(), reason: r0.ok ? 'no payment was required' : `the resource answered ${r0.status}` };
|
||||
const pr = dinB64json(r0.headers.get('payment-required')) || (await r0.json().catch(() => null));
|
||||
if (!pr || pr.x402Version !== 2 || !Array.isArray(pr.accepts)) return { paid: false, status: 402, reason: 'the 402 response carries no x402 v2 PaymentRequired' };
|
||||
const w = await wallet.status();
|
||||
const req = pr.accepts.find((a) => a.scheme === 'exact' && a.network === w.network && String(a.asset).toLowerCase() === String(w.asset).toLowerCase());
|
||||
if (!req) return { paid: false, status: 402, reason: `no accepted payment is in this wallet's asset on ${w.network}` };
|
||||
// 1. plata, in registru, inainte de orice semnatura
|
||||
const r = ledger.record(x402Action(w.address, pr.resource || null, req), { approvals });
|
||||
if (!r.allowed) return { paid: false, status: 402, reason: `the agent's policy refused the payment: ${r.reason}`, entry: r.entry };
|
||||
// 2. semnatura portofelului, care judeca din nou tot registrul
|
||||
const a = await wallet.authorize({ requirements: req, resource: pr.resource || null, ledger: ledger.export() });
|
||||
if (!a.ok) return { paid: false, status: 402, reason: `the wallet refused: ${a.error}`, entry: r.entry, equivocation: a.equivocation };
|
||||
// 3. cererea reluata, cu plata
|
||||
const r1 = await fetchImpl(url, { headers: { 'PAYMENT-SIGNATURE': a.header } });
|
||||
const settlement = dinB64json(r1.headers.get('payment-response'));
|
||||
const body = await r1.text();
|
||||
return { paid: r1.ok && !!(settlement && settlement.success), status: r1.status, body, settlement, entry: r.entry, receipt: a.receipt, decision: a.decision,
|
||||
...(r1.ok ? {} : { reason: `the resource answered ${r1.status} after payment: ${body.slice(0, 160)}` }) };
|
||||
}
|
||||
88
agents/x402/control-negativ-wallet.mjs
Normal file
88
agents/x402/control-negativ-wallet.mjs
Normal file
@ -0,0 +1,88 @@
|
||||
// Controlul negativ al portofelului de plati x402 (proba-wallet.mjs) si al verificatorului de plati (proba-verifica-plati.mjs): fiecare paznic se strica intr-o COPIE (agent-policy/*.mjs si
|
||||
// x402/*.mjs, in aceeasi asezare), proba ruleaza pe copie si trebuie sa iasa rosie EXACT pe verificarea numita, cu proba chiar rulata.
|
||||
// Plantarile sunt conditii false la rulare sau randuri scoase, deci copia se incarca intotdeauna; o ancora care nu apare exact o data,
|
||||
// sau o proba care nu ajunge la rezumat, e un esec al controlului (STRICAT), nu o linie informativa.
|
||||
// node control-negativ-wallet.mjs iesire 0 = martorul verde si toate plantarile rosii pe proba lor
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SUS = path.resolve(AICI, '..');
|
||||
// ethers se rezolva de langa original (copia nu are node_modules); calea se da copiei prin AERE_ETHERS
|
||||
let ETHERS = process.env.AERE_ETHERS || null; // o copie a acestui dosar (controlul portii) primeste calea din mediu
|
||||
if (!ETHERS) try { ETHERS = createRequire(path.join(AICI, 'wallet.mjs')).resolve('ethers'); }
|
||||
catch { try { ETHERS = createRequire(path.join(AICI, 'wallet.mjs')).resolve(path.resolve(SUS, '..', '..', 'contracts', 'node_modules', 'ethers')); } catch { console.log('NEMASURAT: ethers nu se gaseste (npm install aici, sau AERE_ETHERS=<cale>)'); process.exit(2); } }
|
||||
const V = 'proba-verifica-plati.mjs';
|
||||
const PLANTARI = [
|
||||
// [nume, fisier (relativ la x402/), tipar, inlocuire, textul verificarii care trebuie sa iasa rosie]
|
||||
['cererile nu mai sunt judecate una cate una', 'wallet.mjs', 'function authorize(x) { const r = coada.then(() => autorizeaza(x)); coada = r.catch(() => {}); return r; }', 'function authorize(x) { return autorizeaza(x); }', '12. ATAC'],
|
||||
['registrul nu mai trebuie sa verifice', 'wallet.mjs', ' if (!v.ok) {', " if (!v.ok && process.env.AERE_PLANTA_NICIODATA === 'da') {", '3. ATAC'],
|
||||
['intrarea nu mai trebuie sa numeasca aceasta cumparatura', 'wallet.mjs', "for (const k of ['from', 'to', 'amount', 'asset', 'ref']) if (", 'for (const k of []) if (', '4. ATAC'],
|
||||
['aceeasi intrare poate plati de doua ori', 'wallet.mjs', 'if (S.last && e.seq <= S.last.seq) return refuz(', "if (S.last && e.seq <= S.last.seq && process.env.AERE_PLANTA_NICIODATA === 'da') return refuz(", '5. ATAC'],
|
||||
['intrarea nu mai trebuie sa fie de acum', 'wallet.mjs', 'if (!(Number(b.at) >= t - TOL && Number(b.at) <= t + 5)) return', "if (!(Number(b.at) >= t - TOL && Number(b.at) <= t + 5) && process.env.AERE_PLANTA_NICIODATA === 'da') return", '10. ATAC'],
|
||||
['limita nu mai e judecata fata de ce a semnat portofelul', 'wallet.mjs', 'stare.signed.map((s) => ({ amount: s.amount, at: s.at }))', '[]', '7. politica noua'],
|
||||
['aprobarile intrarii nu mai sunt date limitei portofelului', 'wallet.mjs', 'if (r.ok) aprobatori.push(r.humanId); }', '}', '9. 30000'],
|
||||
['dovada de echivocare nu mai e data', 'wallet.mjs', 'if (p.agentId === policy.agentId && verifyEquivocation(p).ok) r.equivocation = p;', '', '6. si portofelul da dovada'],
|
||||
['nonce-ul autorizarii nu mai numeste intrarea', 'wallet.mjs', 'nonce: nonceForEntry(e.hash) };', "nonce: '0x' + crypto.randomBytes(32).toString('hex') };", '1. nonce-ul fiecarei'],
|
||||
['revocarile primite nu mai ajung la verificarea registrului', 'wallet.mjs', 'revocations: stare.revocations, anchors', 'revocations: [], anchors', '8. dupa revocare'],
|
||||
['serverul de resurse primeste o plata pentru alta cerinta', 'resource-server.mjs', 'if (cheie(payload.accepted) !== cheie(requirement)) return cere(', "if (cheie(payload.accepted) !== cheie(requirement) && process.env.AERE_PLANTA_NICIODATA === 'da') return cere(", '11. CONTROL'],
|
||||
['clientul cere semnatura si cand politica a refuzat', 'client.mjs', "if (!r.allowed) return { paid: false, status: 402, reason: `the agent's policy refused the payment: ${r.reason}`, entry: r.entry };", '', '2. CONTROL'],
|
||||
['portofelul porneste si fara limita in activul lui', 'wallet.mjs', 'if (!policy.spend || policy.spend.asset !== assetId(network, token)) throw', "if ((!policy.spend || policy.spend.asset !== assetId(network, token)) && process.env.AERE_PLANTA_NICIODATA === 'da') throw", 'o politica fara limita'],
|
||||
// verificatorul platilor, pe raspunsurile inregistrate de pe 28001
|
||||
['verificatorul nu mai cere ca registrul sa verifice', 'verifica-plati.mjs', 'entries)`, v.ok, v.error', 'entries)`, true, v.error', '2. CONTROL: o suma schimbata', V],
|
||||
['verificatorul nu mai cere ca intrarea platii sa fie in registru', 'verifica-plati.mjs', '!!e && e.hash === p.entryHash)', '!!e)', '3. CONTROL', V],
|
||||
['verificatorul nu mai cere portofelul in intrare', 'verifica-plati.mjs', "String(a.from).toLowerCase() === wallet && ", '', '4. CONTROL', V],
|
||||
['verificatorul nu mai cere status 1', 'verifica-plati.mjs', "!!rc && rc.status === '0x1'", '!!rc', '5. CONTROL', V],
|
||||
['verificatorul nu mai cere nonce-ul intrarii pe lant', 'verifica-plati.mjs', '&& l.topics[2].toLowerCase() === nonce));', '));', '6. CONTROL', V],
|
||||
['verificatorul nu mai cere suma din Transfer', 'verifica-plati.mjs', '&& BigInt(l.data) === BigInt(a.amount)));', '));', '7. CONTROL', V],
|
||||
['verificatorul nu mai cere ca orice Transfer sa fie in registru', 'verifica-plati.mjs', 'straine.length === 0,', 'true,', '8. CONTROL', V],
|
||||
['verificatorul nu mai cere lantul dosarului', 'verifica-plati.mjs', 'lantul === chain,', 'true,', '9. CONTROL', V],
|
||||
];
|
||||
function copie() {
|
||||
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-wallet-ctl-'));
|
||||
for (const f of fs.readdirSync(SUS).filter((n) => n.endsWith('.mjs'))) fs.copyFileSync(path.join(SUS, f), path.join(t, f));
|
||||
fs.mkdirSync(path.join(t, 'x402'));
|
||||
for (const f of fs.readdirSync(AICI).filter((n) => n.endsWith('.mjs'))) fs.copyFileSync(path.join(AICI, f), path.join(t, 'x402', f));
|
||||
fs.cpSync(path.join(AICI, 'dovezi-28001'), path.join(t, 'x402', 'dovezi-28001'), { recursive: true });
|
||||
return t;
|
||||
}
|
||||
// probele ruleaza cate PARALEL deodata (asincron), ca tot controlul sa incapa in termenul rulatorului comun
|
||||
const PARALEL = 4;
|
||||
function ruleaza(t, proba = 'proba-wallet.mjs') {
|
||||
return new Promise((resolve) => {
|
||||
const c = spawn(process.execPath, [path.join(t, 'x402', proba)], { env: { ...process.env, AERE_ETHERS: ETHERS } }); let out = '';
|
||||
const ceas = setTimeout(() => c.kill(), 240000);
|
||||
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; });
|
||||
c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /(agent-wallet|verifica-plati): \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => /^\s+RAU\s/.test(l)) }); });
|
||||
});
|
||||
}
|
||||
async function inGrup(lucrari) { const rez = new Array(lucrari.length); let i = 0;
|
||||
await Promise.all(Array.from({ length: PARALEL }, async () => { while (i < lucrari.length) { const k = i++; rez[k] = await lucrari[k](); } })); return rez; }
|
||||
let esecuri = 0;
|
||||
const martori = await inGrup(['proba-wallet.mjs', V].map((proba) => async () => { const t0 = copie(); try { return [proba, await ruleaza(t0, proba)]; } finally { fs.rmSync(t0, { recursive: true, force: true }); } }));
|
||||
for (const [proba, m0] of martori) {
|
||||
if (m0.cod === 0 && m0.rulat && !m0.rosii.length) console.log(` OK martorul ${proba}: copia neatinsa verde`);
|
||||
else { esecuri++; console.log(` RAU martorul ${proba} nu e verde (cod ${m0.cod}, ${m0.rulat ? m0.rosii.length + ' RAU' : 'nu a ajuns la rezumat'})`); }
|
||||
}
|
||||
const linii = await inGrup(PLANTARI.map(([nume, f, din, inl, tinta, proba = 'proba-wallet.mjs']) => async () => {
|
||||
let t = null;
|
||||
try {
|
||||
t = copie(); const fp = path.join(t, 'x402', f);
|
||||
if (!fs.existsSync(fp)) return [false, ` RAU ${nume}: plantarea numeste un fisier care nu exista (${f})`];
|
||||
const src = fs.readFileSync(fp, 'utf8');
|
||||
if (src.split(din).length !== 2) return [false, ` RAU ${nume}: tiparul apare de ${src.split(din).length - 1} ori in ${f} (STRICAT)`];
|
||||
fs.writeFileSync(fp, src.replace(din, inl));
|
||||
const r = await ruleaza(t, proba);
|
||||
if (!r.rulat) return [false, ` RAU ${nume}: proba nu a ajuns la rezumat (STRICAT, cod ${r.cod})`];
|
||||
if (r.cod !== 0 && r.rosii.some((l) => l.includes(tinta))) return [true, ` OK ${nume}: '${tinta}' ROSIE (${r.rosii.length} RAU)`];
|
||||
return [false, ` RAU ${nume}: '${tinta}' a ramas verde (${r.rosii.length} RAU altundeva)`];
|
||||
} catch (e) { return [false, ` RAU ${nume}: controlul a cazut pe ea (${String(e.message).slice(0, 80)})`]; }
|
||||
finally { if (t) fs.rmSync(t, { recursive: true, force: true }); }
|
||||
}));
|
||||
for (const [bun, l] of linii) { console.log(l); if (!bun) esecuri++; }
|
||||
console.log(esecuri ? `RAU: ${esecuri} esecuri ale controlului` : `DOVEDIT: martorii verzi, ${PLANTARI.length} din ${PLANTARI.length} plantari rosii pe verificarea lor`);
|
||||
process.exitCode = esecuri ? 1 : 0;
|
||||
164
agents/x402/dovezi-28001/plati-agent-2026-09-29-20-23-16.json
Normal file
164
agents/x402/dovezi-28001/plati-agent-2026-09-29-20-23-16.json
Normal file
@ -0,0 +1,164 @@
|
||||
{
|
||||
"v": 1,
|
||||
"kind": "aere-agent-x402-payments",
|
||||
"network": "eip155:28001",
|
||||
"token": "0x8215bA247a3574af8EBC36606eB437811E318FBd",
|
||||
"wallet": "0xe61F2aC98f18465071AfD3F2E9A39eE1b2a564E9",
|
||||
"fromBlock": 4022094,
|
||||
"facilitator": "https://testnet-rpc.aere.network/x402",
|
||||
"policy": {
|
||||
"v": 1,
|
||||
"kind": "aere-agent-policy",
|
||||
"agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c",
|
||||
"spend": {
|
||||
"amount": "30000",
|
||||
"windowSeconds": 3600,
|
||||
"asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd"
|
||||
},
|
||||
"tools": null,
|
||||
"recipients": [
|
||||
"0x5b63415ab4b96d55987c39a00f22a12f2732edc3"
|
||||
],
|
||||
"wallet": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9"
|
||||
},
|
||||
"policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d",
|
||||
"ledger": {
|
||||
"version": "aere-agent-ledger/2 (2026-09-29)",
|
||||
"agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n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\npHDKIeTs\n-----END PUBLIC KEY-----\n",
|
||||
"policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d",
|
||||
"session": "c6ff3295bd5220f2f63b8d22f303ec71",
|
||||
"entries": [
|
||||
{
|
||||
"seq": 0,
|
||||
"prev": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"body": {
|
||||
"v": 2,
|
||||
"agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c",
|
||||
"policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d",
|
||||
"session": "c6ff3295bd5220f2f63b8d22f303ec71",
|
||||
"at": 1790713379,
|
||||
"action": {
|
||||
"kind": "payment",
|
||||
"from": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"to": "0x5b63415ab4b96d55987c39a00f22a12f2732edc3",
|
||||
"amount": "10000",
|
||||
"asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"ref": "0x5298562c9b9ecccf42306cc056d2f0b325efabf15789042fe1a4899fdd8de0ab",
|
||||
"at": 1790713379
|
||||
},
|
||||
"decision": {
|
||||
"allowed": true,
|
||||
"reason": "under the limit"
|
||||
},
|
||||
"provenance": null,
|
||||
"seq": 0
|
||||
},
|
||||
"signature": "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",
|
||||
"hash": "242009507a0fd0b54edeb9ad783884700d895d1982846ee4ef58c4246a29d031"
|
||||
},
|
||||
{
|
||||
"seq": 1,
|
||||
"prev": "242009507a0fd0b54edeb9ad783884700d895d1982846ee4ef58c4246a29d031",
|
||||
"body": {
|
||||
"v": 2,
|
||||
"agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c",
|
||||
"policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d",
|
||||
"session": "c6ff3295bd5220f2f63b8d22f303ec71",
|
||||
"at": 1790713384,
|
||||
"action": {
|
||||
"kind": "payment",
|
||||
"from": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"to": "0x5b63415ab4b96d55987c39a00f22a12f2732edc3",
|
||||
"amount": "10000",
|
||||
"asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"ref": "0x5298562c9b9ecccf42306cc056d2f0b325efabf15789042fe1a4899fdd8de0ab",
|
||||
"at": 1790713384
|
||||
},
|
||||
"decision": {
|
||||
"allowed": true,
|
||||
"reason": "under the limit"
|
||||
},
|
||||
"provenance": null,
|
||||
"seq": 1
|
||||
},
|
||||
"signature": "GfVTdJ2sTw6C6cLJC1Ao0smob/J0BPNX8BOsskLfR8G+yxRi2sXwBopkOJlWEY9nN4VccOH3DFF8gEOyRhaCICo/ONP7QBza0qTE1uPfpFlgYGvZOAUUROik9D3uZ5M/ALmj95nfrOd1cwpS6dBnTxHufFa9vzYWp4Kg48a2K8GKrGNeg4bYGKNUQaFzuskILEg6N+ojBFNp98EsP1JeNEoMhFYdl+I/G1sgmsWN4eoXdABAJeoH49e2j2whB6DhBPy4Hgc3WDI9fmo9NFVvpHPU0FedMiuuipkXxZgOvfCCjSuyV9Y/WVZ9drSFtDPN9tX0HIE2l6RNr/KHAzF3LQ/k18yKLK3LUowyQCabiMY9gKUZH+42qQiIdaOHTei3X6/cNrn6h9HtDlROG4RZDfh1yB6SVyZRrjfX1mh6Y35JnrTIQzaCZc3ecQ8853WMtO7T+GfDmMtuTbnwI5+Ijxf52sMfjXLeTtt3JSt9ghlIdPqZ8SmQldjfxKKOIQLAMbwBtEAMXbiajxCDz7uniQjmNG6BxyCtRubKDorSfiaK18B8szxfa5TIsGYiJ3DnixPyL2LzPAk1jBvIt1lry+jx/8rM0rA9VMB9VmnIjn3W4qdFtd1s6XoGrdfk7ewv+aMfb6JnA5dbNRIOJvyyX3hfk1xORJ0GEsnLkdvcsWrr9OozXLAurfPqi/kfhdNQdIdvHw2TzID/Bf/RfS1TSTKQWt94j7ew8Fz8v1knBqGKy5jmI663+G8HHttkjjWW3MbqDNoJBJqK7PAwVErJjuzsgwub702ah2esRkreriJ6LSnoDffqxgbkSDT8Lyl7+UUUa1rGi2UFN40t8/md4hr0noKD7j8tl8IznWg1uKEtCIkGH9/BNTU6uEl/P6Td92gux+/O+IkNJFnCebuKu+XsIc0JfZf3tMSU4gkW03Fds4QmQ+4uEOQmt8Nb2ZebR0CFidP502YAwkQNfBAxOcC/06rMouFev78xIlMNWzKDLKWupYsHTtRKhy5sAM9+FkpOed5qGldJ31FlFNXpq8tEkRJKamKlgHNVGQUtEzHhfT8tY9dRpbzjJO/FSV7ZAaCi6z+VjaQmTT7GnXhdTQ4X51yZMRYhDvW7OFtLf7TA+KkAbzn1p+eXw4f+kKlqzkZC0JP63pxu3wB7loeEZ1NSm3Wt01FlRgBq7JFj52RrACTztSfKWHeMOIYlVTTgd5JqmqQuwy1KZJVcfoGPB1t8jEtANvWAHG9c4rB6DkrDMQTf+b8H9arKTJRDneFEv0GG1F6GR3ZkbKcWIFEy1zb0Lonqs+BKRbSJGyV2vmWbzf/YfqH59zKCOHcfCbgQ5VT9srTLkTBt6NeAZzcuYICA2qfoKS8nP1FjPed3bqZgubRRLDC/TLbpGy6vozkQuF+Y/wYkwVtX1G2w11C19ce+kxpQ+ZqCL8ZDQ2+t1uQYVEl3Zve5NdoBghFT8acgEBPmb3j/OTlEQA8pOAwx8oeqhLJJ6XsGld1TOCRAjfgzcOcxAzGGBAEPfizoO7jPHwC8TrG4m+hZ4STinI/38kxRoCcQ+tbLGr5p7eDthwmTKg7dZuvCvo7zOJfQE6WVpFdvFtx8t1KN0fpxcK6rcNRzpiW8BqD4yb/DqLL/6y9Okud7va+Aauu4YAZgBqalhu0gmY3GmCObPxCn18HFc+Bj1cGus2PG9rMsG9BqyS32enB/p4Sc6BBhIW2TRR7PhYqXU/DfIK12bYJOc0xp4SPdkK1LxH/ZqV1UVK+LGuB1ZZxIPP8/ptCQAvtp3WokxWKWMTDPJ06NsWmVjIZss0mmKKdsr08TejJTx+lks2EitdCc1t44BqF2/nvbVfttUfmCBbmvWpeA13PM3ZFqmWD3+stbLPYChAPnS6qgQq4N8Tv6pHQviYLDiMen0AhCowlpZ1mejUUq60sxJyvYRoSMJqozSxMMuWTChbCCY7uNbdotrQnkb6eUli4tOBH4d5blH0i8DA7bmolaqT2fGzW974cXLqS8EiOkczu8Bw6EHJ9ceaexPZ3nssSiH4TIXw7ve9evuFzkssA60ypODnTMGzzb3AnBd+OOCPXu6LZB01tqDK5/d5ZQcra+5+xZdynuqhF2+ibsSRhP1wmwRcdwUMTFJfKIxF8Gpj2JzGqY+8ww3ZsG/rBmaEy6iwMQJ3cx48kmo5pucN/EMKD+bHxlNJs5+uvTPdW/ja7MTMR8ex2ds97UqZSHiYMbv9rf+0ralBp75nM/R51lMWSykKxq3W5uwn6xTPqeoPfKRjzmH2W20g2uYYGeYi6rfWbcear+5bHkShl3lLTZoindjjUam+oIZwY3QT+2eCXa8SUW0B2kdOuoHpiNJWmFAAJE61sjw1iVNpz1GgkGRyV3zz5LU27cwSx0LI0HHLsTo7m0EyFAK8WMGwHAFfzRNowVFo/Pt1W2sN+daTXC/HmVbzv+OweEAGdv6xgxjQjfL66QfF5lE4zb/WgQ9jN4aZSm6908pzxMIMqYJ0TLtBt5rbeZ4S21FlyFT49GSWYlJ6w0OjWmoJ9H5OgjXFTA8BjiS4WmOsEcdZZATqjkwNGGSoWF8/BJA6gJv9pSkdsQ+GwnMFJh3TvMhWrJngJZgfHZIvh/gG39bvhMPvEoqJrUhnlyoaRJvEcW+giWjzYweXwjQAiJsOid/+zKK8FsbSzeFWy/Tx8oIbDATCk+h0DO7kkUBUhwCA+OLviVgvyjXtgbP8AqxUW5kRQjgbhwSf+hvtmsSmrhQrms08NpElhSmZcbbgBAOFvKO88FSDuD8LFKVJCWnlN8kcqUVyrdQvj9mcSR+nBHh+3oBIkHVEFWm8/nj8JQweDPiUClNJwiV7SOKgnrVE9yTb96hj4eO/wUpjBF5Dw/ltYmdRM69rJy1EGu3+HFYaqZlChAYbPm1jDShXiu0RQ0JLtmXytPREb8uoD+vFXglNK65+bt1vASb4d4kYuiEGnbhIK1vjCfAHgKMJVdLZztgesKvT8OUyuLBUj2cPuKgkFfhMcYKKu6eYvRXi9gR4kpZvkP5DRTGmlxcTD37KjK4LaUB0rGWMHZBCHZd75UlhIz5PNEMOd1EAXIEWLnqyCFefrHlhqtoIulm3lRGLBLpIROzi5+B6Hmbyua7XngrHzUwrmzzBohLN/K6mhggeY2lOQK8jcKbRIz/5jo5AnOMPyKd0uhe47+vJ2ebGxXhrhrSywBXbYhd/pkdo1TUFSYjt5fIi7fUyOi3t+GVEw1cBJTnhcQAQF+cz7wbEM8m52FC/VOjkDkIGiWrajwLCpsb2dxhgx1Ax7KQFwadFxErvwC0cQXLRlJGhXTWlW+Mnq1YQZPRT37R2C34DLb7Ybk0WOPTTxfDTX7PRM0lLAdYn/VvcpMj0jhAaYLGUDofLqz39eVWUeq4tV/zJDMIAMSHqtL+LFNUIYdlF2/XXXDWj/U4TuawW3q/8jLPq/WtfMfn2bYyzM/VGhYn/zxqCaBs7qsAtc9mbo/YxfLEw76mB3NxcfDC0/1n+orDawfm9UI+9is601gAGWyUIq3Q9x+4Osj/4NXXKhz/nFxIiAp9cZM/YdndA00kiaO0cjcpwC7+Up5GKpauVP1E2gwCtjKeHBk6nOqgUwMwjWkkd7mPNcHD2XyYCcUlMLPAA6QLVNf66TW+Aag0zxyIqFeNT+44kyt8GvcEMSRbgxSrKQepeUlGHXsnA5QfxV8PTH0BbHzESBgea01Ikhss+aXubhjpkGqaqjBC2mlNNrp0lNWvegSUB6AfdsD6y3Y4o31dmtW3rZm1oRA8Gi3olVbf5zWl8sQAsoYgt2aDpGiXUeCcYAQvf+I7K4BMLczmAE8WZ9hnYsSUXODZKOypaK5MwYXKZW56e9YcqcGM3/0OjCxElgB+Qls55RglvefBesw7we07L1A4T1EJ9Ee0Uv6C7hzR/P8qi379ziLj3+C9Kxto/xztITDeDHldAml+96m66svPZ7z6sdVxjPJtulEkJKxWdQa0ChpyvKf1a/lQV8NSIxln4UmOpQCK8NZNYRE6D2GmaVkigM3paIYeWoFWUcCjKBXYiPv5JccpYUSiMcg9vvSn9BbpLl/W7nheUgeR+wvJzci1iJO9S6OOv2VkgFMryPs4ak4sgbxnzJgTGkosMSOsWqkqPhA9keiC5sm2P581HOThJL6DngbC8e5cpDuYC0wJQCOKIO29aA+TCYv1es7cMqOHntw+eYiozj/ucQvGierCZ30i9OowWpnQ0ym6sYTje2lAOwNflbe5H9wE7Waqy3XRmLJ7vFd3/hpV4JsK68eUczt/pQLBI9+SZK3ig64hxUfCghaZXefvAxwkrnc6vc4R1trjMYMKUBben2AjkFItLwZkuf2/wAAAAAAAAAAAAAAAAAAAAAAAAAABQwSGh4j",
|
||||
"hash": "5099803b654458f65044420af98b1a9ddb1aeab6956f318d9123b85b504edf2f"
|
||||
},
|
||||
{
|
||||
"seq": 2,
|
||||
"prev": "5099803b654458f65044420af98b1a9ddb1aeab6956f318d9123b85b504edf2f",
|
||||
"body": {
|
||||
"v": 2,
|
||||
"agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c",
|
||||
"policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d",
|
||||
"session": "c6ff3295bd5220f2f63b8d22f303ec71",
|
||||
"at": 1790713389,
|
||||
"action": {
|
||||
"kind": "payment",
|
||||
"from": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"to": "0x5b63415ab4b96d55987c39a00f22a12f2732edc3",
|
||||
"amount": "10000",
|
||||
"asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"ref": "0x5298562c9b9ecccf42306cc056d2f0b325efabf15789042fe1a4899fdd8de0ab",
|
||||
"at": 1790713389
|
||||
},
|
||||
"decision": {
|
||||
"allowed": true,
|
||||
"reason": "under the limit"
|
||||
},
|
||||
"provenance": null,
|
||||
"seq": 2
|
||||
},
|
||||
"signature": "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",
|
||||
"hash": "647151d57f48ad4629d8e943969b7fa80a9d7e835823b2cd3da0430460e03d0a"
|
||||
},
|
||||
{
|
||||
"seq": 3,
|
||||
"prev": "647151d57f48ad4629d8e943969b7fa80a9d7e835823b2cd3da0430460e03d0a",
|
||||
"body": {
|
||||
"v": 2,
|
||||
"agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c",
|
||||
"policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d",
|
||||
"session": "c6ff3295bd5220f2f63b8d22f303ec71",
|
||||
"at": 1790713393,
|
||||
"action": {
|
||||
"kind": "payment",
|
||||
"from": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"to": "0x5b63415ab4b96d55987c39a00f22a12f2732edc3",
|
||||
"amount": "10000",
|
||||
"asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"ref": "0x5298562c9b9ecccf42306cc056d2f0b325efabf15789042fe1a4899fdd8de0ab",
|
||||
"at": 1790713393
|
||||
},
|
||||
"decision": {
|
||||
"allowed": false,
|
||||
"reason": "over the limit: 40000 > 30000 per 3600s"
|
||||
},
|
||||
"provenance": null,
|
||||
"seq": 3
|
||||
},
|
||||
"signature": "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",
|
||||
"hash": "b53c460c2d5aa8958a1aaf272a8fd1cb2cb6a1d33b18c6b3caa9177c5a835cef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"payments": [
|
||||
{
|
||||
"entrySeq": 0,
|
||||
"entryHash": "242009507a0fd0b54edeb9ad783884700d895d1982846ee4ef58c4246a29d031",
|
||||
"transaction": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e"
|
||||
},
|
||||
{
|
||||
"entrySeq": 1,
|
||||
"entryHash": "5099803b654458f65044420af98b1a9ddb1aeab6956f318d9123b85b504edf2f",
|
||||
"transaction": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9"
|
||||
},
|
||||
{
|
||||
"entrySeq": 2,
|
||||
"entryHash": "647151d57f48ad4629d8e943969b7fa80a9d7e835823b2cd3da0430460e03d0a",
|
||||
"transaction": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-09-29T20:23:16.661Z"
|
||||
}
|
||||
@ -0,0 +1,205 @@
|
||||
{
|
||||
"v": 1,
|
||||
"recordedAt": "2026-09-29T20:23:48.503Z",
|
||||
"rpc": "https://testnet-rpc.aere.network",
|
||||
"chainId": "0x6d61",
|
||||
"receipts": {
|
||||
"0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e": {
|
||||
"blockHash": "0x57b530673634ee3454a25cf2fef87ae019bdb7f7dc29041309f7248e5bb637ab",
|
||||
"blockNumber": "0x3d5f58",
|
||||
"contractAddress": null,
|
||||
"cumulativeGasUsed": "0x144c4",
|
||||
"from": "0x50f2a153be2c248b7050914a08f4f6f4498c4e48",
|
||||
"gasUsed": "0x144c4",
|
||||
"effectiveGasPrice": "0x3b9aca00",
|
||||
"logs": [
|
||||
{
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"topics": [
|
||||
"0x98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a5",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x56f33b19d8186232d0a9f9d6c049389d88e0ab7d4f81eb9bd00ffc98b6605a48"
|
||||
],
|
||||
"data": "0x",
|
||||
"blockNumber": "0x3d5f58",
|
||||
"transactionHash": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e",
|
||||
"transactionIndex": "0x0",
|
||||
"blockHash": "0x57b530673634ee3454a25cf2fef87ae019bdb7f7dc29041309f7248e5bb637ab",
|
||||
"blockTimestamp": "0x6abc1e24",
|
||||
"logIndex": "0x0",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000002710",
|
||||
"blockNumber": "0x3d5f58",
|
||||
"transactionHash": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e",
|
||||
"transactionIndex": "0x0",
|
||||
"blockHash": "0x57b530673634ee3454a25cf2fef87ae019bdb7f7dc29041309f7248e5bb637ab",
|
||||
"blockTimestamp": "0x6abc1e24",
|
||||
"logIndex": "0x1",
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000200000000000000008000000000000008000000000000000000000080000000000000000000000000000080000000000000000000000000000000000000000050000000000000002000000000000000000000000000000000000000004000000000000000000000000000000000040000010000000004000000000000000000000000000000000002000002000000000000000000000000000000000000000000000000000000000000000000000080000000000000002000000000000000000000000020",
|
||||
"status": "0x1",
|
||||
"to": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"transactionHash": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e",
|
||||
"transactionIndex": "0x0",
|
||||
"type": "0x2"
|
||||
},
|
||||
"0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9": {
|
||||
"blockHash": "0xc91066be5b1ddfa9cfe7a507f8d8279a7745ab4733aab012eac76909b91c4f6a",
|
||||
"blockNumber": "0x3d5f61",
|
||||
"contractAddress": null,
|
||||
"cumulativeGasUsed": "0x101f8",
|
||||
"from": "0x50f2a153be2c248b7050914a08f4f6f4498c4e48",
|
||||
"gasUsed": "0x101f8",
|
||||
"effectiveGasPrice": "0x3b9aca00",
|
||||
"logs": [
|
||||
{
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"topics": [
|
||||
"0x98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a5",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x65a47f30ec24141e06c9c82740601a3099f596a3c01c7bb80d3351753f8d8818"
|
||||
],
|
||||
"data": "0x",
|
||||
"blockNumber": "0x3d5f61",
|
||||
"transactionHash": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9",
|
||||
"transactionIndex": "0x0",
|
||||
"blockHash": "0xc91066be5b1ddfa9cfe7a507f8d8279a7745ab4733aab012eac76909b91c4f6a",
|
||||
"blockTimestamp": "0x6abc1e29",
|
||||
"logIndex": "0x0",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000002710",
|
||||
"blockNumber": "0x3d5f61",
|
||||
"transactionHash": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9",
|
||||
"transactionIndex": "0x0",
|
||||
"blockHash": "0xc91066be5b1ddfa9cfe7a507f8d8279a7745ab4733aab012eac76909b91c4f6a",
|
||||
"blockTimestamp": "0x6abc1e29",
|
||||
"logIndex": "0x1",
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000200000000000000008000000000000009000000000000000000000080000000000000000000000000000080000000000000004000000000000000000000000010000000000000002000000000000000000000000000000000000000004000000000000000000000000000000000000000010000000004000000000000000000000000000000000002000002000000000000000400000000000000000000000000000000000000000000000000000080000000000000002000000000000000000000000000",
|
||||
"status": "0x1",
|
||||
"to": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"transactionHash": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9",
|
||||
"transactionIndex": "0x0",
|
||||
"type": "0x2"
|
||||
},
|
||||
"0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63": {
|
||||
"blockHash": "0x8ff8184bb076ad26e8b4e637ffdc596c63ba89f6e7eb94c53d300dd55a93a99b",
|
||||
"blockNumber": "0x3d5f69",
|
||||
"contractAddress": null,
|
||||
"cumulativeGasUsed": "0x101ec",
|
||||
"from": "0x50f2a153be2c248b7050914a08f4f6f4498c4e48",
|
||||
"gasUsed": "0x101ec",
|
||||
"effectiveGasPrice": "0x3b9aca00",
|
||||
"logs": [
|
||||
{
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"topics": [
|
||||
"0x98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a5",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x1ae82892234488ec0d6e8f19a324a50682f0c7afccf2ec0e1e66a9cce1a5f443"
|
||||
],
|
||||
"data": "0x",
|
||||
"blockNumber": "0x3d5f69",
|
||||
"transactionHash": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63",
|
||||
"transactionIndex": "0x0",
|
||||
"blockHash": "0x8ff8184bb076ad26e8b4e637ffdc596c63ba89f6e7eb94c53d300dd55a93a99b",
|
||||
"blockTimestamp": "0x6abc1e2d",
|
||||
"logIndex": "0x0",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000002710",
|
||||
"blockNumber": "0x3d5f69",
|
||||
"transactionHash": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63",
|
||||
"transactionIndex": "0x0",
|
||||
"blockHash": "0x8ff8184bb076ad26e8b4e637ffdc596c63ba89f6e7eb94c53d300dd55a93a99b",
|
||||
"blockTimestamp": "0x6abc1e2d",
|
||||
"logIndex": "0x1",
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"logsBloom": "0x00000000000000002000000000000000000000000000000008000000000000000000000000000000000020000000000000000000000000000000000200000000000000008000000000000008000000000000000000000080000000000000000000000000000080000000000000000000000000000000000000000010000000000000002000000000000000000000000000000000000000004000000000000000000000000000000000000000010000000004000000000000000000000000000000000002000002000000000000000000000000000000000000000000000000000000000000000000000080000000000000002010000000000000000000000000",
|
||||
"status": "0x1",
|
||||
"to": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"transactionHash": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63",
|
||||
"transactionIndex": "0x0",
|
||||
"type": "0x2"
|
||||
}
|
||||
},
|
||||
"transfersOut": [
|
||||
{
|
||||
"logIndex": "0x1",
|
||||
"removed": false,
|
||||
"blockNumber": "0x3d5f58",
|
||||
"blockHash": "0x57b530673634ee3454a25cf2fef87ae019bdb7f7dc29041309f7248e5bb637ab",
|
||||
"blockTimestamp": "0x6abc1e24",
|
||||
"transactionHash": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e",
|
||||
"transactionIndex": "0x0",
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000002710",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"logIndex": "0x1",
|
||||
"removed": false,
|
||||
"blockNumber": "0x3d5f61",
|
||||
"blockHash": "0xc91066be5b1ddfa9cfe7a507f8d8279a7745ab4733aab012eac76909b91c4f6a",
|
||||
"blockTimestamp": "0x6abc1e29",
|
||||
"transactionHash": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9",
|
||||
"transactionIndex": "0x0",
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000002710",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"logIndex": "0x1",
|
||||
"removed": false,
|
||||
"blockNumber": "0x3d5f69",
|
||||
"blockHash": "0x8ff8184bb076ad26e8b4e637ffdc596c63ba89f6e7eb94c53d300dd55a93a99b",
|
||||
"blockTimestamp": "0x6abc1e2d",
|
||||
"transactionHash": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63",
|
||||
"transactionIndex": "0x0",
|
||||
"address": "0x8215ba247a3574af8ebc36606eb437811e318fbd",
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000002710",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9",
|
||||
"0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
53
agents/x402/facilitator-local.mjs
Normal file
53
agents/x402/facilitator-local.mjs
Normal file
@ -0,0 +1,53 @@
|
||||
// Un facilitator x402 v2 LOCAL, pentru probe fara retea (2026-09-29): aceleasi verificari ca facilitatorul de pe testnetul 28001
|
||||
// (x402/facilitator/facilitator.mjs din depozitul de dezvoltare: forma, reteaua, activul, payTo, suma, fereastra, semnatura EIP-712
|
||||
// recuperata, nonce nefolosit, soldul), dar soldurile si nonce-urile folosite stau in memorie, iar /settle le muta in memorie in loc
|
||||
// sa trimita o tranzactie. Nu e un facilitator de folosit in productie: nu atinge niciun lant.
|
||||
import http from 'node:http';
|
||||
import { incarcaEthers, EIP3009_TYPES } from './wallet.mjs';
|
||||
|
||||
export function createLocalFacilitator({ network, token, balances = {} }) {
|
||||
const ethers = incarcaEthers();
|
||||
const domeniu = { name: token.name, version: token.version, chainId: Number(network.split(':')[1]), verifyingContract: ethers.getAddress(token.address) };
|
||||
const sold = new Map(Object.entries(balances).map(([a, v]) => [a.toLowerCase(), BigInt(v)]));
|
||||
const folosite = new Set();
|
||||
function verifica(body) {
|
||||
const pp = body && body.paymentPayload, pr = body && body.paymentRequirements;
|
||||
if (!pp || !pr || body.x402Version !== 2 || pp.x402Version !== 2) return { ok: false, reason: 'invalid_payload' };
|
||||
if (pr.scheme !== 'exact' || pr.network !== network) return { ok: false, reason: 'unsupported_network' };
|
||||
if (String(pr.asset).toLowerCase() !== token.address.toLowerCase()) return { ok: false, reason: 'unsupported_asset' };
|
||||
const a = pp.payload && pp.payload.authorization, sig = pp.payload && pp.payload.signature;
|
||||
if (!a || !sig) return { ok: false, reason: 'invalid_payload' };
|
||||
if (String(a.to).toLowerCase() !== String(pr.payTo).toLowerCase()) return { ok: false, reason: 'invalid_payment_requirements', payer: a.from };
|
||||
if (BigInt(a.value) < BigInt(pr.amount)) return { ok: false, reason: 'insufficient_amount', payer: a.from };
|
||||
const acum = BigInt(Math.floor(Date.now() / 1000));
|
||||
if (acum <= BigInt(a.validAfter)) return { ok: false, reason: 'authorization_not_yet_valid', payer: a.from };
|
||||
if (acum + 6n >= BigInt(a.validBefore)) return { ok: false, reason: 'authorization_expired', payer: a.from };
|
||||
let semnatar = null;
|
||||
try { semnatar = ethers.verifyTypedData(domeniu, EIP3009_TYPES, { ...a, value: BigInt(a.value), validAfter: BigInt(a.validAfter), validBefore: BigInt(a.validBefore) }, sig); } catch { semnatar = null; }
|
||||
if (!semnatar || semnatar.toLowerCase() !== String(a.from).toLowerCase()) return { ok: false, reason: 'invalid_signature', payer: a.from };
|
||||
if (folosite.has(`${a.from.toLowerCase()}:${a.nonce}`)) return { ok: false, reason: 'nonce_already_used', payer: a.from };
|
||||
if ((sold.get(a.from.toLowerCase()) || 0n) < BigInt(a.value)) return { ok: false, reason: 'insufficient_funds', payer: a.from };
|
||||
return { ok: true, payer: a.from, a };
|
||||
}
|
||||
let n = 0;
|
||||
const srv = http.createServer((req, res) => {
|
||||
let s = ''; req.on('data', (x) => { s += x; }); req.on('end', () => {
|
||||
const trimite = (o) => { const b = JSON.stringify(o); res.writeHead(200, { 'content-type': 'application/json' }); res.end(b); };
|
||||
let body; try { body = JSON.parse(s || 'null'); } catch { body = null; }
|
||||
const url = (req.url || '').split('?')[0];
|
||||
const v = verifica(body);
|
||||
if (url === '/verify') return trimite(v.ok ? { isValid: true, payer: v.payer } : { isValid: false, invalidReason: v.reason, payer: v.payer || '' });
|
||||
if (url === '/settle') {
|
||||
if (!v.ok) return trimite({ success: false, errorReason: v.reason, payer: v.payer || '', transaction: '', network });
|
||||
const a = v.a; folosite.add(`${a.from.toLowerCase()}:${a.nonce}`);
|
||||
sold.set(a.from.toLowerCase(), sold.get(a.from.toLowerCase()) - BigInt(a.value));
|
||||
sold.set(a.to.toLowerCase(), (sold.get(a.to.toLowerCase()) || 0n) + BigInt(a.value));
|
||||
return trimite({ success: true, payer: a.from, transaction: '0x' + (++n).toString(16).padStart(64, '0'), network });
|
||||
}
|
||||
res.writeHead(404); res.end();
|
||||
});
|
||||
});
|
||||
return { server: srv, balanceOf: (a) => sold.get(String(a).toLowerCase()) || 0n, used: folosite,
|
||||
fund: (a, v) => sold.set(String(a).toLowerCase(), (sold.get(String(a).toLowerCase()) || 0n) + BigInt(v)),
|
||||
listen: () => new Promise((r) => srv.listen(0, '127.0.0.1', () => r(`http://127.0.0.1:${srv.address().port}`))), close: () => srv.close() };
|
||||
}
|
||||
20
agents/x402/inregistreaza-rpc.mjs
Normal file
20
agents/x402/inregistreaza-rpc.mjs
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
// inregistreaza-rpc.mjs: scoate de pe lant, O DATA, raspunsurile RPC de care are nevoie verifica-plati.mjs pentru un dosar-dovada
|
||||
// (eth_chainId, chitanta fiecarei plati, Transfer-urile din portofel), ca proba verificatorului sa ruleze apoi fara retea pe date REALE.
|
||||
// node inregistreaza-rpc.mjs <dovada.json> --rpc <url> --out <rpc-inregistrat.json>
|
||||
import fs from 'node:fs';
|
||||
import { incarcaEthers } from './wallet.mjs';
|
||||
|
||||
const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; };
|
||||
const f = a[0]; const rpc = get('--rpc'); const out = get('--out');
|
||||
if (!f || !rpc || !out) { console.error('usage: node inregistreaza-rpc.mjs <evidence.json> --rpc <url> --out <file>'); process.exit(2); }
|
||||
const ethers = incarcaEthers();
|
||||
const d = JSON.parse(fs.readFileSync(f, 'utf8'));
|
||||
const apel = async (method, params) => { const r = await fetch(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); const j = await r.json(); if (j.error) throw new Error(j.error.message); return j.result; };
|
||||
const chainId = await apel('eth_chainId', []);
|
||||
const receipts = {};
|
||||
for (const p of d.payments) receipts[p.transaction.toLowerCase()] = await apel('eth_getTransactionReceipt', [p.transaction]);
|
||||
const logs = await apel('eth_getLogs', [{ address: d.token, fromBlock: '0x' + Number(d.fromBlock).toString(16), toBlock: 'latest',
|
||||
topics: [ethers.id('Transfer(address,address,uint256)'), '0x' + '0'.repeat(24) + d.wallet.toLowerCase().slice(2)] }]);
|
||||
fs.writeFileSync(out, JSON.stringify({ v: 1, recordedAt: new Date().toISOString(), rpc, chainId, receipts, transfersOut: logs }, null, 1) + '\n');
|
||||
console.log(`recorded chain ${Number(chainId)}: ${Object.keys(receipts).length} receipts, ${logs.length} transfers out of the wallet -> ${out}`);
|
||||
9
agents/x402/package.json
Normal file
9
agents/x402/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "aere-agent-wallet",
|
||||
"private": true,
|
||||
"description": "Aere agent wallet: pays x402 only what the agent ledger and the owner policy allow",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"engines": { "node": ">=24" },
|
||||
"dependencies": { "ethers": "6.16.0" }
|
||||
}
|
||||
63
agents/x402/proba-verifica-plati.mjs
Normal file
63
agents/x402/proba-verifica-plati.mjs
Normal file
@ -0,0 +1,63 @@
|
||||
// Proba verificatorului de plati (verifica-plati.mjs), fara retea: un RPC local care serveste raspunsurile INREGISTRATE de pe testnetul
|
||||
// 28001 pentru dosarul-dovada din dovezi-28001/ (inregistreaza-rpc.mjs), deci date reale de pe lant. Fiecare verificare are cazul ei
|
||||
// care trebuie sa o inroseasca, construit din datele reale schimbate intr-un singur loc.
|
||||
// node proba-verifica-plati.mjs iesire 0 = toate cum trebuia
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { verificaPlati } from './verifica-plati.mjs';
|
||||
|
||||
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
||||
const D = path.join(AICI, 'dovezi-28001');
|
||||
const dovadaF = fs.readdirSync(D).filter((n) => n.startsWith('plati-agent-') && n.endsWith('.json')).sort().pop();
|
||||
const inregF = fs.readdirSync(D).filter((n) => n.startsWith('rpc-inregistrat-') && n.endsWith('.json')).sort().pop();
|
||||
let ok = 0, rau = 0;
|
||||
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; };
|
||||
if (!dovadaF || !inregF) { console.log(`NEMASURAT: lipseste dosarul-dovada sau inregistrarea RPC in ${D}`); process.exit(2); }
|
||||
const dovada = JSON.parse(fs.readFileSync(path.join(D, dovadaF), 'utf8'));
|
||||
const inreg = JSON.parse(fs.readFileSync(path.join(D, inregF), 'utf8'));
|
||||
const copie = (o) => JSON.parse(JSON.stringify(o));
|
||||
|
||||
// un RPC local peste inregistrare (sau peste o varianta schimbata a ei)
|
||||
async function rpcDin(rec) {
|
||||
const srv = http.createServer((req, res) => { let s = ''; req.on('data', (x) => { s += x; }); req.on('end', () => {
|
||||
const q = JSON.parse(s); let result = null;
|
||||
if (q.method === 'eth_chainId') result = rec.chainId;
|
||||
else if (q.method === 'eth_getTransactionReceipt') result = rec.receipts[String(q.params[0]).toLowerCase()] || null;
|
||||
else if (q.method === 'eth_getLogs') result = rec.transfersOut;
|
||||
res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: q.id, result }));
|
||||
}); });
|
||||
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
||||
return { url: `http://127.0.0.1:${srv.address().port}`, close: () => srv.close() };
|
||||
}
|
||||
async function judeca(dov, rec, allTransfers = true) { const r = await rpcDin(rec); try { return await verificaPlati(dov, { rpc: r.url, allTransfers }); } finally { r.close(); } }
|
||||
const pica = (v, re) => v.checks.some((c) => c.pass === false && re.test(c.name));
|
||||
|
||||
try {
|
||||
const v0 = await judeca(dovada, inreg);
|
||||
cer(v0.verdict === 'VALID' && v0.checks.every((c) => c.pass === true), `1. dosarul real, peste raspunsurile reale de pe 28001: VALID (${v0.checks.length} verificari)`);
|
||||
const tx1 = dovada.payments[1].transaction.toLowerCase();
|
||||
|
||||
const d2 = copie(dovada); d2.ledger.entries[dovada.payments[1].entrySeq].body.action.amount = '1';
|
||||
cer(pica(await judeca(d2, inreg), /the ledger verifies/), '2. CONTROL: o suma schimbata in registru -> registrul nu mai verifica');
|
||||
const d3 = copie(dovada); d3.payments[1].entryHash = 'ab'.repeat(32);
|
||||
cer(pica(await judeca(d3, inreg), /the entry is in the ledger/), '3. CONTROL: plata numeste o intrare care nu e in registru -> INVALID');
|
||||
const d4 = copie(dovada); d4.wallet = '0x' + '11'.repeat(20);
|
||||
cer(pica(await judeca(d4, inreg, false), /allowed payment from the wallet/), '4. CONTROL: alt portofel decat cel din intrari -> INVALID');
|
||||
const r5 = copie(inreg); r5.receipts[tx1].status = '0x0';
|
||||
cer(pica(await judeca(dovada, r5), /status 1/), '5. CONTROL: chitanta cu status 0 -> INVALID');
|
||||
const r6 = copie(inreg); for (const l of r6.receipts[tx1].logs) if (l.topics.length === 3 && l.topics[2] && !/^0x0{24}/.test(l.topics[2])) l.topics[2] = '0x' + 'cd'.repeat(32);
|
||||
cer(pica(await judeca(dovada, r6), /AuthorizationUsed/), '6. CONTROL: nonce-ul de pe lant nu e sha256(hash-ul intrarii) -> INVALID');
|
||||
const r7 = copie(inreg); for (const l of r7.receipts[tx1].logs) if (l.data && l.data !== '0x') l.data = '0x' + (1n).toString(16).padStart(64, '0');
|
||||
cer(pica(await judeca(dovada, r7), /Transfer\(wallet/), '7. CONTROL: suma din Transfer-ul de pe lant alta decat in intrare -> INVALID');
|
||||
const r8 = copie(inreg); r8.transfersOut = [...r8.transfersOut, { ...r8.transfersOut[0], transactionHash: '0x' + 'ee'.repeat(32) }];
|
||||
cer(pica(await judeca(dovada, r8), /every Transfer out of the wallet/), '8. CONTROL: un Transfer din portofel fara plata in registru -> INVALID');
|
||||
const r9 = copie(inreg); r9.chainId = '0xaf0';
|
||||
const v9 = await judeca(dovada, r9);
|
||||
cer(v9.verdict === 'UNMEASURED', `9. CONTROL: un RPC al altui lant (2800) -> ${v9.verdict}, nu VALID si nu INVALID`);
|
||||
const v10 = await verificaPlati(dovada, { rpc: 'http://127.0.0.1:9', allTransfers: true });
|
||||
cer(v10.verdict === 'UNMEASURED', `10. CONTROL: un RPC care nu raspunde -> ${v10.verdict}`);
|
||||
} catch (e) { cer(false, `proba s-a oprit: ${String(e.message || e).slice(0, 160)}`); }
|
||||
console.log(`\nverifica-plati: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
|
||||
process.exitCode = rau ? 1 : 0;
|
||||
185
agents/x402/proba-wallet.mjs
Normal file
185
agents/x402/proba-wallet.mjs
Normal file
@ -0,0 +1,185 @@
|
||||
// Proba portofelului de plati x402 al agentilor (wallet.mjs + client.mjs + resource-server.mjs), fara retea: un facilitator LOCAL cu
|
||||
// aceleasi verificari ca cel de pe testnet (facilitator-local.mjs), semnaturi EIP-712 reale, chei ML-DSA-65 reale. Adversarul are cheia
|
||||
// AGENTULUI (isi poate scrie si re-semna registrul oricum), nu cheia portofelului si nici cheile oamenilor. Fiecare afirmatie cu perechea
|
||||
// ei negativa.
|
||||
// node proba-wallet.mjs iesire 0 = toate cum trebuia
|
||||
import crypto from 'node:crypto';
|
||||
import { definePolicy } from '../agent-policy.mjs';
|
||||
import { newAgentIdentity, openLedger, resumeLedger, verifyEquivocation, canonical } from '../agent-ledger.mjs';
|
||||
import { newHumanIdentity, approve, revoke } from '../agent-aprobare.mjs';
|
||||
import { createWallet, serve, x402Action, assetId, nonceForEntry, incarcaEthers } from './wallet.mjs';
|
||||
import { payWithAgent, walletOverHttp, dinB64json } from './client.mjs';
|
||||
import { createResourceServer } from './resource-server.mjs';
|
||||
import { createLocalFacilitator } from './facilitator-local.mjs';
|
||||
|
||||
const ethers = incarcaEthers();
|
||||
let ok = 0, rau = 0;
|
||||
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; };
|
||||
const arunca = (fn) => { try { fn(); return null; } catch (e) { return e.message; } };
|
||||
const NET = 'eip155:28001';
|
||||
const TOKEN = { address: '0x8215bA247a3574af8EBC36606eB437811E318FBd', name: 'AereTestUSD', version: '2' };
|
||||
const ASSET = assetId(NET, TOKEN.address);
|
||||
const cerinta = (payTo, amount = '10000') => ({ scheme: 'exact', network: NET, amount, asset: TOKEN.address, payTo, maxTimeoutSeconds: 60, extra: { name: TOKEN.name, version: TOKEN.version } });
|
||||
const acum = () => Math.floor(Date.now() / 1000);
|
||||
// o intrare scrisa si semnata de mana cu cheia agentului (adversarul ocoleste biblioteca registrului)
|
||||
function intrareFalsa(exp, identity, body) {
|
||||
const r = JSON.parse(JSON.stringify(exp)); const seq = r.entries.length; const prev = seq ? r.entries[seq - 1].hash : '0'.repeat(64);
|
||||
const b = { v: 2, agentId: r.agentId, policyHash: r.policyHash, session: r.session, provenance: null, ...body, seq };
|
||||
const signature = crypto.sign(null, Buffer.from(`${seq}|${prev}|${canonical(b)}`, 'utf8'), identity.privateKey).toString('base64');
|
||||
r.entries.push({ seq, prev, body: b, signature, hash: crypto.createHash('sha256').update(`${seq}|${prev}|${canonical(b)}|${signature}`).digest('hex') });
|
||||
return r;
|
||||
}
|
||||
// un agent cu portofelul lui: politica numeste portofelul, limita 50000 pe ora in activul x402
|
||||
function agentCuPortofel({ limita = '50000', extra = {}, payee, state } = {}) {
|
||||
const agent = newAgentIdentity(); const cheie = ethers.Wallet.createRandom();
|
||||
const { policy, policyHash } = definePolicy({ agentId: agent.agentId, spend: { amount: limita, windowSeconds: 3600, asset: ASSET },
|
||||
recipients: payee ? [payee] : undefined, wallet: cheie.address, ...extra });
|
||||
const wallet = createWallet({ policy, policyHash, evmPrivateKey: cheie.privateKey, network: NET, token: TOKEN, state });
|
||||
return { agent, cheie, policy, policyHash, wallet };
|
||||
}
|
||||
|
||||
// serverele deschise se inchid in `finally`, si cand proba s-a oprit la jumatate: altfel procesul ramane viu dupa rezumat
|
||||
// (masurat: controlul negativ astepta 240 s pe o copie oprita la jumatate)
|
||||
const deschise = [];
|
||||
const inchide = (s) => { if (!s) return; try { s.closeAllConnections && s.closeAllConnections(); s.close(); } catch { /* deja inchis */ } };
|
||||
// un pas care arunca (o plantare care strica un raspuns) inroseste proba cu motivul, nu o opreste inainte de rezumat
|
||||
try {
|
||||
const payee = ethers.Wallet.createRandom().address.toLowerCase();
|
||||
const A = agentCuPortofel({ payee });
|
||||
const fac = createLocalFacilitator({ network: NET, token: TOKEN, balances: { [A.cheie.address]: '1000000' } });
|
||||
const facUrl = await fac.listen(); deschise.push(fac.server);
|
||||
const rs = createResourceServer({ requirement: cerinta(payee), facilitator: facUrl });
|
||||
const url = await rs.listen(); deschise.push(rs.server);
|
||||
const L = openLedger({ identity: A.agent, policy: A.policy, policyHash: A.policyHash });
|
||||
|
||||
// 1. cinci cumparaturi platite prin facilitator, fiecare scrisa intai in registru
|
||||
const plati = [];
|
||||
for (let i = 0; i < 5; i++) plati.push(await payWithAgent({ url, ledger: L, wallet: A.wallet }));
|
||||
cer(plati.every((p) => p.paid && p.status === 200 && p.body === 'the paid content' && p.settlement.success), `1. cinci cumparaturi de 10000: platite si servite (${plati.map((p) => p.status).join(',')})`);
|
||||
cer(fac.balanceOf(payee) === 50000n && fac.balanceOf(A.cheie.address) === 950000n, `1. soldurile la facilitator: vanzatorul 50000, portofelul 950000 (${fac.balanceOf(payee)})`);
|
||||
cer(plati.every((p) => p.receipt.nonce === nonceForEntry(p.entry.hash) && fac.used.has(`${A.cheie.address.toLowerCase()}:${p.receipt.nonce}`)), '1. nonce-ul fiecarei autorizari EIP-3009 e sha256(hash-ul intrarii din registru), si chiar el e consumat');
|
||||
cer(plati.every((p) => p.decision.statement.allowed === true && p.decision.statement.policyHash === A.policyHash), '1. fiecare plata vine cu plicul AIP-23 al deciziei, sub politica fixata');
|
||||
|
||||
// 2. a sasea depaseste limita: politica agentului o refuza, portofelul nici nu e chemat
|
||||
const p6 = await payWithAgent({ url, ledger: L, wallet: A.wallet });
|
||||
cer(!p6.paid && /policy refused.*over the limit/.test(p6.reason) && fac.balanceOf(payee) === 50000n && A.wallet.status().signed === 5, `2. CONTROL: a sasea cumparatura depaseste limita: refuzata inainte de portofel, soldul neschimbat (${p6.reason})`);
|
||||
|
||||
// 3. ATAC: agentul ocoleste biblioteca si scrie singur o plata "permisa" peste limita, re-semnata cu cheia lui
|
||||
const r0 = await fetch(url); const pr = dinB64json(r0.headers.get('payment-required'));
|
||||
const falsa = intrareFalsa(L.export(), A.agent, { at: acum(), action: { ...x402Action(A.cheie.address, pr.resource, pr.accepts[0]), at: acum() }, decision: { allowed: true, reason: 'under the limit' } });
|
||||
const r3 = await A.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: falsa });
|
||||
cer(!r3.ok && /false decision/.test(r3.error) && A.wallet.status().signed === 5, `3. ATAC: plata "permisa" peste limita, scrisa si semnata de agent -> portofelul re-ruleaza politica si refuza (${(r3.error || "").slice(0, 90)})`);
|
||||
|
||||
// 4. ATAC: intrarea numeste alta cumparatura decat cea platita (alta suma / alt destinatar)
|
||||
const B = agentCuPortofel({ payee });
|
||||
const LB = openLedger({ identity: B.agent, policy: B.policy, policyHash: B.policyHash });
|
||||
LB.record({ ...x402Action(B.cheie.address, pr.resource, pr.accepts[0]), amount: '100' });
|
||||
const r4 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB.export() });
|
||||
cer(!r4.ok && /another amount/.test(r4.error), `4. ATAC: intrarea spune 100, cumparatura costa 10000 -> refuzat (${r4.error})`);
|
||||
const LB2 = openLedger({ identity: B.agent, policy: B.policy, policyHash: B.policyHash }); // o intrare curata, pentru controlul pozitiv
|
||||
LB2.record(x402Action(B.cheie.address, pr.resource, pr.accepts[0]));
|
||||
const r4b = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() });
|
||||
cer(r4b.ok, '4. CONTROL: aceeasi cumparatura, cu intrarea care o numeste exact -> semnata');
|
||||
|
||||
// 5. ATAC: acelasi registru trimis din nou (aceeasi ultima intrare)
|
||||
const r5 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() });
|
||||
cer(!r5.ok && /already seen/.test(r5.error), `5. ATAC: aceeasi intrare a doua oara -> refuzat (${r5.error})`);
|
||||
|
||||
// 6. ATAC: o ramura (registrul luat de la un prefix si continuat altfel) -> refuzat, cu dovada de echivocare
|
||||
LB2.record(x402Action(B.cheie.address, pr.resource, pr.accepts[0]));
|
||||
const r6a = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() });
|
||||
const prefix = { ...LB2.export(), entries: LB2.export().entries.slice(0, 1) };
|
||||
const ramura = resumeLedger({ identity: B.agent, policy: B.policy, ledger: prefix });
|
||||
ramura.record(x402Action(B.cheie.address, { url: 'http://altundeva/x' }, pr.accepts[0]));
|
||||
const r6 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: { url: 'http://altundeva/x' }, ledger: ramura.export() });
|
||||
cer(r6a.ok && !r6.ok && /another branch/.test(r6.error), `6. ATAC: o ramura a registrului -> refuzata (${(r6.error || "").slice(0, 80)})`);
|
||||
cer(!!r6.equivocation && verifyEquivocation(r6.equivocation).ok, '6. si portofelul da dovada de echivocare, verificabila de oricine cu cheia publica a agentului');
|
||||
|
||||
// 7. politica schimbata: registrul nou incepe de la zero, portofelul isi aminteste ce a semnat
|
||||
const C = agentCuPortofel({ payee });
|
||||
const LC = openLedger({ identity: C.agent, policy: C.policy, policyHash: C.policyHash });
|
||||
let semnate = 0;
|
||||
for (let i = 0; i < 5; i++) { LC.record(x402Action(C.cheie.address, { url: 'http://r/' + i }, cerinta(payee))); if ((await C.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://r/' + i }, ledger: LC.export() })).ok) semnate++; }
|
||||
const polB = definePolicy({ ...C.policy, tools: ['search'] });
|
||||
const Cb = createWallet({ policy: polB.policy, policyHash: polB.policyHash, evmPrivateKey: C.cheie.privateKey, network: NET, token: TOKEN, state: C.wallet.exportState() });
|
||||
const LCb = openLedger({ identity: C.agent, policy: polB.policy, policyHash: polB.policyHash });
|
||||
const rc = LCb.record(x402Action(C.cheie.address, { url: 'http://r/nou' }, cerinta(payee)));
|
||||
const r7 = await Cb.authorize({ requirements: cerinta(payee), resource: { url: 'http://r/nou' }, ledger: LCb.export() });
|
||||
cer(semnate === 5 && rc.allowed && !r7.ok && /by what this wallet has signed.*over the limit/.test(r7.error), `7. politica noua, registru nou (el permite: ${rc.allowed}), dar portofelul a semnat deja 50000 in ora asta -> refuzat (${(r7.error || '').slice(0, 70)})`);
|
||||
|
||||
// 8. revocarea data portofelului de proprietar, pe care agentul nu o scrie in registru
|
||||
const O = newHumanIdentity();
|
||||
const D = agentCuPortofel({ payee, extra: { owner: O.humanId } });
|
||||
const LD = openLedger({ identity: D.agent, policy: D.policy, policyHash: D.policyHash });
|
||||
LD.record(x402Action(D.cheie.address, { url: 'http://d/1' }, cerinta(payee)));
|
||||
const r8a = await D.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://d/1' }, ledger: LD.export() });
|
||||
cer(!D.wallet.addRevocation(revoke({ owner: newHumanIdentity(), agentId: D.agent.agentId, policyHash: D.policyHash, revokedAt: acum() - 5 })).ok, '8. CONTROL: o revocare semnata de altcineva decat proprietarul e refuzata de portofel');
|
||||
cer(D.wallet.addRevocation(revoke({ owner: O, agentId: D.agent.agentId, policyHash: D.policyHash, revokedAt: acum() - 5 })).ok, '8. revocarea proprietarului e primita de portofel');
|
||||
LD.record(x402Action(D.cheie.address, { url: 'http://d/2' }, cerinta(payee)));
|
||||
const r8 = await D.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://d/2' }, ledger: LD.export() });
|
||||
cer(r8a.ok && !r8.ok && /false decision.*revoked/.test(r8.error), `8. dupa revocare, plata urmatoare e refuzata desi agentul nu a scris revocarea (${(r8.error || '').slice(0, 80)})`);
|
||||
|
||||
// 9. aprobarea umana: peste 20000 cere un om
|
||||
const H = newHumanIdentity();
|
||||
const E = agentCuPortofel({ payee, limita: '100000', extra: { approval: { approvers: [H.humanId], threshold: 1, above: '20000' } } });
|
||||
const LE = openLedger({ identity: E.agent, policy: E.policy, policyHash: E.policyHash });
|
||||
const mare = cerinta(payee, '30000');
|
||||
const fara = LE.record(x402Action(E.cheie.address, { url: 'http://e/1' }, mare));
|
||||
const act = x402Action(E.cheie.address, { url: 'http://e/2' }, mare);
|
||||
const apr = approve({ human: H, agentId: E.agent.agentId, policyHash: E.policyHash, action: act, issuedAt: acum() - 5, expiresAt: acum() + 300 });
|
||||
LE.record(act, { approvals: [apr] });
|
||||
const r9 = await E.wallet.authorize({ requirements: mare, resource: { url: 'http://e/2' }, ledger: LE.export() });
|
||||
cer(!fara.allowed && r9.ok, `9. 30000 fara aprobare: refuzat de politica; cu aprobarea omului numit: semnat (${fara.reason})`);
|
||||
|
||||
// 10. ATAC: o plata antedatata (ceasul agentului dat inapoi)
|
||||
const F = agentCuPortofel({ payee });
|
||||
const LF = openLedger({ identity: F.agent, policy: F.policy, policyHash: F.policyHash, now: () => acum() - 600 });
|
||||
LF.record(x402Action(F.cheie.address, { url: 'http://f/1' }, cerinta(payee)));
|
||||
const r10 = await F.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://f/1' }, ledger: LF.export() });
|
||||
cer(!r10.ok && /not now/.test(r10.error), `10. ATAC: plata datata cu 10 minute in urma -> refuzata (${(r10.error || '').slice(0, 70)})`);
|
||||
|
||||
// 11. serverul de resurse: plata pentru alta cerinta, si reluarea aceleiasi plati
|
||||
const alta = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify({ ...r4b.paymentPayload, accepted: cerinta(payee, '1') })).toString('base64') } });
|
||||
cer(alta.status === 402 && /another requirement/.test(await alta.text()), '11. CONTROL: o plata pentru alta cerinta decat a emis serverul -> 402');
|
||||
fac.fund(B.cheie.address, '100000'); // portofelul lui B are acum sold la facilitator
|
||||
const reluare = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(r4b.paymentPayload)).toString('base64') } });
|
||||
const reluare2 = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(r4b.paymentPayload)).toString('base64') } });
|
||||
cer(reluare.status === 200 && reluare2.status === 402 && /nonce_already_used/.test(await reluare2.text()), '11. aceeasi plata de doua ori: prima data servita, a doua oara 402 (nonce folosit la facilitator)');
|
||||
|
||||
// 12. doua ramuri la aceeasi pozitie, trimise DEODATA: portofelul semneaza cel mult una
|
||||
const G = agentCuPortofel({ payee });
|
||||
const LG1 = openLedger({ identity: G.agent, policy: G.policy, policyHash: G.policyHash });
|
||||
const LG2 = openLedger({ identity: G.agent, policy: G.policy, policyHash: G.policyHash });
|
||||
LG1.record(x402Action(G.cheie.address, { url: 'http://g/1' }, cerinta(payee, '50000')));
|
||||
LG2.record(x402Action(G.cheie.address, { url: 'http://g/2' }, cerinta(payee, '50000')));
|
||||
const [g1, g2] = await Promise.all([G.wallet.authorize({ requirements: cerinta(payee, '50000'), resource: { url: 'http://g/1' }, ledger: LG1.export() }),
|
||||
G.wallet.authorize({ requirements: cerinta(payee, '50000'), resource: { url: 'http://g/2' }, ledger: LG2.export() })]);
|
||||
cer([g1, g2].filter((x) => x.ok).length === 1 && G.wallet.status().signed === 1, `12. ATAC: doua ramuri de cate 50000 trimise deodata (limita 50000) -> exact una semnata (${[g1.ok, g2.ok]})`);
|
||||
|
||||
// 13. portofelul prin HTTP, cap la cap
|
||||
const srv = await serve(A.wallet, { port: 0 }); deschise.push(srv);
|
||||
const W = walletOverHttp(`http://127.0.0.1:${srv.address().port}`);
|
||||
const st = await W.status();
|
||||
const LA = resumeLedger({ identity: A.agent, policy: A.policy, ledger: L.export() });
|
||||
const p13 = await payWithAgent({ url, ledger: LA, wallet: W });
|
||||
cer(st.address === A.cheie.address && st.policyHash === A.policyHash && !p13.paid && /over the limit/.test(p13.reason), `13. prin HTTP: /status spune portofelul si politica; limita tine si dupa reluarea registrului (${(p13.reason || '').slice(0, 60)})`);
|
||||
const H2 = agentCuPortofel({ payee }); const srv2 = await serve(H2.wallet, { port: 0 }); deschise.push(srv2);
|
||||
const rs2 = createResourceServer({ requirement: cerinta(payee), facilitator: facUrl }); const url2 = await rs2.listen(); deschise.push(rs2.server);
|
||||
const LH = openLedger({ identity: H2.agent, policy: H2.policy, policyHash: H2.policyHash });
|
||||
const p13b = await payWithAgent({ url: url2, ledger: LH, wallet: walletOverHttp(`http://127.0.0.1:${srv2.address().port}`) });
|
||||
cer(p13b.status === 402 && /wallet refused|insufficient_funds|not valid/.test(p13b.reason || ''), `13. CONTROL: un portofel fara sold -> plata nu trece la facilitator (${(p13b.reason || '').slice(0, 70)})`);
|
||||
|
||||
// 14. portofelul refuza sa porneasca pe o politica ce nu il numeste
|
||||
const alt = ethers.Wallet.createRandom();
|
||||
cer(/does not name this wallet/.test(arunca(() => createWallet({ policy: A.policy, evmPrivateKey: alt.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: o politica ce numeste alt portofel -> portofelul nu porneste');
|
||||
cer(/pinned policyHash/.test(arunca(() => createWallet({ policy: A.policy, policyHash: '0x' + '00'.repeat(32), evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: un hash fixat care nu e al politicii -> portofelul nu porneste');
|
||||
const faraLimita = definePolicy({ agentId: A.agent.agentId, wallet: A.cheie.address }).policy;
|
||||
const altActiv = definePolicy({ agentId: A.agent.agentId, wallet: A.cheie.address, spend: { amount: '1', windowSeconds: 60 } }).policy;
|
||||
cer(/spending limit in this wallet's asset/.test(arunca(() => createWallet({ policy: faraLimita, evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || '')
|
||||
&& /spending limit in this wallet's asset/.test(arunca(() => createWallet({ policy: altActiv, evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || ''),
|
||||
'14. CONTROL: o politica fara limita, sau cu limita in alt activ (AERE) -> portofelul nu porneste');
|
||||
|
||||
} catch (e) { cer(false, `proba s-a oprit: ${String(e.message || e).slice(0, 160)}`); } finally { for (const s of deschise) inchide(s); }
|
||||
|
||||
console.log(`\nagent-wallet: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
|
||||
process.exitCode = rau ? 1 : 0;
|
||||
100
agents/x402/proba-x402-testnet.mjs
Normal file
100
agents/x402/proba-x402-testnet.mjs
Normal file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
// Proba cap la cap pe testnetul PUBLIC 28001 (2026-09-29, punctele 23, 25): un agent AI cumpara o resursa x402 de pe un server de
|
||||
// resurse local, platind prin portofelul lui (wallet.mjs) si prin facilitatorul x402 de pe testnet, cu decontare EIP-3009 pe lant.
|
||||
// 1. un portofel NOU (cheie generata aici, tinuta numai in memorie) primeste 0,05 tUSD de la cheia de dezvoltator a testnetului
|
||||
// (singura tranzactie trimisa de proba; decontarile le plateste facilitatorul);
|
||||
// 2. politica agentului: 30000 (0,03 tUSD) pe ora, un singur destinatar, portofelul numit;
|
||||
// 3. trei cumparaturi de 10000 platite si servite; a patra refuzata de politica, inainte de portofel; aceeasi plata trimisa din nou:
|
||||
// 402 (nonce folosit pe lant);
|
||||
// 4. pe lant: soldurile, si authorizationState(portofel, sha256(hash-ul intrarii)) pentru fiecare plata;
|
||||
// 5. dosarul-dovada scris (registrul, politica, platile) si verificat cu verifica-plati.mjs: VALID, inclusiv "orice Transfer din
|
||||
// portofel e o plata din registru"; o copie cu o suma schimbata in registru: INVALID.
|
||||
// Refuza orice alt lant decat 28001 (citeste eth_chainId). Cheia de dezvoltator se citeste din fisierul dat in AERE_TESTNET_KEY_FILE
|
||||
// (un rand d=<hex> sau PRIVATE_KEY=0x<hex>) si nu se tipareste.
|
||||
// AERE_TESTNET_KEY_FILE=<fisier> node proba-x402-testnet.mjs [--rpc URL] [--facilitator URL]
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { definePolicy } from '../agent-policy.mjs';
|
||||
import { newAgentIdentity, openLedger } from '../agent-ledger.mjs';
|
||||
import { createWallet, assetId, nonceForEntry, incarcaEthers } from './wallet.mjs';
|
||||
import { payWithAgent } from './client.mjs';
|
||||
import { createResourceServer } from './resource-server.mjs';
|
||||
import { verificaPlati } from './verifica-plati.mjs';
|
||||
|
||||
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
||||
const a = process.argv.slice(2); const get = (f, d) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : d; };
|
||||
const RPC = get('--rpc', 'https://testnet-rpc.aere.network');
|
||||
const FAC = get('--facilitator', 'https://testnet-rpc.aere.network/x402');
|
||||
const NET = 'eip155:28001';
|
||||
const TOKEN = { address: '0x8215bA247a3574af8EBC36606eB437811E318FBd', name: 'AereTestUSD', version: '2' };
|
||||
const ethers = incarcaEthers();
|
||||
let ok = 0, rau = 0;
|
||||
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; return c; };
|
||||
const taie = (s) => String(s || '').replace(/(0x)?[0-9a-fA-F]{60,}/g, '<hex>').slice(0, 160);
|
||||
|
||||
const kf = process.env.AERE_TESTNET_KEY_FILE;
|
||||
if (!kf || !fs.existsSync(kf)) { console.log('NEMASURAT: AERE_TESTNET_KEY_FILE nu numeste un fisier cu cheia de dezvoltator a testnetului'); process.exit(2); }
|
||||
const rand = fs.readFileSync(kf, 'utf8').split(/\r?\n/).map((l) => l.trim()).find((l) => /^(d|PRIVATE_KEY)=/.test(l)) || '';
|
||||
const hex = rand.replace(/^(d|PRIVATE_KEY)=/, '').replace(/^0x/, '').trim();
|
||||
if (!/^[0-9a-fA-F]{1,64}$/.test(hex)) { console.log('NEMASURAT: fisierul cheii nu are un rand d=<hex> sau PRIVATE_KEY=0x<hex>'); process.exit(2); }
|
||||
const provider = new ethers.JsonRpcProvider(RPC, undefined, { staticNetwork: false });
|
||||
const lant = Number((await provider.getNetwork()).chainId);
|
||||
if (lant !== 28001) { console.log(`REFUZ: RPC-ul serveste lantul ${lant}; proba ruleaza numai pe testnetul 28001`); process.exit(2); }
|
||||
const dev = new ethers.Wallet('0x' + hex.padStart(64, '0'), provider);
|
||||
const tUSD = new ethers.Contract(TOKEN.address, ['function transfer(address,uint256) returns (bool)', 'function balanceOf(address) view returns (uint256)',
|
||||
'function authorizationState(address,bytes32) view returns (bool)'], dev);
|
||||
|
||||
try {
|
||||
// 1. portofelul nou, finantat
|
||||
const cheie = ethers.Wallet.createRandom(); const payee = ethers.Wallet.createRandom().address;
|
||||
const tx0 = await tUSD.transfer(cheie.address, 50000n); const rc0 = await tx0.wait(1, 120000);
|
||||
cer(rc0 && rc0.status === 1, `1. portofelul nou ${cheie.address} primeste 0,05 tUSD (bloc ${rc0 && rc0.blockNumber})`);
|
||||
// 2. agentul si politica lui
|
||||
const agent = newAgentIdentity();
|
||||
const { policy, policyHash } = definePolicy({ agentId: agent.agentId, spend: { amount: '30000', windowSeconds: 3600, asset: assetId(NET, TOKEN.address) },
|
||||
recipients: [payee], wallet: cheie.address });
|
||||
const wallet = createWallet({ policy, policyHash, evmPrivateKey: cheie.privateKey, network: NET, token: TOKEN });
|
||||
const L = openLedger({ identity: agent, policy, policyHash });
|
||||
const cerinta = { scheme: 'exact', network: NET, amount: '10000', asset: TOKEN.address, payTo: payee, maxTimeoutSeconds: 120, extra: { name: TOKEN.name, version: TOKEN.version } };
|
||||
const rs = createResourceServer({ requirement: cerinta, facilitator: FAC, content: 'the paid content' });
|
||||
const url = await rs.listen();
|
||||
// 3. trei cumparaturi platite, a patra refuzata
|
||||
const plati = [];
|
||||
for (let i = 0; i < 3; i++) plati.push(await payWithAgent({ url, ledger: L, wallet }));
|
||||
cer(plati.every((p) => p.paid && p.status === 200 && p.body === 'the paid content' && /^0x[0-9a-f]{64}$/.test(p.settlement.transaction || '')),
|
||||
`3. trei cumparaturi de 0,01 tUSD platite prin facilitatorul testnetului si servite (${plati.map((p) => p.status + (p.reason ? ' ' + taie(p.reason) : '')).join('; ')})`);
|
||||
const p4 = await payWithAgent({ url, ledger: L, wallet });
|
||||
cer(!p4.paid && /policy refused.*over the limit/.test(p4.reason || '') && wallet.status().signed === 3, `3. CONTROL: a patra depaseste 0,03 pe ora: refuzata de politica, portofelul a semnat tot 3 (${taie(p4.reason)})`);
|
||||
// reluarea: acelasi PaymentPayload pe care serverul l-a primit la prima plata
|
||||
const prima = rs.lastPayloads ? rs.lastPayloads[0] : null;
|
||||
if (prima) {
|
||||
const r = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(prima)).toString('base64') } });
|
||||
cer(r.status === 402 && /nonce_already_used|already/.test(await r.text()), '3. CONTROL: prima plata trimisa din nou -> 402 (autorizarea e deja folosita pe lant)');
|
||||
} else cer(false, '3. serverul de resurse nu a pastrat plata primita (lastPayloads)');
|
||||
rs.server.close();
|
||||
// 4. pe lant
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
const soldPayee = await tUSD.balanceOf(payee), soldPortofel = await tUSD.balanceOf(cheie.address);
|
||||
cer(soldPayee === 30000n && soldPortofel === 20000n, `4. pe lant: vanzatorul are 30000, portofelul 20000 (${soldPayee}, ${soldPortofel})`);
|
||||
const folosite = await Promise.all(plati.map((p) => tUSD.authorizationState(cheie.address, nonceForEntry(p.entry.hash))));
|
||||
cer(folosite.every(Boolean), '4. pe lant: autorizarea cu nonce-ul sha256(hash-ul intrarii) e folosita, pentru fiecare plata');
|
||||
// 5. dosarul-dovada, verificat din afara
|
||||
const dovada = { v: 1, kind: 'aere-agent-x402-payments', network: NET, token: TOKEN.address, wallet: cheie.address, fromBlock: rc0.blockNumber,
|
||||
facilitator: FAC, policy, policyHash, ledger: L.export(),
|
||||
payments: plati.map((p) => ({ entrySeq: p.entry.seq, entryHash: p.entry.hash, transaction: p.settlement.transaction })), createdAt: new Date().toISOString() };
|
||||
const dir = path.join(AICI, 'dovezi-28001'); fs.mkdirSync(dir, { recursive: true });
|
||||
const f = path.join(dir, `plati-agent-${dovada.createdAt.slice(0, 19).replace(/[:T]/g, '-')}.json`);
|
||||
fs.writeFileSync(f, JSON.stringify(dovada, null, 1) + '\n');
|
||||
const v = await verificaPlati(dovada, { rpc: RPC, allTransfers: true });
|
||||
cer(v.verdict === 'VALID', `5. verifica-plati pe dosarul-dovada: ${v.verdict} (${v.checks.length} verificari, printre ele: orice Transfer din portofel e o plata din registru)`);
|
||||
const stricat = JSON.parse(JSON.stringify(dovada)); stricat.ledger.entries[plati[1].entry.seq].body.action.amount = '1';
|
||||
const vs = await verificaPlati(stricat, { rpc: RPC });
|
||||
cer(vs.verdict === 'INVALID', `5. CONTROL: o suma schimbata in registrul din dosar -> ${vs.verdict}`);
|
||||
const fara = JSON.parse(JSON.stringify(dovada)); fara.payments = fara.payments.slice(1);
|
||||
const vf = await verificaPlati(fara, { rpc: RPC, allTransfers: true });
|
||||
cer(vf.verdict === 'INVALID', `5. CONTROL: o plata de pe lant scoasa din dosar -> ${vf.verdict} (un Transfer din portofel fara intrare)`);
|
||||
console.log(` dosarul-dovada: ${path.relative(process.cwd(), f)}`);
|
||||
} catch (e) { cer(false, `proba s-a oprit: ${taie(e.shortMessage || e.message)}`); }
|
||||
console.log(`\nagent-x402-testnet: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
|
||||
process.exitCode = rau ? 1 : 0;
|
||||
39
agents/x402/resource-server.mjs
Normal file
39
agents/x402/resource-server.mjs
Normal file
@ -0,0 +1,39 @@
|
||||
// Un server de resurse x402 v2, minimal (2026-09-29): raspunde 402 cu PAYMENT-REQUIRED, iar la reluare trimite plata facilitatorului
|
||||
// (POST /verify, apoi POST /settle) si serveste resursa numai dupa o decontare reusita, cu PAYMENT-RESPONSE. E piesa pe care o are
|
||||
// orice vanzator x402; aici e ca probele agentilor sa plateasca cap la cap printr-un facilitator adevarat (cel de pe testnetul 28001)
|
||||
// sau printr-unul local. Nu are voie sa accepte o plata pentru alta cerinta decat a emis-o: `accepted` trebuie sa fie chiar ea.
|
||||
import http from 'node:http';
|
||||
import { b64json, dinB64json } from './client.mjs';
|
||||
|
||||
/**
|
||||
* @param {object} o { requirement:{scheme,network,amount,asset,payTo,maxTimeoutSeconds,extra}, facilitator: URL, path, content, description }
|
||||
*/
|
||||
export function createResourceServer({ requirement, facilitator, path: cale = '/premium', content = 'the paid content', description = 'a paid resource', fetchImpl = fetch }) {
|
||||
const fac = String(facilitator).replace(/\/+$/, '');
|
||||
const post = async (p, body) => { const r = await fetchImpl(fac + p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); return r.json(); };
|
||||
const cheie = (x) => JSON.stringify(['scheme', 'network', 'amount', 'asset', 'payTo', 'maxTimeoutSeconds'].map((k) => String(x && x[k]).toLowerCase()));
|
||||
const decontari = [], primite = [];
|
||||
const srv = http.createServer(async (req, res) => {
|
||||
const url = (req.url || '/').split('?')[0];
|
||||
if (url !== cale) { res.writeHead(404); return res.end('not found'); }
|
||||
const resource = { url: `http://${req.headers.host}${cale}`, description, mimeType: 'text/plain' };
|
||||
const cere = (error) => { const pr = { x402Version: 2, error, resource, accepts: [requirement] };
|
||||
res.writeHead(402, { 'content-type': 'application/json', 'PAYMENT-REQUIRED': b64json(pr) }); res.end(JSON.stringify(pr)); };
|
||||
const h = req.headers['payment-signature'];
|
||||
if (!h) return cere('payment required');
|
||||
const payload = dinB64json(h);
|
||||
if (!payload || payload.x402Version !== 2 || !payload.payload) return cere('the PAYMENT-SIGNATURE header is not an x402 v2 payment payload');
|
||||
if (cheie(payload.accepted) !== cheie(requirement)) return cere('the payment is for another requirement than this resource issued');
|
||||
primite.push(payload);
|
||||
try {
|
||||
const body = { x402Version: 2, paymentPayload: payload, paymentRequirements: requirement };
|
||||
const v = await post('/verify', body);
|
||||
if (!v.isValid) return cere(`payment not valid: ${v.invalidReason}`);
|
||||
const s = await post('/settle', body);
|
||||
decontari.push(s);
|
||||
if (!s.success) return cere(`settlement failed: ${String(s.errorReason).slice(0, 120)}`);
|
||||
res.writeHead(200, { 'content-type': 'text/plain', 'PAYMENT-RESPONSE': b64json(s) }); res.end(content);
|
||||
} catch (e) { res.writeHead(502, { 'content-type': 'text/plain' }); res.end('facilitator unreachable: ' + String(e.message).slice(0, 80)); }
|
||||
});
|
||||
return { server: srv, settlements: decontari, lastPayloads: primite, listen: (port = 0, host = '127.0.0.1') => new Promise((r) => srv.listen(port, host, () => r(`http://${host}:${srv.address().port}${cale}`))) };
|
||||
}
|
||||
70
agents/x402/verifica-plati.mjs
Normal file
70
agents/x402/verifica-plati.mjs
Normal file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
// verifica-plati.mjs: dovada ca platile de pe lant ale unui portofel de agent au fost PERMISE de politica agentului, verificata de
|
||||
// oricine, din afara (2026-09-29, punctele 23, 25). Primeste o dosar-dovada {policy, policyHash, ledger, wallet, network, token,
|
||||
// payments:[{entrySeq, entryHash, transaction}], fromBlock?} si un RPC, si cere:
|
||||
// 1. registrul verifica fara incredere sub politica (verifyLedger);
|
||||
// 2. fiecare plata numeste o intrare a registrului: plata permisa, din portofel, catre destinatar, cu suma si activul;
|
||||
// 3. pe lant, tranzactia platii are status 1 si emite, din contractul activului, AuthorizationUsed(portofel, nonce) cu nonce =
|
||||
// sha256(hash-ul intrarii) si Transfer(portofel, destinatar, suma) - deci plata de pe lant e chiar cea din registru;
|
||||
// 4. cu --all-transfers: ORICE Transfer din portofel in intervalul de blocuri e una din platile de mai sus (nicio plata fara intrare).
|
||||
// Ce nu dovedeste: ca serviciul cumparat a fost livrat; ca portofelul nu a semnat si autorizari nedecontate (acelea nu misca bani).
|
||||
// node verifica-plati.mjs <dovada.json> --rpc <url> [--all-transfers] iesire 0 VALID, 1 INVALID, 2 NEMASURAT
|
||||
import fs from 'node:fs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { verifyLedger } from '../agent-ledger.mjs';
|
||||
import { incarcaEthers, nonceForEntry, assetId } from './wallet.mjs';
|
||||
|
||||
export async function verificaPlati(d, { rpc, allTransfers = false, fetchImpl = fetch } = {}) {
|
||||
const ethers = incarcaEthers();
|
||||
const rez = []; const ok = (name, pass, detail = '') => { rez.push({ name, pass, detail }); return pass; };
|
||||
const apel = async (method, params) => {
|
||||
const r = await fetchImpl(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
|
||||
const j = await r.json(); if (j.error) throw new Error(`${method}: ${String(j.error.message).slice(0, 80)}`); return j.result;
|
||||
};
|
||||
const chain = Number(String(d.network).split(':')[1]);
|
||||
let lantul;
|
||||
try { lantul = Number(await apel('eth_chainId', [])); } catch (e) { return { verdict: 'UNMEASURED', checks: [{ name: 'rpc', pass: null, detail: e.message }] }; }
|
||||
if (!ok(`the RPC serves chain ${chain}`, lantul === chain, `it serves ${lantul}`)) return { verdict: 'UNMEASURED', checks: rez };
|
||||
const v = verifyLedger(d.ledger, { policy: d.policy, policyHash: d.policyHash, revocations: d.revocations || [] });
|
||||
ok(`the ledger verifies under the policy (${d.ledger.entries.length} entries)`, v.ok, v.error || '');
|
||||
const token = String(d.token).toLowerCase(), wallet = String(d.wallet).toLowerCase(), asset = assetId(d.network, token);
|
||||
const T_AUTH = ethers.id('AuthorizationUsed(address,bytes32)'), T_TRANSFER = ethers.id('Transfer(address,address,uint256)');
|
||||
const topicAdresa = (a) => '0x' + '0'.repeat(24) + String(a).toLowerCase().slice(2);
|
||||
const txPlati = new Set();
|
||||
for (const p of d.payments || []) {
|
||||
const e = d.ledger.entries[p.entrySeq];
|
||||
if (!ok(`payment ${p.entrySeq}: the entry is in the ledger`, !!e && e.hash === p.entryHash)) continue;
|
||||
const a = e.body.action;
|
||||
ok(`payment ${p.entrySeq}: the entry is an allowed payment from the wallet in the asset`, e.body.decision.allowed && a.kind === 'payment' && String(a.from).toLowerCase() === wallet && a.asset === asset);
|
||||
let rc;
|
||||
try { rc = await apel('eth_getTransactionReceipt', [p.transaction]); } catch (x) { ok(`payment ${p.entrySeq}: receipt readable`, false, x.message); continue; }
|
||||
if (!ok(`payment ${p.entrySeq}: transaction ${String(p.transaction).slice(0, 12)} is on chain with status 1`, !!rc && rc.status === '0x1')) continue;
|
||||
txPlati.add(String(p.transaction).toLowerCase());
|
||||
const logs = rc.logs.filter((l) => l.address.toLowerCase() === token);
|
||||
const nonce = nonceForEntry(e.hash).toLowerCase();
|
||||
ok(`payment ${p.entrySeq}: AuthorizationUsed(wallet, sha256(entry hash)) is emitted by the asset`, logs.some((l) => l.topics[0] === T_AUTH && l.topics[1].toLowerCase() === topicAdresa(wallet) && l.topics[2].toLowerCase() === nonce));
|
||||
ok(`payment ${p.entrySeq}: Transfer(wallet, ${String(a.to).slice(0, 10)}.., ${a.amount}) is emitted by the asset`, logs.some((l) => l.topics[0] === T_TRANSFER && l.topics[1].toLowerCase() === topicAdresa(wallet)
|
||||
&& l.topics[2].toLowerCase() === topicAdresa(a.to) && BigInt(l.data) === BigInt(a.amount)));
|
||||
}
|
||||
if (allTransfers) {
|
||||
try {
|
||||
const logs = await apel('eth_getLogs', [{ address: token, fromBlock: '0x' + Number(d.fromBlock || 0).toString(16), toBlock: 'latest', topics: [T_TRANSFER, topicAdresa(wallet)] }]);
|
||||
const straine = logs.filter((l) => !txPlati.has(l.transactionHash.toLowerCase()));
|
||||
ok(`every Transfer out of the wallet since block ${d.fromBlock || 0} is a payment in the ledger (${logs.length} transfers)`, straine.length === 0, straine.map((l) => l.transactionHash.slice(0, 12)).join(', '));
|
||||
} catch (x) { rez.push({ name: 'all transfers out of the wallet', pass: null, detail: x.message }); }
|
||||
}
|
||||
const verdict = rez.some((c) => c.pass === false) ? 'INVALID' : rez.some((c) => c.pass === null) ? 'UNMEASURED' : 'VALID';
|
||||
return { verdict, checks: rez };
|
||||
}
|
||||
|
||||
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
||||
const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; };
|
||||
const f = a.find((x) => !x.startsWith('--') && x !== get('--rpc'));
|
||||
if (!f || !get('--rpc')) { console.error('usage: node verifica-plati.mjs <evidence.json> --rpc <url> [--all-transfers]'); process.exitCode = 2; }
|
||||
else {
|
||||
const r = await verificaPlati(JSON.parse(fs.readFileSync(f, 'utf8')), { rpc: get('--rpc'), allTransfers: a.includes('--all-transfers') });
|
||||
for (const c of r.checks) console.log(` ${c.pass === true ? 'OK ' : c.pass === false ? 'FAIL' : '-- '} ${c.name}${c.detail ? ' (' + c.detail + ')' : ''}`);
|
||||
console.log(r.verdict);
|
||||
process.exitCode = r.verdict === 'VALID' ? 0 : r.verdict === 'INVALID' ? 1 : 2;
|
||||
}
|
||||
}
|
||||
218
agents/x402/wallet.mjs
Normal file
218
agents/x402/wallet.mjs
Normal file
@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env node
|
||||
// Aere Agent Wallet (roadmap punctele 23, 25, 26, 2026-09-29): portofelul de plati x402 al unui agent AI, care semneaza o plata NUMAI
|
||||
// daca agentul a inregistrat-o in registrul lui (agent-ledger.mjs) si politica proprietarului o permite. Agentul NU tine cheia de
|
||||
// plata: o tine portofelul (serviciul proprietarului). Agentul are numai cheia lui ML-DSA-65 cu care isi semneaza registrul; cererea
|
||||
// catre portofel e autentificata chiar de intrarea noua din registru, pe care numai agentul o poate semna.
|
||||
//
|
||||
// CE VERIFICA, la fiecare cerere (authorize):
|
||||
// 1. cerinta x402 (schema exact, reteaua si activul portofelului, payTo, suma intreaga, termenul);
|
||||
// 2. registrul intreg, fara incredere (verifyLedger): politica FIXATA de proprietar la pornire (hash-ul ei), identitatea, fiecare
|
||||
// semnatura, lantul, decizia re-rulata, revocarile primite de portofel de la proprietar, si ANCORELE: capetele pe care portofelul
|
||||
// le-a vazut el insusi, cu ora lui. Portofelul e martorul: un registru care nu continua ce a vazut e o ramura (si daca a pastrat
|
||||
// intrarea, scoate dovada de echivocare), iar o intrare scrisa dupa un cap nu poate declara o ora dinaintea lui;
|
||||
// 3. ultima intrare e o plata NOUA, permisa, din portofelul acesta, catre payTo, cu suma si activul cerute, legata de cumparatura prin
|
||||
// `ref` (hash-ul cerintei), scrisa ACUM dupa ceasul portofelului;
|
||||
// 4. limita, a doua oara, fata de ce a SEMNAT portofelul (nu numai fata de ce spune registrul): chiar un registru rescris sau ramificat
|
||||
// nu poate scoate din portofel mai mult decat permite politica.
|
||||
// Apoi semneaza o autorizare EIP-3009 (TransferWithAuthorization) al carei nonce e sha256(hash-ul intrarii): plata de pe lant se leaga
|
||||
// de intrarea din registru, si aceeasi intrare nu poate plati de doua ori (nici la portofel, nici pe lant). Intoarce PaymentPayload-ul
|
||||
// x402 v2, de pus de agent in antetul PAYMENT-SIGNATURE, si un plic AIP-23 cu decizia.
|
||||
//
|
||||
// CE NU FACE: nu trimite tranzactii (decontarea o face facilitatorul x402 al serverului de resurse); nu tine alt activ decat unul
|
||||
// EIP-3009 configurat; nu e custodie fara incredere: cine tine cheia portofelului (proprietarul) poate plati fara agent. Un portofel-
|
||||
// contract 2-din-2 (agentul + politica, ERC-1271) ar scoate si increderea asta; nu e facut.
|
||||
//
|
||||
// node wallet.mjs serve --config wallet.json [--port 8793] serviciul HTTP (POST /authorize, POST /revocations, GET /status)
|
||||
// wallet.json: { policy, policyHash, network, token:{address,name,version}, evmKeyFile, stateFile, toleranceSeconds?, maxValiditySeconds? }
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import crypto from 'node:crypto';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { hashPolicy, checkAction, decisionEnvelope, canonical } from '../agent-policy.mjs';
|
||||
import { verifyLedger, verifyEquivocation, sessionId } from '../agent-ledger.mjs';
|
||||
import { verifyApproval, verifyRevocation } from '../agent-aprobare.mjs';
|
||||
|
||||
export const VERSION = 'aere-agent-wallet/1 (2026-09-29)';
|
||||
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/** ethers: AERE_ETHERS (o cale, pentru copiile controlului negativ), langa portofel (npm install aici), sau din contracts/node_modules */
|
||||
export function incarcaEthers() {
|
||||
const req = createRequire(import.meta.url);
|
||||
if (process.env.AERE_ETHERS) return req(process.env.AERE_ETHERS);
|
||||
try { return req('ethers'); } catch { /* nu e langa portofel */ }
|
||||
try { return req(path.resolve(AICI, '..', '..', '..', 'contracts', 'node_modules', 'ethers')); } catch { /* nici in depozitul de dezvoltare */ }
|
||||
throw new Error('the wallet needs the ethers package: run `npm install` in this directory');
|
||||
}
|
||||
const sha = (s) => '0x' + crypto.createHash('sha256').update(typeof s === 'string' ? Buffer.from(s, 'utf8') : s).digest('hex');
|
||||
const ADRESA = /^0x[0-9a-fA-F]{40}$/;
|
||||
const INTREG = /^[0-9]+$/;
|
||||
const TIPURI = { TransferWithAuthorization: [
|
||||
{ name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' },
|
||||
{ name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' }] };
|
||||
export { TIPURI as EIP3009_TYPES };
|
||||
|
||||
/** activul, ca identificator CAIP-19: eip155:<chain>/erc20:<adresa cu litere mici> */
|
||||
export const assetId = (network, token) => `${network}/erc20:${String(token).toLowerCase()}`;
|
||||
/** legatura intrarii din registru cu cumparatura: hash-ul canonic al resursei si al cerintei x402 */
|
||||
export function purchaseRef(resource, req) {
|
||||
return sha(canonical({ resource: resource && resource.url ? String(resource.url) : null, scheme: String(req.scheme), network: String(req.network),
|
||||
asset: String(req.asset).toLowerCase(), payTo: String(req.payTo).toLowerCase(), amount: String(req.amount) }));
|
||||
}
|
||||
/** actiunea pe care agentul o scrie in registru inainte sa ceara plata (acelasi calcul la agent si la portofel) */
|
||||
export function x402Action(walletAddress, resource, req) {
|
||||
return { kind: 'payment', from: String(walletAddress).toLowerCase(), to: String(req.payTo).toLowerCase(), amount: String(req.amount),
|
||||
asset: assetId(req.network, req.asset), ref: purchaseRef(resource, req) };
|
||||
}
|
||||
/** nonce-ul EIP-3009 al platii: sha256(hash-ul intrarii), deci plata de pe lant numeste intrarea */
|
||||
export const nonceForEntry = (entryHash) => sha(Buffer.from(String(entryHash), 'utf8'));
|
||||
|
||||
/**
|
||||
* @param {object} c { policy, policyHash, evmPrivateKey, network, token:{address,name,version}, state?, saveState?, now?, toleranceSeconds?, maxValiditySeconds? }
|
||||
*/
|
||||
export function createWallet(c) {
|
||||
const ethers = incarcaEthers();
|
||||
const { policy, policyHash } = hashPolicy(c.policy);
|
||||
if (c.policyHash != null && String(c.policyHash).toLowerCase() !== policyHash) throw new Error('agent-wallet: the policy does not hash to the pinned policyHash');
|
||||
const cont = new ethers.Wallet(String(c.evmPrivateKey || '').trim());
|
||||
const adresa = cont.address.toLowerCase();
|
||||
if (policy.wallet !== adresa) throw new Error('agent-wallet: the policy does not name this wallet (policy.wallet must be its address)');
|
||||
if (!/^eip155:[0-9]+$/.test(String(c.network))) throw new Error('agent-wallet: network must be eip155:<chainId>');
|
||||
if (!c.token || !ADRESA.test(String(c.token.address)) || !c.token.name || !c.token.version) throw new Error('agent-wallet: token needs address, name and version (its EIP-712 domain)');
|
||||
const network = String(c.network), token = String(c.token.address).toLowerCase();
|
||||
const domeniu = { name: String(c.token.name), version: String(c.token.version), chainId: Number(network.split(':')[1]), verifyingContract: ethers.getAddress(token) };
|
||||
// un portofel fara limita de cheltuiala in activul lui ar semna orice suma pe care o scrie agentul: nu porneste
|
||||
if (!policy.spend || policy.spend.asset !== assetId(network, token)) throw new Error(`agent-wallet: the policy needs a spending limit in this wallet's asset (spend.asset = ${assetId(network, token)})`);
|
||||
const now = c.now || (() => Math.floor(Date.now() / 1000));
|
||||
const TOL = Number(c.toleranceSeconds ?? 120);
|
||||
const VALID = Number(c.maxValiditySeconds ?? 300);
|
||||
// starea: pe SESIUNE (o sesiune = agentul sub o politica) capetele vazute (ancore) si ultima intrare (pentru dovada de echivocare);
|
||||
// pe AGENT platile SEMNATE si revocarile primite. Platile semnate raman peste o schimbare de politica: un registru nou (politica noua,
|
||||
// sesiune noua) incepe de la zero cheltuiala, portofelul nu. Se salveaza INAINTE de a intoarce semnatura.
|
||||
const stare = c.state || { v: 1, agentId: policy.agentId, sessions: {}, signed: [], revocations: [] };
|
||||
if (stare.agentId !== policy.agentId) throw new Error('agent-wallet: the saved state belongs to another agent');
|
||||
const sesiune = sessionId(policy.agentId, policyHash);
|
||||
const S = stare.sessions[sesiune] || (stare.sessions[sesiune] = { anchors: [], last: null, lastEntry: null });
|
||||
const salveaza = () => { if (c.saveState) c.saveState(stare); };
|
||||
const refuz = (error, extra = {}) => ({ ok: false, error, ...extra });
|
||||
|
||||
async function autorizeaza({ requirements: req, resource = null, ledger } = {}) {
|
||||
const t = Number(now());
|
||||
// 1. cerinta
|
||||
if (!req || req.scheme !== 'exact') return refuz('only the exact scheme is supported');
|
||||
if (req.network !== network) return refuz(`the requirement is for network ${String(req.network).slice(0, 40)}, this wallet pays on ${network}`);
|
||||
if (String(req.asset || '').toLowerCase() !== token) return refuz('the requirement is for another asset than this wallet holds');
|
||||
if (!ADRESA.test(String(req.payTo || ''))) return refuz('payTo is not an address');
|
||||
if (!INTREG.test(String(req.amount || '')) || BigInt(req.amount) === 0n) return refuz('amount is not a positive integer');
|
||||
const timeout = Number(req.maxTimeoutSeconds);
|
||||
if (!Number.isInteger(timeout) || timeout < 10) return refuz('maxTimeoutSeconds is missing or below 10 seconds');
|
||||
// 2. registrul, fara incredere, fata de ce a vazut portofelul
|
||||
if (!ledger || !Array.isArray(ledger.entries) || !ledger.entries.length) return refuz('a ledger with the payment entry is required');
|
||||
const v = verifyLedger(ledger, { policy, policyHash, maxTime: t + 5, revocations: stare.revocations, anchors: S.anchors, anchorTolerance: TOL });
|
||||
if (!v.ok) {
|
||||
const r = refuz(`the ledger does not verify${v.seq != null ? ` at seq ${v.seq}` : ''}: ${v.error}`);
|
||||
// o ramura: intrarea pastrata de la ultimul cap si cea de acum, la aceeasi pozitie, semnate amandoua de agent, sunt dovada
|
||||
const acum = S.lastEntry && ledger.entries[S.lastEntry.seq];
|
||||
if (acum && acum.hash !== S.lastEntry.hash) {
|
||||
const p = { v: 1, kind: 'aere-agent-equivocation', agentId: ledger.agentId, session: ledger.session, publicKeyPem: ledger.publicKeyPem,
|
||||
seq: S.lastEntry.seq, a: S.lastEntry, b: acum };
|
||||
if (p.agentId === policy.agentId && verifyEquivocation(p).ok) r.equivocation = p;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
// 3. ultima intrare: plata noua, permisa, legata de cumparatura, scrisa acum
|
||||
const e = ledger.entries[ledger.entries.length - 1];
|
||||
if (S.last && e.seq <= S.last.seq) return refuz(`the last entry (seq ${e.seq}) was already seen; each payment needs a new entry`);
|
||||
const b = e.body, a = b.action;
|
||||
if (!b.decision.allowed || a.kind !== 'payment') return refuz('the last entry is not an allowed payment');
|
||||
const asteptat = x402Action(adresa, resource, req);
|
||||
for (const k of ['from', 'to', 'amount', 'asset', 'ref']) if (String(a[k] ?? '').toLowerCase() !== String(asteptat[k]).toLowerCase()) return refuz(`the payment entry names another ${k} than this purchase`);
|
||||
if (!(Number(b.at) >= t - TOL && Number(b.at) <= t + 5)) return refuz(`the payment entry is dated ${b.at}, not now (${t}) by this wallet's clock`);
|
||||
// 4. limita, fata de ce a SEMNAT portofelul (aprobarile intrarii, verificate; refolosirea unei aprobari o prinde deja verifyLedger,
|
||||
// iar o aprobare e legata de politica, deci nu trece intr-o sesiune noua)
|
||||
const aprobatori = [];
|
||||
for (const ap of b.approvals || []) { const r = verifyApproval(ap, { policy, policyHash, action: a, at: Number(b.at) }); if (r.ok) aprobatori.push(r.humanId); }
|
||||
// (revocarile nu se mai dau aici: verifyLedger le-a judecat deja, iar o revocare e legata de politica, deci nu e alta multime)
|
||||
const d = checkAction(policy, { ...a, at: Number(b.at) }, stare.signed.map((s) => ({ amount: s.amount, at: s.at })), { approvers: aprobatori });
|
||||
if (!d.allowed) return refuz(`by what this wallet has signed: ${d.reason}`);
|
||||
// semnatura EIP-3009
|
||||
const authorization = { from: cont.address, to: ethers.getAddress(req.payTo), value: String(req.amount), validAfter: String(t - 5),
|
||||
validBefore: String(t + Math.min(timeout, VALID)), nonce: nonceForEntry(e.hash) };
|
||||
const signature = await cont.signTypedData(domeniu, TIPURI, { ...authorization, value: BigInt(authorization.value),
|
||||
validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) });
|
||||
S.anchors.push({ seq: e.seq, hash: e.hash, at: t });
|
||||
S.last = { seq: e.seq, hash: e.hash }; S.lastEntry = e;
|
||||
stare.signed.push({ seq: e.seq, entryHash: e.hash, amount: String(req.amount), at: Number(b.at), payTo: authorization.to, nonce: authorization.nonce });
|
||||
salveaza();
|
||||
const paymentPayload = { x402Version: 2, ...(resource ? { resource } : {}), accepted: req, payload: { signature, authorization },
|
||||
extensions: { 'aere-agent-ledger': { agentId: ledger.agentId, policyHash, session: ledger.session, entrySeq: e.seq, entryHash: e.hash } } };
|
||||
return { ok: true, paymentPayload, header: Buffer.from(JSON.stringify(paymentPayload), 'utf8').toString('base64'),
|
||||
receipt: { entrySeq: e.seq, entryHash: e.hash, nonce: authorization.nonce, from: cont.address, to: authorization.to, value: authorization.value },
|
||||
decision: decisionEnvelope({ policyHash, action: a, decision: d, createdAt: new Date(t * 1000).toISOString() }) };
|
||||
}
|
||||
// cererile se judeca UNA CATE UNA: verificarea si semnatura au un `await` intre ele, iar doua cereri deodata ar trece amandoua de
|
||||
// limita inainte ca vreuna sa fie scrisa in stare
|
||||
let coada = Promise.resolve();
|
||||
function authorize(x) { const r = coada.then(() => autorizeaza(x)); coada = r.catch(() => {}); return r; }
|
||||
function addRevocation(rv) {
|
||||
const r = verifyRevocation(rv, { policy, policyHash });
|
||||
if (!r.ok) return refuz(`revocation refused: ${r.error}`);
|
||||
stare.revocations.push(rv); salveaza();
|
||||
return { ok: true, revokedAt: r.revokedAt };
|
||||
}
|
||||
const status = () => ({ version: VERSION, address: cont.address, network, asset: token, agentId: policy.agentId, policyHash,
|
||||
lastSeq: S.last ? S.last.seq : null, signed: stare.signed.length, revocations: stare.revocations.length });
|
||||
// starea, ca sa fie pastrata peste o schimbare de politica (acelasi agent, acelasi portofel): o copie, nu referinta
|
||||
const exportState = () => JSON.parse(JSON.stringify(stare));
|
||||
return { address: cont.address, network, token, policyHash, authorize, addRevocation, status, exportState };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- serviciul HTTP (numai pe 127.0.0.1 implicit)
|
||||
export function serve(wallet, { port = 8793, host = '127.0.0.1', limit = 8 << 20 } = {}) {
|
||||
const trimite = (res, cod, o) => { const b = JSON.stringify(o); res.writeHead(cod, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(b) }); res.end(b); };
|
||||
const citeste = (req) => new Promise((resolve) => { const parti = []; let n = 0;
|
||||
req.on('data', (x) => { n += x.length; if (n > limit) { resolve(null); req.destroy(); } else parti.push(x); });
|
||||
req.on('end', () => { try { resolve(JSON.parse(Buffer.concat(parti).toString('utf8'))); } catch { resolve(null); } }); });
|
||||
const srv = http.createServer(async (req, res) => {
|
||||
const url = (req.url || '/').split('?')[0];
|
||||
try {
|
||||
if (req.method === 'GET' && url === '/status') return trimite(res, 200, wallet.status());
|
||||
if (req.method === 'POST' && (url === '/authorize' || url === '/revocations')) {
|
||||
const body = await citeste(req);
|
||||
if (!body) return trimite(res, 400, { ok: false, error: 'invalid or too large JSON' });
|
||||
const r = url === '/authorize' ? await wallet.authorize(body) : wallet.addRevocation(body);
|
||||
return trimite(res, r.ok ? 200 : 403, r);
|
||||
}
|
||||
trimite(res, 404, { ok: false, error: 'not found' });
|
||||
} catch (e) { trimite(res, 500, { ok: false, error: String(e.message || e).replace(/(0x)?[0-9a-fA-F]{40,}/g, '<hex>').slice(0, 200) }); }
|
||||
});
|
||||
return new Promise((resolve) => srv.listen(port, host, () => resolve(srv)));
|
||||
}
|
||||
|
||||
/** un portofel din fisierul de configurare: cheia EVM citita dintr-un fisier (un singur rand hex sau d=/PRIVATE_KEY=), starea intr-un fisier */
|
||||
export function walletFromConfig(file) {
|
||||
const c = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
const brut = fs.readFileSync(path.resolve(path.dirname(file), c.evmKeyFile), 'utf8').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
||||
const rand = brut.find((l) => /^(d|PRIVATE_KEY)=/.test(l)) || brut[0] || '';
|
||||
const hex = rand.replace(/^(d|PRIVATE_KEY)=/, '').replace(/^0x/, '').trim();
|
||||
if (!/^[0-9a-fA-F]{1,64}$/.test(hex)) throw new Error('the EVM key file has no hex key (a line of hex, d=<hex> or PRIVATE_KEY=0x<hex>)');
|
||||
const sf = c.stateFile ? path.resolve(path.dirname(file), c.stateFile) : null;
|
||||
const state = sf && fs.existsSync(sf) ? JSON.parse(fs.readFileSync(sf, 'utf8')) : undefined;
|
||||
const saveState = sf ? (s) => { const tmp = `${sf}.${process.pid}.tmp`; fs.writeFileSync(tmp, JSON.stringify(s)); fs.renameSync(tmp, sf); } : null;
|
||||
return createWallet({ ...c, evmPrivateKey: '0x' + hex.padStart(64, '0'), state, saveState });
|
||||
}
|
||||
|
||||
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
||||
const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; };
|
||||
if (a[0] !== 'serve' || !get('--config')) { console.error('usage: node wallet.mjs serve --config wallet.json [--port 8793]'); process.exitCode = 2; }
|
||||
else {
|
||||
try {
|
||||
const w = walletFromConfig(get('--config'));
|
||||
const port = Number(get('--port') || 8793);
|
||||
await serve(w, { port });
|
||||
const s = w.status();
|
||||
console.log(`agent wallet on 127.0.0.1:${port}: ${s.address} pays ${s.asset} on ${s.network} for ${s.agentId} under policy ${s.policyHash}`);
|
||||
} catch (e) { console.error('agent-wallet: ' + String(e.message || e).replace(/(0x)?[0-9a-fA-F]{40,}/g, '<hex>').slice(0, 200)); process.exitCode = 1; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user