Skip to content

Commit 84a84b5

Browse files
committed
feat(sdk): add timestamps, pagination improvements, and dashboard cleanup
SDK: - Add approximate createdAt timestamps to parsed attestations - Redesign listAllAgents: 0-based offset, newest-first default, returns totalAgents - Add buildFeedbackContent(), hexToBytes(), bytesToHex() helpers - Add reputationScoreSchema/credential typed getters - Extend FeedbackContent with reviewer, feedbackURI, feedbackHash fields Dashboard: - Replace raw RPC calls with SDK methods (getCurrentSlot, listAllAgents) - Remove duplicated hex utils, use SDK exports - Fix stale module-level config in VerifiedFeedbackDialog - Add localStorage query cache persistence with BigInt deserialization - Sort feedbacks/validations newest-first - Use counterparty filter server-side instead of client-side - Use typed FeedbackContent instead of Record<string, unknown> casts - Consolidate Sati client creation in worker
1 parent 92a42e9 commit 84a84b5

19 files changed

Lines changed: 364 additions & 283 deletions

File tree

apps/dashboard/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
"@solana/kit": "^5.1.0",
3737
"@solana/react-hooks": "^1.1.1",
3838
"@tailwindcss/vite": "^4.1.17",
39+
"@tanstack/query-async-storage-persister": "^5.90.22",
3940
"@tanstack/react-query": "^5.90.11",
41+
"@tanstack/react-query-persist-client": "^5.90.22",
4042
"@x402/core": "^2.2.0",
4143
"@x402/fetch": "^2.2.0",
4244
"@x402/hono": "^2.2.0",

apps/dashboard/src/react-app/components/VerifiedFeedbackDialog.tsx

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import { toast } from "sonner";
1616
import type { Address } from "@solana/kit";
1717
import {
1818
type Outcome,
19-
loadDeployedConfig,
2019
MAX_COUNTERPARTY_SIGNED_CONTENT_SIZE,
2120
buildCounterpartyMessage,
2221
serializeFeedback,
2322
type FeedbackData,
2423
handleTransactionError,
24+
hexToBytes,
25+
bytesToHex,
2526
} from "@cascade-fyi/sati-sdk";
27+
import { getSatiClient } from "@/lib/sati";
2628
import { getChain, getNetwork, getSolscanUrl, getRpcUrl } from "@/lib/network";
2729
import { createPaymentFetch } from "@/lib/x402";
2830

@@ -40,9 +42,10 @@ import { Label } from "@/components/ui/label";
4042
import { Textarea } from "@/components/ui/textarea";
4143
import { Loader2, CheckCircle2 } from "lucide-react";
4244

43-
// Get deployed Feedback schema address (DualSignature mode)
44-
const deployedConfig = loadDeployedConfig(getNetwork());
45-
const FEEDBACK_SCHEMA_ADDRESS = deployedConfig?.schemas?.feedback as Address | undefined;
45+
// Get deployed Feedback schema address (DualSignature mode) - fetched fresh from client
46+
function getFeedbackSchemaAddress(): Address | undefined {
47+
return getSatiClient().feedbackSchema;
48+
}
4649

4750
// Pre-filled agent signature data (for skipping Step 1)
4851
interface PrefilledSignatureData {
@@ -107,25 +110,6 @@ interface SubmitFeedbackResponse {
107110
error?: string;
108111
}
109112

110-
// Hex helpers
111-
function bytesToHex(bytes: Uint8Array): string {
112-
return Array.from(bytes)
113-
.map((b) => b.toString(16).padStart(2, "0"))
114-
.join("");
115-
}
116-
117-
function hexToBytes(hex: string): Uint8Array {
118-
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
119-
if (!/^[0-9a-fA-F]*$/.test(cleanHex) || cleanHex.length % 2 !== 0) {
120-
throw new Error(`Invalid hex string: ${hex.slice(0, 20)}${hex.length > 20 ? "..." : ""}`);
121-
}
122-
const bytes = new Uint8Array(cleanHex.length / 2);
123-
for (let i = 0; i < bytes.length; i++) {
124-
bytes[i] = parseInt(cleanHex.slice(i * 2, i * 2 + 2), 16);
125-
}
126-
return bytes;
127-
}
128-
129113
// Agent signature data from Step 1
130114
interface AgentSignatureData {
131115
taskRef: string; // hex
@@ -199,7 +183,8 @@ export function VerifiedFeedbackDialog({
199183
// Step 1: Interact - Pay and get agent's blind signature
200184
// ==========================================================================
201185
const handleInteract = async () => {
202-
if (!session || !FEEDBACK_SCHEMA_ADDRESS) return;
186+
const feedbackSchema = getFeedbackSchemaAddress();
187+
if (!session || !feedbackSchema) return;
203188
setIsInteracting(true);
204189

205190
const toastId = toast.loading("Preparing interaction...");
@@ -225,7 +210,7 @@ export function VerifiedFeedbackDialog({
225210

226211
// Call /api/echo with x402 payment - agent signs WITHOUT knowing outcome
227212
const echoRequest: EchoRequest = {
228-
sasSchema: FEEDBACK_SCHEMA_ADDRESS,
213+
sasSchema: feedbackSchema,
229214
taskRef,
230215
agentMint,
231216
dataHash,
@@ -270,7 +255,8 @@ export function VerifiedFeedbackDialog({
270255
// ==========================================================================
271256
const submitMutation = useMutation({
272257
mutationFn: async (selectedOutcome: Outcome) => {
273-
if (!session || !agentSignatureData || !FEEDBACK_SCHEMA_ADDRESS) {
258+
const feedbackSchema = getFeedbackSchemaAddress();
259+
if (!session || !agentSignatureData || !feedbackSchema) {
274260
throw new Error("Missing required data");
275261
}
276262

@@ -323,7 +309,7 @@ export function VerifiedFeedbackDialog({
323309

324310
const submitRequest: SubmitFeedbackRequest = {
325311
network: getNetwork(),
326-
sasSchema: FEEDBACK_SCHEMA_ADDRESS,
312+
sasSchema: feedbackSchema,
327313
taskRef: agentSignatureData.taskRef,
328314
agentMint,
329315
dataHash: agentSignatureData.dataHash,
@@ -462,7 +448,7 @@ export function VerifiedFeedbackDialog({
462448

463449
<Button
464450
onClick={handleInteract}
465-
disabled={!session || isInteracting || !FEEDBACK_SCHEMA_ADDRESS}
451+
disabled={!session || isInteracting || !getFeedbackSchemaAddress()}
466452
className="w-full"
467453
size="lg"
468454
>

apps/dashboard/src/react-app/lib/sati.ts

Lines changed: 26 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import {
99
Sati,
10-
SATI_PROGRAM_ADDRESS,
1110
type AgentIdentity,
1211
type ParsedAttestation,
1312
type ParsedValidationAttestation,
@@ -134,6 +133,11 @@ export function getFeedbackSchemaType(feedback: ParsedFeedback): FeedbackSchemaT
134133
return "unknown";
135134
}
136135

136+
/** Sort attestations by createdAt descending (newest first) */
137+
function sortByCreatedAtDesc<T extends { createdAt?: number }>(items: T[]): T[] {
138+
return items.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
139+
}
140+
137141
/**
138142
* Parsed feedback from SDK - re-exported for convenience
139143
*/
@@ -159,7 +163,7 @@ export async function listAgentFeedbacks(agentMint: Address): Promise<ParsedFeed
159163
allFeedbacks.push(...result.items);
160164
}
161165

162-
return allFeedbacks;
166+
return sortByCreatedAtDesc(allFeedbacks);
163167
} catch (e) {
164168
console.error("Failed to list agent feedbacks:", e);
165169
return [];
@@ -179,7 +183,7 @@ export async function listAgentValidations(agentMint: Address): Promise<ParsedVa
179183

180184
try {
181185
const result = await sati.listValidations({ sasSchema: validationSchema, agentMint });
182-
return result.items;
186+
return sortByCreatedAtDesc(result.items);
183187
} catch (e) {
184188
console.error("Failed to list agent validations:", e);
185189
return [];
@@ -207,7 +211,7 @@ export async function listAllFeedbacks(): Promise<ParsedFeedback[]> {
207211
allFeedbacks.push(...result.items);
208212
}
209213

210-
return allFeedbacks;
214+
return sortByCreatedAtDesc(allFeedbacks);
211215
} catch (e) {
212216
console.error("Failed to list all feedbacks:", e);
213217
return [];
@@ -218,10 +222,18 @@ export async function listAllFeedbacks(): Promise<ParsedFeedback[]> {
218222
* List feedbacks submitted by a specific counterparty
219223
*/
220224
export async function listFeedbacksByCounterparty(counterparty: Address): Promise<ParsedFeedback[]> {
221-
// Counterparty is in the schema data, filter client-side
225+
const sati = getSatiClient();
226+
const { feedback, feedbackPublic } = getFeedbackSchemas();
227+
const schemas = [feedback, feedbackPublic].filter(Boolean) as Address[];
228+
if (schemas.length === 0) return [];
229+
222230
try {
223-
const allFeedbacks = await listAllFeedbacks();
224-
return allFeedbacks.filter((f) => f.data.counterparty === counterparty);
231+
const all: ParsedFeedback[] = [];
232+
for (const sasSchema of schemas) {
233+
const result = await sati.listFeedbacks({ sasSchema, counterparty });
234+
all.push(...result.items);
235+
}
236+
return sortByCreatedAtDesc(all);
225237
} catch (e) {
226238
console.error("Failed to list feedbacks by counterparty:", e);
227239
return [];
@@ -295,44 +307,21 @@ export function formatSlotTime(slot: bigint, currentSlot: bigint): string {
295307
*/
296308
export async function getCurrentSlot(): Promise<bigint> {
297309
try {
298-
const rpcUrl = getCurrentRpcUrl();
299-
const response = await fetch(rpcUrl, {
300-
method: "POST",
301-
headers: { "Content-Type": "application/json" },
302-
body: JSON.stringify({
303-
jsonrpc: "2.0",
304-
id: "get-slot",
305-
method: "getSlot",
306-
params: [{ commitment: "confirmed" }],
307-
}),
308-
});
309-
const data = await response.json();
310-
return BigInt(data.result ?? 0);
310+
const sati = getSatiClient();
311+
const rpc = sati.getRpc();
312+
const slot = await rpc.getSlot({ commitment: "confirmed" }).send();
313+
return slot;
311314
} catch {
312315
return 0n;
313316
}
314317
}
315318

316-
/**
317-
* Get deployed reputation score schema address (always fresh for current network)
318-
*/
319-
export function getReputationScoreSchema(): Address | undefined {
320-
return getSatiClient().deployedConfig?.schemas?.reputationScore as Address | undefined;
321-
}
322-
323-
/**
324-
* Get deployed SATI credential address (always fresh for current network)
325-
*/
326-
export function getSatiCredential(): Address | undefined {
327-
return getSatiClient().deployedConfig?.credential as Address | undefined;
328-
}
329-
330319
/**
331320
* List reputation scores for a specific agent.
332321
*/
333322
export async function listAgentReputationScores(agentMint: Address): Promise<ReputationScoreData[]> {
334323
const sati = getSatiClient();
335-
const reputationSchema = getReputationScoreSchema();
324+
const reputationSchema = sati.reputationScoreSchema;
336325

337326
if (!reputationSchema) {
338327
return [];
@@ -349,121 +338,13 @@ export async function listAgentReputationScores(agentMint: Address): Promise<Rep
349338
// Re-export getSolscanUrl from network module
350339
export { getSolscanUrl } from "./network";
351340

352-
/**
353-
* Helius getTransactionsForAddress response types
354-
*/
355-
interface HeliusTransaction {
356-
transaction: {
357-
message: {
358-
accountKeys: Array<{ pubkey: string; signer: boolean; writable: boolean } | string>;
359-
};
360-
};
361-
}
362-
363-
interface HeliusResponse {
364-
result?: {
365-
data?: HeliusTransaction[];
366-
};
367-
error?: { message: string };
368-
}
369-
370341
/**
371342
* List all agents registered in the SATI registry.
372-
*
373-
* Uses Helius getTransactionsForAddress to discover mints, then SDK's registry.load
374-
* for proper ownership resolution (owner = current token holder, not registrant).
343+
* Uses SDK's AgentIndex-based enumeration for reliable discovery.
375344
*/
376345
export async function listAllAgents(params?: { offset?: number; limit?: number }): Promise<ListAgentsResult> {
377-
const { offset = 0, limit = 20 } = params ?? {};
378-
379346
const sati = getSatiClient();
380-
const rpcUrl = getCurrentRpcUrl();
381-
const stats = await sati.getRegistryStats();
382-
const groupMint = stats.groupMint;
383-
384-
try {
385-
// Step 1: Discover mints via Helius getTransactionsForAddress
386-
const response = await fetch(rpcUrl, {
387-
method: "POST",
388-
headers: { "Content-Type": "application/json" },
389-
body: JSON.stringify({
390-
jsonrpc: "2.0",
391-
id: "list-agents",
392-
method: "getTransactionsForAddress",
393-
params: [
394-
SATI_PROGRAM_ADDRESS,
395-
{
396-
transactionDetails: "full",
397-
encoding: "jsonParsed",
398-
limit: 100,
399-
sortOrder: "desc",
400-
filters: { status: "succeeded" },
401-
},
402-
],
403-
}),
404-
});
405-
406-
const data: HeliusResponse = await response.json();
407-
408-
if (data.error) {
409-
console.warn("Helius getTransactionsForAddress error:", data.error.message);
410-
return { agents: [], totalAgents: stats.totalAgents };
411-
}
412-
413-
const transactions = data.result?.data ?? [];
414-
415-
if (transactions.length === 0) {
416-
return { agents: [], totalAgents: stats.totalAgents };
417-
}
418-
419-
// Step 2: Extract agent mints from registerAgent transactions
420-
const agentMints: string[] = [];
421-
422-
for (const tx of transactions) {
423-
if (!tx?.transaction?.message) continue;
424-
425-
const accounts = tx.transaction.message.accountKeys;
426-
const pubkeys = accounts.map((acc) => (typeof acc === "object" && "pubkey" in acc ? acc.pubkey : String(acc)));
427-
428-
// Check if this transaction involves the group mint (indicates registerAgent)
429-
if (!pubkeys.includes(groupMint)) continue;
430-
431-
// Find the agent mint - it's the second signer (first is payer)
432-
const signerAccounts = accounts.filter((acc) => typeof acc === "object" && "signer" in acc && acc.signer);
433-
434-
if (signerAccounts.length >= 2) {
435-
const agentMint =
436-
typeof signerAccounts[1] === "object" && "pubkey" in signerAccounts[1] ? signerAccounts[1].pubkey : null;
437-
438-
// Skip if this is the groupMint itself (from initialize transaction)
439-
if (agentMint && agentMint !== groupMint && !agentMints.includes(agentMint)) {
440-
agentMints.push(agentMint);
441-
}
442-
}
443-
}
444-
445-
if (agentMints.length === 0) {
446-
return { agents: [], totalAgents: stats.totalAgents };
447-
}
448-
449-
// Step 3: Apply pagination
450-
const paginatedMints = agentMints.slice(offset, offset + limit);
451-
452-
// Step 4: Load agents via SDK (handles owner lookup correctly)
453-
const agentPromises = paginatedMints.map((mint) => sati.loadAgent(mint as Address));
454-
const loadedAgents = await Promise.all(agentPromises);
455-
456-
// Filter out nulls (failed loads)
457-
const agents = loadedAgents.filter((agent): agent is AgentIdentity => agent !== null);
458-
459-
return {
460-
agents,
461-
totalAgents: stats.totalAgents,
462-
};
463-
} catch (error) {
464-
console.warn("Failed to fetch agents:", error);
465-
return { agents: [], totalAgents: stats.totalAgents };
466-
}
347+
return sati.listAllAgents(params);
467348
}
468349

469350
/**

0 commit comments

Comments
 (0)