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.
64 lines
5.1 KiB
JavaScript
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() };
|
|
}
|