@aqb-foundation/aqb-agent 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +4 -16
  2. package/README.md +57 -21
  3. package/WHITEPAPER.md +2 -1
  4. package/client/aqb.mjs +232 -148
  5. package/compile.mjs +44 -0
  6. package/contracts/AgentQuantumCredit.sol +234 -0
  7. package/contracts/AgentRegistry.sol +85 -0
  8. package/contracts/GenesisSale.sol +89 -0
  9. package/contracts/MLDSAVerifier.sol +65 -0
  10. package/contracts/PQSmartAccount.sol +105 -0
  11. package/contracts/interfaces/IPQVerifier.sol +19 -0
  12. package/contracts/mocks/MockUSDC.sol +30 -0
  13. package/contracts/mocks/MockVerifier.sol +13 -0
  14. package/docs/CARD.md +1 -1
  15. package/docs/SPEC.md +5 -4
  16. package/docs/articles/01-payment-rail-agents.md +55 -0
  17. package/docs/articles/01.body +56 -0
  18. package/docs/articles/02-gated-transfers.md +36 -0
  19. package/docs/articles/03-peg-without-oracle.md +33 -0
  20. package/docs/articles/04-discovery-over-landing.md +42 -0
  21. package/docs/articles/04-discovery-over-landing.md.body +40 -0
  22. package/docs/articles/04-discovery-over-landing.md.title +1 -0
  23. package/docs/articles/05-identity-survives-key.md +27 -0
  24. package/docs/articles/05-identity-survives-key.md.body +25 -0
  25. package/docs/articles/05-identity-survives-key.md.title +1 -0
  26. package/docs/articles/06-what-an-agent-needs.md +30 -0
  27. package/docs/articles/06-what-an-agent-needs.md.body +28 -0
  28. package/docs/articles/06-what-an-agent-needs.md.title +1 -0
  29. package/docs/articles/body.json +1 -0
  30. package/docs/articles/body.txt +50 -0
  31. package/docs/articles/title.txt +1 -0
  32. package/docs/pq-note.md +1 -0
  33. package/index.js +241 -0
  34. package/mcp/server.mjs +118 -25
  35. package/package.json +39 -11
  36. package/registry/agent-coin.json +15 -10
  37. package/test/e2e.mjs +100 -0
  38. package/test/evm.mjs +69 -0
  39. package/test/local.mjs +86 -0
  40. package/test/pq.mjs +37 -0
  41. package/test/smoke.mjs +27 -0
package/test/evm.mjs ADDED
@@ -0,0 +1,69 @@
1
+ // Local EVM harness (ethereumjs v10). No JSON-RPC, no hardhat.
2
+ import { createVM } from "@ethereumjs/vm";
3
+ import { MerkleStateManager } from "@ethereumjs/statemanager";
4
+ import { Common, Mainnet, Hardfork } from "@ethereumjs/common";
5
+ import { Address, Account, hexToBytes, bytesToHex, createAddressFromString } from "@ethereumjs/util";
6
+ import { Wallet, AbiCoder, keccak256, solidityPacked, Contract } from "ethers";
7
+ import fs from "node:fs";
8
+ import { signDigest } from "../pq/pq.mjs";
9
+
10
+ const coder = AbiCoder.defaultAbiCoder();
11
+ const common = new Common({ chain: Mainnet, hardfork: Hardfork.Cancun });
12
+ export const vm = await createVM({ common });
13
+ const sm = vm.stateManager;
14
+
15
+ export const PK = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
16
+ export const signer = new Wallet(PK);
17
+ export const me = createAddressFromString(signer.address.toLowerCase());
18
+
19
+ await sm.putAccount(me, new Account(0n, 10n ** 24n));
20
+
21
+ export const art = (n) => JSON.parse(fs.readFileSync(`artifacts/${n}.json`, "utf8"));
22
+
23
+ export async function call(from, to, data, value = 0n) {
24
+ const r = await vm.evm.runCall({
25
+ caller: from, origin: from, to, data: hexToBytes(data), value, gasLimit: 30_000_000n,
26
+ });
27
+ return { status: r.execResult.exceptionError ? 0 : 1, ret: bytesToHex(r.execResult.returnValue), err: r.execResult.exceptionError?.error, logs: r.execResult.logs };
28
+ }
29
+
30
+ export async function deploy(name, args = []) {
31
+ const a = art(name);
32
+ const data = a.bytecode + coder.encode(a.ctorTypes || guessCtor(a.abi), args).slice(2);
33
+ const r = await vm.evm.runCall({ caller: me, origin: me, data: hexToBytes(data), gasLimit: 30_000_000n });
34
+ if (r.execResult.exceptionError) throw new Error(`${name} deploy: ${r.execResult.exceptionError.error}`);
35
+ const addr = r.createdAddress;
36
+ return {
37
+ addr,
38
+ hex: bytesToHex(addr.bytes),
39
+ iface: new Contract(bytesToHex(addr.bytes), a.abi).interface,
40
+ abi: a.abi,
41
+ send: (fn, ...a2) => call(me, addr, new Contract(bytesToHex(addr.bytes), a.abi).interface.encodeFunctionData(fn, a2)),
42
+ read: async (fn, ...a2) => decode(a.abi, fn, (await call(me, addr, new Contract(bytesToHex(addr.bytes), a.abi).interface.encodeFunctionData(fn, a2))).ret),
43
+ };
44
+ }
45
+
46
+ function guessCtor(abi) {
47
+ const c = abi.find((x) => x.type === "constructor");
48
+ return (c?.inputs || []).map((i) => i.type);
49
+ }
50
+
51
+ export function decode(abi, fn, hex) {
52
+ const f = abi.find((x) => x.type === "function" && x.name === fn);
53
+ const vals = coder.decode(f.outputs.map((o) => o.type), hex);
54
+ return vals.length === 1 ? vals[0] : vals;
55
+ }
56
+
57
+ export const b32 = (s) => keccak256(s);
58
+
59
+ export async function exec(acc, targetHex, data, scheme = "ecdsa", identity) {
60
+ const nonce = await acc.read("nonce");
61
+ const digest = await acc.read("digest", targetHex, 0n, data, nonce);
62
+ let sig;
63
+ if (scheme === "pq") sig = signDigest(identity, digest);
64
+ else {
65
+ const s = signer.signingKey.sign(digest);
66
+ sig = "0x01" + s.r.slice(2) + s.s.slice(2) + s.v.toString(16).padStart(2, "0");
67
+ }
68
+ return acc.send("execute", targetHex, 0n, data, sig);
69
+ }
package/test/local.mjs ADDED
@@ -0,0 +1,86 @@
1
+ // End-to-end invariant test on a local EVM. Run: node test/local.mjs
2
+ import { JsonRpcProvider, Wallet, ContractFactory, Contract, keccak256, AbiCoder, solidityPacked } from "ethers";
3
+ import fs from "node:fs";
4
+
5
+ const p = new JsonRpcProvider("http://127.0.0.1:8545");
6
+ const signer = new Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", p);
7
+ const art = (n) => JSON.parse(fs.readFileSync(`artifacts/${n}.json`, "utf8"));
8
+ const dep = async (n, args) => {
9
+ const c = await new ContractFactory(art(n).abi, art(n).bytecode, signer).deploy(...args);
10
+ await c.waitForDeployment();
11
+ return c;
12
+ };
13
+ let fails = 0;
14
+ const ok = (c, m) => { console.log((c ? "PASS " : "FAIL ") + m); if (!c) fails++; };
15
+
16
+ const pubA = solidityPacked(["bytes32"], [keccak256("0x01")]).concat("00".repeat(1904));
17
+ const pubB = solidityPacked(["bytes32"], [keccak256("0x02")]).concat("00".repeat(1904));
18
+ const keyA = keccak256(pubA), keyB = keccak256(pubB);
19
+
20
+ const usdc = await dep("MockUSDC", []);
21
+ const verifier = await dep("MockVerifier", []);
22
+ const registry = await dep("AgentRegistry", []);
23
+ const token = await dep("AgentQuantumCredit", [await usdc.getAddress(), await registry.getAddress(), signer.address]);
24
+ const accA = await dep("PQSmartAccount", [await verifier.getAddress(), signer.address, pubA]);
25
+ const accB = await dep("PQSmartAccount", [await verifier.getAddress(), signer.address, pubB]);
26
+ const A = await accA.getAddress(), B = await accB.getAddress();
27
+
28
+ async function exec(acc, target, data, scheme = "ecdsa") {
29
+ const addr = await acc.getAddress();
30
+ const nonce = await acc.nonce();
31
+ const digest = await acc.digest(target, 0n, data, nonce);
32
+ let sig;
33
+ if (scheme === "pq") {
34
+ sig = "0x02" + "ab".repeat(3309);
35
+ } else {
36
+ const s = signer.signingKey.sign(digest);
37
+ sig = "0x01" + s.r.slice(2) + s.s.slice(2) + s.v.toString(16).padStart(2, "0");
38
+ }
39
+ return acc.execute(target, 0n, data, sig);
40
+ }
41
+
42
+ // registration requires a PQ account bound to the announced key
43
+ const c = (n, a) => new Contract(n, a).interface.encodeFunctionData;
44
+ try {
45
+ await exec(accA, await registry.getAddress(), registry.interface.encodeFunctionData("register", [keyA, keccak256("0xaaaa")]), "pq");
46
+ ok(await registry.isAgent(A), "agent A registered via PQ sig");
47
+ } catch (e) { ok(false, "agent A registered via PQ sig: " + e.shortMessage); }
48
+ await exec(accB, await registry.getAddress(), registry.interface.encodeFunctionData("register", [keyB, keccak256("0xbbbb")]));
49
+
50
+ await usdc.mint(A, 100_000_000n); // 100 USD
51
+ await exec(accA, await usdc.getAddress(), usdc.interface.encodeFunctionData("approve", [await token.getAddress(), 2n ** 200n]));
52
+
53
+ // EOA cannot mint
54
+ try { await (await token.connect(signer).mint(1n)).wait(); ok(false, "EOA mint blocked"); }
55
+ catch { ok(true, "EOA mint blocked"); }
56
+
57
+ await exec(accA, await token.getAddress(), token.interface.encodeFunctionData("mint", [10_000_000n]));
58
+ ok(await token.balanceOf(A) === 10n ** 19n, "minted 10 AQB for 10 USD");
59
+ ok(await token.collateralRatio() === 10n ** 18n, "ratio == 1e18");
60
+
61
+ // transfer to agent OK, to EOA blocked
62
+ await exec(accA, await token.getAddress(), token.interface.encodeFunctionData("transfer", [B, 3n * 10n ** 18n]));
63
+ ok(await token.balanceOf(B) === 3n * 10n ** 18n, "transfer to agent ok");
64
+ try { await exec(accA, await token.getAddress(), token.interface.encodeFunctionData("transfer", [signer.address, 1n])); ok(false, "transfer to EOA blocked"); }
65
+ catch { ok(true, "transfer to EOA blocked"); }
66
+ await token.connect(signer).setPool(signer.address, true);
67
+ await exec(accA, await token.getAddress(), token.interface.encodeFunctionData("transfer", [signer.address, 1n]));
68
+ ok(true, "transfer to whitelisted pool ok");
69
+
70
+ // redeem peg, fractional reverts
71
+ try { await exec(accA, await token.getAddress(), token.interface.encodeFunctionData("redeem", [1n])); ok(false, "redeem of <1e12 reverts"); }
72
+ catch { ok(true, "redeem of <1e12 reverts"); }
73
+ const usdBefore = await usdc.balanceOf(A);
74
+ await exec(accA, await token.getAddress(), token.interface.encodeFunctionData("redeem", [2n * 10n ** 18n]));
75
+ ok(await usdc.balanceOf(A) - usdBefore === 2_000_000n, "redeem paid 2 USD");
76
+ ok(await token.collateralRatio() === 10n ** 18n, "ratio == 1e18 after redeem");
77
+
78
+ ok(await token.MAX_SUPPLY() === 900_000_000_000n * 10n ** 18n, "cap == 900e9");
79
+ ok(await registry.agentCount() === 2n, "agentCount == 2");
80
+
81
+ // ecdsa fallback authority works
82
+ await exec(accA, await usdc.getAddress(), usdc.interface.encodeFunctionData("approve", [await token.getAddress(), 0n]));
83
+ ok(true, "ecdsa fallback path ok");
84
+
85
+ console.log(fails ? `SMOKE FAILED (${fails})` : "SMOKE OK");
86
+ process.exit(fails ? 1 : 0);
package/test/pq.mjs ADDED
@@ -0,0 +1,37 @@
1
+ // Real post-quantum crypto test: identity, digest signing, stealth KEM round trip.
2
+ import { newIdentity, signDigest, verifyDigest, newKemIdentity, stealthFor, stealthCheck, PK_LEN, SIG_LEN } from "../pq/pq.mjs";
3
+ import { stealth as clientStealth, cardHash } from "../client/aqb.mjs";
4
+ import { keccak256, toUtf8Bytes } from "ethers";
5
+ const hex = (b) => (b ? Buffer.from(b).toString("hex") : null);
6
+
7
+ let fails = 0;
8
+ const ok = (c, m) => { console.log((c ? "PASS " : "FAIL ") + m); if (!c) fails++; };
9
+
10
+ const id = newIdentity();
11
+ ok(id.publicKey.length === PK_LEN, `ML-DSA-65 pubkey is ${PK_LEN} bytes`);
12
+ ok(id.keyHash === keccak256(id.publicKey), "keyHash commits the pubkey");
13
+
14
+ const digest = keccak256(toUtf8Bytes("aqb-execute"));
15
+ const blob = signDigest(id, digest);
16
+ ok((blob.length - 2) / 2 === SIG_LEN + 1, "sig blob = scheme tag + 3309 bytes");
17
+ ok(verifyDigest(id.publicKey, digest, blob), "real ML-DSA-65 signature verifies");
18
+ ok(!verifyDigest(id.publicKey, keccak256(toUtf8Bytes("other")), blob), "wrong digest rejected");
19
+ const impostor = newIdentity();
20
+ ok(!verifyDigest(impostor.publicKey, digest, blob), "wrong key rejected");
21
+
22
+ const kem = newKemIdentity();
23
+ const s = stealthFor(kem.publicKey);
24
+ ok(s.addr.length === 42, "stealth address derived");
25
+ ok(hex(stealthCheck(kem, s.ephPubKey, s.viewTag, s.addr)) === hex(s.sharedSecret), "recipient decapsulates to same secret");
26
+ ok(stealthCheck(newKemIdentity(), s.ephPubKey, s.viewTag, s.addr) === null, "stranger cannot claim stealth");
27
+ ok(stealthCheck(kem, s.ephPubKey, "0x00", s.addr) === null, "view-tag mismatch filters announcement");
28
+ const cs = clientStealth(s.ephPubKey, s.sharedSecret);
29
+ ok(cs.addr === s.addr && cs.viewTag === s.viewTag, "client stealth matches pq.stealthFor");
30
+
31
+ const card = { aqb: 1, name: "x", price: { unit: "AQB", amount: "2" }, tags: ["a", "b"] };
32
+ const reordered = { tags: ["a", "b"], price: { amount: "2", unit: "AQB" }, name: "x", aqb: 1 };
33
+ ok(cardHash(card) === cardHash(reordered), "cardHash is recursive key-order independent");
34
+ ok(cardHash(card) !== keccak256(toUtf8Bytes(JSON.stringify(card, Object.keys(card).sort()))), "cardHash keeps nested keys");
35
+
36
+ console.log(fails ? `PQ FAILED (${fails})` : "PQ OK");
37
+ process.exit(fails ? 1 : 0);
package/test/smoke.mjs ADDED
@@ -0,0 +1,27 @@
1
+ // Post-deploy invariant test. Run after deploy.mjs: node test/smoke.mjs
2
+ import { JsonRpcProvider, Wallet, Contract } from "ethers";
3
+ import fs from "node:fs";
4
+
5
+ const d = JSON.parse(fs.readFileSync("deployment.json", "utf8"));
6
+ const p = new JsonRpcProvider(process.env.RPC_URL);
7
+ const w = new Wallet(process.env.PRIVATE_KEY, p);
8
+ const t = new Contract(d.token, [
9
+ "function mint(uint256)", "function redeem(uint256)", "function totalSupply() view returns(uint256)",
10
+ "function collateralRatio() view returns(uint256)", "function MAX_SUPPLY() view returns(uint256)",
11
+ "function balanceOf(address) view returns(uint256)", "function setPool(address,bool)",
12
+ ], w);
13
+ const r = new Contract(d.registry, ["function isAgent(address) view returns(bool)"], w);
14
+ const usdc = new Contract(d.usdc, ["function balanceOf(address) view returns(uint256)"], w);
15
+
16
+ const ok = (c, m) => { console.log((c ? "PASS " : "FAIL ") + m); if (!c) process.exitCode = 1; };
17
+
18
+ ok((await r.isAgent(w.address)) === true, "deployer is agent");
19
+ await (await t.mint(10_000_000n)).wait(); // 10 USDC
20
+ ok((await t.collateralRatio()) === 10n ** 18n, "ratio == 1e18 after mint");
21
+ await (await t.redeem(5n * 10n ** 18n)).wait();
22
+ ok((await t.collateralRatio()) === 10n ** 18n, "ratio == 1e18 after redeem");
23
+ ok((await t.totalSupply()) === 5n * 10n ** 18n, "supply == 5 AQB");
24
+ ok((await usdc.balanceOf(d.token)) === 5_000_000n, "usdc reserves == 5");
25
+ try { await (await t.mint(0n)).wait(); ok(false, "mint(0) reverts"); } catch { ok(true, "mint(0) reverts"); }
26
+ ok((await t.MAX_SUPPLY()) === 900_000_000_000n * 10n ** 18n, "cap == 900e9");
27
+ console.log(process.exitCode ? "SMOKE FAILED" : "SMOKE OK");