UNPKG

erc-8004-js

Version:

TypeScript SDK for ERC-8004 Trustless Agents protocol

244 lines (217 loc) 7.45 kB
/** * Identity Client for ERC-8004 * Handles agent registration and identity management */ import { BlockchainAdapter } from './adapters/types'; import { MetadataEntry, AgentRegistrationFile } from './types'; import IdentityRegistryABI from './abis/IdentityRegistry.json'; export class IdentityClient { private adapter: BlockchainAdapter; private contractAddress: string; constructor(adapter: BlockchainAdapter, contractAddress: string) { this.adapter = adapter; this.contractAddress = contractAddress; } /** * Register a new agent with no URI (URI can be set later) * Spec: function register() returns (uint256 agentId) */ async register(): Promise<{ agentId: bigint; txHash: string }> { const result = await this.adapter.send( this.contractAddress, IdentityRegistryABI, 'register', [] ); // Parse agentId from receipt logs // This is implementation-agnostic - we just need to find the Registered event const agentId = this.extractAgentIdFromReceipt(result); return { agentId, txHash: result.txHash, }; } /** * Register a new agent with a token URI * Spec: function register(string tokenURI) returns (uint256 agentId) * @param tokenURI - URI pointing to agent registration file (MAY use ipfs://, https://, etc.) */ async registerWithURI(tokenURI: string): Promise<{ agentId: bigint; txHash: string }> { const result = await this.adapter.send( this.contractAddress, IdentityRegistryABI, 'register(string)', [tokenURI] ); const agentId = this.extractAgentIdFromReceipt(result); return { agentId, txHash: result.txHash, }; } /** * Register a new agent with URI and optional on-chain metadata * Spec: function register(string tokenURI, MetadataEntry[] calldata metadata) returns (uint256 agentId) * @param tokenURI - URI pointing to agent registration file * @param metadata - OPTIONAL on-chain metadata entries */ async registerWithMetadata( tokenURI: string, metadata: MetadataEntry[] = [] ): Promise<{ agentId: bigint; txHash: string }> { // Convert metadata to contract format const metadataFormatted = metadata.map(m => ({ key: m.key, value: this.stringToBytes(m.value) })); const result = await this.adapter.send( this.contractAddress, IdentityRegistryABI, 'register(string,(string,bytes)[])', [tokenURI, metadataFormatted] ); const agentId = this.extractAgentIdFromReceipt(result); return { agentId, txHash: result.txHash, }; } /** * Get the token URI for an agent * Spec: Standard ERC-721 tokenURI function * @param agentId - The agent's ID * @returns URI string (MAY be ipfs://, https://, etc.) */ async getTokenURI(agentId: bigint): Promise<string> { return await this.adapter.call( this.contractAddress, IdentityRegistryABI, 'tokenURI', [agentId] ); } /** * Set the token URI for an agent * Note: This is an implementation-specific extension (not in base spec). * Assumes implementation exposes setAgentUri with owner/operator checks. * @param agentId - The agent's ID * @param uri - New URI string */ async setAgentUri(agentId: bigint, uri: string): Promise<{ txHash: string }> { const result = await this.adapter.send( this.contractAddress, IdentityRegistryABI, 'setAgentUri', [agentId, uri] ); return { txHash: result.txHash }; } /** * Get the owner of an agent * Spec: Standard ERC-721 ownerOf function * @param agentId - The agent's ID */ async getOwner(agentId: bigint): Promise<string> { return await this.adapter.call( this.contractAddress, IdentityRegistryABI, 'ownerOf', [agentId] ); } /** * Get on-chain metadata for an agent * Spec: function getMetadata(uint256 agentId, string key) returns (bytes) * @param agentId - The agent's ID * @param key - Metadata key */ async getMetadata(agentId: bigint, key: string): Promise<string> { const bytes = await this.adapter.call( this.contractAddress, IdentityRegistryABI, 'getMetadata', [agentId, key] ); return this.bytesToString(bytes); } /** * Set on-chain metadata for an agent * Spec: function setMetadata(uint256 agentId, string key, bytes value) * @param agentId - The agent's ID * @param key - Metadata key * @param value - Metadata value */ async setMetadata(agentId: bigint, key: string, value: string): Promise<{ txHash: string }> { const result = await this.adapter.send( this.contractAddress, IdentityRegistryABI, 'setMetadata', [agentId, key, this.stringToBytes(value)] ); return { txHash: result.txHash }; } /** * Fetch and parse the agent registration file from the token URI * This is a convenience function that fetches the URI and parses it * Note: Does not validate - spec says ERC-8004 cannot cryptographically guarantee * that advertised capabilities are functional * @param agentId - The agent's ID */ async getRegistrationFile(agentId: bigint): Promise<AgentRegistrationFile> { const uri = await this.getTokenURI(agentId); // Handle different URI schemes if (uri.startsWith('ipfs://')) { // IPFS gateway - implementation specific, using public gateway const cid = uri.replace('ipfs://', ''); const httpUri = `https://ipfs.io/ipfs/${cid}`; const response = await fetch(httpUri); return await response.json() as AgentRegistrationFile; } else if (uri.startsWith('https://') || uri.startsWith('http://')) { const response = await fetch(uri); return await response.json() as AgentRegistrationFile; } else { throw new Error(`Unsupported URI scheme: ${uri}`); } } /** * Helper: Extract agentId from transaction receipt * Looks for the Registered event which contains the agentId */ private extractAgentIdFromReceipt(result: any): bigint { // Look for Registered event in parsed events if (result.events && result.events.length > 0) { const registeredEvent = result.events.find((e: any) => e.name === 'Registered'); if (registeredEvent && registeredEvent.args) { return BigInt(registeredEvent.args.agentId || registeredEvent.args[0]); } } throw new Error( 'Could not extract agentId from transaction receipt - Registered event not found. ' + 'This usually means the contract is not deployed or the ABI does not match the deployed contract.' ); } /** * Helper: Convert string to bytes (adapter-agnostic) */ private stringToBytes(value: string): Uint8Array { return new TextEncoder().encode(value); } /** * Helper: Convert bytes to string (adapter-agnostic) */ private bytesToString(bytes: any): string { if (bytes instanceof Uint8Array) { return new TextDecoder().decode(bytes); } // Handle hex string format (ethers returns this) if (typeof bytes === 'string' && bytes.startsWith('0x')) { const hex = bytes.slice(2); const arr = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { arr[i / 2] = parseInt(hex.slice(i, i + 2), 16); } return new TextDecoder().decode(arr); } return bytes.toString(); } }