-
Notifications
You must be signed in to change notification settings - Fork 1
Added ERC-8004 agent identity support #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
f07f8ab
59ae089
db50d83
80de323
af29134
243dce3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ import { | |
| FindDemosIdByWeb2IdentityQuery, | ||
| FindDemosIdByWeb3IdentityQuery, | ||
| UDIdentityPayload, | ||
| AgentIdentityPayload, | ||
| DemosOwnershipProof, | ||
| } from "@/types/abstraction" | ||
| import { UnifiedDomainResolution } from "@/abstraction/types/UDResolution" | ||
| import { Demos } from "@/websdk/demosclass" | ||
|
|
@@ -74,7 +76,7 @@ export class Identities { | |
| */ | ||
| private async inferIdentity( | ||
| demos: Demos, | ||
| context: "xm" | "web2" | "pqc" | "ud", | ||
| context: "xm" | "web2" | "pqc" | "ud" | "agent", | ||
| payload: any, | ||
| ): Promise<RPCResponseWithValidityData> { | ||
| if (context === "web2") { | ||
|
|
@@ -89,11 +91,10 @@ export class Identities { | |
| ) | ||
| ) { | ||
| // construct informative error message | ||
| const errorMessage = `Invalid ${ | ||
| payload.context | ||
| } proof format. Supported formats are: ${this.formats.web2[ | ||
| payload.context | ||
| ].join(", ")}` | ||
| const errorMessage = `Invalid ${payload.context | ||
| } proof format. Supported formats are: ${this.formats.web2[ | ||
| payload.context | ||
| ].join(", ")}` | ||
| throw new Error(errorMessage) | ||
| } | ||
| } | ||
|
|
@@ -133,7 +134,7 @@ export class Identities { | |
| */ | ||
| private async removeIdentity( | ||
| demos: Demos, | ||
| context: "xm" | "web2" | "pqc" | "ud", | ||
| context: "xm" | "web2" | "pqc" | "ud" | "agent", | ||
| payload: any, | ||
| ): Promise<RPCResponseWithValidityData> { | ||
| const tx = DemosTransactions.empty() | ||
|
|
@@ -957,7 +958,7 @@ export class Identities { | |
|
|
||
| throw new Error( | ||
| `Unrecognized address format: ${address}. ` + | ||
| `Expected EVM (0x...) or Solana (base58) address.`, | ||
| `Expected EVM (0x...) or Solana (base58) address.`, | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -1113,4 +1114,276 @@ export class Identities { | |
| async getUDIdentities(demos: Demos, address?: string) { | ||
| return await this.getIdentities(demos, "getUDIdentities", address) | ||
| } | ||
|
|
||
| // SECTION: ERC-8004 Agent Identities | ||
|
|
||
| /** | ||
| * ERC-8004 IdentityRegistry contract address on Base Sepolia | ||
| */ | ||
| static readonly AGENT_REGISTRY_ADDRESS = | ||
| "0x8004AA63c570c570eBF15376c0dB199918BFe9Fb" | ||
|
|
||
| /** | ||
| * Base Sepolia chain configuration for agent identity | ||
| */ | ||
| static readonly AGENT_CHAIN_CONFIG = { | ||
| chainId: 84532, | ||
| chain: "base", | ||
| subchain: "sepolia", | ||
| rpc: "https://sepolia.base.org", | ||
| } | ||
|
|
||
| /** | ||
| * Generate the ownership proof message for ERC-8004 agent registration. | ||
| * | ||
| * This message must be signed by the user's Demos wallet to authorize | ||
| * linking an EVM address (which owns the agent NFT) to their Demos identity. | ||
| * | ||
| * @param demosPublicKey The user's Demos public key (hex string) | ||
| * @param agentId The ERC-8004 token ID being claimed | ||
| * @param evmAddress The EVM address that owns the agent NFT | ||
| * @returns Object containing the message and timestamp | ||
| */ | ||
| generateAgentOwnershipMessage( | ||
| demosPublicKey: string, | ||
| agentId: string, | ||
| evmAddress: string, | ||
| ): { message: string; timestamp: number } { | ||
| const timestamp = Date.now() | ||
| const message = `I authorize EVM address ${evmAddress} to register ERC-8004 agent #${agentId} for Demos identity ${demosPublicKey}. Timestamp: ${timestamp}` | ||
| return { message, timestamp } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Create an ownership proof by signing the ownership message with the Demos wallet. | ||
| * | ||
| * @param demos A Demos instance to sign the message | ||
| * @param agentId The ERC-8004 token ID being claimed | ||
| * @param evmAddress The EVM address that owns the agent NFT | ||
| * @returns The complete ownership proof object | ||
| */ | ||
| async createAgentOwnershipProof( | ||
| demos: Demos, | ||
| agentId: string, | ||
| evmAddress: string, | ||
| ): Promise<DemosOwnershipProof> { | ||
| const demosPublicKey = await demos.getEd25519Address() | ||
| const { message, timestamp } = this.generateAgentOwnershipMessage( | ||
| demosPublicKey, | ||
| agentId, | ||
| evmAddress, | ||
| ) | ||
|
|
||
| const signature = await demos.crypto.sign( | ||
| demos.algorithm, | ||
| new TextEncoder().encode(message), | ||
| ) | ||
|
|
||
| return { | ||
| type: "demos-signature", | ||
| message, | ||
| signature: { | ||
| type: demos.algorithm, | ||
| data: uint8ArrayToHex(signature.signature), | ||
| }, | ||
| demosPublicKey, | ||
| agentId, | ||
| evmAddress, | ||
| timestamp, | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Add an ERC-8004 agent identity to the GCR. | ||
| * | ||
| * This links an ERC-8004 agent NFT (registered on Base Sepolia) to a Demos identity. | ||
| * | ||
| * Requirements: | ||
| * - User must have an EVM wallet linked to their Demos identity | ||
| * - That EVM wallet must own the agent NFT | ||
| * - User must sign an ownership proof with their Demos wallet | ||
| * | ||
| * @param demos A Demos instance to communicate with the RPC | ||
| * @param payload The agent identity payload containing: | ||
| * - agentId: The ERC-8004 token ID | ||
| * - evmAddress: The EVM address owning the agent | ||
| * - chain: The chain where agent is registered (e.g., "base.sepolia") | ||
| * - txHash: The registration transaction hash | ||
| * - tokenUri: The token URI pointing to agent card metadata | ||
| * - proof: The ownership proof signed by Demos wallet | ||
| * @param referralCode Optional referral code for points | ||
| * @returns The validity data of the identity transaction | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const identities = new Identities() | ||
| * | ||
| * // Create ownership proof | ||
| * const proof = await identities.createAgentOwnershipProof(demos, evmAddress) | ||
| * | ||
| * // Add agent identity | ||
| * await identities.addAgentIdentity(demos, { | ||
| * agentId: "123", | ||
| * evmAddress: "0x...", | ||
| * chain: "base.sepolia", | ||
| * txHash: "0x...", | ||
| * tokenUri: "ipfs://...", | ||
| * proof | ||
| * }) | ||
| * ``` | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| */ | ||
| async addAgentIdentity( | ||
| demos: Demos, | ||
| payload: AgentIdentityPayload, | ||
| referralCode?: string, | ||
| ): Promise<RPCResponseWithValidityData> { | ||
| // Validate the payload | ||
| if (!payload.agentId) { | ||
| throw new Error("Agent ID is required") | ||
| } | ||
| if (!payload.evmAddress) { | ||
| throw new Error("EVM address is required") | ||
| } | ||
| if (!payload.proof) { | ||
| throw new Error("Ownership proof is required") | ||
| } | ||
|
|
||
| // Verify the EVM address format | ||
| const evmPattern = /^0x[0-9a-fA-F]{40}$/ | ||
| if (!evmPattern.test(payload.evmAddress)) { | ||
| throw new Error( | ||
| `Invalid EVM address format: ${payload.evmAddress}`, | ||
| ) | ||
| } | ||
|
|
||
| // Verify the proof contains the correct EVM address | ||
| if ( | ||
| payload.proof.evmAddress.toLowerCase() !== | ||
| payload.evmAddress.toLowerCase() | ||
| ) { | ||
| throw new Error( | ||
| "Ownership proof EVM address doesn't match payload EVM address", | ||
| ) | ||
| } | ||
|
Comment on lines
+1321
to
+1348
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate full proof-to-payload binding in Current checks only bind Suggested fix // Verify the proof contains the correct EVM address
if (
payload.proof.evmAddress.toLowerCase() !==
payload.evmAddress.toLowerCase()
) {
throw new Error(
"Ownership proof EVM address doesn't match payload EVM address",
)
}
+
+ if (payload.proof.agentId !== payload.agentId) {
+ throw new Error(
+ "Ownership proof agentId doesn't match payload agentId",
+ )
+ }
+
+ const connectedDemosKey = await demos.getEd25519Address()
+ if (payload.proof.demosPublicKey !== connectedDemosKey) {
+ throw new Error(
+ "Ownership proof demosPublicKey doesn't match connected identity",
+ )
+ }🤖 Prompt for AI Agents |
||
|
|
||
| return await this.inferIdentity(demos, "agent", { | ||
| ...payload, | ||
| referralCode, | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * Remove an ERC-8004 agent identity from the GCR. | ||
| * | ||
| * @param demos A Demos instance to communicate with the RPC | ||
| * @param agentId The ERC-8004 token ID to remove | ||
| * @param chain The chain where the agent is registered (e.g., "base.sepolia") | ||
| * @returns The validity data response from the RPC | ||
| */ | ||
| async removeAgentIdentity( | ||
| demos: Demos, | ||
| agentId: string, | ||
| chain: string = "base.sepolia", | ||
| ): Promise<RPCResponseWithValidityData> { | ||
| return await this.removeIdentity(demos, "agent", { agentId, chain }) | ||
| } | ||
|
|
||
| /** | ||
| * Get the ERC-8004 agent identities associated with an address. | ||
| * | ||
| * @param demos A Demos instance to communicate with the RPC | ||
| * @param address The Demos address to get agent identities for. | ||
| * Defaults to the connected wallet's address. | ||
| * @returns The agent identities associated with the address. | ||
| */ | ||
| async getAgentIdentities(demos: Demos, address?: string) { | ||
| return await this.getIdentities(demos, "getAgentIdentities", address) | ||
| } | ||
|
|
||
| /** | ||
| * Verify that an EVM address owns a specific ERC-8004 agent NFT. | ||
| * | ||
| * This is a client-side helper to verify ownership before submitting | ||
| * the identity to the network. The node will also verify this. | ||
| * | ||
| * @param agentId The ERC-8004 token ID | ||
| * @param expectedOwner The expected EVM address owner | ||
| * @returns True if the address owns the agent, false otherwise | ||
| */ | ||
| async verifyAgentOwnership( | ||
| agentId: string, | ||
| expectedOwner: string, | ||
| ): Promise<boolean> { | ||
| const registryAbi = [ | ||
| "function ownerOf(uint256 tokenId) external view returns (address)", | ||
| ] | ||
|
|
||
| try { | ||
| const provider = new ethers.JsonRpcProvider( | ||
| Identities.AGENT_CHAIN_CONFIG.rpc, | ||
| ) | ||
| const contract = new ethers.Contract( | ||
| Identities.AGENT_REGISTRY_ADDRESS, | ||
| registryAbi, | ||
| provider, | ||
| ) | ||
|
|
||
| const owner = await contract.ownerOf(agentId) | ||
| return owner.toLowerCase() === expectedOwner.toLowerCase() | ||
| } catch (error: any) { | ||
| // Token doesn't exist or other error | ||
| return false | ||
| } | ||
|
Comment on lines
+1402
to
+1417
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not collapse all verification errors into Returning Suggested fix try {
@@
const owner = await contract.ownerOf(agentId)
return owner.toLowerCase() === expectedOwner.toLowerCase()
} catch (error: any) {
- // Token doesn't exist or other error
- return false
+ const message = String(error?.message || "")
+ if (
+ message.toLowerCase().includes("nonexistent token") ||
+ message.toLowerCase().includes("erc721")
+ ) {
+ return false
+ }
+ throw new Error(`Failed to verify agent ownership: ${message}`)
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
| * Fetch the agent card metadata from the token URI. | ||
| * | ||
| * @param agentId The ERC-8004 token ID | ||
| * @returns The agent card metadata or null if not found | ||
| */ | ||
| async getAgentCard(agentId: string): Promise<any | null> { | ||
| const registryAbi = [ | ||
| "function tokenURI(uint256 tokenId) external view returns (string)", | ||
| ] | ||
|
|
||
| try { | ||
| const provider = new ethers.JsonRpcProvider( | ||
| Identities.AGENT_CHAIN_CONFIG.rpc, | ||
| ) | ||
| const contract = new ethers.Contract( | ||
| Identities.AGENT_REGISTRY_ADDRESS, | ||
| registryAbi, | ||
| provider, | ||
| ) | ||
|
|
||
| const tokenUri = await contract.tokenURI(agentId) | ||
|
|
||
| // Handle different URI formats | ||
| if (tokenUri.startsWith("data:application/json;base64,")) { | ||
| // Base64 encoded JSON (data URI) | ||
| const base64 = tokenUri.replace( | ||
| "data:application/json;base64,", | ||
| "", | ||
| ) | ||
| const json = Buffer.from(base64, "base64").toString("utf-8") | ||
| return JSON.parse(json) | ||
| } else if (tokenUri.startsWith("ipfs://")) { | ||
| // IPFS URI - fetch via gateway | ||
| const ipfsHash = tokenUri.replace("ipfs://", "") | ||
| const response = await axios.get( | ||
| `https://ipfs.io/ipfs/${ipfsHash}`, | ||
| ) | ||
|
Comment on lines
+1467
to
+1473
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize IPFS URIs before gateway resolution.
Suggested fix } else if (tokenUri.startsWith("ipfs://")) {
// IPFS URI - fetch via gateway
- const ipfsHash = tokenUri.replace("ipfs://", "")
+ const ipfsPath = tokenUri.replace(/^ipfs:\/\//, "")
+ const normalizedIpfsPath = ipfsPath.replace(/^ipfs\//, "")
const response = await axios.get(
- `https://ipfs.io/ipfs/${ipfsHash}`,
+ `https://ipfs.io/ipfs/${normalizedIpfsPath}`,
{ timeout: 10_000 },
)
return response.data🤖 Prompt for AI Agents |
||
| return response.data | ||
| } else if (tokenUri.startsWith("http")) { | ||
| // HTTP(S) URI | ||
| const response = await axios.get(tokenUri) | ||
| return response.data | ||
| } | ||
|
|
||
| return null | ||
| } catch (error) { | ||
| return null | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.