Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: foundry-rs/foundry-toolchain@v1
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
Expand All @@ -57,7 +58,6 @@ jobs:
uses: nick-fields/retry@v3
env:
FORK_URL: ${{ secrets.FORK_URL }}
TEST_PRIVATE_KEY: ${{ secrets.TEST_PRIVATE_KEY }}
with:
timeout_minutes: 10
max_attempts: 2
Expand Down
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:cov": "vitest run --coverage",
"test": "vitest run --project unit",
"test:cov": "vitest run --project unit --coverage",
"test:fork": "vitest run --project fork",
"typecheck": "tsc --noEmit",
"generate": "wagmi generate",
"check": "biome check",
Expand All @@ -49,9 +50,11 @@
"@arethetypeswrong/cli": "^0.18.2",
"@biomejs/biome": "^2.4.10",
"@size-limit/file": "^12.0.1",
"@types/node": "^25.5.2",
"@vitest/coverage-v8": "^4.1.2",
"@wagmi/cli": "^2.10.0",
"knip": "^6.3.0",
"prool": "^0.2.4",
"publint": "^0.3.18",
"size-limit": "^12.0.1",
"tsup": "^8.5.1",
Expand All @@ -64,6 +67,14 @@
"path": "dist/index.js",
"limit": "5 kB"
},
{
"path": "dist/identity/index.js",
"limit": "5 kB"
},
{
"path": "dist/reputation/index.js",
"limit": "5 kB"
},
{
"path": "dist/abis/index.js",
"limit": "50 kB"
Expand Down
326 changes: 316 additions & 10 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,29 @@ export {
setMetadata,
verifyAgentId,
} from './identity/index.js'
export type {
AppendResponseParameters,
Feedback,
FeedbackEntry,
GetClientsParameters,
GetLastIndexParameters,
GetResponseCountParameters,
GetSummaryParameters,
GiveFeedbackParameters,
ReadAllFeedbackParameters,
ReadFeedbackParameters,
ReputationSummary,
RevokeFeedbackParameters,
} from './reputation/index.js'
export {
appendResponse,
getClients,
getLastIndex,
getResponseCount,
getSummary,
giveFeedback,
readAllFeedback,
readFeedback,
revokeFeedback,
} from './reputation/index.js'
export type { Erc8004Addresses } from './types.js'
7 changes: 7 additions & 0 deletions src/internal/resolveRegistryAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,10 @@ export function resolveIdentityRegistry(
): Address {
return resolveRegistry(client, 'identityRegistry', registryAddress)
}

export function resolveReputationRegistry(
client: { chain?: Chain | undefined },
registryAddress?: Address,
): Address {
return resolveRegistry(client, 'reputationRegistry', registryAddress)
}
35 changes: 35 additions & 0 deletions src/reputation/appendResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Hash, WalletClient } from 'viem'
import { reputationRegistryAbi } from '../abis/index.js'
import { requireAccount } from '../internal/requireAccount.js'
import { resolveReputationRegistry } from '../internal/resolveRegistryAddress.js'
import type { AppendResponseParameters } from './types.js'

/**
* Append a response to existing feedback.
* Reverts if `msg.sender` is not the owner of `agentId`.
*/
export async function appendResponse(
walletClient: WalletClient,
parameters: AppendResponseParameters,
): Promise<Hash> {
const account = requireAccount(walletClient)
const registry = resolveReputationRegistry(
walletClient,
parameters.registryAddress,
)

return walletClient.writeContract({
address: registry,
abi: reputationRegistryAbi,
functionName: 'appendResponse',
args: [
parameters.agentId,
parameters.clientAddress,
parameters.feedbackIndex,
parameters.responseURI,
parameters.responseHash,
],
chain: walletClient.chain,
account,
})
}
24 changes: 24 additions & 0 deletions src/reputation/getClients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Address, PublicClient } from 'viem'
import { reputationRegistryAbi } from '../abis/index.js'
import { resolveReputationRegistry } from '../internal/resolveRegistryAddress.js'
import type { GetClientsParameters } from './types.js'

/**
* Get all addresses that have given feedback to an agent.
*/
export async function getClients(
publicClient: PublicClient,
parameters: GetClientsParameters,
): Promise<readonly Address[]> {
const registry = resolveReputationRegistry(
publicClient,
parameters.registryAddress,
)

return publicClient.readContract({
address: registry,
abi: reputationRegistryAbi,
functionName: 'getClients',
args: [parameters.agentId],
})
}
24 changes: 24 additions & 0 deletions src/reputation/getLastIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { PublicClient } from 'viem'
import { reputationRegistryAbi } from '../abis/index.js'
import { resolveReputationRegistry } from '../internal/resolveRegistryAddress.js'
import type { GetLastIndexParameters } from './types.js'

/**
* Get the latest feedback index for a specific agent-client pair.
*/
export async function getLastIndex(
publicClient: PublicClient,
parameters: GetLastIndexParameters,
): Promise<bigint> {
const registry = resolveReputationRegistry(
publicClient,
parameters.registryAddress,
)

return publicClient.readContract({
address: registry,
abi: reputationRegistryAbi,
functionName: 'getLastIndex',
args: [parameters.agentId, parameters.clientAddress],
})
}
30 changes: 30 additions & 0 deletions src/reputation/getResponseCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { PublicClient } from 'viem'
import { reputationRegistryAbi } from '../abis/index.js'
import { resolveReputationRegistry } from '../internal/resolveRegistryAddress.js'
import type { GetResponseCountParameters } from './types.js'

/**
* Get the number of responses to a specific feedback entry from
* a set of responder addresses.
*/
export async function getResponseCount(
publicClient: PublicClient,
parameters: GetResponseCountParameters,
): Promise<bigint> {
const registry = resolveReputationRegistry(
publicClient,
parameters.registryAddress,
)

return publicClient.readContract({
address: registry,
abi: reputationRegistryAbi,
functionName: 'getResponseCount',
args: [
parameters.agentId,
parameters.clientAddress,
parameters.feedbackIndex,
parameters.responders,
],
})
}
33 changes: 33 additions & 0 deletions src/reputation/getSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { PublicClient } from 'viem'
import { reputationRegistryAbi } from '../abis/index.js'
import { resolveReputationRegistry } from '../internal/resolveRegistryAddress.js'
import type { GetSummaryParameters, ReputationSummary } from './types.js'

/**
* Get an aggregated reputation summary for an agent, filtered by
* reviewer addresses and tag pair.
*/
export async function getSummary(
publicClient: PublicClient,
parameters: GetSummaryParameters,
): Promise<ReputationSummary> {
const registry = resolveReputationRegistry(
publicClient,
parameters.registryAddress,
)

const [count, summaryValue, summaryValueDecimals] =
await publicClient.readContract({
address: registry,
abi: reputationRegistryAbi,
functionName: 'getSummary',
args: [
parameters.agentId,
parameters.clientAddresses,
parameters.tag1,
parameters.tag2,
],
})

return { count, summaryValue, summaryValueDecimals }
}
39 changes: 39 additions & 0 deletions src/reputation/giveFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type Hash, type WalletClient, zeroHash } from 'viem'
import { reputationRegistryAbi } from '../abis/index.js'
import { requireAccount } from '../internal/requireAccount.js'
import { resolveReputationRegistry } from '../internal/resolveRegistryAddress.js'
import type { GiveFeedbackParameters } from './types.js'

/**
* Give feedback to an agent. The caller (msg.sender) is recorded as the
* reviewer (client). Feedback includes a numeric value, two category tags,
* and optional off-chain data (endpoint, URI, hash).
*/
export async function giveFeedback(
walletClient: WalletClient,
parameters: GiveFeedbackParameters,
): Promise<Hash> {
const account = requireAccount(walletClient)
const registry = resolveReputationRegistry(
walletClient,
parameters.registryAddress,
)

return walletClient.writeContract({
address: registry,
abi: reputationRegistryAbi,
functionName: 'giveFeedback',
args: [
parameters.agentId,
parameters.value,
parameters.valueDecimals,
parameters.tag1,
parameters.tag2,
parameters.endpoint ?? '',
parameters.feedbackURI ?? '',
parameters.feedbackHash ?? zeroHash,
],
chain: walletClient.chain,
account,
})
}
25 changes: 24 additions & 1 deletion src/reputation/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
// Reputation functions will be added in Step 5.
export { appendResponse } from './appendResponse.js'
export { getClients } from './getClients.js'
export { getLastIndex } from './getLastIndex.js'
export { getResponseCount } from './getResponseCount.js'
export { getSummary } from './getSummary.js'
export { giveFeedback } from './giveFeedback.js'
export { readAllFeedback } from './readAllFeedback.js'
export { readFeedback } from './readFeedback.js'
export { revokeFeedback } from './revokeFeedback.js'

export type {
AppendResponseParameters,
Feedback,
FeedbackEntry,
GetClientsParameters,
GetLastIndexParameters,
GetResponseCountParameters,
GetSummaryParameters,
GiveFeedbackParameters,
ReadAllFeedbackParameters,
ReadFeedbackParameters,
ReputationSummary,
RevokeFeedbackParameters,
} from './types.js'
52 changes: 52 additions & 0 deletions src/reputation/readAllFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { PublicClient } from 'viem'
import { reputationRegistryAbi } from '../abis/index.js'
import { resolveReputationRegistry } from '../internal/resolveRegistryAddress.js'
import type { FeedbackEntry, ReadAllFeedbackParameters } from './types.js'

/**
* Read all feedback for an agent, filtered by reviewer addresses, tags,
* and revocation status.
*
* The contract returns 7 parallel arrays — this function transforms them
* into an array of structured objects for better DX.
*/
export async function readAllFeedback(
publicClient: PublicClient,
parameters: ReadAllFeedbackParameters,
): Promise<readonly FeedbackEntry[]> {
const registry = resolveReputationRegistry(
publicClient,
parameters.registryAddress,
)

const [
clients,
feedbackIndexes,
values,
valueDecimals,
tag1s,
tag2s,
revokedStatuses,
] = await publicClient.readContract({
address: registry,
abi: reputationRegistryAbi,
functionName: 'readAllFeedback',
args: [
parameters.agentId,
parameters.clientAddresses,
parameters.tag1,
parameters.tag2,
parameters.includeRevoked ?? false,
],
})

return clients.map((client, i) => ({
client,
feedbackIndex: feedbackIndexes[i],
value: values[i],
valueDecimals: valueDecimals[i],
tag1: tag1s[i],
tag2: tag2s[i],
isRevoked: revokedStatuses[i],
}))
}
31 changes: 31 additions & 0 deletions src/reputation/readFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { PublicClient } from 'viem'
import { reputationRegistryAbi } from '../abis/index.js'
import { resolveReputationRegistry } from '../internal/resolveRegistryAddress.js'
import type { Feedback, ReadFeedbackParameters } from './types.js'

/**
* Read a single feedback entry by agent, client address, and index.
*/
export async function readFeedback(
publicClient: PublicClient,
parameters: ReadFeedbackParameters,
): Promise<Feedback> {
const registry = resolveReputationRegistry(
publicClient,
parameters.registryAddress,
)

const [value, valueDecimals, tag1, tag2, isRevoked] =
await publicClient.readContract({
address: registry,
abi: reputationRegistryAbi,
functionName: 'readFeedback',
args: [
parameters.agentId,
parameters.clientAddress,
parameters.feedbackIndex,
],
})

return { value, valueDecimals, tag1, tag2, isRevoked }
}
Loading
Loading