@agentic-trust/8004-ext-sdk
Version:
ERC-8004 Agentic Trust SDK - A TypeScript SDK for managing AI agents with ENS integration, identity management, and reputation systems
417 lines • 17.4 kB
JavaScript
/**
* Agentic Trust SDK - Identity Client
* Extends the base ERC-8004 IdentityClient with AA-centric helpers.
*
* Uses AccountProvider (Ports & Adapters pattern) for chain I/O.
*/
import { createPublicClient, http, hexToString, } from 'viem';
import { sepolia, baseSepolia, optimismSepolia } from 'viem/chains';
import { BaseIdentityClient, ViemAccountProvider, } from '@agentic-trust/8004-sdk';
import IdentityRegistryABI from './abis/IdentityRegistry.json';
function getChainById(chainId) {
switch (chainId) {
case 11155111: // ETH Sepolia
return sepolia;
case 84532: // Base Sepolia
return baseSepolia;
case 11155420: // Optimism Sepolia
return optimismSepolia;
default:
console.warn(`Unknown chainId ${chainId}, defaulting to ETH Sepolia`);
return sepolia;
}
}
export class AIAgentIdentityClient extends BaseIdentityClient {
chain = null;
identityRegistryAddress;
publicClient = null;
walletClient = null;
// accountProvider is protected in BaseIdentityClient, so we need to keep it accessible
accountProvider;
constructor(options) {
let accountProvider;
let chain = null;
let publicClient = null;
let walletClient = null;
let identityRegistryAddress;
if ('accountProvider' in options) {
// Option 1: Use provided AccountProvider (recommended)
accountProvider = options.accountProvider;
identityRegistryAddress = options.identityRegistryAddress;
// Try to extract publicClient from AccountProvider if it's a ViemAccountProvider
const viemProvider = accountProvider;
if (viemProvider.publicClient) {
publicClient = viemProvider.publicClient;
}
if (viemProvider.walletClient) {
walletClient = viemProvider.walletClient;
}
if (viemProvider.chainConfig?.chain) {
chain = viemProvider.chainConfig.chain;
}
}
else if ('publicClient' in options) {
// Option 2: Use viem clients directly (simplest, native viem)
publicClient = options.publicClient;
walletClient = options.walletClient ?? null;
identityRegistryAddress = options.identityRegistryAddress;
// Create ChainConfig
const chainConfig = options.chainConfig || {
id: publicClient.chain?.id || 11155111,
rpcUrl: publicClient.transport?.url || '',
name: publicClient.chain?.name || 'Unknown',
chain: publicClient.chain || undefined,
};
// Create ViemAccountProvider from the clients
accountProvider = new ViemAccountProvider({
publicClient,
walletClient: walletClient ?? null,
account: walletClient?.account,
chainConfig,
});
}
else {
// Option 3: Legacy pattern - create from chainId/rpcUrl
chain = getChainById(options.chainId);
// @ts-ignore - viem version compatibility issue
publicClient = createPublicClient({ chain, transport: http(options.rpcUrl) });
walletClient = options.walletClient ?? null;
// Create ChainConfig
const chainConfig = {
id: options.chainId,
rpcUrl: options.rpcUrl,
name: chain.name,
chain: chain,
bundlerUrl: options.bundlerUrl,
paymasterUrl: options.paymasterUrl,
};
// Create ViemAccountProvider
accountProvider = new ViemAccountProvider({
publicClient,
walletClient: walletClient ?? null,
account: options.account || walletClient?.account,
chainConfig,
});
identityRegistryAddress = options.identityRegistryAddress;
}
// Pass accountProvider to BaseIdentityClient
super(accountProvider, identityRegistryAddress);
this.chain = chain;
this.publicClient = publicClient;
this.walletClient = walletClient;
this.identityRegistryAddress = identityRegistryAddress;
this.accountProvider = accountProvider;
}
/**
* Get metadata using AccountProvider
*/
async getMetadata(agentId, key) {
const bytes = await this.accountProvider.call({
to: this.identityRegistryAddress,
abi: IdentityRegistryABI,
functionName: 'getMetadata',
args: [agentId, key],
});
return hexToString(bytes);
}
/**
* Encode function call data using AccountProvider
*/
async encodeFunctionData(abi, functionName, args) {
return await this.accountProvider.encodeFunctionData({
abi,
functionName,
args,
});
}
/**
* Legacy method - delegates to encodeFunctionData
* @deprecated Use encodeFunctionData instead
*/
encodeCall(abi, functionName, args) {
// This is a synchronous method, but encodeFunctionData is async
// For backward compatibility, we'll use ethers for now
// TODO: Consider making this async or removing it
const { ethers } = require('ethers');
const iface = new ethers.Interface(abi);
return iface.encodeFunctionData(functionName, args);
}
/**
* Encode register calldata without sending (for bundler/AA - like EAS SDK pattern)
* This override exists in the Agentic Trust SDK to keep AA helpers here.
*/
async encodeRegisterWithMetadata(tokenUri, metadata = []) {
// Format metadata: convert string values to hex strings (Viem expects hex for bytes)
const metadataFormatted = metadata.map(m => {
// Use stringToBytes from base class (via inheritance)
const bytes = this.stringToBytes(m.value);
// Convert to hex string (Viem requires hex strings, not Uint8Array)
const hexString = this.bytesToHex(bytes);
return {
key: m.key,
value: hexString,
};
});
// Use AccountProvider's encodeFunctionData
return await this.accountProvider.encodeFunctionData({
abi: IdentityRegistryABI,
functionName: 'register',
args: [tokenUri, metadataFormatted],
});
}
async encodeRegister(name, agentAccount, tokenUri) {
console.info("name: ", name);
console.info("agentAccount: ", agentAccount);
return await this.encodeRegisterWithMetadata(tokenUri, [{ key: 'agentName', value: name }, { key: 'agentAccount', value: agentAccount }]);
}
async prepareRegisterCalls(name, agentAccount, tokenUri) {
const data = await this.encodeRegisterWithMetadata(tokenUri, [{ key: 'agentName', value: name }, { key: 'agentAccount', value: agentAccount }]);
const calls = [];
calls.push({
to: this.identityRegistryAddress,
data: data
});
return { calls };
}
async encodeSetRegistrationUri(agentId, uri) {
const data = await this.accountProvider.encodeFunctionData({
abi: IdentityRegistryABI,
functionName: 'setAgentUri',
args: [agentId, uri],
});
return data;
}
async prepareSetRegistrationUriCalls(agentId, uri) {
const calls = [];
const data = await this.encodeSetRegistrationUri(agentId, uri);
calls.push({
to: this.identityRegistryAddress,
data: data
});
return { calls };
}
/**
* Prepare a complete transaction for client-side signing (similar to prepareCall for bundlers)
* All Ethereum logic (encoding, gas estimation, nonce) is handled server-side
* Client only needs to sign and send with MetaMask
* @param tokenUri - IPFS token URI for the agent registration
* @param metadata - Metadata entries for the agent
* @param fromAddress - Address that will sign the transaction (only address needed, no client)
* @returns Prepared transaction object ready for client-side signing
*/
async prepareRegisterTransaction(tokenUri, metadata, fromAddress) {
// Encode the transaction data
const encodedData = await this.encodeRegisterWithMetadata(tokenUri, metadata);
// Get chain ID using AccountProvider
const chainId = await this.accountProvider.chainId();
// Initialize gas estimation variables
let gasEstimate;
let gasPrice;
let maxFeePerGas;
let maxPriorityFeePerGas;
let nonce;
try {
// Get current block data to check for EIP-1559 support
const blockData = await this.accountProvider.getBlock('latest');
// Prefer EIP-1559 (maxFeePerGas/maxPriorityFeePerGas) if available
// Otherwise fall back to legacy gasPrice
if (blockData && 'baseFeePerGas' in blockData && blockData.baseFeePerGas) {
// EIP-1559: Use maxFeePerGas and maxPriorityFeePerGas
// Set a reasonable priority fee (1-2 gwei typically)
// maxFeePerGas should be baseFeePerGas + maxPriorityFeePerGas + buffer
maxPriorityFeePerGas = 1000000000n; // 1 gwei as priority fee
maxFeePerGas = (blockData.baseFeePerGas * 2n) + maxPriorityFeePerGas; // 2x base + priority (buffer for safety)
}
else {
// Legacy: Use gasPrice
gasPrice = await this.accountProvider.getGasPrice();
}
// Estimate gas using AccountProvider
gasEstimate = await this.accountProvider.estimateGas({
account: fromAddress,
to: this.identityRegistryAddress,
data: encodedData,
});
// Get nonce using AccountProvider
nonce = await this.accountProvider.getTransactionCount(fromAddress, 'pending');
}
catch (error) {
console.warn('Could not estimate gas or get transaction parameters:', error);
// Continue without gas estimates - client can estimate
}
// Build transaction object - return hex strings for all bigint values (Viem accepts hex strings directly)
// This format can be used directly with Viem's sendTransaction without client-side conversion
const txParams = {
to: this.identityRegistryAddress,
data: encodedData,
value: '0x0', // Hex string for value
gas: gasEstimate ? `0x${gasEstimate.toString(16)}` : undefined,
nonce,
chainId,
};
// Include EIP-1559 fields if available, otherwise legacy gasPrice
// All as hex strings for direct Viem compatibility
if (maxFeePerGas && maxPriorityFeePerGas) {
txParams.maxFeePerGas = `0x${maxFeePerGas.toString(16)}`;
txParams.maxPriorityFeePerGas = `0x${maxPriorityFeePerGas.toString(16)}`;
}
else if (gasPrice) {
txParams.gasPrice = `0x${gasPrice.toString(16)}`;
}
return txParams;
}
async isValidAgentAccount(agentAccount) {
try {
// Use AccountProvider's ReadClient interface - check if address has code
// We can use a simple call to check if it's a contract
// For now, we'll use publicClient if available, otherwise return null
if (this.publicClient) {
const code = await this.publicClient.getBytecode({ address: agentAccount });
return code ? true : false;
}
// AccountProvider doesn't expose getBytecode directly, so we check via isContractSigner
// This is a workaround - ideally AccountProvider would expose getBytecode
return null;
}
catch {
return null;
}
}
/**
* Extract agentId from a user operation/transaction receipt
* Public in this SDK to support AA flows explicitly.
*/
extractAgentIdFromReceiptPublic(receipt) {
// Look for parsed events first
if (receipt?.events) {
const registeredEvent = receipt.events.find((e) => e.name === 'Registered');
if (registeredEvent?.args) {
const val = registeredEvent.args.agentId ?? registeredEvent.args[0];
if (val !== undefined)
return BigInt(val);
}
const transferEvent = receipt.events.find((e) => e.name === 'Transfer' && (e.args.from === '0x0000000000000000000000000000000000000000' || e.args.from === 0 || e.args.from === 0n));
if (transferEvent?.args) {
const val = transferEvent.args.tokenId ?? transferEvent.args[2];
if (val !== undefined)
return BigInt(val);
}
}
// Fallback: raw logs array
if (receipt?.logs && Array.isArray(receipt.logs)) {
for (const log of receipt.logs) {
// Transfer(address,address,uint256)
if (log.topics && log.topics[0] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') {
const from = log.topics[1];
if (from === '0x0000000000000000000000000000000000000000000000000000000000000000') {
const tokenId = BigInt(log.topics[3] || log.data);
return tokenId;
}
}
}
}
throw new Error('Could not extract agentId from transaction receipt - Registered or Transfer event not found');
}
/**
* Get the owner (EOA) of an account address
*
* @param accountAddress - The account address (smart account or contract)
* @returns The owner address (EOA) or null if not found or error
*/
async getAccountOwner(accountAddress) {
try {
const owner = await this.accountProvider.call({
to: accountAddress,
abi: [{ name: 'owner', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] }],
functionName: 'owner',
args: [],
});
return owner;
}
catch {
return null;
}
}
/**
* @deprecated Use getAccountOwner instead
*/
async getAgentEoaByAgentAccount(agentAccount) {
return this.getAccountOwner(agentAccount);
}
/**
* Get agentName from on-chain metadata (string value)
*/
async getAgentName(agentId) {
try {
const name = await this.getMetadata(agentId, 'agentName');
if (typeof name === 'string') {
const trimmed = name.trim();
return trimmed.length > 0 ? trimmed : null;
}
return name ? String(name) : null;
}
catch (error) {
console.info("++++++++++++++++++++++++ getAgentName: error", error);
return null;
}
}
/**
* Get agentAccount address from on-chain metadata.
* Supports CAIP-10 format like "eip155:11155111:0x..." or raw 0x address.
*/
async getAgentAccount(agentId) {
try {
const value = await this.getMetadata(agentId, 'agentAccount');
if (!value)
return null;
if (typeof value === 'string') {
const v = value.trim();
if (v.startsWith('eip155:')) {
const parts = v.split(':');
const addr = parts[2];
if (addr && /^0x[a-fA-F0-9]{40}$/.test(addr))
return addr;
}
if (/^0x[a-fA-F0-9]{40}$/.test(v))
return v;
}
return null;
}
catch {
return null;
}
}
/**
* Keep compatibility: delegate to receipt extractor.
*/
extractAgentIdFromLogs(receipt) {
return this.extractAgentIdFromReceiptPublic(receipt);
}
/**
* Get the approved operator address for an agent NFT token
* Returns the address approved to operate on the token, or null if no operator is set
*
* @param agentId - The agent ID (token ID)
* @returns The approved operator address, or null if no operator is set (zero address)
*/
async getNFTOperator(agentId) {
try {
const operatorAddress = await this.accountProvider.call({
to: this.identityRegistryAddress,
abi: IdentityRegistryABI,
functionName: 'getApproved',
args: [agentId],
});
// Check if operator is set (not zero address)
if (operatorAddress && operatorAddress !== '0x0000000000000000000000000000000000000000') {
return operatorAddress;
}
return null;
}
catch (error) {
console.error('Failed to get NFT operator:', error);
return null;
}
}
}
//# sourceMappingURL=AIAgentIdentityClient.js.map