diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 63ead25f..fdc21a40 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -3,10 +3,7 @@ name: Vana SDK Pre-release on: push: branches: - - feature/data-portability-sdk-v1 - - feat/compressed-key-support - - tim/pro-526-personal-server-agent-operation-poc - - chore/PRO-775/integrate-pge + - feat/data-access-v1 jobs: publish-prerelease: diff --git a/examples/vana-console/src/app/(dashboard)/datasets/page.tsx b/examples/vana-console/src/app/(dashboard)/datasets/page.tsx new file mode 100644 index 00000000..af6ac99d --- /dev/null +++ b/examples/vana-console/src/app/(dashboard)/datasets/page.tsx @@ -0,0 +1,654 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useVana } from "@/providers/VanaProvider"; +import { useAccount, useChainId } from "wagmi"; +import { + Card, + CardBody, + CardHeader, + Button, + Input, + Spinner, + Chip, + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + Select, + SelectItem, + Tooltip, +} from "@heroui/react"; +import { + FileText, + Check, + X, + RefreshCw, + Plus, + ExternalLink, + Copy, +} from "lucide-react"; +import type { Dataset } from "@opendatalabs/vana-sdk"; +import { AddressDisplay } from "@/components/ui/AddressDisplay"; + +// Unified file row type +type FileRow = { + fileId: number; + datasetId: number; + status: "pending" | "accepted"; + ownerAddress: string; + addedAtBlock: bigint; + url: string; + datasetOwner: string; + schemaId: number; +}; + +export default function DatasetsPage() { + const { vana } = useVana(); + const { address } = useAccount(); + const chainId = useChainId(); + + // Helper function to get block explorer URL + const getBlockExplorerUrl = (fileId: number) => { + const baseUrl = + chainId === 1480 ? "https://vanascan.io" : "https://moksha.vanascan.io"; + return `${baseUrl}/tx/${fileId}`; // Update with correct path for files + }; + + // Helper function to copy to clipboard + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + // User's datasets state + const [userDatasets, setUserDatasets] = useState< + Array + >([]); + const [loadingUserDatasets, setLoadingUserDatasets] = useState(false); + + // Unified files state + const [allFiles, setAllFiles] = useState([]); + const [loadingFiles, setLoadingFiles] = useState(false); + + // Filters + const [statusFilter, setStatusFilter] = useState< + "all" | "pending" | "accepted" + >("all"); + const [datasetFilter, setDatasetFilter] = useState(""); + + // Selection + const [selectedKeys, setSelectedKeys] = useState>(new Set()); + + // Create dataset state + const [showCreate, setShowCreate] = useState(false); + const [schemaId, setSchemaId] = useState(""); + const [creating, setCreating] = useState(false); + + // Processing state + const [processing, setProcessing] = useState>(new Set()); + const [error, setError] = useState(null); + + // Load user's owned datasets + const loadUserDatasets = async () => { + if (!vana || !address) return; + + setLoadingUserDatasets(true); + try { + const datasets = await vana.dataset.getUserDatasets({ + owner: address, + }); + setUserDatasets(datasets); + } catch (err) { + console.error("Failed to load user datasets:", err); + setError(err instanceof Error ? err.message : "Failed to load datasets"); + } finally { + setLoadingUserDatasets(false); + } + }; + + // Load all files from all datasets + const loadAllFiles = async () => { + if (!vana || userDatasets.length === 0) return; + + setLoadingFiles(true); + try { + const filesPromises: Promise[] = []; + + for (const dataset of userDatasets) { + // Load pending files + for (const fileId of dataset.pendingFileIds) { + filesPromises.push( + vana.data.getFileById(fileId).then((file) => ({ + fileId, + datasetId: dataset.id, + status: "pending" as const, + ownerAddress: file.ownerAddress, + addedAtBlock: file.addedAtBlock, + url: file.url, + datasetOwner: dataset.owner, + schemaId: dataset.schemaId, + })), + ); + } + + // Load accepted files + for (const fileId of dataset.fileIds) { + filesPromises.push( + vana.data.getFileById(fileId).then((file) => ({ + fileId, + datasetId: dataset.id, + status: "accepted" as const, + ownerAddress: file.ownerAddress, + addedAtBlock: file.addedAtBlock, + url: file.url, + datasetOwner: dataset.owner, + schemaId: dataset.schemaId, + })), + ); + } + } + + const files = await Promise.all(filesPromises); + setAllFiles(files); + } catch (err) { + console.error("Failed to load files:", err); + setError(err instanceof Error ? err.message : "Failed to load files"); + } finally { + setLoadingFiles(false); + } + }; + + // Load datasets on mount + useEffect(() => { + if (vana && address) { + void loadUserDatasets(); + } + }, [vana, address]); + + // Load files when datasets change + useEffect(() => { + if (userDatasets.length > 0) { + void loadAllFiles(); + } + }, [userDatasets]); + + // Filter files based on current filters + const filteredFiles = React.useMemo(() => { + return allFiles.filter((file) => { + // Status filter + if (statusFilter !== "all" && file.status !== statusFilter) { + return false; + } + + // Dataset filter + if (datasetFilter && file.datasetId.toString() !== datasetFilter) { + return false; + } + + return true; + }); + }, [allFiles, statusFilter, datasetFilter]); + + // Handle accept file + const handleAcceptFile = async (datasetId: number, fileId: number) => { + if (!vana) return; + + const key = `${datasetId}-${fileId}`; + setProcessing((prev) => new Set(prev).add(key)); + setError(null); + + try { + const tx = await vana.dataset.acceptFile(datasetId, fileId); + await vana.publicClient.waitForTransactionReceipt({ hash: tx.hash }); + + // Reload data + await loadUserDatasets(); + } catch (err) { + console.error("Failed to accept file:", err); + setError(err instanceof Error ? err.message : "Failed to accept file"); + } finally { + setProcessing((prev) => { + const next = new Set(prev); + next.delete(key); + return next; + }); + } + }; + + // Handle reject file + const handleRejectFile = async (datasetId: number, fileId: number) => { + if (!vana) return; + + const key = `${datasetId}-${fileId}`; + setProcessing((prev) => new Set(prev).add(key)); + setError(null); + + try { + const tx = await vana.dataset.rejectFile(datasetId, fileId); + await vana.publicClient.waitForTransactionReceipt({ hash: tx.hash }); + + // Reload data + await loadUserDatasets(); + } catch (err) { + console.error("Failed to reject file:", err); + setError(err instanceof Error ? err.message : "Failed to reject file"); + } finally { + setProcessing((prev) => { + const next = new Set(prev); + next.delete(key); + return next; + }); + } + }; + + // Handle batch accept + const handleBatchAccept = async () => { + const selectedFiles = Array.from(selectedKeys).map((key) => { + const [datasetId, fileId] = key.split("-").map(Number); + return { datasetId, fileId }; + }); + + for (const { datasetId, fileId } of selectedFiles) { + await handleAcceptFile(datasetId, fileId); + } + + setSelectedKeys(new Set()); + }; + + // Handle batch reject + const handleBatchReject = async () => { + const selectedFiles = Array.from(selectedKeys).map((key) => { + const [datasetId, fileId] = key.split("-").map(Number); + return { datasetId, fileId }; + }); + + for (const { datasetId, fileId } of selectedFiles) { + await handleRejectFile(datasetId, fileId); + } + + setSelectedKeys(new Set()); + }; + + // Handle create dataset + const handleCreateDataset = async () => { + if (!vana || !schemaId) return; + + setCreating(true); + setError(null); + + try { + const tx = await vana.dataset.createDataset(Number(schemaId)); + await vana.publicClient.waitForTransactionReceipt({ hash: tx.hash }); + + setShowCreate(false); + setSchemaId(""); + await loadUserDatasets(); + } catch (err) { + console.error("Failed to create dataset:", err); + setError(err instanceof Error ? err.message : "Failed to create dataset"); + } finally { + setCreating(false); + } + }; + + // Check if user is owner of the dataset + const isOwner = (datasetOwner: string) => { + return datasetOwner.toLowerCase() === address?.toLowerCase(); + }; + + // Count pending files that can be acted upon + const pendingFileCount = filteredFiles.filter( + (f) => f.status === "pending" && isOwner(f.datasetOwner), + ).length; + + return ( +
+ {/* Header */} +
+
+

Dataset Files

+

+ Manage files across all your datasets +

+
+ +
+ + {/* Create Dataset Form */} + {showCreate && ( + + +

Create New Dataset

+
+ + { + setSchemaId(e.target.value); + }} + type="number" + /> +
+ + +
+
+
+ )} + + {/* Error Display */} + {error && ( + + +

{error}

+
+
+ )} + + {/* Files Table */} + + +
+
+ +
+

Files

+

+ {filteredFiles.length} file + {filteredFiles.length !== 1 ? "s" : ""}{" "} + {pendingFileCount > 0 && + `ยท ${pendingFileCount} pending review`} +

+
+
+ +
+ + + + + {selectedKeys.size > 0 && ( +
+ + +
+ )} + +
+
+
+ + {loadingUserDatasets || loadingFiles ? ( +
+ + Loading files... +
+ ) : filteredFiles.length === 0 ? ( +
+ +

+ No files found.{" "} + {userDatasets.length === 0 + ? "Create a dataset to get started." + : ""} +

+
+ ) : ( + { + if (keys === "all") { + // Select all pending files that user owns + const ownedPending = filteredFiles + .filter( + (f) => f.status === "pending" && isOwner(f.datasetOwner), + ) + .map((f) => `${f.datasetId}-${f.fileId}`); + setSelectedKeys(new Set(ownedPending)); + } else { + setSelectedKeys(new Set(keys as Set)); + } + }} + > + + File ID + Dataset ID + Status + Contributor + Block + URL + Actions + + + {filteredFiles.map((file) => { + const key = `${file.datasetId}-${file.fileId}`; + const isProcessing = processing.has(key); + const canAct = + file.status === "pending" && isOwner(file.datasetOwner); + + return ( + + +
+ #{file.fileId} + + + + + + +
+
+ + + #{file.datasetId} + + + + + {file.status} + + + + + + + {Number(file.addedAtBlock)} + + +
+ + {file.url} + + + + + + + +
+
+ + {canAct && ( +
+ + +
+ )} +
+
+ ); + })} +
+
+ )} +
+
+
+ ); +} diff --git a/examples/vana-console/src/app/(dashboard)/dlp-operations/page.tsx b/examples/vana-console/src/app/(dashboard)/dlp-operations/page.tsx new file mode 100644 index 00000000..92d7b2cf --- /dev/null +++ b/examples/vana-console/src/app/(dashboard)/dlp-operations/page.tsx @@ -0,0 +1,289 @@ +"use client"; + +import React, { useState } from "react"; +import { + Card, + CardHeader, + CardBody, + Button, + Input, + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + Spinner, + Chip, + useDisclosure, +} from "@heroui/react"; +import { Users, Plus, RefreshCw, Eye } from "lucide-react"; +import type { RuntimePermission } from "@opendatalabs/vana-sdk/browser"; +import { useVana } from "@/providers/VanaProvider"; +import { CreateRuntimePermissionModal } from "@/components/ui/CreateRuntimePermissionModal"; +import { CopyButton } from "@/components/ui/CopyButton"; +import { PermissionDetailsModal } from "@/components/ui/PermissionDetailsModal"; + +/** + * DLP Operations page - Manage runtime permissions for dataset access + * + * This page allows DLP operators to create and manage runtime permissions + * for their datasets. Data consumers can request access, pay for operations, + * and execute tasks on encrypted data through the Vana Runtime TEE environment. + */ +export default function DLPOperationsPage() { + const { vana } = useVana(); + + // State + const [datasetIdInput, setDatasetIdInput] = useState(""); + const [selectedDatasetId, setSelectedDatasetId] = useState( + null, + ); + const [permissions, setPermissions] = useState([]); + const [isLoadingPermissions, setIsLoadingPermissions] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [error, setError] = useState(null); + + // Grant details modal state + const { isOpen, onOpen, onClose } = useDisclosure(); + const [selectedPermissionForModal, setSelectedPermissionForModal] = useState<{ + id: string; + grantUrl: string; + } | null>(null); + + // Load permissions for selected dataset + const loadPermissions = async () => { + if (!vana || !datasetIdInput) { + setError("Please enter a dataset ID"); + return; + } + + setIsLoadingPermissions(true); + setError(null); + + try { + const datasetId = BigInt(datasetIdInput); + setSelectedDatasetId(datasetId); + + const perms = + await vana.runtimePermissions.getDatasetPermissions(datasetId); + setPermissions(perms); + } catch (err) { + console.error("Failed to load permissions:", err); + setError( + err instanceof Error ? err.message : "Failed to load permissions", + ); + setPermissions([]); + } finally { + setIsLoadingPermissions(false); + } + }; + + // Handle permission created + const handlePermissionCreated = () => { + setIsModalOpen(false); + void loadPermissions(); // Refresh the list + }; + + return ( +
+ {/* Header */} +
+

+ DLP Operations +

+

+ Grant runtime access to collective datasets +

+
+ + {/* Dataset Selector */} + + +
+ +

Select Dataset

+
+
+ +
+ { + setDatasetIdInput(e.target.value); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + void loadPermissions(); + } + }} + className="flex-1" + /> + +
+ {error &&

{error}

} +
+
+ + {/* Permissions Table */} + {selectedDatasetId !== null && ( + + +
+

+ Runtime Permissions for Dataset #{selectedDatasetId.toString()} +

+
+ +
+ + {isLoadingPermissions ? ( +
+ +
+ ) : permissions.length > 0 ? ( + + + Permission ID + Grantee + Created + Expires + Grant + + + {permissions.map((permission) => { + const permissionId = permission.id.toString(); + + return ( + + +
+ + {permissionId} + + +
+
+ +
+ + {`0x${permission.granteeId.toString(16).padStart(40, "0")}`} + + +
+
+ + + Block {permission.startBlock.toString()} + + + + + {permission.endBlock.toString() === + (2n ** 256n - 1n).toString() + ? "Never" + : `Block ${permission.endBlock.toString()}`} + + + + + +
+ ); + })} +
+
+ ) : ( +
+

+ No permissions found for this dataset. +

+

+ Create a permission to allow data consumers to access your + dataset. +

+
+ )} +
+
+ )} + + {/* Create Permission Modal */} + { + setIsModalOpen(false); + }} + datasetId={selectedDatasetId} + onSuccess={handlePermissionCreated} + /> + + {/* Permission Details Modal */} + {selectedPermissionForModal && ( + { + onClose(); + setSelectedPermissionForModal(null); + }} + permissionId={selectedPermissionForModal.id} + grantUrl={selectedPermissionForModal.grantUrl} + /> + )} +
+ ); +} diff --git a/examples/vana-console/src/app/(dashboard)/layout.tsx b/examples/vana-console/src/app/(dashboard)/layout.tsx index c0286162..878810b7 100644 --- a/examples/vana-console/src/app/(dashboard)/layout.tsx +++ b/examples/vana-console/src/app/(dashboard)/layout.tsx @@ -1,13 +1,10 @@ "use client"; -import React, { useState } from "react"; -import { useAccount, useChainId } from "wagmi"; +import React, { useState, useEffect } from "react"; +import { useAccount, useChainId, useDisconnect } from "wagmi"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useModal, useAccount as useParaAccount } from "@getpara/react-sdk"; import { - Card, - CardHeader, - CardBody, useDisclosure, Modal, ModalContent, @@ -25,6 +22,10 @@ import { SDKConfigProvider, useSDKConfig } from "@/providers/SDKConfigProvider"; import { VanaProvider } from "@/providers/VanaProvider"; import { GrantPreviewModalContent } from "@/components/GrantPreviewModalContent"; import type { GrantPermissionParams } from "@opendatalabs/vana-sdk/browser"; +import { + WalletProviderToggle, + type WalletProvider, +} from "@/components/ui/WalletProviderToggle"; // Types for grant preview modal interface GrantPreview { @@ -42,13 +43,60 @@ interface GrantPreview { // Inner component that consumes SDKConfigContext function DashboardLayoutInner({ children }: { children: React.ReactNode }) { - const useRainbow = - process.env.NEXT_PUBLIC_WALLET_PROVIDER === "rainbow" || - !process.env.NEXT_PUBLIC_WALLET_PROVIDER; + // Wallet provider selection with localStorage persistence + const [walletProvider, setWalletProvider] = + useState("rainbow"); + const [mounted, setMounted] = useState(false); + + // Check if both wallet providers are configured + const isParaConfigured = !!process.env.NEXT_PUBLIC_PARA_KEY; + const isRainbowConfigured = true; // Rainbow is always available + const showProviderToggle = isParaConfigured && isRainbowConfigured; + + // Load wallet provider preference from localStorage on mount + useEffect(() => { + setMounted(true); + const savedProvider = localStorage.getItem( + "vana-console-wallet-provider", + ) as WalletProvider | null; + + // If only one provider is configured, use that one + if (!showProviderToggle) { + setWalletProvider(isParaConfigured ? "para" : "rainbow"); + return; + } + + if (savedProvider === "rainbow" || savedProvider === "para") { + setWalletProvider(savedProvider); + } else { + // Default based on env var for backwards compatibility + const envProvider = + process.env.NEXT_PUBLIC_WALLET_PROVIDER === "rainbow" || + !process.env.NEXT_PUBLIC_WALLET_PROVIDER + ? "rainbow" + : "para"; + setWalletProvider(envProvider); + } + }, [showProviderToggle, isParaConfigured]); + + // Save to localStorage when provider changes and disconnect if needed + const handleProviderChange = (provider: WalletProvider) => { + // If switching providers while connected, disconnect first + if (walletConnected) { + disconnect(); + } + + // Update provider preference + setWalletProvider(provider); + localStorage.setItem("vana-console-wallet-provider", provider); + }; + + const useRainbow = walletProvider === "rainbow"; // Wagmi hooks (work with both Rainbow and Para) const { isConnected } = useAccount(); const chainId = useChainId(); + const { disconnect } = useDisconnect(); // Para wallet hooks const { openModal } = useModal?.() || {}; @@ -110,54 +158,37 @@ function DashboardLayoutInner({ children }: { children: React.ReactNode }) { } }; - // If not connected, show wallet connection prompt - if (!walletConnected) { - return ( -
- - -

- Vana Console{walletConnected ? "" : " (๐Ÿ”’ Read-Only)"} -

-
- - {renderConnectButton()} - -
- -
- - -
Get Started
-
- -

- Connect your wallet above to begin exploring the Vana SDK - capabilities. -

-
-
-
-
- ); - } - - // Main dashboard layout with VanaProvider + // Main dashboard layout with VanaProvider (always render, supports read-only mode) return (
-

- Vana Console - {appConfig.enableReadOnlyMode - ? " (๐Ÿ“– Read-Only)" - : walletConnected - ? "" - : " (๐Ÿ”’ Disconnected)"} -

+

Vana Console

+ + {!walletConnected && ( + + + ๐Ÿ”’ Read-Only Mode + + (Browsing: 0x000...000) + + + + )} + + {mounted && showProviderToggle && ( + + + + )} {renderConnectButton()}
diff --git a/examples/vana-console/src/app/(dashboard)/runtime-servers/page.tsx b/examples/vana-console/src/app/(dashboard)/runtime-servers/page.tsx new file mode 100644 index 00000000..6ac665c3 --- /dev/null +++ b/examples/vana-console/src/app/(dashboard)/runtime-servers/page.tsx @@ -0,0 +1,314 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { + Card, + CardBody, + Button, + Tabs, + Tab, + Input, + Textarea, +} from "@heroui/react"; +import { Server, Plus, RefreshCw, CheckCircle2, XCircle } from "lucide-react"; +import { useRuntimeServers } from "@/hooks/useRuntimeServers"; +import { useSDKConfig } from "@/providers/SDKConfigProvider"; +import { EmptyState } from "@/components/ui/EmptyState"; + +/** + * Runtime Servers Page + * + * Manage TEE runtime servers registered in the VanaRuntimeServers contract. + * Provides functionality to view registered runtimes and register new ones. + */ +export default function RuntimeServersPage() { + const { effectiveAddress } = useSDKConfig(); + const { + // State + runtimeServers, + isLoadingRuntimeServers, + isRegisteringRuntime, + registerRuntimeError, + + // Form state + runtimeAddress, + publicKey, + escrowedPrivateKey, + runtimeUrl, + + // Actions + loadRuntimeServers, + handleRegisterRuntime, + setRuntimeAddress, + setPublicKey, + setEscrowedPrivateKey, + setRuntimeUrl, + setRegisterRuntimeError, + } = useRuntimeServers(); + + const [activeTab, setActiveTab] = useState("runtimes"); + + // Load runtime servers on mount + useEffect(() => { + if (effectiveAddress) { + loadRuntimeServers(); + } + }, [effectiveAddress, loadRuntimeServers]); + + const handleSubmitRegistration = async () => { + setRegisterRuntimeError(""); + await handleRegisterRuntime(); + }; + + return ( +
+ {/* Header */} +
+
+

+ Runtime Servers +

+

+ Manage TEE runtime servers for secure data processing +

+
+
+ + {/* Main Content */} + + + { + setActiveTab(key as string); + }} + classNames={{ + tabList: "w-full relative rounded-none", + cursor: "w-full", + tab: "max-w-fit px-6 h-12", + tabContent: "group-data-[selected=true]:text-primary", + }} + > + {/* Registered Runtimes Tab */} + + + Registered Runtimes +
+ } + > +
+ {/* Refresh Button */} +
+ +
+ + {/* Runtime Servers List */} + {isLoadingRuntimeServers ? ( +
+ Loading runtime servers... +
+ ) : runtimeServers.length === 0 ? ( + } + title="No runtime servers registered" + description="Register your first runtime server to get started" + action={ + + } + /> + ) : ( +
+ {runtimeServers.map((runtime, index) => ( + + +
+ {/* Runtime Address */} +
+

+ Runtime Address +

+

+ {runtime.runtimeAddress} +

+
+ + {/* URL */} +
+

+ URL +

+

{runtime.url}

+
+ + {/* Owner */} +
+

+ Owner +

+

+ {runtime.owner} +

+
+ + {/* Public Key */} +
+

+ Public Key +

+

+ {runtime.publicKey.substring(0, 32)}... +

+
+
+
+
+ ))} +
+ )} +
+ + + {/* Register Runtime Tab */} + + + Register Runtime +
+ } + > +
+
+ {/* Info Card */} + + +

+ Register a new TEE runtime server to enable secure data + processing. You'll need the runtime's address, public + key, escrowed private key, and URL. +

+
+
+ + {/* Registration Form */} +
+ {/* Runtime Address */} + + + {/* Public Key */} +