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