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 ( + + ); +}; 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 ( + + ); +}; 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 ( + +
+
+ +
+
Job ID
+
{job.jobId}
+
+ + {environment && ( +
+
+ 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: