aere-quantum/agents/x402/facilitator-local.mjs
Liviu 8b608fe57f agents/x402: a 2-of-2 contract wallet (ERC-1271) that neither the agent nor its owner can spend from alone
AereAgentWallet2of2 holds the agent's tokens and accepts only the agent's signature followed by the policy
service's, over the same digest. In the new cosign mode the wallet service signs only its half, after every
check it already made, and the agent adds its half only after it recomputes the payment itself (payer,
recipient, amount, the nonce of its own ledger entry, validity, digest, declared policy signer).
verifica-plati.mjs requires the wallet's code on chain to be exactly the compiled contract with the two
signers; recompileaza-contract.mjs recompiles the published artifact byte for byte with solc 0.8.23.

Tests: co-signing 16/16, payment verifier 14/14, negative control 30/30, wallet 25/25. On the public
testnet 28001 on 2026-09-29: 13/13 with the 2-of-2 wallet (the agent alone and the policy signer alone
refused by the facilitator and by the token asked on chain) and 9/9 with the wallet key. Evidence in
dovezi-28001/. Nothing here has been run on the Aere Network mainnet.
2026-09-30 00:48:10 +03:00

64 lines
5.1 KiB
JavaScript

// 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';
// `contracts`: portofele-contract 2-din-2 emulate (ERC-1271 ca in AereAgentWallet2of2.sol): { [adresa]: { agentSigner, policySigner } };
// logica adevarata a contractului o proba hardhat (contracts/test/AereAgentWallet2of2.test.js) si testnetul
export function createLocalFacilitator({ network, token, balances = {}, contracts = {} }) {
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;
const mesaj = { ...a, value: BigInt(a.value), validAfter: BigInt(a.validAfter), validBefore: BigInt(a.validBefore) };
try { semnatar = ethers.verifyTypedData(domeniu, EIP3009_TYPES, mesaj, sig); } catch { semnatar = null; }
let valid = !!semnatar && semnatar.toLowerCase() === String(a.from).toLowerCase();
const k = Object.entries(contracts).find(([adr]) => adr.toLowerCase() === String(a.from).toLowerCase());
if (!valid && k && /^0x[0-9a-fA-F]{260}$/.test(String(sig))) {
const digest = ethers.TypedDataEncoder.hash(domeniu, EIP3009_TYPES, mesaj);
const rec = (h) => { try { return ethers.recoverAddress(digest, h).toLowerCase(); } catch { return null; } };
valid = rec(ethers.dataSlice(sig, 0, 65)) === k[1].agentSigner.toLowerCase() && rec(ethers.dataSlice(sig, 65, 130)) === k[1].policySigner.toLowerCase();
}
if (!valid) 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() };
}