diff --git a/worker/src/task/0-setup.ts b/worker/src/task/0-setup.ts index 29522cb..4a7b003 100644 --- a/worker/src/task/0-setup.ts +++ b/worker/src/task/0-setup.ts @@ -1,4 +1,5 @@ +import { task } from "../utils/task/task"; export async function setup(): Promise { - // define any steps that must be executed before the task starts - console.log("CUSTOM SETUP"); + // Setup a cron job to run every 1 minutes + task(); } diff --git a/worker/src/task/1-task.ts b/worker/src/task/1-task.ts index 81a7163..f97d00a 100644 --- a/worker/src/task/1-task.ts +++ b/worker/src/task/1-task.ts @@ -3,12 +3,13 @@ import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper"; import "dotenv/config"; import { status, middleServerUrl } from "../utils/constant"; import dotenv from "dotenv"; -import { checkAnthropicAPIKey, isValidAnthropicApiKey } from "../utils/anthropicCheck"; -import { checkGitHub } from "../utils/githubCheck"; +// import { checkAnthropicAPIKey, isValidAnthropicApiKey } from "../utils/check/anthropicCheck"; +// import { checkGitHub } from "../utils/check/githubCheck"; import { LogLevel } from "@_koii/namespace-wrapper/dist/types"; import { actionMessage } from "../utils/constant"; import { errorMessage } from "../utils/constant"; import { v4 as uuidv4 } from "uuid"; +import { preRunCheck } from "../utils/check/checks"; dotenv.config(); export async function task(roundNumber: number): Promise { @@ -20,176 +21,15 @@ export async function task(roundNumber: number): Promise { // FORCE TO PAUSE 30 SECONDS // No submission on Round 0 so no need to trigger fetch audit result before round 3 // Changed from 3 to 4 to have more time - if (roundNumber >= 4) { - const triggerFetchAuditResult = await fetch(`${middleServerUrl}/summarizer/worker/update-audit-result`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ taskId: TASK_ID, round: roundNumber - 4 }), - }); - console.log( - `[TASK] Trigger fetch audit result for round ${roundNumber - 3}. Result is ${triggerFetchAuditResult.status}.`, - ); - } - console.log(`[TASK] EXECUTE TASK FOR ROUND ${roundNumber}`); - try { - const orcaClient = await getOrcaClient(); - // check if the env variable is valid - if (!process.env.ANTHROPIC_API_KEY) { - await namespaceWrapper.logMessage( - LogLevel.Error, - errorMessage.ANTHROPIC_API_KEY_INVALID, - actionMessage.ANTHROPIC_API_KEY_INVALID, - ); - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ANTHROPIC_API_KEY_INVALID); - return; - } - if (!isValidAnthropicApiKey(process.env.ANTHROPIC_API_KEY!)) { - await namespaceWrapper.logMessage( - LogLevel.Error, - errorMessage.ANTHROPIC_API_KEY_INVALID, - actionMessage.ANTHROPIC_API_KEY_INVALID, - ); - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ANTHROPIC_API_KEY_INVALID); - return; - } - const isAnthropicAPIKeyValid = await checkAnthropicAPIKey(process.env.ANTHROPIC_API_KEY!); - if (!isAnthropicAPIKeyValid) { - await namespaceWrapper.logMessage( - LogLevel.Error, - errorMessage.ANTHROPIC_API_KEY_NO_CREDIT, - actionMessage.ANTHROPIC_API_KEY_NO_CREDIT, - ); - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ANTHROPIC_API_KEY_NO_CREDIT); - return; - } - if (!process.env.GITHUB_USERNAME || !process.env.GITHUB_TOKEN) { - await namespaceWrapper.logMessage( - LogLevel.Error, - errorMessage.GITHUB_CHECK_FAILED, - actionMessage.GITHUB_CHECK_FAILED, - ); - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.GITHUB_CHECK_FAILED); - return; - } - const isGitHubValid = await checkGitHub(process.env.GITHUB_USERNAME!, process.env.GITHUB_TOKEN!); - if (!isGitHubValid) { - await namespaceWrapper.logMessage( - LogLevel.Error, - errorMessage.GITHUB_CHECK_FAILED, - actionMessage.GITHUB_CHECK_FAILED, - ); - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.GITHUB_CHECK_FAILED); - return; - } - if (!orcaClient) { - await namespaceWrapper.logMessage(LogLevel.Error, errorMessage.NO_ORCA_CLIENT, actionMessage.NO_ORCA_CLIENT); - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.NO_ORCA_CLIENT); - return; - } - - const stakingKeypair = await namespaceWrapper.getSubmitterAccount(); - if (!stakingKeypair) { - throw new Error("No staking keypair found"); - } - const stakingKey = stakingKeypair.publicKey.toBase58(); - const pubKey = await namespaceWrapper.getMainAccountPubkey(); - if (!pubKey) { - throw new Error("No public key found"); - } - - /****************** All these issues need to be generate a markdown file ******************/ - - const signature = await namespaceWrapper.payloadSigning( - { - taskId: TASK_ID, - roundNumber: roundNumber, - action: "fetch-todo", - githubUsername: stakingKey, - stakingKey: stakingKey, - }, - stakingKeypair.secretKey, - ); - - // const initializedDocumentSummarizeIssues = await getInitializedDocumentSummarizeIssues(existingIssues); - - console.log(`[TASK] Making Request to Middle Server with taskId: ${TASK_ID} and round: ${roundNumber}`); - - let requiredWorkResponse: Response = new Response(); - let retryCount = 0; - const maxRetries = 36; // 6 minutes with 10 second intervals - const retryDelay = 10000; // 10 seconds in milliseconds - - while (retryCount < maxRetries) { - requiredWorkResponse = await fetch(`${middleServerUrl}/summarizer/worker/fetch-todo`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ signature: signature, stakingKey: stakingKey }), - }); - - if (requiredWorkResponse.status === 200) { - break; - } - - console.log(`[TASK] Server returned status ${requiredWorkResponse.status}, retrying in ${retryDelay/1000} seconds... (Attempt ${retryCount + 1}/${maxRetries})`); - await new Promise(resolve => setTimeout(resolve, retryDelay)); - retryCount++; - } - - // check if the response is 200 after all retries - if (requiredWorkResponse.status !== 200) { - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.NO_ISSUES_PENDING_TO_BE_SUMMARIZED); - return; - } - const requiredWorkResponseData = await requiredWorkResponse.json(); - console.log("[TASK] requiredWorkResponseData: ", requiredWorkResponseData); - const uuid = uuidv4(); - const alreadyAssigned = await namespaceWrapper.storeGet(JSON.stringify(requiredWorkResponseData.data.id)); - if (alreadyAssigned) { - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.NOT_FINISHED_TASK); - return; - }else{ - await namespaceWrapper.storeSet(JSON.stringify(requiredWorkResponseData.data.id), "true"); - } - - await namespaceWrapper.storeSet(`uuid-${roundNumber}`, uuid); - - const podcallPayload = { - taskId: TASK_ID, - roundNumber, - uuid, - }; - - const podCallSignature = await namespaceWrapper.payloadSigning(podcallPayload, stakingKeypair.secretKey); - - const jsonBody = { - task_id: TASK_ID, - round_number: roundNumber, - repo_url: `https://github.com/${requiredWorkResponseData.data.repo_owner}/${requiredWorkResponseData.data.repo_name}`, - podcall_signature: podCallSignature, - }; - console.log("[TASK] jsonBody: ", jsonBody); - try { - const repoSummaryResponse = await orcaClient.podCall(`worker-task/${roundNumber}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(jsonBody), - }); - console.log("[TASK] repoSummaryResponse: ", repoSummaryResponse); - if (repoSummaryResponse.status !== 200) { - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_SUMMARIZATION_FAILED); - } - } catch (error) { - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_SUMMARIZATION_FAILED); - console.error("[TASK] EXECUTE TASK ERROR:", error); - } - } catch (error) { - await namespaceWrapper.storeSet(`result-${roundNumber}`, status.UNKNOWN_ERROR); - console.error("[TASK] EXECUTE TASK ERROR:", error); - } + + // if (roundNumber >= 4) { + // const auditRound = roundNumber - 4; + // const response = await fetch(`${middleServerUrl}/summarizer/worker/update-audit-result`, { + // method: "POST", + // headers: { "Content-Type": "application/json" }, + // body: JSON.stringify({ taskId: TASK_ID, round: auditRound }), + // }); + // console.log(`[TASK] Fetched audit result for round ${auditRound}. Status: ${response.status}`); + // } + // console.log(`[TASK] EXECUTE TASK FOR ROUND ${roundNumber}`); } \ No newline at end of file diff --git a/worker/src/task/2-submission.ts b/worker/src/task/2-submission.ts index 52ca5c7..d03a868 100644 --- a/worker/src/task/2-submission.ts +++ b/worker/src/task/2-submission.ts @@ -3,7 +3,21 @@ import { getOrcaClient } from "@_koii/task-manager/extensions"; import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper"; import { middleServerUrl, status } from "../utils/constant"; import { preRunCheck } from "../utils/check/checks"; -export async function submission(roundNumber: number) : Promise { + +interface SubmissionData { + prUrl: string; + [key: string]: any; +} + +interface SubmissionParams { + orcaClient: any; + roundNumber: number; + stakingKey: string; + publicKey: string; + secretKey: Uint8Array; +} + +export async function submission(roundNumber: number): Promise { /** * Retrieve the task proofs from your container and submit for auditing * Must return a string of max 512 bytes to be submitted on chain @@ -25,20 +39,21 @@ export async function submission(roundNumber: number) : Promise { console.log(`[SUBMISSION] Starting submission process for round ${roundNumber}`); try { - console.log("[SUBMISSION] Initializing Orca client..."); - const orcaClient = await getOrcaClient(); - if (!orcaClient) { - console.error("[SUBMISSION] Failed to initialize Orca client"); - return; - } - console.log("[SUBMISSION] Orca client initialized successfully"); - console.log(`[SUBMISSION] Fetching task result for round ${roundNumber}...`); + const orcaClient = await initializeOrcaClient(); const shouldMakeSubmission = await namespaceWrapper.storeGet(`shouldMakeSubmission`); + if (!shouldMakeSubmission || shouldMakeSubmission !== "true") { return; } - - const cid = await makeSubmission({orcaClient, roundNumber, stakingKey, publicKey: pubKey, secretKey}); + + const cid = await makeSubmission({ + orcaClient, + roundNumber, + stakingKey, + publicKey: pubKey, + secretKey + }); + return cid || void 0; } catch (error) { console.error("[SUBMISSION] Error during submission process:", error); @@ -46,77 +61,127 @@ export async function submission(roundNumber: number) : Promise { } } -async function makeSubmission({orcaClient, roundNumber, stakingKey, publicKey, secretKey}: {orcaClient: any, roundNumber: number, stakingKey: string, publicKey: string, secretKey: Uint8Array}) { +async function initializeOrcaClient() { + console.log("[SUBMISSION] Initializing Orca client..."); + const orcaClient = await getOrcaClient(); + + if (!orcaClient) { + console.error("[SUBMISSION] Failed to initialize Orca client"); + throw new Error("Failed to initialize Orca client"); + } + + console.log("[SUBMISSION] Orca client initialized successfully"); + return orcaClient; +} + +async function makeSubmission(params: SubmissionParams): Promise { + const { orcaClient, roundNumber, stakingKey, publicKey, secretKey } = params; + const swarmBountyId = await namespaceWrapper.storeGet(`swarmBountyId`); if (!swarmBountyId) { console.log("[SUBMISSION] No swarm bounty id found for this round"); return; } - console.log(`[SUBMISSION] Fetching submission data for round ${roundNumber}. and submission roundnumber ${swarmBountyId}`); - const result = await orcaClient.podCall(`submission/${swarmBountyId}`); - let submission; - console.log("[SUBMISSION] Submission result:", result); - console.log("[SUBMISSION] Submission result data:", result.data); + const submissionData = await fetchSubmissionData(orcaClient, swarmBountyId); + if (!submissionData) { + return; + } + + await notifyMiddleServer({ + taskId: TASK_ID!, + swarmBountyId, + prUrl: submissionData.prUrl, + stakingKey, + publicKey, + secretKey + }); + + const signature = await signSubmissionPayload({ + taskId: TASK_ID, + roundNumber, + stakingKey, + pubKey: publicKey, + ...submissionData + }, secretKey); + + const cid = await storeSubmissionOnIPFS(signature); + await cleanupSubmissionState(); + + return cid; +} + +async function fetchSubmissionData(orcaClient: any, swarmBountyId: string): Promise { + console.log(`[SUBMISSION] Fetching submission data for swarm bounty ${swarmBountyId}`); + const result = await orcaClient.podCall(`submission/${swarmBountyId}`); + if (!result || result.data === "No submission") { console.log("[SUBMISSION] No existing submission found"); - return; - } else { - // Add extra error handling for https://koii-workspace.slack.com/archives/C0886H01JM8/p1746137232538419 - if (typeof result.data === 'object' && 'data' in result.data) { - console.log("[SUBMISSION] Submission result data is an object with 'data' property"); - submission = result.data.data; - } else { - console.log("[SUBMISSION] Submission result data is not an object with 'data' property"); - submission = result.data; - } + return null; } - if (!submission.prUrl) { - console.error("[SUBMISSION] Missing PR URL in submission"); + const submission = typeof result.data === 'object' && 'data' in result.data + ? result.data.data + : result.data; + + if (!submission?.prUrl) { throw new Error("Submission is missing PR URL"); } - const middleServerPayload = { - taskId: TASK_ID, - swarmBountyId, - prUrl: submission.prUrl, - stakingKey, - publicKey, - action: "add-round-number", - }; - - const middleServerSignature = await namespaceWrapper.payloadSigning(middleServerPayload, secretKey); - const middleServerResponse = await fetch(`${middleServerUrl}/summarizer/worker/add-round-number`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ signature: middleServerSignature, stakingKey: stakingKey }), - }); - console.log("[TASK] Add PR Response: ", middleServerResponse); + return submission as SubmissionData; +} - if (middleServerResponse.status !== 200) { - throw new Error(`Posting to middle server failed: ${middleServerResponse.statusText}`); - } - const signature = await namespaceWrapper.payloadSigning( - { - taskId: TASK_ID, - roundNumber, - stakingKey, - pubKey:publicKey, - // action: "audit", - ...submission, - }, - secretKey, - ); - console.log("[SUBMISSION] Payload signed successfully"); +async function notifyMiddleServer(params: { + taskId: string; + swarmBountyId: string; + prUrl: string; + stakingKey: string; + publicKey: string; + secretKey: Uint8Array; +}) { + const { taskId, swarmBountyId, prUrl, stakingKey, publicKey, secretKey } = params; - console.log("[SUBMISSION] Storing submission on IPFS..."); - const cid = await storeFile({ signature }, "submission.json"); - console.log("[SUBMISSION] Submission stored successfully. CID:", cid); - // If done please set the shouldMakeSubmission to false - await namespaceWrapper.storeSet(`shouldMakeSubmission`, "false"); - await namespaceWrapper.storeSet(`swarmBountyId`, ""); - return cid; + const payload = { + taskId, + swarmBountyId, + prUrl, + stakingKey, + publicKey, + action: "add-round-number", + }; + + const signature = await namespaceWrapper.payloadSigning(payload, secretKey); + const response = await fetch(`${middleServerUrl}/summarizer/worker/add-round-number`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ signature, stakingKey }), + }); + + console.log("[TASK] Add PR Response: ", response); + + if (response.status !== 200) { + throw new Error(`Posting to middle server failed: ${response.statusText}`); + } +} + +async function signSubmissionPayload(payload: any, secretKey: Uint8Array): Promise { + console.log("[SUBMISSION] Signing submission payload..."); + const signature = await namespaceWrapper.payloadSigning(payload, secretKey); + console.log("[SUBMISSION] Payload signed successfully"); + return signature!; +} + +async function storeSubmissionOnIPFS(signature: string): Promise { + console.log("[SUBMISSION] Storing submission on IPFS..."); + const cid = await storeFile({ signature }, "submission.json"); + if (!cid) { + throw new Error("Failed to store submission on IPFS"); + } + console.log("[SUBMISSION] Submission stored successfully. CID:", cid); + return cid; +} + +async function cleanupSubmissionState(): Promise { + await namespaceWrapper.storeSet(`shouldMakeSubmission`, "false"); + await namespaceWrapper.storeSet(`swarmBountyId`, ""); } \ No newline at end of file diff --git a/worker/src/utils/check/anthropicCheck.ts b/worker/src/utils/check/anthropicCheck.ts new file mode 100644 index 0000000..0cc627a --- /dev/null +++ b/worker/src/utils/check/anthropicCheck.ts @@ -0,0 +1,36 @@ +export function isValidAnthropicApiKey(key: string) { + const regex = /^sk-ant-[a-zA-Z0-9_-]{32,}$/; + return regex.test(key); +} + +export async function checkAnthropicAPIKey(apiKey: string) { + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: 'claude-3-opus-20240229', // or a cheaper model + max_tokens: 1, // minimal usage + messages: [{ role: 'user', content: 'Hi' }], + }), + }); + + if (response.status === 200) { + console.log('✅ API key is valid and has credit.'); + return true; + } else { + const data = await response.json().catch(() => ({})); + if (response.status === 401) { + console.log('❌ Invalid API key.'); + } else if (response.status === 403 && data.error?.message?.includes('billing')) { + console.log('❌ API key has no credit or is not authorized.'); + } else { + console.log('⚠️ Unexpected error:', data); + } + return false; + } +} + diff --git a/worker/src/utils/orcaHandler/orcaHandler.ts b/worker/src/utils/orcaHandler/orcaHandler.ts deleted file mode 100644 index b5afe57..0000000 --- a/worker/src/utils/orcaHandler/orcaHandler.ts +++ /dev/null @@ -1,46 +0,0 @@ - -import dotenv from "dotenv"; -dotenv.config(); - - -import { getOrcaClient } from "@_koii/task-manager/extensions"; -export async function handleOrcaClientCreation(){ - try { - // if (process.env.NODE_ENV !== "development") { - // const { getOrcaClient } = await import("@_koii/task-manager/extensions"); - const orcaClient = await getOrcaClient(); - if (!orcaClient) { - throw new Error("Orca client not found"); - } - return orcaClient; - // }else{ - // return null; - // } - }catch{ - throw new Error("Orca client not found"); - } -} -export async function handleRequest({orcaClient, route, bodyJSON}:{orcaClient:any, route:string, bodyJSON:any}){ - // if (process.env.NODE_ENV === "development") { - // const response = await fetch(`${process.env.LOCAL_CONTAINER_TEST_URL}/${route}`, { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify(bodyJSON), - // }); - // return response; - // }else{ - if (!orcaClient) { - throw new Error("Orca client not found"); - } - const response = await orcaClient.podCall(`${route}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(bodyJSON), - }); - return response; - // } -} diff --git a/worker/src/utils/task/task.ts b/worker/src/utils/task/task.ts new file mode 100644 index 0000000..b19d153 --- /dev/null +++ b/worker/src/utils/task/task.ts @@ -0,0 +1,109 @@ +// import { namespaceWrapper } from "@_koii/namespace-wrapper"; + +import { getOrcaClient } from "@_koii/task-manager/extensions"; +import { actionMessage, errorMessage, middleServerUrl } from "../constant"; + +import { TASK_ID, namespaceWrapper } from "@_koii/namespace-wrapper"; +import { LogLevel } from "@_koii/namespace-wrapper/dist/types"; +export async function task(){ + while (true) { + try { + let requiredWorkResponse; + const orcaClient = await getOrcaClient(); + // check if the env variable is valid + const stakingKeypair = await namespaceWrapper.getSubmitterAccount()!; + const pubKey = await namespaceWrapper.getMainAccountPubkey(); + if (!orcaClient || !stakingKeypair || !pubKey) { + await namespaceWrapper.logMessage(LogLevel.Error, errorMessage.NO_ORCA_CLIENT, actionMessage.NO_ORCA_CLIENT); + // Wait for 1 minute before retrying + await new Promise(resolve => setTimeout(resolve, 60000)); + continue; + } + const stakingKey = stakingKeypair.publicKey.toBase58(); + + /****************** All these issues need to be generate a markdown file ******************/ + + const signature = await namespaceWrapper.payloadSigning( + { + taskId: TASK_ID, + // roundNumber: roundNumber, + action: "fetch-todo", + githubUsername: stakingKey, + stakingKey: stakingKey, + }, + stakingKeypair.secretKey, + ); + + const retryDelay = 10000; // 10 seconds in milliseconds + + while (true) { + requiredWorkResponse = await fetch(`${middleServerUrl}/summarizer/worker/fetch-todo`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ signature: signature, stakingKey: stakingKey }), + }); + + if (requiredWorkResponse.status === 200) { + break; + } + + console.log(`[TASK] Server returned status ${requiredWorkResponse.status}, retrying in ${retryDelay/1000} seconds...`); + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + + // check if the response is 200 after all retries + if (!requiredWorkResponse || requiredWorkResponse.status !== 200) { + // await namespaceWrapper.storeSet(`result-${roundNumber}`, status.NO_ISSUES_PENDING_TO_BE_SUMMARIZED); + return; + } + const requiredWorkResponseData = await requiredWorkResponse.json(); + console.log("[TASK] requiredWorkResponseData: ", requiredWorkResponseData); + // const uuid = uuidv4(); + const alreadyAssigned = await namespaceWrapper.storeGet(JSON.stringify(requiredWorkResponseData.data.id)); + if (alreadyAssigned) { + return; + }else{ + await namespaceWrapper.storeSet(JSON.stringify(requiredWorkResponseData.data.id), "initialized"); + } + + const podcallPayload = { + taskId: TASK_ID, + }; + + const podCallSignature = await namespaceWrapper.payloadSigning(podcallPayload, stakingKeypair.secretKey); + + const jsonBody = { + task_id: TASK_ID, + swarmBountyId: requiredWorkResponseData.data.id, + repo_url: `https://github.com/${requiredWorkResponseData.data.repo_owner}/${requiredWorkResponseData.data.repo_name}`, + podcall_signature: podCallSignature, + }; + console.log("[TASK] jsonBody: ", jsonBody); + try { + const repoSummaryResponse = await orcaClient.podCall(`worker-task`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(jsonBody), + }); + console.log("[TASK] repoSummaryResponse: ", repoSummaryResponse); + if (repoSummaryResponse.status !== 200) { + // await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_SUMMARIZATION_FAILED); + } + } catch (error) { + // await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_SUMMARIZATION_FAILED); + console.error("[TASK] EXECUTE TASK ERROR:", error); + } + } catch (error) { + console.error("[TASK] EXECUTE TASK ERROR:", error); + // Wait for 1 minute before retrying on error + await new Promise(resolve => setTimeout(resolve, 60000)); + } + + // Wait for 1 minute before starting the next iteration + await new Promise(resolve => setTimeout(resolve, 60000)); + } +} \ No newline at end of file