@a3stack/identity 0.1.0
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.
- package/bin/agent-id.ts +113 -0
- package/package.json +34 -0
- package/src/constants.ts +159 -0
- package/src/index.ts +34 -0
- package/src/multichain.ts +146 -0
- package/src/registry.ts +273 -0
- package/src/types.ts +102 -0
- package/src/verify.ts +268 -0
package/bin/agent-id.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent-id CLI ā verify and inspect ERC-8004 agent identities
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @a3stack/identity verify <globalId>
|
|
7
|
+
* npx @a3stack/identity lookup <walletAddress>
|
|
8
|
+
* npx @a3stack/identity chains
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { verifyAgent, getMcpEndpoint, getAgentCount, parseAgentId, SUPPORTED_CHAINS } from "../src/index.js";
|
|
12
|
+
import { findAllRegistrations } from "../src/multichain.js";
|
|
13
|
+
|
|
14
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
15
|
+
|
|
16
|
+
const HELP = `
|
|
17
|
+
š agent-id ā ERC-8004 Agent Identity CLI
|
|
18
|
+
|
|
19
|
+
Commands:
|
|
20
|
+
verify <globalId> Verify an agent's on-chain identity
|
|
21
|
+
lookup <wallet> Find all registrations for a wallet address
|
|
22
|
+
chains List supported chains
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
npx @a3stack/identity verify "eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432#2376"
|
|
26
|
+
npx @a3stack/identity lookup 0xYOUR_WALLET_ADDRESS
|
|
27
|
+
npx @a3stack/identity chains
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
32
|
+
console.log(HELP);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (cmd === "verify") {
|
|
37
|
+
const globalId = args[0];
|
|
38
|
+
if (!globalId) {
|
|
39
|
+
console.error("Usage: agent-id verify <globalId>");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`\nš Verifying: ${globalId}\n`);
|
|
44
|
+
|
|
45
|
+
const ref = parseAgentId(globalId);
|
|
46
|
+
console.log(` Chain: ${ref.chainId}`);
|
|
47
|
+
console.log(` Registry: ${ref.registry}`);
|
|
48
|
+
console.log(` Agent ID: ${ref.agentId}`);
|
|
49
|
+
|
|
50
|
+
const result = await verifyAgent(globalId);
|
|
51
|
+
|
|
52
|
+
if (!result.valid) {
|
|
53
|
+
console.log(`\n ā FAILED: ${result.error}\n`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\n ā
Verified!`);
|
|
58
|
+
console.log(` Owner: ${result.owner}`);
|
|
59
|
+
console.log(` Payment wallet: ${result.paymentWallet ?? "(defaults to owner)"}`);
|
|
60
|
+
|
|
61
|
+
if (result.registration) {
|
|
62
|
+
console.log(` Name: ${result.registration.name}`);
|
|
63
|
+
console.log(` Active: ${result.registration.active}`);
|
|
64
|
+
console.log(` x402 Payments: ${result.registration.x402Support}`);
|
|
65
|
+
|
|
66
|
+
if (result.registration.services?.length) {
|
|
67
|
+
console.log(`\n Services:`);
|
|
68
|
+
for (const s of result.registration.services) {
|
|
69
|
+
console.log(` - ${s.name}: ${s.endpoint}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
console.log();
|
|
74
|
+
|
|
75
|
+
} else if (cmd === "lookup") {
|
|
76
|
+
const wallet = args[0] as `0x${string}`;
|
|
77
|
+
if (!wallet?.startsWith("0x")) {
|
|
78
|
+
console.error("Usage: agent-id lookup <0xWalletAddress>");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`\nš Scanning all chains for ${wallet}...\n`);
|
|
83
|
+
const regs = await findAllRegistrations(wallet, { timeoutMs: 15000 });
|
|
84
|
+
|
|
85
|
+
if (regs.length === 0) {
|
|
86
|
+
console.log(" No registrations found.\n");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(` Found ${regs.length} registration(s):\n`);
|
|
91
|
+
for (const r of regs) {
|
|
92
|
+
console.log(` ā
${r.chainName.padEnd(12)} #${String(r.agentId).padEnd(6)} ${r.globalId}`);
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
|
|
96
|
+
} else if (cmd === "chains") {
|
|
97
|
+
console.log("\nš Supported ERC-8004 chains:\n");
|
|
98
|
+
for (const [id, chain] of Object.entries(SUPPORTED_CHAINS)) {
|
|
99
|
+
console.log(` ${chain.name.padEnd(15)} chain ${id}`);
|
|
100
|
+
}
|
|
101
|
+
console.log(`\n Total: ${Object.keys(SUPPORTED_CHAINS).length} chains\n`);
|
|
102
|
+
|
|
103
|
+
} else {
|
|
104
|
+
console.error(`Unknown command: ${cmd}`);
|
|
105
|
+
console.log(HELP);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
console.error("Error:", err.message);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a3stack/identity",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ERC-8004 agent identity registration, verification, and discovery",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"agent-id": "./bin/agent-id.ts"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --outDir dist",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"viem": "^2.39.3",
|
|
25
|
+
"zod": "^3.24.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.4.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERC-8004 contract addresses and ABIs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ERC-8004 Identity Registry ā deployed at same address on all supported chains
|
|
7
|
+
* The address 0x8004... is a vanity address (mirrors the EIP number)
|
|
8
|
+
* Verified on-chain: has bytecode on Base (8453) and Ethereum (1)
|
|
9
|
+
*/
|
|
10
|
+
export const IDENTITY_REGISTRY_ADDRESS =
|
|
11
|
+
"0x8004A169FB4a3325136EB29fA0ceB6D2e539a432" as const;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Chains where ERC-8004 is confirmed deployed (as of Feb 2026)
|
|
15
|
+
*/
|
|
16
|
+
export const SUPPORTED_CHAINS: Record<
|
|
17
|
+
number,
|
|
18
|
+
{ name: string; rpc: string; chainId: number }
|
|
19
|
+
> = {
|
|
20
|
+
1: { name: "Ethereum", rpc: "https://eth.llamarpc.com", chainId: 1 },
|
|
21
|
+
8453: { name: "Base", rpc: "https://mainnet.base.org", chainId: 8453 },
|
|
22
|
+
10: { name: "Optimism", rpc: "https://mainnet.optimism.io", chainId: 10 },
|
|
23
|
+
42161: { name: "Arbitrum", rpc: "https://arb1.arbitrum.io/rpc", chainId: 42161 },
|
|
24
|
+
137: { name: "Polygon", rpc: "https://polygon-rpc.com", chainId: 137 },
|
|
25
|
+
56: { name: "BNB Chain", rpc: "https://bsc-dataseed.binance.org", chainId: 56 },
|
|
26
|
+
100: { name: "Gnosis", rpc: "https://rpc.gnosischain.com", chainId: 100 },
|
|
27
|
+
42220: { name: "Celo", rpc: "https://forno.celo.org", chainId: 42220 },
|
|
28
|
+
59144: { name: "Linea", rpc: "https://rpc.linea.build", chainId: 59144 },
|
|
29
|
+
534352: { name: "Scroll", rpc: "https://rpc.scroll.io", chainId: 534352 },
|
|
30
|
+
167000: { name: "Taiko", rpc: "https://rpc.mainnet.taiko.xyz", chainId: 167000 },
|
|
31
|
+
43114: { name: "Avalanche", rpc: "https://api.avax.network/ext/bc/C/rpc", chainId: 43114 },
|
|
32
|
+
5000: { name: "Mantle", rpc: "https://rpc.mantle.xyz", chainId: 5000 },
|
|
33
|
+
1088: { name: "Metis", rpc: "https://andromeda.metis.io/?owner=1088", chainId: 1088 },
|
|
34
|
+
2741: { name: "Abstract", rpc: "https://api.mainnet.abs.xyz", chainId: 2741 },
|
|
35
|
+
143: { name: "Monad", rpc: "https://rpc.monad.xyz", chainId: 143 },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ERC-8004 Identity Registry ABI
|
|
40
|
+
*/
|
|
41
|
+
export const IDENTITY_REGISTRY_ABI = [
|
|
42
|
+
// Core registration
|
|
43
|
+
{
|
|
44
|
+
name: "register",
|
|
45
|
+
type: "function",
|
|
46
|
+
stateMutability: "nonpayable",
|
|
47
|
+
inputs: [{ name: "agentURI", type: "string" }],
|
|
48
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "register",
|
|
52
|
+
type: "function",
|
|
53
|
+
stateMutability: "nonpayable",
|
|
54
|
+
inputs: [
|
|
55
|
+
{ name: "agentURI", type: "string" },
|
|
56
|
+
{ name: "metadataKeys", type: "string[]" },
|
|
57
|
+
{ name: "metadataValues", type: "bytes[]" },
|
|
58
|
+
],
|
|
59
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "setAgentURI",
|
|
63
|
+
type: "function",
|
|
64
|
+
stateMutability: "nonpayable",
|
|
65
|
+
inputs: [
|
|
66
|
+
{ name: "agentId", type: "uint256" },
|
|
67
|
+
{ name: "newURI", type: "string" },
|
|
68
|
+
],
|
|
69
|
+
outputs: [],
|
|
70
|
+
},
|
|
71
|
+
// ERC-721 standard
|
|
72
|
+
{
|
|
73
|
+
name: "balanceOf",
|
|
74
|
+
type: "function",
|
|
75
|
+
stateMutability: "view",
|
|
76
|
+
inputs: [{ name: "owner", type: "address" }],
|
|
77
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "ownerOf",
|
|
81
|
+
type: "function",
|
|
82
|
+
stateMutability: "view",
|
|
83
|
+
inputs: [{ name: "tokenId", type: "uint256" }],
|
|
84
|
+
outputs: [{ name: "", type: "address" }],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "tokenURI",
|
|
88
|
+
type: "function",
|
|
89
|
+
stateMutability: "view",
|
|
90
|
+
inputs: [{ name: "tokenId", type: "uint256" }],
|
|
91
|
+
outputs: [{ name: "", type: "string" }],
|
|
92
|
+
},
|
|
93
|
+
// Transfer event (for extracting agentId from receipt)
|
|
94
|
+
{
|
|
95
|
+
name: "Transfer",
|
|
96
|
+
type: "event",
|
|
97
|
+
inputs: [
|
|
98
|
+
{ name: "from", type: "address", indexed: true },
|
|
99
|
+
{ name: "to", type: "address", indexed: true },
|
|
100
|
+
{ name: "tokenId", type: "uint256", indexed: true },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
// Payment wallet
|
|
104
|
+
{
|
|
105
|
+
name: "setAgentWallet",
|
|
106
|
+
type: "function",
|
|
107
|
+
stateMutability: "nonpayable",
|
|
108
|
+
inputs: [
|
|
109
|
+
{ name: "agentId", type: "uint256" },
|
|
110
|
+
{ name: "newWallet", type: "address" },
|
|
111
|
+
{ name: "deadline", type: "uint256" },
|
|
112
|
+
{ name: "signature", type: "bytes" },
|
|
113
|
+
],
|
|
114
|
+
outputs: [],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "getAgentWallet",
|
|
118
|
+
type: "function",
|
|
119
|
+
stateMutability: "view",
|
|
120
|
+
inputs: [{ name: "agentId", type: "uint256" }],
|
|
121
|
+
outputs: [{ name: "", type: "address" }],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "unsetAgentWallet",
|
|
125
|
+
type: "function",
|
|
126
|
+
stateMutability: "nonpayable",
|
|
127
|
+
inputs: [{ name: "agentId", type: "uint256" }],
|
|
128
|
+
outputs: [],
|
|
129
|
+
},
|
|
130
|
+
// On-chain metadata
|
|
131
|
+
{
|
|
132
|
+
name: "getMetadata",
|
|
133
|
+
type: "function",
|
|
134
|
+
stateMutability: "view",
|
|
135
|
+
inputs: [
|
|
136
|
+
{ name: "agentId", type: "uint256" },
|
|
137
|
+
{ name: "metadataKey", type: "string" },
|
|
138
|
+
],
|
|
139
|
+
outputs: [{ name: "", type: "bytes" }],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "setMetadata",
|
|
143
|
+
type: "function",
|
|
144
|
+
stateMutability: "nonpayable",
|
|
145
|
+
inputs: [
|
|
146
|
+
{ name: "agentId", type: "uint256" },
|
|
147
|
+
{ name: "metadataKey", type: "string" },
|
|
148
|
+
{ name: "metadataValue", type: "bytes" },
|
|
149
|
+
],
|
|
150
|
+
outputs: [],
|
|
151
|
+
},
|
|
152
|
+
] as const;
|
|
153
|
+
|
|
154
|
+
export const REGISTRATION_TYPE =
|
|
155
|
+
"https://eips.ethereum.org/EIPS/eip-8004#registration-v1";
|
|
156
|
+
|
|
157
|
+
/** Zero address ā returned when no payment wallet is set */
|
|
158
|
+
export const ZERO_ADDRESS =
|
|
159
|
+
"0x0000000000000000000000000000000000000000" as const;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @a3stack/identity
|
|
3
|
+
* ERC-8004 agent identity: registration, verification, and discovery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { AgentIdentity } from "./registry.js";
|
|
7
|
+
export {
|
|
8
|
+
parseAgentId,
|
|
9
|
+
formatAgentId,
|
|
10
|
+
fetchRegistrationFile,
|
|
11
|
+
verifyAgent,
|
|
12
|
+
getMcpEndpoint,
|
|
13
|
+
getA2aEndpoint,
|
|
14
|
+
getAgentCount,
|
|
15
|
+
} from "./verify.js";
|
|
16
|
+
export {
|
|
17
|
+
IDENTITY_REGISTRY_ADDRESS,
|
|
18
|
+
IDENTITY_REGISTRY_ABI,
|
|
19
|
+
SUPPORTED_CHAINS,
|
|
20
|
+
REGISTRATION_TYPE,
|
|
21
|
+
} from "./constants.js";
|
|
22
|
+
export { findAllRegistrations } from "./multichain.js";
|
|
23
|
+
export type { ChainRegistration } from "./multichain.js";
|
|
24
|
+
export type {
|
|
25
|
+
AgentService,
|
|
26
|
+
AgentRegistrationFile,
|
|
27
|
+
AgentRegistrationRef,
|
|
28
|
+
AgentRef,
|
|
29
|
+
VerificationResult,
|
|
30
|
+
RegisterOptions,
|
|
31
|
+
RegisterResult,
|
|
32
|
+
IdentityConfig,
|
|
33
|
+
DiscoverOptions,
|
|
34
|
+
} from "./types.js";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-chain agent discovery ā find all registrations across supported chains
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createPublicClient, http } from "viem";
|
|
6
|
+
import {
|
|
7
|
+
IDENTITY_REGISTRY_ADDRESS,
|
|
8
|
+
IDENTITY_REGISTRY_ABI,
|
|
9
|
+
SUPPORTED_CHAINS,
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
import { fetchRegistrationFile } from "./verify.js";
|
|
12
|
+
import type { AgentRegistrationFile } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export interface ChainRegistration {
|
|
15
|
+
chainId: number;
|
|
16
|
+
chainName: string;
|
|
17
|
+
agentId: number;
|
|
18
|
+
owner: `0x${string}`;
|
|
19
|
+
agentUri: string;
|
|
20
|
+
registration: AgentRegistrationFile | null;
|
|
21
|
+
globalId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find all ERC-8004 registrations for a wallet address across all supported chains.
|
|
26
|
+
* Scans Transfer events (mint from 0x0) on each chain in parallel.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const regs = await findAllRegistrations("0x1be93C...");
|
|
31
|
+
* console.log(`Found ${regs.length} registrations across ${regs.map(r => r.chainName).join(", ")}`);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export async function findAllRegistrations(
|
|
35
|
+
walletAddress: `0x${string}`,
|
|
36
|
+
options?: {
|
|
37
|
+
/** Only scan these chain IDs (default: all supported) */
|
|
38
|
+
chainIds?: number[];
|
|
39
|
+
/** Fetch and parse registration files (slower, default: false) */
|
|
40
|
+
fetchRegistration?: boolean;
|
|
41
|
+
/** Registry address override */
|
|
42
|
+
registry?: `0x${string}`;
|
|
43
|
+
/** Timeout per chain in ms (default: 10000) */
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
}
|
|
46
|
+
): Promise<ChainRegistration[]> {
|
|
47
|
+
const registry = options?.registry ?? IDENTITY_REGISTRY_ADDRESS;
|
|
48
|
+
const chainIds = options?.chainIds ?? Object.keys(SUPPORTED_CHAINS).map(Number);
|
|
49
|
+
const fetchReg = options?.fetchRegistration ?? false;
|
|
50
|
+
const timeout = options?.timeoutMs ?? 10000;
|
|
51
|
+
|
|
52
|
+
const results: ChainRegistration[] = [];
|
|
53
|
+
|
|
54
|
+
// Scan all chains in parallel
|
|
55
|
+
const promises = chainIds.map(async (chainId) => {
|
|
56
|
+
const chain = SUPPORTED_CHAINS[chainId];
|
|
57
|
+
if (!chain) return;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const client = createPublicClient({ transport: http(chain.rpc) });
|
|
61
|
+
|
|
62
|
+
// Check balance first (fast)
|
|
63
|
+
const balance = (await Promise.race([
|
|
64
|
+
client.readContract({
|
|
65
|
+
address: registry,
|
|
66
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
67
|
+
functionName: "balanceOf",
|
|
68
|
+
args: [walletAddress],
|
|
69
|
+
}),
|
|
70
|
+
new Promise<bigint>((_, reject) =>
|
|
71
|
+
setTimeout(() => reject(new Error("timeout")), timeout)
|
|
72
|
+
),
|
|
73
|
+
])) as bigint;
|
|
74
|
+
|
|
75
|
+
if (balance === 0n) return;
|
|
76
|
+
|
|
77
|
+
// Find the agent IDs via Transfer events (mint from 0x0)
|
|
78
|
+
const paddedAddress = walletAddress.toLowerCase().replace("0x", "0x000000000000000000000000");
|
|
79
|
+
const logs = await client.getLogs({
|
|
80
|
+
address: registry,
|
|
81
|
+
event: {
|
|
82
|
+
type: "event",
|
|
83
|
+
name: "Transfer",
|
|
84
|
+
inputs: [
|
|
85
|
+
{ type: "address", indexed: true, name: "from" },
|
|
86
|
+
{ type: "address", indexed: true, name: "to" },
|
|
87
|
+
{ type: "uint256", indexed: true, name: "tokenId" },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
args: {
|
|
91
|
+
from: "0x0000000000000000000000000000000000000000" as `0x${string}`,
|
|
92
|
+
to: walletAddress,
|
|
93
|
+
},
|
|
94
|
+
fromBlock: 0n,
|
|
95
|
+
toBlock: "latest",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
for (const log of logs) {
|
|
99
|
+
const agentId = Number(log.args.tokenId);
|
|
100
|
+
|
|
101
|
+
// Get the current owner (could have been transferred)
|
|
102
|
+
const currentOwner = (await client.readContract({
|
|
103
|
+
address: registry,
|
|
104
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
105
|
+
functionName: "ownerOf",
|
|
106
|
+
args: [BigInt(agentId)],
|
|
107
|
+
})) as `0x${string}`;
|
|
108
|
+
|
|
109
|
+
// Only include if still owned by the queried wallet
|
|
110
|
+
if (currentOwner.toLowerCase() !== walletAddress.toLowerCase()) continue;
|
|
111
|
+
|
|
112
|
+
// Get agent URI
|
|
113
|
+
const agentUri = (await client.readContract({
|
|
114
|
+
address: registry,
|
|
115
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
116
|
+
functionName: "tokenURI",
|
|
117
|
+
args: [BigInt(agentId)],
|
|
118
|
+
})) as string;
|
|
119
|
+
|
|
120
|
+
let registration: AgentRegistrationFile | null = null;
|
|
121
|
+
if (fetchReg) {
|
|
122
|
+
try {
|
|
123
|
+
registration = await fetchRegistrationFile(agentUri);
|
|
124
|
+
} catch { /* ignore fetch errors */ }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
results.push({
|
|
128
|
+
chainId,
|
|
129
|
+
chainName: chain.name,
|
|
130
|
+
agentId,
|
|
131
|
+
owner: currentOwner,
|
|
132
|
+
agentUri,
|
|
133
|
+
registration,
|
|
134
|
+
globalId: `eip155:${chainId}:${registry}#${agentId}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Chain unreachable or other error ā skip silently
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await Promise.allSettled(promises);
|
|
143
|
+
|
|
144
|
+
// Sort by chainId for consistent ordering
|
|
145
|
+
return results.sort((a, b) => a.chainId - b.chainId);
|
|
146
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentIdentity ā register and manage ERC-8004 agent identity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createPublicClient, createWalletClient, http, parseEventLogs } from "viem";
|
|
6
|
+
import type { Account, Chain, WalletClient } from "viem";
|
|
7
|
+
import {
|
|
8
|
+
IDENTITY_REGISTRY_ADDRESS,
|
|
9
|
+
IDENTITY_REGISTRY_ABI,
|
|
10
|
+
REGISTRATION_TYPE,
|
|
11
|
+
ZERO_ADDRESS,
|
|
12
|
+
SUPPORTED_CHAINS,
|
|
13
|
+
} from "./constants.js";
|
|
14
|
+
import type {
|
|
15
|
+
IdentityConfig,
|
|
16
|
+
RegisterOptions,
|
|
17
|
+
RegisterResult,
|
|
18
|
+
AgentRegistrationFile,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
|
|
21
|
+
export class AgentIdentity {
|
|
22
|
+
private config: IdentityConfig;
|
|
23
|
+
private registry: `0x${string}`;
|
|
24
|
+
private rpcUrl: string;
|
|
25
|
+
|
|
26
|
+
constructor(config: IdentityConfig) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.registry = config.registry ?? IDENTITY_REGISTRY_ADDRESS;
|
|
29
|
+
|
|
30
|
+
// Resolve RPC URL
|
|
31
|
+
const chainDefaults = SUPPORTED_CHAINS[config.chain.id];
|
|
32
|
+
this.rpcUrl =
|
|
33
|
+
config.rpc ??
|
|
34
|
+
config.chain.rpcUrls?.default?.http?.[0] ??
|
|
35
|
+
chainDefaults?.rpc ??
|
|
36
|
+
"";
|
|
37
|
+
|
|
38
|
+
if (!this.rpcUrl) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`No RPC URL provided for chain ${config.chain.id}. Pass rpc option or use a supported chain.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private getPublicClient() {
|
|
46
|
+
return createPublicClient({
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
chain: this.config.chain as any,
|
|
49
|
+
transport: http(this.rpcUrl),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private getWalletClient() {
|
|
54
|
+
// The account must be a full viem Account for write operations
|
|
55
|
+
if (!this.config.account?.signTransaction && !this.config.account?.sign) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"Write operations require a full viem Account with signing capability. " +
|
|
58
|
+
"Use privateKeyToAccount() from viem/accounts."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return createWalletClient({
|
|
62
|
+
account: this.config.account as Account,
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
chain: this.config.chain as any,
|
|
65
|
+
transport: http(this.rpcUrl),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the registration JSON and encode as data URI
|
|
71
|
+
*/
|
|
72
|
+
buildRegistrationUri(options: RegisterOptions, agentId?: number): string {
|
|
73
|
+
const chainId = this.config.chain.id;
|
|
74
|
+
const registrations = options.existingRegistrations
|
|
75
|
+
? [...options.existingRegistrations]
|
|
76
|
+
: [];
|
|
77
|
+
|
|
78
|
+
// Add current chain registration if we have an agentId
|
|
79
|
+
if (agentId !== undefined) {
|
|
80
|
+
registrations.unshift({
|
|
81
|
+
agentId,
|
|
82
|
+
agentRegistry: `eip155:${chainId}:${this.registry}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const file: AgentRegistrationFile = {
|
|
87
|
+
type: REGISTRATION_TYPE,
|
|
88
|
+
name: options.name,
|
|
89
|
+
description: options.description,
|
|
90
|
+
...(options.image ? { image: options.image } : {}),
|
|
91
|
+
services: options.services ?? [],
|
|
92
|
+
x402Support: options.x402Support ?? false,
|
|
93
|
+
active: options.active ?? true,
|
|
94
|
+
registrations,
|
|
95
|
+
...(options.supportedTrust ? { supportedTrust: options.supportedTrust } : {}),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const json = JSON.stringify(file);
|
|
99
|
+
return "data:application/json;base64," + Buffer.from(json).toString("base64");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if this wallet is already registered on the current chain
|
|
104
|
+
*/
|
|
105
|
+
async isRegistered(): Promise<{ registered: boolean; agentId?: number }> {
|
|
106
|
+
const client = this.getPublicClient();
|
|
107
|
+
const balance = await client.readContract({
|
|
108
|
+
address: this.registry,
|
|
109
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
110
|
+
functionName: "balanceOf",
|
|
111
|
+
args: [this.config.account.address],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (balance === 0n) {
|
|
115
|
+
return { registered: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// We'd need to scan Transfer events to find the agentId ā return basic result
|
|
119
|
+
return { registered: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Register this agent on the current chain
|
|
124
|
+
*/
|
|
125
|
+
async register(options: RegisterOptions): Promise<RegisterResult> {
|
|
126
|
+
const walletClient = this.getWalletClient();
|
|
127
|
+
|
|
128
|
+
// Build a temporary URI without agentId first (we don't know it yet)
|
|
129
|
+
// We'll build it with a placeholder, then update after we have the ID
|
|
130
|
+
const placeholderUri = this.buildRegistrationUri(options);
|
|
131
|
+
|
|
132
|
+
// Estimate gas if not provided
|
|
133
|
+
const publicClient = this.getPublicClient();
|
|
134
|
+
|
|
135
|
+
const { request } = await publicClient.simulateContract({
|
|
136
|
+
account: this.config.account as Account,
|
|
137
|
+
address: this.registry,
|
|
138
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
139
|
+
functionName: "register",
|
|
140
|
+
args: [placeholderUri],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const txHash = await walletClient.writeContract({
|
|
144
|
+
...request,
|
|
145
|
+
...(options.gasLimit ? { gas: options.gasLimit } : {}),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Wait for receipt
|
|
149
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
150
|
+
|
|
151
|
+
// Extract agentId from Transfer event (ERC-721 mint: from=0x0)
|
|
152
|
+
const logs = parseEventLogs({
|
|
153
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
154
|
+
eventName: "Transfer",
|
|
155
|
+
logs: receipt.logs,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const mintLog = logs.find(
|
|
159
|
+
(log) =>
|
|
160
|
+
"args" in log &&
|
|
161
|
+
log.args.from === ZERO_ADDRESS
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (!mintLog || !("args" in mintLog)) {
|
|
165
|
+
throw new Error("Could not find Transfer event in registration receipt");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const agentId = Number(mintLog.args.tokenId);
|
|
169
|
+
const chainId = this.config.chain.id;
|
|
170
|
+
|
|
171
|
+
// Now update the URI with the actual agentId
|
|
172
|
+
const finalUri = this.buildRegistrationUri(options, agentId);
|
|
173
|
+
await this.setAgentURI(agentId, finalUri);
|
|
174
|
+
|
|
175
|
+
const globalId = `eip155:${chainId}:${this.registry}#${agentId}`;
|
|
176
|
+
|
|
177
|
+
return { agentId, txHash, globalId, agentUri: finalUri };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Update the agent's URI (registration file location)
|
|
182
|
+
*/
|
|
183
|
+
async setAgentURI(agentId: number, newUri: string): Promise<`0x${string}`> {
|
|
184
|
+
const walletClient = this.getWalletClient();
|
|
185
|
+
const publicClient = this.getPublicClient();
|
|
186
|
+
|
|
187
|
+
const { request } = await publicClient.simulateContract({
|
|
188
|
+
account: this.config.account as Account,
|
|
189
|
+
address: this.registry,
|
|
190
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
191
|
+
functionName: "setAgentURI",
|
|
192
|
+
args: [BigInt(agentId), newUri],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return walletClient.writeContract(request);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get the agentURI for a given agentId
|
|
200
|
+
*/
|
|
201
|
+
async getAgentURI(agentId: number): Promise<string> {
|
|
202
|
+
const client = this.getPublicClient();
|
|
203
|
+
return client.readContract({
|
|
204
|
+
address: this.registry,
|
|
205
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
206
|
+
functionName: "tokenURI",
|
|
207
|
+
args: [BigInt(agentId)],
|
|
208
|
+
}) as Promise<string>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get the payment wallet for a given agentId
|
|
213
|
+
*/
|
|
214
|
+
async getPaymentWallet(agentId: number): Promise<`0x${string}` | null> {
|
|
215
|
+
const client = this.getPublicClient();
|
|
216
|
+
const wallet = (await client.readContract({
|
|
217
|
+
address: this.registry,
|
|
218
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
219
|
+
functionName: "getAgentWallet",
|
|
220
|
+
args: [BigInt(agentId)],
|
|
221
|
+
})) as `0x${string}`;
|
|
222
|
+
|
|
223
|
+
return wallet === ZERO_ADDRESS ? null : wallet;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get on-chain metadata for an agent
|
|
228
|
+
*/
|
|
229
|
+
async getMetadata(agentId: number, key: string): Promise<string | null> {
|
|
230
|
+
const client = this.getPublicClient();
|
|
231
|
+
const raw = (await client.readContract({
|
|
232
|
+
address: this.registry,
|
|
233
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
234
|
+
functionName: "getMetadata",
|
|
235
|
+
args: [BigInt(agentId), key],
|
|
236
|
+
})) as `0x${string}`;
|
|
237
|
+
|
|
238
|
+
if (!raw || raw === "0x") return null;
|
|
239
|
+
return Buffer.from(raw.slice(2), "hex").toString("utf8");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Set on-chain metadata for an agent (requires ownership)
|
|
244
|
+
*/
|
|
245
|
+
async setMetadata(agentId: number, key: string, value: string): Promise<`0x${string}`> {
|
|
246
|
+
const walletClient = this.getWalletClient();
|
|
247
|
+
const publicClient = this.getPublicClient();
|
|
248
|
+
|
|
249
|
+
const valueBytes = `0x${Buffer.from(value, "utf8").toString("hex")}` as `0x${string}`;
|
|
250
|
+
const { request } = await publicClient.simulateContract({
|
|
251
|
+
account: this.config.account as Account,
|
|
252
|
+
address: this.registry,
|
|
253
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
254
|
+
functionName: "setMetadata",
|
|
255
|
+
args: [BigInt(agentId), key, valueBytes],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return walletClient.writeContract(request);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get the owner of an agent
|
|
263
|
+
*/
|
|
264
|
+
async getOwner(agentId: number): Promise<`0x${string}`> {
|
|
265
|
+
const client = this.getPublicClient();
|
|
266
|
+
return client.readContract({
|
|
267
|
+
address: this.registry,
|
|
268
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
269
|
+
functionName: "ownerOf",
|
|
270
|
+
args: [BigInt(agentId)],
|
|
271
|
+
}) as Promise<`0x${string}`>;
|
|
272
|
+
}
|
|
273
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERC-8004 Agent Identity Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface AgentService {
|
|
6
|
+
name: string;
|
|
7
|
+
endpoint: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
skills?: string[];
|
|
10
|
+
domains?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentRegistrationFile {
|
|
14
|
+
type: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
services: AgentService[];
|
|
19
|
+
x402Support: boolean;
|
|
20
|
+
active: boolean;
|
|
21
|
+
registrations: AgentRegistrationRef[];
|
|
22
|
+
supportedTrust?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AgentRegistrationRef {
|
|
26
|
+
agentId: number;
|
|
27
|
+
agentRegistry: string; // "eip155:{chainId}:{registryAddress}"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentRef {
|
|
31
|
+
namespace: string; // "eip155"
|
|
32
|
+
chainId: number; // 8453
|
|
33
|
+
registry: `0x${string}`; // "0x8004..."
|
|
34
|
+
agentId: number; // 2376
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface VerificationResult {
|
|
38
|
+
valid: boolean;
|
|
39
|
+
agentId: number;
|
|
40
|
+
owner: `0x${string}` | null;
|
|
41
|
+
paymentWallet: `0x${string}` | null;
|
|
42
|
+
registration: AgentRegistrationFile | null;
|
|
43
|
+
globalId: string;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RegisterOptions {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
image?: string;
|
|
51
|
+
services?: AgentService[];
|
|
52
|
+
x402Support?: boolean;
|
|
53
|
+
active?: boolean;
|
|
54
|
+
supportedTrust?: string[];
|
|
55
|
+
/** Extra cross-chain registrations to include in the file */
|
|
56
|
+
existingRegistrations?: AgentRegistrationRef[];
|
|
57
|
+
/** Store as data URI (on-chain) vs IPFS URL. Default: data URI */
|
|
58
|
+
storage?: "data-uri" | "ipfs";
|
|
59
|
+
/** Optional gas limit override */
|
|
60
|
+
gasLimit?: bigint;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RegisterResult {
|
|
64
|
+
agentId: number;
|
|
65
|
+
txHash: `0x${string}`;
|
|
66
|
+
globalId: string;
|
|
67
|
+
agentUri: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface IdentityConfig {
|
|
71
|
+
/**
|
|
72
|
+
* viem Account (e.g. from privateKeyToAccount())
|
|
73
|
+
* For read-only operations, a partial account with just address works.
|
|
74
|
+
*/
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
account: any;
|
|
77
|
+
/**
|
|
78
|
+
* viem Chain object (e.g. import { base } from "viem/chains")
|
|
79
|
+
* Any object with at least { id, name } works.
|
|
80
|
+
*/
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
chain: any;
|
|
83
|
+
/** RPC URL override */
|
|
84
|
+
rpc?: string;
|
|
85
|
+
/** Registry contract address override */
|
|
86
|
+
registry?: `0x${string}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface DiscoverOptions {
|
|
90
|
+
chain?: {
|
|
91
|
+
id: number;
|
|
92
|
+
rpcUrls?: { default?: { http?: string[] } };
|
|
93
|
+
};
|
|
94
|
+
/** Filter agents that have this service type (e.g. "MCP") */
|
|
95
|
+
hasService?: string;
|
|
96
|
+
/** Filter by x402Support flag */
|
|
97
|
+
x402Support?: boolean;
|
|
98
|
+
/** Filter by active flag */
|
|
99
|
+
active?: boolean;
|
|
100
|
+
/** Max number of results */
|
|
101
|
+
limit?: number;
|
|
102
|
+
}
|
package/src/verify.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent identity verification and resolution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createPublicClient, http } from "viem";
|
|
6
|
+
import {
|
|
7
|
+
IDENTITY_REGISTRY_ADDRESS,
|
|
8
|
+
IDENTITY_REGISTRY_ABI,
|
|
9
|
+
ZERO_ADDRESS,
|
|
10
|
+
SUPPORTED_CHAINS,
|
|
11
|
+
} from "./constants.js";
|
|
12
|
+
import type { AgentRef, VerificationResult, AgentRegistrationFile } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse an ERC-8004 global agent ID string
|
|
16
|
+
* Format: "eip155:{chainId}:{registry}#{agentId}"
|
|
17
|
+
* Example: "eip155:8453:0x8004...#2376"
|
|
18
|
+
*/
|
|
19
|
+
export function parseAgentId(globalId: string): AgentRef {
|
|
20
|
+
// Support both "eip155:8453:0x8004...#2376" and "eip155:8453:0x8004.../2376"
|
|
21
|
+
const normalized = globalId.replace("/", "#");
|
|
22
|
+
const match = normalized.match(/^(eip155):(\d+):(0x[0-9a-fA-F]+)#(\d+)$/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid agent global ID format: "${globalId}"\n` +
|
|
26
|
+
`Expected: "eip155:{chainId}:{registry}#{agentId}" (e.g. "eip155:8453:0x8004...#2376")`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
namespace: match[1],
|
|
31
|
+
chainId: Number(match[2]),
|
|
32
|
+
registry: match[3] as `0x${string}`,
|
|
33
|
+
agentId: Number(match[4]),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format a parsed AgentRef back to a global ID string
|
|
39
|
+
*/
|
|
40
|
+
export function formatAgentId(ref: AgentRef): string {
|
|
41
|
+
return `${ref.namespace}:${ref.chainId}:${ref.registry}#${ref.agentId}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch and parse an agent's registration file from its URI
|
|
46
|
+
* Handles: data: URIs, https:// URLs, ipfs:// CIDs (via gateway)
|
|
47
|
+
*/
|
|
48
|
+
export async function fetchRegistrationFile(
|
|
49
|
+
agentUri: string,
|
|
50
|
+
ipfsGateway = "https://ipfs.io/ipfs/"
|
|
51
|
+
): Promise<AgentRegistrationFile> {
|
|
52
|
+
let json: string;
|
|
53
|
+
|
|
54
|
+
if (agentUri.startsWith("data:application/json;base64,")) {
|
|
55
|
+
const b64 = agentUri.slice("data:application/json;base64,".length);
|
|
56
|
+
json = Buffer.from(b64, "base64").toString("utf8");
|
|
57
|
+
} else if (agentUri.startsWith("data:application/json,")) {
|
|
58
|
+
json = decodeURIComponent(agentUri.slice("data:application/json,".length));
|
|
59
|
+
} else if (agentUri.startsWith("ipfs://")) {
|
|
60
|
+
const cid = agentUri.slice("ipfs://".length);
|
|
61
|
+
const response = await fetch(`${ipfsGateway}${cid}`);
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Failed to fetch IPFS URI: ${agentUri} (${response.status})`);
|
|
64
|
+
}
|
|
65
|
+
json = await response.text();
|
|
66
|
+
} else if (agentUri.startsWith("https://") || agentUri.startsWith("http://")) {
|
|
67
|
+
const response = await fetch(agentUri, {
|
|
68
|
+
headers: { Accept: "application/json" },
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`Failed to fetch agent URI: ${agentUri} (${response.status})`);
|
|
72
|
+
}
|
|
73
|
+
json = await response.text();
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Unsupported URI scheme: "${agentUri}". ` +
|
|
77
|
+
`Supported: data:, ipfs://, https://, http://`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const parsed = JSON.parse(json) as AgentRegistrationFile;
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify an agent's identity from its global ID
|
|
87
|
+
* Performs full on-chain + off-chain verification
|
|
88
|
+
*/
|
|
89
|
+
export async function verifyAgent(
|
|
90
|
+
globalIdOrRef:
|
|
91
|
+
| string
|
|
92
|
+
| { chainId: number; agentId: number; registry?: `0x${string}`; rpc?: string }
|
|
93
|
+
): Promise<VerificationResult> {
|
|
94
|
+
let ref: AgentRef;
|
|
95
|
+
|
|
96
|
+
if (typeof globalIdOrRef === "string") {
|
|
97
|
+
ref = parseAgentId(globalIdOrRef);
|
|
98
|
+
} else {
|
|
99
|
+
ref = {
|
|
100
|
+
namespace: "eip155",
|
|
101
|
+
chainId: globalIdOrRef.chainId,
|
|
102
|
+
registry: globalIdOrRef.registry ?? IDENTITY_REGISTRY_ADDRESS,
|
|
103
|
+
agentId: globalIdOrRef.agentId,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const globalId = formatAgentId(ref);
|
|
108
|
+
const chainDefaults = SUPPORTED_CHAINS[ref.chainId];
|
|
109
|
+
const rpcUrl =
|
|
110
|
+
(typeof globalIdOrRef === "object" && "rpc" in globalIdOrRef
|
|
111
|
+
? globalIdOrRef.rpc
|
|
112
|
+
: undefined) ??
|
|
113
|
+
chainDefaults?.rpc;
|
|
114
|
+
|
|
115
|
+
if (!rpcUrl) {
|
|
116
|
+
return {
|
|
117
|
+
valid: false,
|
|
118
|
+
agentId: ref.agentId,
|
|
119
|
+
owner: null,
|
|
120
|
+
paymentWallet: null,
|
|
121
|
+
registration: null,
|
|
122
|
+
globalId,
|
|
123
|
+
error: `No RPC URL available for chain ${ref.chainId}. Add to SUPPORTED_CHAINS or pass rpc option.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const client = createPublicClient({
|
|
128
|
+
transport: http(rpcUrl),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Step 1: Get owner
|
|
133
|
+
const owner = (await client.readContract({
|
|
134
|
+
address: ref.registry,
|
|
135
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
136
|
+
functionName: "ownerOf",
|
|
137
|
+
args: [BigInt(ref.agentId)],
|
|
138
|
+
})) as `0x${string}`;
|
|
139
|
+
|
|
140
|
+
// Step 2: Get payment wallet
|
|
141
|
+
const paymentWalletRaw = (await client.readContract({
|
|
142
|
+
address: ref.registry,
|
|
143
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
144
|
+
functionName: "getAgentWallet",
|
|
145
|
+
args: [BigInt(ref.agentId)],
|
|
146
|
+
})) as `0x${string}`;
|
|
147
|
+
|
|
148
|
+
const paymentWallet =
|
|
149
|
+
paymentWalletRaw === ZERO_ADDRESS ? null : paymentWalletRaw;
|
|
150
|
+
|
|
151
|
+
// Step 3: Get agent URI and fetch registration file
|
|
152
|
+
const agentUri = (await client.readContract({
|
|
153
|
+
address: ref.registry,
|
|
154
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
155
|
+
functionName: "tokenURI",
|
|
156
|
+
args: [BigInt(ref.agentId)],
|
|
157
|
+
})) as string;
|
|
158
|
+
|
|
159
|
+
let registration: AgentRegistrationFile | null = null;
|
|
160
|
+
try {
|
|
161
|
+
registration = await fetchRegistrationFile(agentUri);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return {
|
|
164
|
+
valid: false,
|
|
165
|
+
agentId: ref.agentId,
|
|
166
|
+
owner,
|
|
167
|
+
paymentWallet,
|
|
168
|
+
registration: null,
|
|
169
|
+
globalId,
|
|
170
|
+
error: `Failed to fetch registration file: ${(e as Error).message}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Step 4: Verify back-reference (registration file should list this chain's registration)
|
|
175
|
+
const registryPrefix = `eip155:${ref.chainId}:${ref.registry.toLowerCase()}`;
|
|
176
|
+
const hasBackref = registration.registrations?.some(
|
|
177
|
+
(r) =>
|
|
178
|
+
r.agentId === ref.agentId &&
|
|
179
|
+
r.agentRegistry.toLowerCase() === registryPrefix
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!hasBackref) {
|
|
183
|
+
return {
|
|
184
|
+
valid: false,
|
|
185
|
+
agentId: ref.agentId,
|
|
186
|
+
owner,
|
|
187
|
+
paymentWallet,
|
|
188
|
+
registration,
|
|
189
|
+
globalId,
|
|
190
|
+
error:
|
|
191
|
+
`Registration file does not contain a back-reference to this chain's registry. ` +
|
|
192
|
+
`Expected registrations entry: { agentId: ${ref.agentId}, agentRegistry: "${registryPrefix}" }`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
valid: true,
|
|
198
|
+
agentId: ref.agentId,
|
|
199
|
+
owner,
|
|
200
|
+
paymentWallet,
|
|
201
|
+
registration,
|
|
202
|
+
globalId,
|
|
203
|
+
};
|
|
204
|
+
} catch (e) {
|
|
205
|
+
// ownerOf throws if token doesn't exist
|
|
206
|
+
const message = (e as Error).message;
|
|
207
|
+
return {
|
|
208
|
+
valid: false,
|
|
209
|
+
agentId: ref.agentId,
|
|
210
|
+
owner: null,
|
|
211
|
+
paymentWallet: null,
|
|
212
|
+
registration: null,
|
|
213
|
+
globalId,
|
|
214
|
+
error: message.includes("revert") || message.includes("ERC721")
|
|
215
|
+
? `Agent #${ref.agentId} does not exist on chain ${ref.chainId}`
|
|
216
|
+
: message,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get the MCP endpoint for an agent by resolving their registration
|
|
223
|
+
*/
|
|
224
|
+
export async function getMcpEndpoint(globalId: string): Promise<string | null> {
|
|
225
|
+
const result = await verifyAgent(globalId);
|
|
226
|
+
if (!result.valid || !result.registration) return null;
|
|
227
|
+
|
|
228
|
+
const mcpService = result.registration.services.find(
|
|
229
|
+
(s) => s.name.toUpperCase() === "MCP"
|
|
230
|
+
);
|
|
231
|
+
return mcpService?.endpoint ?? null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get the A2A endpoint for an agent
|
|
236
|
+
*/
|
|
237
|
+
export async function getA2aEndpoint(globalId: string): Promise<string | null> {
|
|
238
|
+
const result = await verifyAgent(globalId);
|
|
239
|
+
if (!result.valid || !result.registration) return null;
|
|
240
|
+
|
|
241
|
+
const a2aService = result.registration.services.find(
|
|
242
|
+
(s) => s.name.toUpperCase() === "A2A"
|
|
243
|
+
);
|
|
244
|
+
return a2aService?.endpoint ?? null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Lightweight check: does this address own any agents on a given chain?
|
|
249
|
+
*/
|
|
250
|
+
export async function getAgentCount(
|
|
251
|
+
address: `0x${string}`,
|
|
252
|
+
chainId: number,
|
|
253
|
+
rpc?: string
|
|
254
|
+
): Promise<number> {
|
|
255
|
+
const chainDefaults = SUPPORTED_CHAINS[chainId];
|
|
256
|
+
const rpcUrl = rpc ?? chainDefaults?.rpc;
|
|
257
|
+
if (!rpcUrl) throw new Error(`No RPC for chain ${chainId}`);
|
|
258
|
+
|
|
259
|
+
const client = createPublicClient({ transport: http(rpcUrl) });
|
|
260
|
+
const balance = (await client.readContract({
|
|
261
|
+
address: IDENTITY_REGISTRY_ADDRESS,
|
|
262
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
263
|
+
functionName: "balanceOf",
|
|
264
|
+
args: [address],
|
|
265
|
+
})) as bigint;
|
|
266
|
+
|
|
267
|
+
return Number(balance);
|
|
268
|
+
}
|