// ERC-8004 V2 registries (identity / reputation / validation), transcribed from the draft text. // Every test names the draft sentence it enforces; negative cases are the draft's MUST NOTs. const { expect } = require("chai"); const { ethers } = require("hardhat"); const IFACE = { erc165: "0x01ffc9a7", erc721: "0x80ac58cd", erc721Metadata: "0x5b5e139f" }; describe("ERC-8004 V2 registries (draft-conformant)", function () { let idr, rep, val, owner, other, client, validator, wallet2; beforeEach(async function () { [owner, other, client, validator, wallet2] = await ethers.getSigners(); idr = await (await ethers.getContractFactory("AereIdentityRegistry8004V2")).deploy(); await idr.waitForDeployment(); rep = await (await ethers.getContractFactory("AereReputationRegistry8004V2")).deploy(await idr.getAddress()); val = await (await ethers.getContractFactory("AereValidationRegistry8004V2")).deploy(ethers.ZeroAddress); await val.initialize(await idr.getAddress()); }); describe("Identity Registry", function () { it("is an ERC-721 with URIStorage: ERC-165 ids for 165, 721 and 721Metadata", async function () { for (const id of Object.values(IFACE)) expect(await idr.supportsInterface(id)).to.equal(true); expect(await idr.supportsInterface("0xffffffff")).to.equal(false); }); it("register(string): mints to the caller, sets tokenURI, initialises agentWallet to the owner, emits Registered + MetadataSet(agentWallet)", async function () { const tx = await idr.connect(owner)["register(string)"]("ipfs://agent-1"); await expect(tx).to.emit(idr, "Registered").withArgs(1, "ipfs://agent-1", owner.address); await expect(tx).to.emit(idr, "MetadataSet").withArgs(1, "agentWallet", "agentWallet", ethers.AbiCoder.defaultAbiCoder().encode(["address"], [owner.address])); expect(await idr.ownerOf(1)).to.equal(owner.address); expect(await idr.tokenURI(1)).to.equal("ipfs://agent-1"); expect(await idr.getAgentWallet(1)).to.equal(owner.address); expect(await idr.agentCount()).to.equal(1); }); it("register() and register(string,(string,bytes)[]) overloads; metadata entries emit MetadataSet; ids never repeat", async function () { await idr.connect(owner)["register()"](); const tx = await idr.connect(other)["register(string,(string,bytes)[])"]("ipfs://a2", [{ metadataKey: "model", metadataValue: ethers.toUtf8Bytes("claude") }]); await expect(tx).to.emit(idr, "MetadataSet").withArgs(2, "model", "model", ethers.hexlify(ethers.toUtf8Bytes("claude"))); expect(await idr.getMetadata(2, "model")).to.equal(ethers.hexlify(ethers.toUtf8Bytes("claude"))); expect(await idr.ownerOf(2)).to.equal(other.address); expect(await idr.tokenURI(1)).to.equal(""); }); it("the reserved key agentWallet cannot be set through setMetadata or the register metadata array", async function () { await idr.connect(owner)["register()"](); await expect(idr.connect(owner).setMetadata(1, "agentWallet", "0x01")).to.be.revertedWithCustomError(idr, "ReservedMetadataKey"); await expect(idr.connect(owner)["register(string,(string,bytes)[])"]("", [{ metadataKey: "agentWallet", metadataValue: "0x01" }])).to.be.revertedWithCustomError(idr, "ReservedMetadataKey"); }); it("setAgentURI and setMetadata are owner-or-operator only; URIUpdated carries updatedBy", async function () { await idr.connect(owner)["register()"](); await expect(idr.connect(other).setAgentURI(1, "x")).to.be.revertedWithCustomError(idr, "NotAgentOwnerOrOperator"); await expect(idr.connect(owner).setAgentURI(1, "ipfs://new")).to.emit(idr, "URIUpdated").withArgs(1, "ipfs://new", owner.address); await idr.connect(owner).setApprovalForAll(other.address, true); await expect(idr.connect(other).setAgentURI(1, "ipfs://op")).to.emit(idr, "URIUpdated").withArgs(1, "ipfs://op", other.address); await expect(idr.connect(client).setMetadata(1, "k", "0x02")).to.be.revertedWithCustomError(idr, "NotAgentOwnerOrOperator"); }); async function walletSig(signer, agentId, newWallet, ownerAddr, deadline) { const net = await ethers.provider.getNetwork(); const domain = { name: "ERC8004IdentityRegistry", version: "1", chainId: net.chainId, verifyingContract: await idr.getAddress() }; const types = { AgentWalletSet: [{ name: "agentId", type: "uint256" }, { name: "newWallet", type: "address" }, { name: "owner", type: "address" }, { name: "deadline", type: "uint256" }] }; return signer.signTypedData(domain, types, { agentId, newWallet, owner: ownerAddr, deadline }); } it("setAgentWallet: the NEW wallet proves control with an EIP-712 signature (EOA)", async function () { await idr.connect(owner)["register()"](); const deadline = Math.floor(Date.now() / 1000) + 3600; const sig = await walletSig(wallet2, 1, wallet2.address, owner.address, deadline); await expect(idr.connect(owner).setAgentWallet(1, wallet2.address, deadline, sig)).to.emit(idr, "MetadataSet"); expect(await idr.getAgentWallet(1)).to.equal(wallet2.address); expect(await idr.getMetadata(1, "agentWallet")).to.equal(ethers.AbiCoder.defaultAbiCoder().encode(["address"], [wallet2.address])); }); it("setAgentWallet negatives: wrong signer, expired deadline, signature bound to another owner, non-owner caller", async function () { await idr.connect(owner)["register()"](); const deadline = Math.floor(Date.now() / 1000) + 3600; const wrong = await walletSig(other, 1, wallet2.address, owner.address, deadline); // signed by someone else await expect(idr.connect(owner).setAgentWallet(1, wallet2.address, deadline, wrong)).to.be.revertedWithCustomError(idr, "InvalidWalletSignature"); const good = await walletSig(wallet2, 1, wallet2.address, owner.address, 1); // expired await expect(idr.connect(owner).setAgentWallet(1, wallet2.address, 1, good)).to.be.revertedWithCustomError(idr, "DeadlinePassed"); const forOther = await walletSig(wallet2, 1, wallet2.address, other.address, deadline); // bound to a different owner await expect(idr.connect(owner).setAgentWallet(1, wallet2.address, deadline, forOther)).to.be.revertedWithCustomError(idr, "InvalidWalletSignature"); const ok = await walletSig(wallet2, 1, wallet2.address, owner.address, deadline); await expect(idr.connect(other).setAgentWallet(1, wallet2.address, deadline, ok)).to.be.revertedWithCustomError(idr, "NotAgentOwnerOrOperator"); }); it("setAgentWallet accepts an ERC-1271 contract wallet", async function () { await idr.connect(owner)["register()"](); const cw = await (await ethers.getContractFactory("Mock1271Wallet")).deploy(wallet2.address); const cwAddr = await cw.getAddress(); const deadline = Math.floor(Date.now() / 1000) + 3600; const sig = await walletSig(wallet2, 1, cwAddr, owner.address, deadline); // the contract wallet's owner signs await idr.connect(owner).setAgentWallet(1, cwAddr, deadline, sig); expect(await idr.getAgentWallet(1)).to.equal(cwAddr); const bad = await walletSig(other, 1, cwAddr, owner.address, deadline); await expect(idr.connect(owner).setAgentWallet(1, cwAddr, deadline, bad)).to.be.revertedWithCustomError(idr, "InvalidWalletSignature"); }); it("transfer clears agentWallet (must be re-verified by the new owner); unsetAgentWallet clears it too", async function () { await idr.connect(owner)["register()"](); await expect(idr.connect(owner).transferFrom(owner.address, other.address, 1)).to.emit(idr, "MetadataSet").withArgs(1, "agentWallet", "agentWallet", ethers.AbiCoder.defaultAbiCoder().encode(["address"], [ethers.ZeroAddress])); expect(await idr.getAgentWallet(1)).to.equal(ethers.ZeroAddress); await idr.connect(other)["register()"](); expect(await idr.getAgentWallet(2)).to.equal(other.address); await idr.connect(other).unsetAgentWallet(2); expect(await idr.getAgentWallet(2)).to.equal(ethers.ZeroAddress); }); }); describe("Reputation Registry", function () { beforeEach(async function () { await idr.connect(owner)["register(string)"]("ipfs://a1"); }); it("getIdentityRegistry; initialize runs once", async function () { expect(await rep.getIdentityRegistry()).to.equal(await idr.getAddress()); await expect(rep.initialize(other.address)).to.be.revertedWithCustomError(rep, "AlreadyInitialized"); const fresh = await (await ethers.getContractFactory("AereReputationRegistry8004V2")).deploy(ethers.ZeroAddress); await fresh.initialize(await idr.getAddress()); expect(await fresh.getIdentityRegistry()).to.equal(await idr.getAddress()); }); it("giveFeedback stores value/decimals/tags, emits NewFeedback with the exact field order, indexes from 1", async function () { const h = ethers.keccak256(ethers.toUtf8Bytes("file")); await expect(rep.connect(client).giveFeedback(1, 87, 0, "starred", "", "https://a/x", "ipfs://f", h)) .to.emit(rep, "NewFeedback").withArgs(1, client.address, 1, 87, 0, "starred", "starred", "", "https://a/x", "ipfs://f", h); const r = await rep.readFeedback(1, client.address, 1); expect(r.value).to.equal(87); expect(r.valueDecimals).to.equal(0); expect(r.tag1).to.equal("starred"); expect(r.isRevoked).to.equal(false); expect(await rep.getLastIndex(1, client.address)).to.equal(1); expect(await rep.getClients(1)).to.deep.equal([client.address]); }); it("the submitter MUST NOT be the agent owner or an approved operator; valueDecimals MUST be 0..18; agent must exist", async function () { await expect(rep.connect(owner).giveFeedback(1, 1, 0, "", "", "", "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "SelfFeedback"); await idr.connect(owner).setApprovalForAll(other.address, true); await expect(rep.connect(other).giveFeedback(1, 1, 0, "", "", "", "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "SelfFeedback"); await expect(rep.connect(client).giveFeedback(1, 1, 19, "", "", "", "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "BadDecimals"); await expect(rep.connect(client).giveFeedback(99, 1, 0, "", "", "", "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "AgentNotRegistered"); }); it("revokeFeedback (own feedback only, once) and appendResponse (anyone) with their events and counts", async function () { await rep.connect(client).giveFeedback(1, 5, 0, "", "", "", "", ethers.ZeroHash); await expect(rep.connect(other).revokeFeedback(1, 1)).to.be.revertedWithCustomError(rep, "NoSuchFeedback"); await expect(rep.connect(client).revokeFeedback(1, 1)).to.emit(rep, "FeedbackRevoked").withArgs(1, client.address, 1); await expect(rep.connect(client).revokeFeedback(1, 1)).to.be.revertedWithCustomError(rep, "AlreadyRevoked"); await expect(rep.connect(validator).appendResponse(1, client.address, 1, "ipfs://r", ethers.ZeroHash)).to.emit(rep, "ResponseAppended").withArgs(1, client.address, 1, validator.address, "ipfs://r", ethers.ZeroHash); await rep.connect(other).appendResponse(1, client.address, 1, "", ethers.ZeroHash); expect(await rep.getResponseCount(1, client.address, 1, [])).to.equal(2); expect(await rep.getResponseCount(1, client.address, 1, [validator.address])).to.equal(1); await expect(rep.connect(other).appendResponse(1, client.address, 7, "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "NoSuchFeedback"); }); it("getSummary averages at the largest precision, filters by client list and tags, ignores revoked; readAllFeedback honours includeRevoked", async function () { await rep.connect(client).giveFeedback(1, 80, 0, "starred", "", "", "", ethers.ZeroHash); await rep.connect(client).giveFeedback(1, 9977, 2, "uptime", "", "", "", ethers.ZeroHash); await rep.connect(other).giveFeedback(1, 60, 0, "starred", "", "", "", ethers.ZeroHash); await rep.connect(other).giveFeedback(1, 1, 0, "starred", "", "", "", ethers.ZeroHash); await rep.connect(other).revokeFeedback(1, 2); let s = await rep.getSummary(1, [client.address, other.address], "starred", ""); expect(s.count).to.equal(2); expect(s.summaryValue).to.equal(70); expect(s.summaryValueDecimals).to.equal(0); s = await rep.getSummary(1, [client.address], "", ""); expect(s.count).to.equal(2); expect(s.summaryValueDecimals).to.equal(2); expect(s.summaryValue).to.equal(8988); // (8000 + 9977) / 2, impartire intreaga await expect(rep.getSummary(1, [], "", "")).to.be.revertedWithCustomError(rep, "EmptyClientList"); const all = await rep.readAllFeedback(1, [], "", "", false); expect(all.clients.length).to.equal(3); const withRevoked = await rep.readAllFeedback(1, [other.address], "starred", "", true); expect(withRevoked.clients.length).to.equal(2); expect(withRevoked.revokedStatuses[1]).to.equal(true); }); }); describe("Validation Registry", function () { const H = ethers.keccak256(ethers.toUtf8Bytes("req")); beforeEach(async function () { await idr.connect(owner)["register(string)"]("ipfs://a1"); }); it("validationRequest: owner or operator only; unique requestHash; event with indexed validator/agent/requestHash", async function () { await expect(val.connect(other).validationRequest(validator.address, 1, "ipfs://q", H)).to.be.revertedWithCustomError(val, "NotAgentOwnerOrOperator"); await expect(val.connect(owner).validationRequest(validator.address, 1, "ipfs://q", H)).to.emit(val, "ValidationRequest").withArgs(validator.address, 1, "ipfs://q", H); await expect(val.connect(owner).validationRequest(validator.address, 1, "ipfs://q", H)).to.be.revertedWithCustomError(val, "RequestExists"); expect(await val.requestCount()).to.equal(1); expect(await val.getAgentValidations(1)).to.deep.equal([H]); expect(await val.getValidatorRequests(validator.address)).to.deep.equal([H]); await idr.connect(owner).approve(other.address, 1); const H2 = ethers.keccak256(ethers.toUtf8Bytes("req2")); await val.connect(other).validationRequest(validator.address, 1, "", H2); }); it("validationResponse: only the named validator, response 0..100, repeatable with tags (soft then hard finality), status readable", async function () { await val.connect(owner).validationRequest(validator.address, 1, "ipfs://q", H); await expect(val.connect(other).validationResponse(H, 100, "", ethers.ZeroHash, "")).to.be.revertedWithCustomError(val, "NotTheValidator"); await expect(val.connect(validator).validationResponse(H, 101, "", ethers.ZeroHash, "")).to.be.revertedWithCustomError(val, "BadResponse"); await expect(val.connect(validator).validationResponse(H, 60, "ipfs://r1", ethers.ZeroHash, "soft")).to.emit(val, "ValidationResponse").withArgs(validator.address, 1, H, 60, "ipfs://r1", ethers.ZeroHash, "soft"); await val.connect(validator).validationResponse(H, 100, "ipfs://r2", ethers.ZeroHash, "hard"); const st = await val.getValidationStatus(H); expect(st.validatorAddress).to.equal(validator.address); expect(st.agentId).to.equal(1); expect(st.response).to.equal(100); expect(st.tag).to.equal("hard"); await expect(val.getValidationStatus(ethers.ZeroHash)).to.be.revertedWithCustomError(val, "NoSuchRequest"); }); it("getSummary counts responded requests, filtered by validators and tag", async function () { const H2 = ethers.keccak256(ethers.toUtf8Bytes("req2")), H3 = ethers.keccak256(ethers.toUtf8Bytes("req3")); await val.connect(owner).validationRequest(validator.address, 1, "", H); await val.connect(owner).validationRequest(validator.address, 1, "", H2); await val.connect(owner).validationRequest(other.address, 1, "", H3); await val.connect(validator).validationResponse(H, 100, "", ethers.ZeroHash, "hard"); await val.connect(validator).validationResponse(H2, 50, "", ethers.ZeroHash, "soft"); await val.connect(other).validationResponse(H3, 0, "", ethers.ZeroHash, "hard"); let s = await val.getSummary(1, [], ""); expect(s.count).to.equal(3); expect(s.averageResponse).to.equal(50); s = await val.getSummary(1, [validator.address], "hard"); expect(s.count).to.equal(1); expect(s.averageResponse).to.equal(100); }); }); });