Skip to content

Commit bf70916

Browse files
committed
fix(sdk,dashboard): ICP verification round 2 - reputation, REST API, docs
SDK: - getReputationSummary now queries both FeedbackPublicV1 and FeedbackV1 schemas REST API: - compressedAddress returns base58 string (was byte dict) - Name/endpointTypes search scans all agents (was bounded by page window) - Feedback endpoints support limit/offset pagination with total count - POST feedback accepts outcome and message (was hardcoded Neutral) - Missing feedback fields return null (was 0/empty string) - nonTransferable added to single agent endpoint - Reputation summaryValue divides by value count (was total count) - order param passed through to SDK on agents endpoint - Agent reg file fetches parallelized (was sequential) - Removed synthetic feedbackIndex - Added GET /api/stats endpoint Docs: - rest-api.md: full rewrite with all new params, responses, POST field table - SKILL.md: complete listAllFeedbacks fields, FeedbackContent type, AgentIdentity fields, default limits, browser wallet flow fix, PreparedFeedbackData note
1 parent b2b8426 commit bf70916

6 files changed

Lines changed: 215 additions & 81 deletions

File tree

apps/dashboard/src/worker/identity-api.ts

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ interface FeedbackRequest {
100100
valueDecimals?: number;
101101
tag1?: string;
102102
tag2?: string;
103+
message?: string;
103104
endpoint?: string;
105+
outcome?: number;
104106
reviewerAddress?: string;
105107
feedbackURI?: string;
106108
feedbackHash?: string;
@@ -255,12 +257,14 @@ export function createIdentityApi(env: Env) {
255257
const nameFilter = c.req.query("name")?.toLowerCase();
256258
const ownerFilter = c.req.query("owner");
257259
const endpointTypesParam = c.req.query("endpointTypes");
260+
const orderParam = c.req.query("order") === "oldest" ? ("oldest" as const) : ("newest" as const);
258261
const limitParam = Number.parseInt(c.req.query("limit") ?? "20", 10);
259262
const offsetParam = Number.parseInt(c.req.query("offset") ?? "0", 10);
260263
const limit = Math.min(Math.max(limitParam, 1), 50);
261264
const offset = Math.max(offsetParam, 0);
262265

263266
const endpointTypes = endpointTypesParam?.split(",").filter(Boolean) ?? [];
267+
const hasFilters = !!(nameFilter || endpointTypes.length > 0);
264268

265269
try {
266270
const sati = createSatiClient(network, env);
@@ -272,29 +276,42 @@ export function createIdentityApi(env: Env) {
272276
return c.json({ error: "Invalid owner address" }, 400);
273277
}
274278
agents = await sati.listAgentsByOwner(ownerFilter as Address);
279+
} else if (hasFilters) {
280+
// Fetch all agents when filters require full scan
281+
const result = await sati.listAllAgents({
282+
limit: Number(stats.totalAgents),
283+
offset: 0,
284+
order: orderParam,
285+
});
286+
agents = result.agents;
275287
} else {
276-
const result = await sati.listAllAgents({ limit: limit + offset, offset: 0 });
288+
const result = await sati.listAllAgents({ limit: limit + offset, offset: 0, order: orderParam });
277289
agents = result.agents.slice(offset);
278290
}
279291

280-
// Fetch registration files and apply filters
281-
const results = [];
282-
for (const agent of agents) {
283-
if (results.length >= limit) break;
292+
// Fetch registration files in parallel
293+
const regFileResults = await Promise.allSettled(
294+
agents.map((agent) => fetchRegistrationFile(agent.uri, { strict: true })),
295+
);
284296

285-
const regFile = await fetchRegistrationFile(agent.uri, { strict: true });
297+
// Apply filters and collect results
298+
const filtered: Array<Record<string, unknown>> = [];
299+
for (let i = 0; i < agents.length; i++) {
300+
const agent = agents[i];
301+
const settled = regFileResults[i];
302+
const regFile = settled.status === "fulfilled" ? settled.value : null;
286303

287304
if (nameFilter) {
288305
const agentName = (regFile?.name ?? agent.name).toLowerCase();
289306
if (!agentName.includes(nameFilter)) continue;
290307
}
291308

292309
if (endpointTypes.length > 0) {
293-
const serviceNames = (regFile?.services ?? []).map((s) => s.name);
310+
const serviceNames = (regFile?.services ?? []).map((s: { name: string }) => s.name);
294311
if (!endpointTypes.some((t) => serviceNames.includes(t))) continue;
295312
}
296313

297-
results.push({
314+
filtered.push({
298315
mint: agent.mint,
299316
agentId: `${CAIP2_CHAINS[network]}:${agent.mint}`,
300317
owner: agent.owner,
@@ -311,7 +328,10 @@ export function createIdentityApi(env: Env) {
311328
});
312329
}
313330

314-
return c.json({ agents: results, count: results.length, totalAgents: Number(stats.totalAgents) });
331+
// Apply pagination to filtered results
332+
const paginated = hasFilters ? filtered.slice(offset, offset + limit) : filtered.slice(0, limit);
333+
334+
return c.json({ agents: paginated, count: paginated.length, totalAgents: Number(stats.totalAgents) });
315335
} catch (error) {
316336
console.error("[agents] ERROR:", error);
317337
return c.json({ error: error instanceof Error ? error.message : "Failed to list agents" }, 500);
@@ -346,6 +366,7 @@ export function createIdentityApi(env: Env) {
346366

347367
let feedbackCount = 0;
348368
let totalValue = 0;
369+
let valueCount = 0;
349370

350371
for (const schema of feedbackSchemas) {
351372
const feedbacks = await sati.listFeedbacks({
@@ -358,6 +379,7 @@ export function createIdentityApi(env: Env) {
358379
feedbackCount++;
359380
if (parsed?.value !== undefined) {
360381
totalValue += parsed.value;
382+
valueCount++;
361383
}
362384
}
363385
}
@@ -371,14 +393,15 @@ export function createIdentityApi(env: Env) {
371393
image: regFile?.image ?? "",
372394
uri: agent.uri,
373395
memberNumber: Number(agent.memberNumber),
396+
nonTransferable: agent.nonTransferable,
374397
active: regFile?.active ?? true,
375398
services: regFile?.services ?? [],
376399
supportedTrust: regFile?.supportedTrust ?? [],
377400
x402Support: regFile?.x402Support ?? false,
378401
registrations: regFile?.registrations ?? [],
379402
reputation: {
380403
count: feedbackCount,
381-
summaryValue: feedbackCount > 0 ? Math.round(totalValue / feedbackCount) : 0,
404+
summaryValue: valueCount > 0 ? Math.round(totalValue / valueCount) : 0,
382405
summaryValueDecimals: 0,
383406
},
384407
});
@@ -412,7 +435,7 @@ export function createIdentityApi(env: Env) {
412435

413436
let count = 0;
414437
let totalValue = 0;
415-
let hasValues = false;
438+
let valueCount = 0;
416439

417440
for (const schema of feedbackSchemas) {
418441
const feedbacks = await sati.listFeedbacks({
@@ -431,14 +454,14 @@ export function createIdentityApi(env: Env) {
431454
count++;
432455
if (parsed?.value !== undefined) {
433456
totalValue += parsed.value;
434-
hasValues = true;
457+
valueCount++;
435458
}
436459
}
437460
}
438461

439462
return c.json({
440463
count,
441-
summaryValue: hasValues ? Math.round(totalValue / count) : 0,
464+
summaryValue: valueCount > 0 ? Math.round(totalValue / valueCount) : 0,
442465
summaryValueDecimals: 0,
443466
});
444467
} catch (error) {
@@ -466,7 +489,7 @@ export function createIdentityApi(env: Env) {
466489

467490
let count = 0;
468491
let totalValue = 0;
469-
let hasValues = false;
492+
let valueCount = 0;
470493

471494
for (const schema of feedbackSchemas) {
472495
const feedbacks = await sati.listFeedbacks({
@@ -478,12 +501,12 @@ export function createIdentityApi(env: Env) {
478501
count++;
479502
if (parsed?.value !== undefined) {
480503
totalValue += parsed.value;
481-
hasValues = true;
504+
valueCount++;
482505
}
483506
}
484507
}
485508

486-
const score = hasValues ? Math.round(totalValue / count) : 0;
509+
const score = valueCount > 0 ? Math.round(totalValue / valueCount) : 0;
487510
const label = "SATI";
488511
const value = count > 0 ? `${score}/100 (${count})` : "no reviews";
489512
const color = count === 0 ? "#999" : score >= 70 ? "#4c1" : score >= 40 ? "#dfb317" : "#e05d44";
@@ -544,7 +567,6 @@ export function createIdentityApi(env: Env) {
544567
const nowSec = Math.floor(Date.now() / 1000);
545568

546569
const feedbackItems: Array<Record<string, unknown>> = [];
547-
let feedbackIndex = 0;
548570

549571
for (const schema of feedbackSchemas) {
550572
const filter: Record<string, unknown> = { sasSchema: schema as Address };
@@ -564,16 +586,15 @@ export function createIdentityApi(env: Env) {
564586
const createdAt = nowSec - Math.floor(slotDiff * 0.4);
565587

566588
feedbackItems.push({
567-
compressedAddress: fb.address,
589+
compressedAddress: bs58.encode(new Uint8Array(fb.address)),
568590
clientAddress: fb.data.counterparty,
569591
agentMint: fb.data.agentMint,
570-
feedbackIndex: feedbackIndex++,
571-
value: parsed?.value ?? 0,
572-
valueDecimals: parsed?.valueDecimals ?? 0,
573-
tag1: parsed?.tag1 ?? "",
574-
tag2: parsed?.tag2 ?? "",
575-
message: parsed?.m ?? "",
576-
endpoint: parsed?.endpoint ?? "",
592+
value: parsed?.value ?? null,
593+
valueDecimals: parsed?.valueDecimals ?? null,
594+
tag1: parsed?.tag1 ?? null,
595+
tag2: parsed?.tag2 ?? null,
596+
message: parsed?.m ?? null,
597+
endpoint: parsed?.endpoint ?? null,
577598
outcome: fb.data.outcome,
578599
createdAt,
579600
schema: schema === networkConfig.feedbackSchema ? "FeedbackV1" : "FeedbackPublicV1",
@@ -588,21 +609,26 @@ export function createIdentityApi(env: Env) {
588609
app.get("/api/feedback/:mint", async (c) => {
589610
const mint = c.req.param("mint");
590611
const network = getNetwork(c.req.query("network"));
612+
const limitParam = Number.parseInt(c.req.query("limit") ?? "50", 10);
613+
const offsetParam = Number.parseInt(c.req.query("offset") ?? "0", 10);
614+
const limit = Math.min(Math.max(limitParam, 1), 200);
615+
const offset = Math.max(offsetParam, 0);
591616

592617
if (!isAddress(mint)) {
593618
return c.json({ error: "Invalid mint address" }, 400);
594619
}
595620

596621
try {
597622
const sati = createSatiClient(network, env);
598-
const feedbackItems = await queryFeedback(sati, network, mint as Address, {
623+
const allItems = await queryFeedback(sati, network, mint as Address, {
599624
clientAddress: c.req.query("clientAddress"),
600625
tag1: c.req.query("tag1"),
601626
tag2: c.req.query("tag2"),
602627
outcome: c.req.query("outcome"),
603628
});
629+
const paginated = allItems.slice(offset, offset + limit);
604630

605-
return c.json({ feedbacks: feedbackItems, count: feedbackItems.length });
631+
return c.json({ feedbacks: paginated, count: paginated.length, total: allItems.length });
606632
} catch (error) {
607633
console.error("[feedback list] ERROR:", error);
608634
return c.json({ error: error instanceof Error ? error.message : "Failed to list feedback" }, 500);
@@ -615,23 +641,51 @@ export function createIdentityApi(env: Env) {
615641

616642
app.get("/api/feedback", async (c) => {
617643
const network = getNetwork(c.req.query("network"));
644+
const limitParam = Number.parseInt(c.req.query("limit") ?? "50", 10);
645+
const offsetParam = Number.parseInt(c.req.query("offset") ?? "0", 10);
646+
const limit = Math.min(Math.max(limitParam, 1), 200);
647+
const offset = Math.max(offsetParam, 0);
618648

619649
try {
620650
const sati = createSatiClient(network, env);
621-
const feedbackItems = await queryFeedback(sati, network, undefined, {
651+
const allItems = await queryFeedback(sati, network, undefined, {
622652
clientAddress: c.req.query("clientAddress"),
623653
tag1: c.req.query("tag1"),
624654
tag2: c.req.query("tag2"),
625655
outcome: c.req.query("outcome"),
626656
});
657+
const paginated = allItems.slice(offset, offset + limit);
627658

628-
return c.json({ feedbacks: feedbackItems, count: feedbackItems.length });
659+
return c.json({ feedbacks: paginated, count: paginated.length, total: allItems.length });
629660
} catch (error) {
630661
console.error("[feedback global] ERROR:", error);
631662
return c.json({ error: error instanceof Error ? error.message : "Failed to list feedback" }, 500);
632663
}
633664
});
634665

666+
// ---------------------------------------------------------------------------
667+
// GET /api/stats - Registry statistics
668+
// ---------------------------------------------------------------------------
669+
670+
app.get("/api/stats", async (c) => {
671+
const network = getNetwork(c.req.query("network"));
672+
673+
try {
674+
const sati = createSatiClient(network, env);
675+
const stats = await sati.getRegistryStats();
676+
677+
return c.json({
678+
totalAgents: Number(stats.totalAgents),
679+
groupMint: stats.groupMint,
680+
authority: stats.authority,
681+
isImmutable: stats.isImmutable,
682+
});
683+
} catch (error) {
684+
console.error("[stats] ERROR:", error);
685+
return c.json({ error: error instanceof Error ? error.message : "Failed to get stats" }, 500);
686+
}
687+
});
688+
635689
// ---------------------------------------------------------------------------
636690
// POST /api/feedback - Give feedback (free, server signs as counterparty)
637691
// ---------------------------------------------------------------------------
@@ -670,12 +724,17 @@ export function createIdentityApi(env: Env) {
670724
const serverPayer = await createKeyPairSignerFromBytes(signerBytes);
671725
const counterpartyAddress = serverPayer.address;
672726

727+
// Map outcome number to enum (0=Negative, 1=Neutral, 2=Positive)
728+
const outcomeValue =
729+
body.outcome === 0 ? OutcomeEnum.Negative : body.outcome === 2 ? OutcomeEnum.Positive : OutcomeEnum.Neutral;
730+
673731
// Build content JSON (ERC-8004 format) using SDK helper
674732
const contentBytes = buildFeedbackContent({
675733
value: body.value,
676734
valueDecimals: body.valueDecimals ?? 0,
677735
tag1: body.tag1,
678736
tag2: body.tag2,
737+
message: body.message,
679738
endpoint: body.endpoint,
680739
reviewer: body.reviewerAddress,
681740
feedbackURI: body.feedbackURI,
@@ -692,7 +751,7 @@ export function createIdentityApi(env: Env) {
692751
agentMint: body.agentMint as Address,
693752
counterparty: counterpartyAddress,
694753
dataHash,
695-
outcome: OutcomeEnum.Neutral,
754+
outcome: outcomeValue,
696755
contentType: ContentType.JSON,
697756
content: contentBytes,
698757
};
@@ -719,7 +778,7 @@ export function createIdentityApi(env: Env) {
719778
agentMint: body.agentMint as Address,
720779
counterparty: counterpartyAddress,
721780
dataHash,
722-
outcome: OutcomeEnum.Neutral,
781+
outcome: outcomeValue,
723782
agentSignature: {
724783
pubkey: counterpartyAddress,
725784
signature: counterpartySignature,

0 commit comments

Comments
 (0)