erc-8004-js
Version:
TypeScript SDK for ERC-8004 Trustless Agents protocol
233 lines (210 loc) • 8.17 kB
text/typescript
/**
* Validation Client for ERC-8004
* Handles validation requests and responses
*/
import { BlockchainAdapter } from './adapters/types';
import { ValidationStatus } from './types';
import ValidationRegistryABI from './abis/ValidationRegistry.json';
import { ethers } from 'ethers';
export interface ValidationRequestParams {
validatorAddress: string; // MANDATORY
agentId: bigint; // MANDATORY
requestUri: string; // MANDATORY
requestHash: string; // MANDATORY (bytes32 hash of content at requestUri)
}
export interface ValidationResponseParams {
requestHash: string; // MANDATORY (bytes32)
response: number; // MANDATORY (0-100)
responseUri?: string; // OPTIONAL
responseHash?: string; // OPTIONAL (bytes32)
tag?: string; // OPTIONAL (bytes32)
}
export class ValidationClient {
private adapter: BlockchainAdapter;
private contractAddress: string;
constructor(adapter: BlockchainAdapter, contractAddress: string) {
this.adapter = adapter;
this.contractAddress = contractAddress;
}
/**
* Request validation from a validator
* Spec: function validationRequest(address validatorAddress, uint256 agentId, string requestUri, bytes32 requestHash)
* Note: MUST be called by owner or operator of agentId
* Note: requestHash MUST be keccak256 of the content at requestUri
*
* @param params - Validation request parameters
* @returns Transaction result with requestHash
*/
async validationRequest(params: ValidationRequestParams): Promise<{ txHash: string; requestHash: string }> {
const result = await this.adapter.send(
this.contractAddress,
ValidationRegistryABI,
'validationRequest',
[params.validatorAddress, params.agentId, params.requestUri, params.requestHash]
);
return {
txHash: result.txHash,
requestHash: params.requestHash,
};
}
/**
* Provide a validation response
* Spec: function validationResponse(bytes32 requestHash, uint8 response, string responseUri, bytes32 responseHash, bytes32 tag)
* Note: MUST be called by the validatorAddress specified in the original request
* Note: Can be called multiple times for the same requestHash
*
* @param params - Validation response parameters
* @returns Transaction result
*/
async validationResponse(params: ValidationResponseParams): Promise<{ txHash: string }> {
// Validate response is 0-100
if (params.response < 0 || params.response > 100) {
throw new Error('Response MUST be between 0 and 100');
}
// Convert optional parameters to proper format
const responseUri = params.responseUri || '';
const responseHash = params.responseHash || ethers.ZeroHash;
const tag = params.tag ? ethers.id(params.tag).slice(0, 66) : ethers.ZeroHash;
const result = await this.adapter.send(
this.contractAddress,
ValidationRegistryABI,
'validationResponse',
[params.requestHash, params.response, responseUri, responseHash, tag]
);
return { txHash: result.txHash };
}
/**
* Get the identity registry address
* Spec: function getIdentityRegistry() external view returns (address identityRegistry)
*/
async getIdentityRegistry(): Promise<string> {
return await this.adapter.call(
this.contractAddress,
ValidationRegistryABI,
'getIdentityRegistry',
[]
);
}
/**
* Get validation status for a request
* Spec (new): function getValidationStatus(bytes32 requestHash) returns (address validatorAddress, uint256 agentId, uint8 response, bytes32 responseHash, bytes32 tag, uint256 lastUpdate)
* Spec (old): function getValidationStatus(bytes32 requestHash) returns (address validatorAddress, uint256 agentId, uint8 response, bytes32 tag, uint256 lastUpdate)
* Note: Backward compatible with both old and new contract versions
*
* @param requestHash - The request hash (bytes32)
* @returns Validation status
*/
async getValidationStatus(requestHash: string): Promise<ValidationStatus> {
try {
// Try with new ABI first (6 return values)
const result = await this.adapter.call(
this.contractAddress,
ValidationRegistryABI,
'getValidationStatus',
[requestHash]
);
return {
validatorAddress: result.validatorAddress || result[0],
agentId: BigInt(result.agentId || result[1]),
response: Number(result.response || result[2]),
responseHash: result.responseHash || result[3],
tag: result.tag || result[4],
lastUpdate: BigInt(result.lastUpdate || result[5]),
};
} catch (error: any) {
// If decoding fails, try with old ABI (5 return values, no responseHash)
if (error.code === 'BAD_DATA' || error.message?.includes('could not decode result data')) {
// Create old ABI for getValidationStatus without responseHash
const oldABI = [
{
inputs: [{ internalType: 'bytes32', name: 'requestHash', type: 'bytes32' }],
name: 'getValidationStatus',
outputs: [
{ internalType: 'address', name: 'validatorAddress', type: 'address' },
{ internalType: 'uint256', name: 'agentId', type: 'uint256' },
{ internalType: 'uint8', name: 'response', type: 'uint8' },
{ internalType: 'bytes32', name: 'tag', type: 'bytes32' },
{ internalType: 'uint256', name: 'lastUpdate', type: 'uint256' }
],
stateMutability: 'view',
type: 'function'
}
];
const result = await this.adapter.call(
this.contractAddress,
oldABI,
'getValidationStatus',
[requestHash]
);
return {
validatorAddress: result.validatorAddress || result[0],
agentId: BigInt(result.agentId || result[1]),
response: Number(result.response || result[2]),
responseHash: ethers.ZeroHash, // Default for old contracts
tag: result.tag || result[3],
lastUpdate: BigInt(result.lastUpdate || result[4]),
};
}
// Re-throw other errors
throw error;
}
}
/**
* Get validation summary for an agent
* Spec: function getSummary(uint256 agentId, address[] validatorAddresses, bytes32 tag) returns (uint64 count, uint8 avgResponse)
* Note: agentId is ONLY mandatory parameter, validatorAddresses and tag are OPTIONAL filters
*
* @param agentId - The agent ID (MANDATORY)
* @param validatorAddresses - OPTIONAL filter by specific validators
* @param tag - OPTIONAL filter by tag
* @returns Summary statistics
*/
async getSummary(
agentId: bigint,
validatorAddresses?: string[],
tag?: string
): Promise<{ count: bigint; avgResponse: number }> {
const validators = validatorAddresses || [];
const tagBytes = tag ? ethers.id(tag).slice(0, 66) : ethers.ZeroHash;
const result = await this.adapter.call(
this.contractAddress,
ValidationRegistryABI,
'getSummary',
[agentId, validators, tagBytes]
);
return {
count: BigInt(result.count || result[0]),
avgResponse: Number(result.avgResponse || result[1]),
};
}
/**
* Get all validation request hashes for an agent
* Spec: function getAgentValidations(uint256 agentId) returns (bytes32[] requestHashes)
*
* @param agentId - The agent ID
* @returns Array of request hashes
*/
async getAgentValidations(agentId: bigint): Promise<string[]> {
return await this.adapter.call(
this.contractAddress,
ValidationRegistryABI,
'getAgentValidations',
[agentId]
);
}
/**
* Get all request hashes for a validator
* Spec: function getValidatorRequests(address validatorAddress) returns (bytes32[] requestHashes)
*
* @param validatorAddress - The validator address
* @returns Array of request hashes
*/
async getValidatorRequests(validatorAddress: string): Promise<string[]> {
return await this.adapter.call(
this.contractAddress,
ValidationRegistryABI,
'getValidatorRequests',
[validatorAddress]
);
}
}