UNPKG

@silvana-one/coordination

Version:

Silvana Coordination Client

301 lines (264 loc) 9.58 kB
import { Transaction } from "@mysten/sui/transactions"; import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui/utils"; import { silvanaRegistryPackage } from "./package.js"; import { fetchSuiDynamicField, fetchSuiObject } from "./fetch.js"; import { Job, JobStatus } from "./job.js"; export interface AppMethod { description?: string; developer: string; agent: string; agentMethod: string; } export interface AppInstance { id: string; silvanaAppName: string; description?: string; metadata: Record<string, string>; kv: Record<string, string>; methods: Record<string, AppMethod>; admin: string; sequence: number; blockNumber: number; previousBlockTimestamp: number; previousBlockLastSequence: number; lastProvedBlockNumber: number; lastSettledBlockNumber: number; lastSettledSequence: number; lastPurgedSequence: number; isPaused: boolean; minTimeBetweenBlocks: number; jobsId: string; createdAt: number; updatedAt: number; } export interface CreateAppJobParams { appInstance: string; description?: string; method: string; sequences?: number[]; data: Uint8Array; } export class AppInstanceManager { private readonly registry: string; constructor(params: { registry: string }) { this.registry = params.registry; } // Note: update_method and remove_method functions are not available in the Move module // These would need to be implemented in app_instance.move if needed createAppJob(params: CreateAppJobParams): Transaction { const { appInstance, description, method, sequences, data } = params; // Debug logging console.log("createAppJob params:", { appInstance, description, method, sequences, data: data instanceof Uint8Array ? `Uint8Array(${data.length})` : typeof data, dataContent: data instanceof Uint8Array ? Array.from(data) : data, }); const tx = new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::app_instance::create_app_job`, arguments: [ tx.object(appInstance), tx.pure.string(method), tx.pure.option("string", description ?? null), tx.pure.option("vector<u64>", sequences ?? null), tx.pure.vector("u8", data), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } startAppJob(params: { appInstance: string; jobId: number }): Transaction { const { appInstance, jobId } = params; const tx = new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::app_instance::start_app_job`, arguments: [ tx.object(appInstance), tx.pure.u64(jobId), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } completeAppJob(params: { appInstance: string; jobId: number }): Transaction { const { appInstance, jobId } = params; const tx = new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::app_instance::complete_app_job`, arguments: [ tx.object(appInstance), tx.pure.u64(jobId), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } failAppJob(params: { appInstance: string; jobId: number; error: string; }): Transaction { const { appInstance, jobId, error } = params; const tx = new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::app_instance::fail_app_job`, arguments: [ tx.object(appInstance), tx.pure.u64(jobId), tx.pure.string(error), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } async getAppInstance( appInstanceId: string ): Promise<AppInstance | undefined> { try { const appInstance = await fetchSuiObject(appInstanceId); if (!appInstance) return undefined; const fields = (appInstance as any)?.data?.content?.fields; if (!fields) return undefined; // Parse methods from VecMap const methods: Record<string, AppMethod> = {}; const methodsArray = fields?.methods?.fields?.contents; if (Array.isArray(methodsArray)) { for (const entry of methodsArray) { const key = entry?.fields?.key; const value = entry?.fields?.value; if (key && value) { // Value might have a fields property too const methodFields = value.fields || value; methods[key] = { description: methodFields.description ?? undefined, developer: methodFields.developer, agent: methodFields.agent, agentMethod: methodFields.agent_method || methodFields.agentMethod, }; } } } // Parse metadata from VecMap const metadata: Record<string, string> = {}; const metadataArray = fields?.metadata?.fields?.contents; if (Array.isArray(metadataArray)) { for (const entry of metadataArray) { const key = entry?.fields?.key; const value = entry?.fields?.value; if (key && value) { metadata[key] = value; } } } // Parse kv from VecMap const kv: Record<string, string> = {}; const kvArray = fields?.kv?.fields?.contents; if (Array.isArray(kvArray)) { for (const entry of kvArray) { const key = entry?.fields?.key; const value = entry?.fields?.value; if (key && value) { kv[key] = value; } } } return { id: fields?.id?.id, silvanaAppName: fields.silvana_app_name, description: fields?.description ?? undefined, metadata, kv, methods, admin: fields.admin, sequence: Number(fields.sequence), blockNumber: Number(fields.block_number), previousBlockTimestamp: Number(fields.previous_block_timestamp), previousBlockLastSequence: Number(fields.previous_block_last_sequence), lastProvedBlockNumber: Number(fields.last_proved_block_number), lastSettledBlockNumber: Number(fields.last_settled_block_number), lastSettledSequence: Number(fields.last_settled_sequence), lastPurgedSequence: Number(fields.last_purged_sequence), isPaused: Boolean(fields.isPaused), minTimeBetweenBlocks: Number(fields.min_time_between_blocks), jobsId: String(fields.jobs?.fields?.id?.id ?? ""), createdAt: Number(fields.created_at), updatedAt: Number(fields.updated_at), }; } catch (error) { console.error("Error fetching app instance:", error); return undefined; } } async getAppJob(params: { appInstance: string; jobId: number; }): Promise<Job | undefined> { try { const appInstanceObj = await fetchSuiObject(params.appInstance); if (!appInstanceObj) return undefined; // Jobs are embedded in the AppInstance - use correct path const jobsTableId = (appInstanceObj as any)?.data?.content?.fields?.jobs?.fields?.jobs?.fields?.id?.id; if (!jobsTableId) return undefined; const job = await fetchSuiDynamicField({ parentID: jobsTableId, fieldName: "jobs", type: "u64", key: String(params.jobId), }); if (!job) return undefined; const parseStatus = (status: any): JobStatus => { // Check variant field format (used by Sui dynamic fields) if (status?.variant === "Pending") return { type: "Pending" }; if (status?.variant === "Running") return { type: "Running" }; if (status?.variant === "Failed") { // Get error from fields or from direct property const error = status?.fields?.error || status?.fields?.[0] || "Unknown error"; return { type: "Failed", error }; } // Legacy formats if (status?.Pending !== undefined) return { type: "Pending" }; if (status?.Running !== undefined) return { type: "Running" }; if (status?.Failed !== undefined) return { type: "Failed", error: status.Failed }; return { type: "Pending" }; }; return { id: (job as any)?.id?.id, jobId: Number((job as any).job_id), description: (job as any)?.description ?? undefined, developer: (job as any).developer, agent: (job as any).agent, agentMethod: (job as any).agent_method, app: (job as any).app, appInstance: (job as any).app_instance, appInstanceMethod: (job as any).app_instance_method, sequences: (job as any)?.sequences?.map((s: string) => Number(s)) ?? undefined, data: new Uint8Array((job as any).data), status: parseStatus((job as any).status), attempts: Number((job as any).attempts), createdAt: Number((job as any).created_at), updatedAt: Number((job as any).updated_at), }; } catch (error) { console.error("Error fetching app job:", error); return undefined; } } async getAppPendingJobs(appInstance: string): Promise<number[]> { try { const appInstanceObj = await fetchSuiObject(appInstance); if (!appInstanceObj) return []; // Jobs are embedded in the AppInstance - use correct path const pendingJobs = (appInstanceObj as any)?.data?.content?.fields?.jobs?.fields?.pending_jobs?.fields?.contents; if (!Array.isArray(pendingJobs)) return []; return pendingJobs.map((id: string) => Number(id)); } catch (error) { console.error("Error fetching app pending jobs:", error); return []; } } }