sdk-js/examples/agent-402-pqc-client.js
Liviu 6d6c18ae41 SDK 0.16.12: the address book and typed clients catch up with the live chain, and client mentions get the same discretion as everywhere else
The published repository served the 0.3.0 line from May while the install
page already pointed here. This carries the uncommitted line: 35+ typed
contract clients over the canonical address book, the compliance surface,
and the hybrid PQC wallet helpers.
2026-08-15 14:01:27 +03:00

95 lines
4.1 KiB
JavaScript

// agent-402-pqc-client.js
//
// Drop-in example: agent-side AERE402FacilitatorPQC client. The agent's spending
// authority is rooted in a quantum-durable Falcon key (an AereAgentDID); each payment
// is authorized by a cheap, short-lived, revocable secp256k1 SESSION key. Rotating or
// revoking the Falcon root instantly halts every downstream payment.
//
// The agent calls a paid endpoint, receives 402 + a PQC quote, signs the DID action
// digest with its SESSION key (a single ecrecover on-chain), and retries. The Falcon
// root is never touched on the payment hot path — it only ever issues sessions.
//
// Run:
// yarn add ethers
// SESSION_PRIVATE_KEY=0x... ENDPOINT=http://localhost:3000/v1/inference node agent-402-pqc-client.js
//
// Prereq: an AereAgentDID agent (Falcon root) exists, a session is issued for
// SESSION_PRIVATE_KEY's address (with scope + spend cap + expiry), and the agent's
// AERE402FacilitatorPQC vault is funded. The server issues the 402 quote; the agent only
// needs its session key here.
const { ethers } = require('ethers');
const SESSION_PK = process.env.SESSION_PRIVATE_KEY;
const ENDPOINT = process.env.ENDPOINT ?? 'http://localhost:3000/v1/inference';
if (!SESSION_PK) {
console.error('SESSION_PRIVATE_KEY env required');
process.exit(1);
}
// Domain tags — mirror the contracts exactly.
const PAYMENT_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes('AERE402FacilitatorPQC.v1.payment'));
const ACTION_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes('AereAgentDID.v1.action'));
// keccak256(abi.encode(PAYMENT_DOMAIN, chainId, facilitator, token, payee, resourceId, deadline))
function paymentActionHash(q) {
const enc = ethers.AbiCoder.defaultAbiCoder().encode(
['bytes32', 'uint256', 'address', 'address', 'address', 'bytes32', 'uint256'],
[PAYMENT_DOMAIN, q.chainId, q.facilitator, q.token, q.payee, q.resourceId, BigInt(q.deadline)],
);
return ethers.keccak256(enc);
}
// keccak256(abi.encode(ACTION_DOMAIN, chainId, did, sessionId, scope, actionHash, amount, actionNonce))
function actionDigest(q, actionHash) {
const enc = ethers.AbiCoder.defaultAbiCoder().encode(
['bytes32', 'uint256', 'address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint64'],
[ACTION_DOMAIN, q.chainId, q.did, BigInt(q.sessionId), q.scope, actionHash, BigInt(q.amount), BigInt(q.actionNonce)],
);
return ethers.keccak256(enc);
}
const session = new ethers.SigningKey(SESSION_PK);
(async () => {
console.log(`session ${ethers.computeAddress(session)} calling ${ENDPOINT}...`);
// 1. Unpaid request.
const r1 = await fetch(ENDPOINT);
if (r1.status !== 402) {
console.log(`unexpected status ${r1.status} on first call`);
console.log(await r1.text());
return;
}
const quoteJson = r1.headers.get('x-aere402pqc-quote');
if (!quoteJson) throw new Error('server did not return X-AERE402PQC-Quote');
const quote = JSON.parse(quoteJson);
// quote = { chainId, facilitator, did, sessionId, scope, token, payee, amount,
// resourceId, deadline, actionNonce }
console.log(' received quote:', quote);
// 2. Derive the actionHash (binds the payment to THIS facilitator + terms) and the DID
// action digest the session key must sign.
const actionHash = paymentActionHash(quote);
const digest = actionDigest(quote, actionHash);
// 3. Sign the raw digest with the SESSION key (canonical low-s; matches on-chain ecrecover).
const signature = session.sign(digest).serialized;
console.log(` signed: ${signature.slice(0, 18)}...`);
// 4. Retry with the signature. The provider POSTs
// settle(sessionId, scope, token, payee, amount, resourceId, deadline, signature)
// to AERE402FacilitatorPQC, which enforces the Falcon root lifecycle + scope + spend
// cap + this session signature + anti-replay in one call, then pays out.
const r2 = await fetch(ENDPOINT, {
headers: {
'X-AERE402PQC-Sig': signature,
'X-AERE402PQC-Quote': quoteJson,
},
});
console.log(` retry status: ${r2.status}`);
console.log(` tx hash: ${r2.headers.get('x-aere402pqc-txhash')}`);
console.log(' body:', await r2.json());
})();