aere-quantum/agents/x402/verifica-plati.mjs
Aere Network 2293c1da86 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).
2026-09-30 00:01:34 +03:00

71 lines
5.9 KiB
JavaScript

#!/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;
}
}