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).
101 lines
8.3 KiB
JavaScript
101 lines
8.3 KiB
JavaScript
#!/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;
|