diff --git a/package.json b/package.json
index a2832484..378eecb1 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"gsap": "3.13.0",
"it-pipe": "^3.0.1",
"json-edit-react": "^1.29.0",
+ "jszip": "^3.10.1",
"libp2p": "^1.8.0",
"myetherwallet-blockies": "^0.1.1",
"next": "^15.4.8",
diff --git a/src/components/button/download-logs-button.tsx b/src/components/button/download-logs-button.tsx
new file mode 100644
index 00000000..9806d614
--- /dev/null
+++ b/src/components/button/download-logs-button.tsx
@@ -0,0 +1,84 @@
+import Button from '@/components/button/button';
+import { useP2P } from '@/contexts/P2PContext';
+import { useOceanAccount } from '@/lib/use-ocean-account';
+import { ComputeJob } from '@/types/jobs';
+import { generateAuthTokenWithSmartAccount } from '@/utils/generateAuthToken';
+import { useSignMessage, useSmartAccountClient } from '@account-kit/react';
+import DownloadIcon from '@mui/icons-material/Download';
+import JSZip from 'jszip';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+
+interface DownloadLogsButtonProps {
+ job: ComputeJob;
+}
+
+export const DownloadLogsButton = ({ job }: DownloadLogsButtonProps) => {
+ const { getComputeResult, isReady } = useP2P();
+ const [isDownloading, setIsDownloading] = useState(false);
+ const { client } = useSmartAccountClient({ type: 'LightAccount' });
+ const { signMessageAsync } = useSignMessage({
+ client,
+ });
+ const { account } = useOceanAccount();
+
+ const handleDownload = async () => {
+ if (!isReady || isDownloading || !account?.address) return;
+
+ try {
+ const jobId = job.environment.split('-')[0] + '-' + job.jobId;
+
+ const authToken = await generateAuthTokenWithSmartAccount(job.peerId, account.address, signMessageAsync);
+
+ const logFiles = job.results.filter((result: any) => result.filename.includes('.log'));
+ const logPromises = logFiles.map((logFile: any) =>
+ getComputeResult(job.peerId, jobId, logFile.index, authToken, account.address)
+ );
+
+ const downloadedLogs = await Promise.all(logPromises);
+ setIsDownloading(true);
+
+ const zip = new JSZip();
+
+ downloadedLogs.forEach((logData, index) => {
+ if (logData instanceof Uint8Array) {
+ if (logData.byteLength !== 0) {
+ zip.file(logFiles[index].filename, logData);
+ }
+ }
+ });
+
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
+
+ const url = URL.createObjectURL(zipBlob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `logs-${job.jobId}.zip`;
+
+ document.body.appendChild(link);
+ link.click();
+
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ } catch (e) {
+ const errorMessage = e instanceof Error ? e.message : 'Failed to download logs';
+ toast.error(errorMessage);
+ } finally {
+ setIsDownloading(false);
+ }
+ };
+
+ return (
+ }
+ disabled={!isReady}
+ onClick={handleDownload}
+ size="md"
+ variant="outlined"
+ >
+ Logs
+
+ );
+};
diff --git a/src/components/button/download-result-button.tsx b/src/components/button/download-result-button.tsx
new file mode 100644
index 00000000..a13953b8
--- /dev/null
+++ b/src/components/button/download-result-button.tsx
@@ -0,0 +1,74 @@
+import Button from '@/components/button/button';
+import { useP2P } from '@/contexts/P2PContext';
+import { useOceanAccount } from '@/lib/use-ocean-account';
+import { ComputeJob } from '@/types/jobs';
+import { generateAuthTokenWithSmartAccount } from '@/utils/generateAuthToken';
+import { useSignMessage, useSmartAccountClient } from '@account-kit/react';
+import DownloadIcon from '@mui/icons-material/Download';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+
+interface DownloadResultButtonProps {
+ job: ComputeJob;
+}
+
+export const DownloadResultButton = ({ job }: DownloadResultButtonProps) => {
+ const { getComputeResult, isReady } = useP2P();
+ const [isDownloading, setIsDownloading] = useState(false);
+ const { client } = useSmartAccountClient({ type: 'LightAccount' });
+ const { signMessageAsync } = useSignMessage({
+ client,
+ });
+ const { account } = useOceanAccount();
+
+ const handleDownload = async () => {
+ if (!isReady || isDownloading || !account?.address) return;
+
+ try {
+ const jobId = job.environment.split('-')[0] + '-' + job.jobId;
+ setIsDownloading(true);
+ const archive = job.results.find((result: any) => result.filename.includes('.tar'));
+
+ const authToken = await generateAuthTokenWithSmartAccount(job.peerId, account.address, signMessageAsync);
+
+ const result = await getComputeResult(job.peerId, jobId, archive?.index, authToken, account.address);
+ if (result instanceof Uint8Array) {
+ if (result.byteLength === 0) {
+ console.log('Received empty response (0 bytes). Skipping download.');
+ return;
+ }
+
+ const blob = new Blob([result as any], { type: 'application/octet-stream' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `outputs-${job.jobId}.tar`;
+
+ document.body.appendChild(link);
+ link.click();
+
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ }
+ } catch (e) {
+ const errorMessage = e instanceof Error ? e.message : 'Failed to download result';
+ toast.error(errorMessage);
+ } finally {
+ setIsDownloading(false);
+ }
+ };
+
+ return (
+ }
+ disabled={!isReady}
+ onClick={handleDownload}
+ size="md"
+ variant="outlined"
+ >
+ Results
+
+ );
+};
diff --git a/src/components/button/job-info-button.tsx b/src/components/button/job-info-button.tsx
new file mode 100644
index 00000000..d26111b5
--- /dev/null
+++ b/src/components/button/job-info-button.tsx
@@ -0,0 +1,26 @@
+import { JobInfoModal } from '@/components/modal/job-info-modal';
+import { ComputeJob } from '@/types/jobs';
+import { useState } from 'react';
+import Button from './button';
+
+interface JobInfoButtonProps {
+ job: ComputeJob;
+}
+
+export const JobInfoButton = ({ job }: JobInfoButtonProps) => {
+ const [open, setOpen] = useState(false);
+
+ const handleOpen = () => setOpen(true);
+ const handleClose = () => setOpen(false);
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default JobInfoButton;
diff --git a/src/components/modal/job-info-modal.tsx b/src/components/modal/job-info-modal.tsx
new file mode 100644
index 00000000..2a5cbd29
--- /dev/null
+++ b/src/components/modal/job-info-modal.tsx
@@ -0,0 +1,63 @@
+import { DownloadLogsButton } from '@/components/button/download-logs-button';
+import { DownloadResultButton } from '@/components/button/download-result-button';
+import EnvironmentCard from '@/components/environment-card/environment-card';
+import Modal from '@/components/modal/modal';
+import { useProfileContext } from '@/context/profile-context';
+import { ComputeJob } from '@/types/jobs';
+import { Stack } from '@mui/material';
+import classNames from 'classnames';
+import { useEffect } from 'react';
+import styles from './modal.module.css';
+
+interface JobInfoModalProps {
+ job: ComputeJob | null;
+ open: boolean;
+ onClose: () => void;
+}
+
+export const JobInfoModal = ({ job, open, onClose }: JobInfoModalProps) => {
+ const { fetchNodeEnv, environment, nodeInfo } = useProfileContext();
+
+ useEffect(() => {
+ if (open && job?.environment) {
+ fetchNodeEnv(job.peerId, job.environment);
+ }
+ }, [open, fetchNodeEnv, job]);
+
+ if (!job) return null;
+
+ return (
+
+
+
+
+
+
+ {environment && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/profile/consumer-environments.module.css b/src/components/profile/consumer-environments.module.css
deleted file mode 100644
index c8136595..00000000
--- a/src/components/profile/consumer-environments.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.list {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
diff --git a/src/components/profile/consumer-environments.tsx b/src/components/profile/consumer-environments.tsx
deleted file mode 100644
index c89ed4bc..00000000
--- a/src/components/profile/consumer-environments.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import Card from '@/components/card/card';
-import EnvironmentCard from '@/components/environment-card/environment-card';
-import { MOCK_ENVS } from '@/mock/environments';
-import styles from './consumer-environments.module.css';
-
-const ConsumerEnvironments = () => {
- return (
-
- Environments used
-
- {MOCK_ENVS.map((env) => (
-
- ))}
-
-
- );
-};
-
-export default ConsumerEnvironments;
diff --git a/src/components/profile/consumer.profile-page.tsx b/src/components/profile/consumer.profile-page.tsx
index d2a383d1..cb563d49 100644
--- a/src/components/profile/consumer.profile-page.tsx
+++ b/src/components/profile/consumer.profile-page.tsx
@@ -1,5 +1,4 @@
import Container from '@/components/container/container';
-import ConsumerEnvironments from '@/components/profile/consumer-environments';
import ConsumerJobs from '@/components/profile/consumer-jobs';
import ConsumerStats from '@/components/profile/consumer-stats';
import ProfileHeader from '@/components/profile/profile-header';
@@ -16,7 +15,6 @@ const ConsumerProfilePage = () => {
diff --git a/src/components/profile/owner-stats.tsx b/src/components/profile/owner-stats.tsx
index 3e7fbfa5..bcf3f36a 100644
--- a/src/components/profile/owner-stats.tsx
+++ b/src/components/profile/owner-stats.tsx
@@ -14,7 +14,7 @@ const OwnerStats = () => {
totalNetworkJobs,
totalBenchmarkJobs,
ownerStatsPerEpoch,
- activeNodes,
+ eligibleNodes,
totalNodes,
fetchOwnerStats,
fetchActiveNodes,
@@ -54,11 +54,11 @@ const OwnerStats = () => {
}}
/>
0 ? Number(((activeNodes / totalNodes) * 100).toFixed(1)) : 0}
+ title="Eligible nodes"
+ value={totalNodes > 0 ? Number(((eligibleNodes / totalNodes) * 100).toFixed(1)) : 0}
valueSuffix="%"
/>
diff --git a/src/components/table/columns.tsx b/src/components/table/columns.tsx
index fb5fd4e1..f9f44949 100644
--- a/src/components/table/columns.tsx
+++ b/src/components/table/columns.tsx
@@ -1,4 +1,5 @@
import InfoButton from '@/components/button/info-button';
+import JobInfoButton from '@/components/button/job-info-button';
import { ComputeJob } from '@/types/jobs';
import { GPUPopularity, Node } from '@/types/nodes';
import { UnbanRequest } from '@/types/unban-requests';
@@ -251,6 +252,17 @@ export const jobsColumns: GridColDef[] = [
(operator) => operator.value === '=' || operator.value === '>' || operator.value === '<'
),
},
+ {
+ align: 'right',
+ field: 'actions',
+ filterable: false,
+ headerAlign: 'center',
+ headerName: 'Actions',
+ sortable: false,
+ renderCell: (params: GridRenderCellParams) => {
+ return ;
+ },
+ },
];
export const unbanRequestsColumns: GridColDef[] = [
diff --git a/src/context/profile-context.tsx b/src/context/profile-context.tsx
index a34dba35..f5ab18d8 100644
--- a/src/context/profile-context.tsx
+++ b/src/context/profile-context.tsx
@@ -1,5 +1,6 @@
import { getApiRoute } from '@/config';
import { useOceanAccount } from '@/lib/use-ocean-account';
+import { EnvNodeInfo } from '@/types/environments';
import { EnsProfile } from '@/types/profile';
import {
ActiveNodes,
@@ -9,8 +10,9 @@ import {
OwnerStats,
OwnerStatsPerEpoch,
} from '@/types/stats';
+import { useSendUserOperation, useSmartAccountClient } from '@account-kit/react';
import axios from 'axios';
-import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react';
+import { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react';
type ProfileContextType = {
ensName: string | undefined;
@@ -22,24 +24,32 @@ type ProfileContextType = {
totalNetworkJobs: number;
totalBenchmarkJobs: number;
ownerStatsPerEpoch: OwnerStatsPerEpoch[];
- activeNodes: number;
+ eligibleNodes: number;
totalNodes: number;
//Consumer stats
totalJobs: number;
totalPaidAmount: number;
consumerStatsPerEpoch: ConsumerStatsPerEpoch[];
successfullJobs: number;
+ environment: any;
+ nodeInfo: EnvNodeInfo;
// API functions
fetchOwnerStats: () => Promise;
fetchConsumerStats: () => Promise;
fetchActiveNodes: () => Promise;
fetchJobsSuccessRate: () => Promise;
+ fetchNodeEnv: (peerId: string, envId: string) => Promise;
};
const ProfileContext = createContext(undefined);
export const ProfileProvider = ({ children }: { children: ReactNode }) => {
const { account } = useOceanAccount();
+ const { client } = useSmartAccountClient({ type: 'LightAccount' });
+ const { sendUserOperationAsync } = useSendUserOperation({
+ client,
+ waitForTxn: true,
+ });
const [ensAddress, setEnsAddress] = useState(undefined);
const [ensName, setEnsName] = useState(undefined);
@@ -52,9 +62,14 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
const [totalJobs, setTotalJobs] = useState(0);
const [totalPaidAmount, setTotalPaidAmount] = useState(0);
const [consumerStatsPerEpoch, setConsumerStatsPerEpoch] = useState([]);
- const [activeNodes, setActiveNodes] = useState(0);
+ const [eligibleNodes, setEligibleNodes] = useState(0);
const [totalNodes, setTotalNodes] = useState(0);
const [successfullJobs, setSuccessfullJobs] = useState(0);
+ const [environment, setEnvironment] = useState(null);
+ const [nodeInfo, setNodeInfo] = useState();
+
+ // Ref to track deployment attempts and prevent infinite loop
+ const deploymentAttempted = useRef(null);
const fetchEnsAddress = useCallback(async (accountId: string) => {
if (!accountId || accountId === '' || !accountId.includes('.')) {
@@ -144,7 +159,7 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
try {
const response = await axios.get(`${getApiRoute('nodesStats')}/${ensAddress}/nodesStats`);
if (response.data) {
- setActiveNodes(response.data.activeCount);
+ setEligibleNodes(response.data.activeCount);
setTotalNodes(response.data.totalCount);
}
} catch (err) {
@@ -166,6 +181,19 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
}
}, []);
+ const fetchNodeEnv = useCallback(async (peerId: string, envId: string) => {
+ try {
+ const response = await axios.get(`${getApiRoute('nodes')}?filters[id][value]=${peerId}`);
+ const sanitizedData = response.data.nodes.map((element: any) => element._source)[0];
+ const env = sanitizedData.computeEnvironments.environments.find((env: any) => env.id === envId);
+ setEnvironment(env);
+ setNodeInfo({ id: sanitizedData.id, friendlyName: sanitizedData.friendlyName });
+ } catch (err) {
+ console.error('Error fetching node env: ', err);
+ }
+ }, []);
+
+ // Handle profile fetching when connected
useEffect(() => {
if (account.status === 'connected' && account.address) {
fetchEnsAddress(account.address);
@@ -178,6 +206,44 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
}
}, [account.address, account.status, fetchEnsAddress, fetchEnsName, fetchEnsProfile]);
+ // Auto-deploy account if needed when user connects
+ useEffect(() => {
+ // Skip if we've already attempted deployment for this address
+ if (deploymentAttempted.current === account.address) {
+ return;
+ }
+
+ const handleAutoDeployment = async () => {
+ if (account.status === 'connected' && account.address && client?.account) {
+ const isDeployed = await client.account.isAccountDeployed();
+
+ if (!isDeployed) {
+ // Mark that we're attempting deployment for this address
+ deploymentAttempted.current = account.address;
+
+ try {
+ console.log('Deploying account for:', account.address);
+ await sendUserOperationAsync({
+ uo: {
+ target: account.address as `0x${string}`,
+ data: '0x' as `0x${string}`,
+ value: BigInt(0),
+ },
+ });
+ console.log('Account deployed successfully');
+ } catch (error) {
+ console.error('Error deploying account:', error);
+ // Reset on error so user can retry manually if needed
+ deploymentAttempted.current = null;
+ }
+ }
+ }
+ };
+
+ handleAutoDeployment();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [account.status, account.address, client]);
+
return (
{
totalJobs,
totalPaidAmount,
consumerStatsPerEpoch,
- activeNodes,
+ eligibleNodes,
totalNodes,
successfullJobs,
+ environment,
+ nodeInfo,
fetchOwnerStats,
fetchConsumerStats,
fetchActiveNodes,
fetchJobsSuccessRate,
+ fetchNodeEnv,
}}
>
{children}
diff --git a/src/context/table/my-jobs-table-context.tsx b/src/context/table/my-jobs-table-context.tsx
index 955275fd..3c7e913d 100644
--- a/src/context/table/my-jobs-table-context.tsx
+++ b/src/context/table/my-jobs-table-context.tsx
@@ -42,7 +42,7 @@ export const MyJobsTableProvider = ({ children, consumer }: { children: ReactNod
};
const fetchUrl = useMemo(() => {
- let url = `${getApiRoute('owners')}/${'0x4d7E4E3395074B3fb96eeddc6bA947767c4E1234'}/computeJobs?page=${crtPage}&size=${pageSize}&sort={"createdAt":"desc"}`;
+ let url = `${getApiRoute('owners')}/${consumer}/computeJobs?page=${crtPage}&size=${pageSize}&sort={"createdAt":"desc"}`;
const gridFilterToJobsFilters = (gridFilter: GridFilterModel): JobsFilters => {
const jobsFilters: JobsFilters = {};
gridFilter.items.forEach((item) => {
@@ -63,10 +63,12 @@ export const MyJobsTableProvider = ({ children, consumer }: { children: ReactNod
url += `&search=${encodeURIComponent(searchTerm)}`;
}
return url;
- }, [crtPage, filterModel, pageSize, searchTerm]);
+ }, [consumer, crtPage, filterModel, pageSize, searchTerm]);
const fetchData = useCallback(async () => {
- if (!consumer) return;
+ if (!consumer) {
+ return;
+ }
setLoading(true);
try {
const response = await axios.get(fetchUrl);
diff --git a/src/context/table/my-nodes-table-context.tsx b/src/context/table/my-nodes-table-context.tsx
index 1b7f18b6..7f90f20e 100644
--- a/src/context/table/my-nodes-table-context.tsx
+++ b/src/context/table/my-nodes-table-context.tsx
@@ -46,6 +46,9 @@ export const MyNodesTableContextProvider = ({
};
const fetchUrl = useMemo(() => {
+ if (!ownerId) {
+ return '';
+ }
let url = `${getApiRoute('admin')}/${ownerId}/myNodes?page=${crtPage}&size=${pageSize}&sort={"totalScore":"desc"}`;
const operatorMap: Record = {
@@ -78,7 +81,9 @@ export const MyNodesTableContextProvider = ({
}, [ownerId, crtPage, pageSize, filterModel, searchTerm]);
const fetchData = useCallback(async () => {
- if (!ownerId) return;
+ if (!ownerId) {
+ return;
+ }
setLoading(true);
try {
const response = await axios.get(fetchUrl);
diff --git a/src/contexts/P2PContext.tsx b/src/contexts/P2PContext.tsx
index 5a00f1b6..c82370ec 100644
--- a/src/contexts/P2PContext.tsx
+++ b/src/contexts/P2PContext.tsx
@@ -1,5 +1,6 @@
import {
fetchNodeConfig,
+ getComputeJobResult,
getNodeEnvs,
getNodeReadyState,
initializeNode,
@@ -12,13 +13,22 @@ import { Libp2p } from 'libp2p';
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
interface P2PContextType {
+ computeLogs: any;
+ computeResult: Record | Uint8Array | undefined;
config: Record;
- node: Libp2p | null;
- isReady: boolean;
- error: string | null;
envs: ComputeEnvironment[];
+ error: string | null;
fetchConfig: (peerId: string, signature: string, expiryTimestamp: number, address: string) => Promise;
+ getComputeResult: (
+ peerId: string,
+ jobId: string,
+ index: number,
+ authToken: string,
+ address: string
+ ) => Promise | Uint8Array>;
getEnvs: (peerId: string) => Promise;
+ isReady: boolean;
+ node: Libp2p | null;
pushConfig: (
peerId: string,
signature: string,
@@ -37,6 +47,8 @@ export function P2PProvider({ children }: { children: React.ReactNode }) {
const [node, setNode] = useState(null);
const [error, setError] = useState(null);
const [isReady, setIsReady] = useState(false);
+ const [computeLogs, setComputeLogs] = useState(undefined);
+ const [computeResult, setComputeResult] = useState | Uint8Array | undefined>(undefined);
useEffect(() => {
let mounted = true;
@@ -95,6 +107,20 @@ export function P2PProvider({ children }: { children: React.ReactNode }) {
[isReady, node]
);
+ const getComputeResult = useCallback(
+ async (peerId: string, jobId: string, index: number, authToken: string, address: string) => {
+ if (!isReady || !node) {
+ throw new Error('Node not ready');
+ }
+
+ const result = await getComputeJobResult(peerId, jobId, index, authToken, address);
+
+ setComputeResult(result);
+ return result;
+ },
+ [isReady, node]
+ );
+
const fetchConfig = useCallback(
async (peerId: string, signature: string, expiryTimestamp: number, address: string) => {
if (!isReady || !node) {
@@ -128,13 +154,16 @@ export function P2PProvider({ children }: { children: React.ReactNode }) {
return (
{
- const account = useAppKitAccount();
+ const { account } = useOceanAccount();
return (
-
+
);
diff --git a/src/pages/profile/owner.tsx b/src/pages/profile/owner.tsx
index 3c37d89a..732d9aa9 100644
--- a/src/pages/profile/owner.tsx
+++ b/src/pages/profile/owner.tsx
@@ -1,12 +1,12 @@
import OwnerProfilePage from '@/components/profile/owner-profile-page';
import { MyNodesTableContextProvider } from '@/context/table/my-nodes-table-context';
-import { useAppKitAccount } from '@reown/appkit/react';
+import { useOceanAccount } from '@/lib/use-ocean-account';
const OwnerProfilePageWrapper: React.FC = () => {
- const account = useAppKitAccount();
+ const { account } = useOceanAccount();
return (
-
+
);
diff --git a/src/services/nodeService.ts b/src/services/nodeService.ts
index 4951bd5a..0ed90bc2 100644
--- a/src/services/nodeService.ts
+++ b/src/services/nodeService.ts
@@ -1,3 +1,4 @@
+import { Command } from '@/types/commands';
import { noise } from '@chainsafe/libp2p-noise';
import { yamux } from '@chainsafe/libp2p-yamux';
import { bootstrap } from '@libp2p/bootstrap';
@@ -277,7 +278,7 @@ export async function sendCommandToPeer(
peerId: string,
command: Record,
protocol: string = DEFAULT_PROTOCOL
-): Promise> {
+): Promise {
try {
if (!nodeInstance) {
throw new Error('Node not initialized');
@@ -304,30 +305,47 @@ export async function sendCommandToPeer(
});
const message = JSON.stringify(command);
- let response = '';
+ const chunks: Uint8Array[] = [];
await stream.sink([uint8ArrayFromString(message)]);
let firstChunk = true;
for await (const chunk of stream.source) {
- const str = uint8ArrayToString(chunk.subarray());
+ const chunkData = chunk.subarray();
if (firstChunk) {
firstChunk = false;
- try {
- const parsed = JSON.parse(str);
- if (parsed.httpStatus !== undefined) {
- continue;
+ let parsed;
+
+ const str = uint8ArrayToString(chunkData);
+ parsed = JSON.parse(str);
+ if (parsed.httpStatus !== undefined) {
+ if (parsed.httpStatus >= 400) {
+ throw new Error(parsed.error);
}
- } catch (e) {}
+ continue;
+ }
}
- response += str;
+ chunks.push(chunkData);
}
await stream.close();
- return JSON.parse(response);
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
+ const combined = new Uint8Array(totalLength);
+ let offset = 0;
+ for (const chunk of chunks) {
+ combined.set(chunk, offset);
+ offset += chunk.length;
+ }
+
+ try {
+ const str = uint8ArrayToString(combined);
+ return JSON.parse(str);
+ } catch (e) {
+ return combined;
+ }
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Command failed:', errorMessage);
@@ -336,7 +354,52 @@ export async function sendCommandToPeer(
}
export async function getNodeEnvs(peerId: string) {
- return sendCommandToPeer(peerId, { command: 'getComputeEnvironments', node: peerId });
+ return sendCommandToPeer(peerId, { command: Command.COMPUTE_GET_ENVIRONMENTS, node: peerId });
+}
+
+export async function getComputeStreamableLogs(peerId: string, jobId: string, authToken: any) {
+ return sendCommandToPeer(peerId, {
+ command: Command.COMPUTE_GET_STREAMABLE_LOGS,
+ jobId,
+ authorization: authToken.token,
+ });
+}
+
+export async function getComputeJobResult(
+ peerId: string,
+ jobId: string,
+ index: number,
+ authToken: any,
+ address: string
+) {
+ return sendCommandToPeer(peerId, {
+ command: Command.COMPUTE_GET_RESULT,
+ jobId,
+ index,
+ consumerAddress: address,
+ authorization: authToken.token,
+ });
+}
+
+export async function getNonce(peerId: string, consumerAddress: string): Promise {
+ return sendCommandToPeer(peerId, {
+ command: Command.NONCE,
+ address: consumerAddress,
+ });
+}
+
+export async function createAuthToken(
+ peerId: string,
+ consumerAddress: string,
+ signature: string,
+ nonce: string
+): Promise {
+ return sendCommandToPeer(peerId, {
+ command: Command.CREATE_AUTH_TOKEN,
+ address: consumerAddress,
+ signature,
+ nonce,
+ });
}
export async function fetchNodeConfig(peerId: string, signature: string, expiryTimestamp: number, address: string) {
diff --git a/src/types/commands.ts b/src/types/commands.ts
new file mode 100644
index 00000000..a6693c36
--- /dev/null
+++ b/src/types/commands.ts
@@ -0,0 +1,7 @@
+export enum Command {
+ COMPUTE_GET_ENVIRONMENTS = 'getComputeEnvironments',
+ COMPUTE_GET_STREAMABLE_LOGS = 'getComputeStreamableLogs',
+ COMPUTE_GET_RESULT = 'getComputeResult',
+ NONCE = 'nonce',
+ CREATE_AUTH_TOKEN = 'createAuthToken',
+}
diff --git a/src/utils/generateAuthToken.ts b/src/utils/generateAuthToken.ts
new file mode 100644
index 00000000..7b613090
--- /dev/null
+++ b/src/utils/generateAuthToken.ts
@@ -0,0 +1,12 @@
+import { createAuthToken, getNonce } from '@/services/nodeService';
+
+export async function generateAuthTokenWithSmartAccount(peerId: string, address: string, signMessageAsync: any) {
+ const nonce = await getNonce(peerId, address);
+ const incrementedNonce = (nonce + 1).toString();
+ const messageToSign = address + incrementedNonce;
+ const signedMessage = await signMessageAsync({
+ message: messageToSign,
+ });
+ const token = await createAuthToken(peerId, address, signedMessage, incrementedNonce);
+ return token;
+}
diff --git a/yarn.lock b/yarn.lock
index c06e2552..5355961b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13109,6 +13109,13 @@ __metadata:
languageName: node
linkType: hard
+"immediate@npm:~3.0.5":
+ version: 3.0.6
+ resolution: "immediate@npm:3.0.6"
+ checksum: 10c0/f8ba7ede69bee9260241ad078d2d535848745ff5f6995c7c7cb41cfdc9ccc213f66e10fa5afb881f90298b24a3f7344b637b592beb4f54e582770cdce3f1f039
+ languageName: node
+ linkType: hard
+
"immer@npm:^10.1.1":
version: 10.2.0
resolution: "immer@npm:10.2.0"
@@ -14140,6 +14147,18 @@ __metadata:
languageName: node
linkType: hard
+"jszip@npm:^3.10.1":
+ version: 3.10.1
+ resolution: "jszip@npm:3.10.1"
+ dependencies:
+ lie: "npm:~3.3.0"
+ pako: "npm:~1.0.2"
+ readable-stream: "npm:~2.3.6"
+ setimmediate: "npm:^1.0.5"
+ checksum: 10c0/58e01ec9c4960383fb8b38dd5f67b83ccc1ec215bf74c8a5b32f42b6e5fb79fada5176842a11409c4051b5b94275044851814a31076bf49e1be218d3ef57c863
+ languageName: node
+ linkType: hard
+
"jwa@npm:^2.0.1":
version: 2.0.1
resolution: "jwa@npm:2.0.1"
@@ -14263,6 +14282,15 @@ __metadata:
languageName: node
linkType: hard
+"lie@npm:~3.3.0":
+ version: 3.3.0
+ resolution: "lie@npm:3.3.0"
+ dependencies:
+ immediate: "npm:~3.0.5"
+ checksum: 10c0/56dd113091978f82f9dc5081769c6f3b947852ecf9feccaf83e14a123bc630c2301439ce6182521e5fbafbde88e88ac38314327a4e0493a1bea7e0699a7af808
+ languageName: node
+ linkType: hard
+
"lines-and-columns@npm:^1.1.6":
version: 1.2.4
resolution: "lines-and-columns@npm:1.2.4"
@@ -15312,6 +15340,7 @@ __metadata:
gsap: "npm:3.13.0"
it-pipe: "npm:^3.0.1"
json-edit-react: "npm:^1.29.0"
+ jszip: "npm:^3.10.1"
libp2p: "npm:^1.8.0"
myetherwallet-blockies: "npm:^0.1.1"
next: "npm:^15.4.8"
@@ -15599,6 +15628,13 @@ __metadata:
languageName: node
linkType: hard
+"pako@npm:~1.0.2":
+ version: 1.0.11
+ resolution: "pako@npm:1.0.11"
+ checksum: 10c0/86dd99d8b34c3930345b8bbeb5e1cd8a05f608eeb40967b293f72fe469d0e9c88b783a8777e4cc7dc7c91ce54c5e93d88ff4b4f060e6ff18408fd21030d9ffbe
+ languageName: node
+ linkType: hard
+
"parent-module@npm:^1.0.0":
version: 1.0.1
resolution: "parent-module@npm:1.0.1"
@@ -16491,7 +16527,7 @@ __metadata:
languageName: node
linkType: hard
-"readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.8":
+"readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6":
version: 2.3.8
resolution: "readable-stream@npm:2.3.8"
dependencies: