transfer from monorepo

This commit is contained in:
Laura Abro
2025-04-24 10:24:42 -03:00
parent a2175785d5
commit 1c6fc5540b
95 changed files with 7110 additions and 0 deletions

20
worker/src/index.ts Normal file
View File

@ -0,0 +1,20 @@
import { initializeTaskManager } from "@_koii/task-manager";
import { setup } from "./task/0-setup";
import { task } from "./task/1-task";
import { submission } from "./task/2-submission";
import { audit } from "./task/3-audit";
import { distribution } from "./task/4-distribution";
import { routes } from "./task/5-routes";
import { initializeOrcaClient } from "@_koii/task-manager/extensions";
import { getConfig } from "./orcaSettings";
initializeTaskManager({
setup,
task,
submission,
audit,
distribution,
routes,
});
initializeOrcaClient(getConfig);

View File

@ -0,0 +1,46 @@
import { TASK_ID, namespaceWrapper } from "@_koii/namespace-wrapper";
import "dotenv/config";
const imageUrl = "docker.io/hermanyiqunliang/summarizer-agent:0.2";
async function createPodSpec(): Promise<string> {
const basePath = await namespaceWrapper.getBasePath();
const podSpec = `apiVersion: v1
kind: Pod
metadata:
name: 247-builder-test
spec:
containers:
- name: user-${TASK_ID}
image: ${imageUrl}
env:
- name: GITHUB_TOKEN
value: "${process.env.GITHUB_TOKEN}"
- name: GITHUB_USERNAME
value: "${process.env.GITHUB_USERNAME}"
- name: ANTHROPIC_API_KEY
value: "${process.env.ANTHROPIC_API_KEY}"
volumeMounts:
- name: builder-data
mountPath: /data
volumes:
- name: builder-data
hostPath:
path: ${basePath}/orca/data
type: DirectoryOrCreate
`;
return podSpec;
}
export async function getConfig(): Promise<{
imageURL: string;
customPodSpec: string;
rootCA: string | null;
}> {
return {
imageURL: imageUrl,
customPodSpec: await createPodSpec(),
rootCA: null,
};
}

View File

@ -0,0 +1,4 @@
export async function setup(): Promise<void> {
// define any steps that must be executed before the task starts
console.log("CUSTOM SETUP");
}

215
worker/src/task/1-task.ts Normal file
View File

@ -0,0 +1,215 @@
import { getOrcaClient } from "@_koii/task-manager/extensions";
import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper";
import "dotenv/config";
import { getRandomNodes } from "../utils/leader";
import { getExistingIssues } from "../utils/existingIssues";
import { status, middleServerUrl } from "../utils/constant";
import dotenv from "dotenv";
import { checkAnthropicAPIKey, isValidAnthropicApiKey } from "../utils/anthropicCheck";
import { checkGitHub } from "../utils/githubCheck";
import { LogLevel } from "@_koii/namespace-wrapper/dist/types";
import { actionMessage } from "../utils/constant";
import { errorMessage } from "../utils/constant";
dotenv.config();
export async function task(roundNumber: number): Promise<void> {
/**
* Run your task and store the proofs to be submitted for auditing
* It is expected you will store the proofs in your container
* The submission of the proofs is done in the submission function
*/
// 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}/api/builder/summarizer/trigger-fetch-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 issues need to be starred ******************/
const existingIssues = await getExistingIssues();
const githubUrls = existingIssues.map((issue) => issue.githubUrl);
try {
await orcaClient.podCall(`star/${roundNumber}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ taskId: TASK_ID, round_number: String(roundNumber), github_urls: githubUrls }),
});
} catch (error) {
await namespaceWrapper.storeSet(`result-${roundNumber}`, status.STAR_ISSUE_FAILED);
console.error("Error starring issues:", error);
}
/****************** All these issues need to be generate a markdown file ******************/
const signature = await namespaceWrapper.payloadSigning(
{
taskId: TASK_ID,
roundNumber: roundNumber,
action: "fetch",
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}`);
const requiredWorkResponse = await fetch(`${middleServerUrl}/api/builder/summarizer/fetch-summarizer-todo`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ signature: signature, stakingKey: stakingKey }),
});
// check if the response is 200
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 jsonBody = {
taskId: TASK_ID,
round_number: String(roundNumber),
repo_url: `https://github.com/${requiredWorkResponseData.data.repo_owner}/${requiredWorkResponseData.data.repo_name}`,
};
console.log("[TASK] jsonBody: ", jsonBody);
try {
const repoSummaryResponse = await orcaClient.podCall(`repo_summary/${roundNumber}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonBody),
});
console.log("[TASK] repoSummaryResponse: ", repoSummaryResponse);
console.log("[TASK] repoSummaryResponse.data.result.data ", repoSummaryResponse.data.result.data);
const payload = {
taskId: TASK_ID,
action: "add",
roundNumber: roundNumber,
prUrl: repoSummaryResponse.data.result.data.pr_url,
stakingKey: stakingKey
}
console.log("[TASK] Signing payload: ", payload);
if (repoSummaryResponse.status === 200) {
try{
const signature = await namespaceWrapper.payloadSigning(
payload,
stakingKeypair.secretKey,
);
console.log("[TASK] signature: ", signature);
const addPrToSummarizerTodoResponse = await fetch(`${middleServerUrl}/api/builder/summarizer/add-pr-to-summarizer-todo`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ signature: signature, stakingKey: stakingKey }),
});
console.log("[TASK] addPrToSummarizerTodoResponse: ", addPrToSummarizerTodoResponse);
}catch(error){
await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO);
console.error("[TASK] Error adding PR to summarizer todo:", error);
}
await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_SUCCESSFULLY_SUMMARIZED);
} else {
// post this summary response to slack` to notify the team
// THE HOOK IS ALREADY DISABLED
// try{
// const slackResponse = await fetch('https://hooks.slack.com/services/', {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// text: `[TASK] Error summarizing issue:\nStatus: ${repoSummaryResponse.status}\nData: ${JSON.stringify(repoSummaryResponse.data, null, 2)}`
// }),
// });
// console.log("[TASK] slackResponse: ", slackResponse);
// }catch(error){
// console.error("[TASK] Error posting to slack:", error);
// }
await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_FAILED_TO_BE_SUMMARIZED);
}
} catch (error) {
await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_FAILED_TO_BE_SUMMARIZED);
// try{
// const slackResponse = await fetch('https://hooks.slack.com/services', {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// text: `[TASK] Error summarizing issue:\n ${JSON.stringify(error)}`
// }),
// });
// console.log("[TASK] slackResponse: ", slackResponse);
// }catch(error){
// console.error("[TASK] Error posting to slack:", error);
// }
console.error("[TASK] EXECUTE TASK ERROR:", error);
}
} catch (error) {
await namespaceWrapper.storeSet(`result-${roundNumber}`, status.UNKNOWN_ERROR);
console.error("[TASK] EXECUTE TASK ERROR:", error);
}
}

View File

@ -0,0 +1,102 @@
import { storeFile } from "../utils/ipfs";
import { getOrcaClient } from "@_koii/task-manager/extensions";
import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper";
import { status } from "../utils/constant";
export async function submission(roundNumber: number) : Promise<string | void> {
/**
* Retrieve the task proofs from your container and submit for auditing
* Must return a string of max 512 bytes to be submitted on chain
* The default implementation handles uploading the proofs to IPFS
* and returning the CID
*/
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 taskResult = await namespaceWrapper.storeGet(`result-${roundNumber}`);
if (!taskResult) {
console.log("[SUBMISSION] No task result found for this round");
return status.NO_DATA_FOR_THIS_ROUND;
}
console.log(`[SUBMISSION] Task result status: ${taskResult}`);
if (taskResult !== status.ISSUE_SUCCESSFULLY_SUMMARIZED) {
console.log(`[SUBMISSION] Task not successfully summarized. Status: ${taskResult}`);
return taskResult;
}
console.log(`[SUBMISSION] Fetching submission data for round ${roundNumber}...`);
const result = await orcaClient.podCall(`submission/${roundNumber}`);
let submission;
console.log("[SUBMISSION] Submission result:", result.data);
if (result.data === "No submission") {
console.log("[SUBMISSION] No existing submission found, creating new submission object");
submission = {
githubUsername: process.env.GITHUB_USERNAME,
prUrl: "none",
roundNumber,
};
} else {
submission = result.data;
}
console.log("[SUBMISSION] Validating submission data...");
if (submission.roundNumber !== roundNumber) {
console.error(`[SUBMISSION] Round number mismatch. Expected: ${roundNumber}, Got: ${submission.roundNumber}`);
throw new Error("Submission is not for the current round");
}
if (!submission.prUrl) {
console.error("[SUBMISSION] Missing PR URL in submission");
throw new Error("Submission is missing PR URL");
}
console.log("[SUBMISSION] Submission data validated successfully:", submission);
console.log("[SUBMISSION] Getting submitter account...");
const stakingKeypair = await namespaceWrapper.getSubmitterAccount();
if (!stakingKeypair) {
console.error("[SUBMISSION] No staking keypair found");
throw new Error("No staking keypair found");
}
console.log("[SUBMISSION] Submitter account retrieved successfully");
const stakingKey = stakingKeypair.publicKey.toBase58();
const pubKey = await namespaceWrapper.getMainAccountPubkey();
console.log("[SUBMISSION] Staking key:", stakingKey);
console.log("[SUBMISSION] Public key:", pubKey);
console.log("[SUBMISSION] Signing submission payload...");
const signature = await namespaceWrapper.payloadSigning(
{
taskId: TASK_ID,
roundNumber,
stakingKey,
pubKey,
action: "audit",
...submission,
},
stakingKeypair.secretKey,
);
console.log("[SUBMISSION] Payload signed successfully");
console.log("[SUBMISSION] Storing submission on IPFS...");
const cid = await storeFile({ signature }, "submission.json");
console.log("[SUBMISSION] Submission stored successfully. CID:", cid);
return cid || void 0;
} catch (error) {
console.error("[SUBMISSION] Error during submission process:", error);
throw error;
}
}

View File

@ -0,0 +1,84 @@
import { getOrcaClient } from "@_koii/task-manager/extensions";
import { middleServerUrl, status } from "../utils/constant";
import { submissionJSONSignatureDecode } from "../utils/submissionJSONSignatureDecode";
// import { status } from '../utils/constant'
export async function audit(cid: string, roundNumber: number, submitterKey: string): Promise<boolean | void> {
/**
* Audit a submission
* This function should return true if the submission is correct, false otherwise
* The default implementation retrieves the proofs from IPFS
* and sends them to your container for auditing
*/
try {
const orcaClient = await getOrcaClient();
if (!orcaClient) {
// await namespaceWrapper.storeSet(`result-${roundNumber}`, status.NO_ORCA_CLIENT);
return;
}
// Check if the cid is one of the status
if (Object.values(status).includes(cid)) {
// This returns a dummy true
return true;
}
const decodeResult = await submissionJSONSignatureDecode({submission_value: cid, submitterPublicKey: submitterKey, roundNumber: roundNumber});
if (!decodeResult) {
console.log("[AUDIT] DECODE RESULT FAILED.")
return false;
}
console.log(`[AUDIT] ✅ Signature decoded successfully`);
console.log(`[AUDIT] Checking summarizer status for submitter ${submitterKey}`);
const checkSummarizerResponse = await fetch(`${middleServerUrl}/api/builder/summarizer/check-summarizer`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
stakingKey: submitterKey,
roundNumber,
githubUsername: decodeResult.githubUsername,
prUrl: decodeResult.prUrl
}),
});
const checkSummarizerJSON = await checkSummarizerResponse.json();
console.log(`[AUDIT] Summarizer check response:`, checkSummarizerJSON);
if (!checkSummarizerJSON.success) {
console.log(`[AUDIT] ❌ Audit failed for ${submitterKey}`);
return false;
}
console.log(`[AUDIT] ✅ Summarizer check passed`);
console.log(`[AUDIT] Sending audit request for submitter: ${submitterKey}`);
console.log(`[AUDIT] Submission data being sent to audit:`, decodeResult);
const result = await orcaClient.podCall(`audit/${roundNumber}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
submission: decodeResult,
}),
});
console.log(`[AUDIT] Raw audit result:`, result);
console.log(`[AUDIT] Audit result data type:`, typeof result.data);
console.log(`[AUDIT] Audit result data value:`, result.data);
if (result.data === true) {
console.log(`[AUDIT] ✅ Audit passed for ${submitterKey}`);
return true;
} else {
console.log(`[AUDIT] ❌ Audit failed for ${submitterKey}`);
console.log(`[AUDIT] Failed audit result data:`, result.data);
return false;
}
} catch (error) {
console.error("[AUDIT] Error auditing submission:", error);
// When Error---NO RETURN;
// return true;
}
}

View File

@ -0,0 +1,64 @@
import { Submitter, DistributionList } from "@_koii/task-manager";
import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper";
import { customReward, status } from "../utils/constant";
import { Submission } from "@_koii/namespace-wrapper/dist/types";
import { middleServerUrl } from "../utils/constant";
import { getOrcaClient } from "@_koii/task-manager/extensions";
import { submissionJSONSignatureDecode } from "../utils/submissionJSONSignatureDecode";
import { getRandomNodes } from "../utils/leader";
const getSubmissionList = async (roundNumber: number): Promise<Record<string, Submission>> => {
const submissionInfo = await namespaceWrapper.getTaskSubmissionInfo(roundNumber);
return submissionInfo?.submissions[roundNumber] || {};
}
export const getEmptyDistributionList = async (
submitters: Submitter[],
): Promise<DistributionList> => {
const distributionList: DistributionList = {};
for (const submitter of submitters) {
distributionList[submitter.publicKey] = 0;
}
return distributionList;
}
export const distribution = async (
submitters: Submitter[],
bounty: number,
roundNumber: number,
): Promise<DistributionList> => {
try {
const distributionList: DistributionList = {};
for (const submitter of submitters) {
console.log(`\n[DISTRIBUTION] Processing submitter: ${submitter.publicKey}`);
console.log(`[DISTRIBUTION] Getting submission list for round ${roundNumber}`);
const submitterSubmissions = await getSubmissionList(roundNumber);
console.log(`[DISTRIBUTION] Total submissions found: ${Object.keys(submitterSubmissions).length}`);
const submitterSubmission = submitterSubmissions[submitter.publicKey];
if (!submitterSubmission || submitterSubmission.submission_value === "") {
console.log(`[DISTRIBUTION] ❌ No valid submission found for submitter ${submitter.publicKey}`);
distributionList[submitter.publicKey] = 0;
continue;
}
if (Object.values(status).includes(submitterSubmission.submission_value)) {
distributionList[submitter.publicKey] = 0;
continue;
}else{
// TODO: Check if I should include = 0 here
if (submitter.votes >= 0) {
distributionList[submitter.publicKey] = customReward;
}else{
distributionList[submitter.publicKey] = 0;
}
}
}
console.log(`[DISTRIBUTION] ✅ Distribution completed successfully`);
console.log(`[DISTRIBUTION] Final distribution list:`, distributionList);
return distributionList;
} catch (error: any) {
console.error(`[DISTRIBUTION] ❌ ERROR IN DISTRIBUTION:`, error);
console.error(`[DISTRIBUTION] Error stack:`, error.stack);
return {};
}
};

View File

@ -0,0 +1,57 @@
import { namespaceWrapper, app } from "@_koii/task-manager/namespace-wrapper";
import { getLeaderNode, getRandomNodes } from "../utils/leader";
import { task } from "./1-task";
import { submission } from "./2-submission";
import { audit } from "./3-audit";
import { distribution } from "./4-distribution";
import { submissionJSONSignatureDecode } from "../utils/submissionJSONSignatureDecode";
import { Submission } from "@_koii/namespace-wrapper/dist/types";
import { taskRunner } from "@_koii/task-manager"
/**
*
* Define all your custom routes here
*
*/
//Example route
export async function routes() {
app.get("/value", async (_req, res) => {
const value = await namespaceWrapper.storeGet("value");
console.log("value", value);
res.status(200).json({ value: value });
});
app.get("/leader/:roundNumber/:submitterPublicKey", async (req, res) => {
const roundNumber = req.params.roundNumber;
const submitterPublicKey = req.params.submitterPublicKey;
const {isLeader, leaderNode} = await getLeaderNode({roundNumber: Number(roundNumber), submitterPublicKey: submitterPublicKey});
res.status(200).json({ isLeader: isLeader, leaderNode: leaderNode });
});
app.get("/task/:roundNumber", async (req, res) => {
console.log("task endpoint called with round number: ", req.params.roundNumber);
const roundNumber = req.params.roundNumber;
const taskResult = await task(Number(roundNumber));
res.status(200).json({ result: taskResult });
});
app.get("/audit/:roundNumber/:cid/:submitterPublicKey", async (req, res) => {
const cid = req.params.cid;
const roundNumber = req.params.roundNumber;
const submitterPublicKey = req.params.submitterPublicKey;
const auditResult = await audit(cid, Number(roundNumber), submitterPublicKey);
res.status(200).json({ result: auditResult });
});
app.get("/submission/:roundNumber", async (req, res) => {
const roundNumber = req.params.roundNumber;
const submissionResult = await submission(Number(roundNumber));
res.status(200).json({ result: submissionResult });
});
app.get("/submitDistribution/:roundNumber", async (req, res) => {
const roundNumber = req.params.roundNumber;
const submitDistributionResult = await taskRunner.submitDistributionList(Number(roundNumber));
res.status(200).json({ result: submitDistributionResult });
});
}

View File

@ -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;
}
}

View File

@ -0,0 +1,57 @@
import dotenv from "dotenv";
dotenv.config();
export const status = {
ISSUE_FAILED_TO_BE_SUMMARIZED: "Issue failed to be summarized",
ISSUE_SUCCESSFULLY_SUMMARIZED: "Issue successfully summarized",
NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "No issues pending to be summarized",
ROUND_LESS_THAN_OR_EQUAL_TO_1: "Round <= 1",
NO_ORCA_CLIENT: "No orca client",
NO_CHOSEN_AS_ISSUE_SUMMARIZER: "No chosen as issue summarizer",
UNKNOWN_ERROR: "Unknown error",
STAR_ISSUE_FAILED: "Star issue failed",
GITHUB_CHECK_FAILED: "GitHub check failed",
ANTHROPIC_API_KEY_INVALID: "Anthropic API key invalid",
ANTHROPIC_API_KEY_NO_CREDIT: "Anthropic API key has no credit",
NO_DATA_FOR_THIS_ROUND: "No data for this round",
ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "Issue failed to add PR to summarizer todo",
}
export const errorMessage = {
ISSUE_FAILED_TO_BE_SUMMARIZED: "We couldn't summarize this issue. Please try again later.",
ISSUE_SUCCESSFULLY_SUMMARIZED: "The issue was successfully summarized.",
NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "There are no issues waiting to be summarized at this time.",
ROUND_LESS_THAN_OR_EQUAL_TO_1: "This operation requires a round number greater than 1.",
NO_ORCA_CLIENT: "The Orca client is not available.",
NO_CHOSEN_AS_ISSUE_SUMMARIZER: "You haven't been selected as an issue summarizer.",
UNKNOWN_ERROR: "An unexpected error occurred. Please try again later.",
STAR_ISSUE_FAILED: "We couldn't star the issue. Please try again later.",
GITHUB_CHECK_FAILED: "The GitHub check failed. Please verify your GitHub Key.",
ANTHROPIC_API_KEY_INVALID: "The Anthropic API Key is not valid. Please check your API key.",
ANTHROPIC_API_KEY_NO_CREDIT: "Your Anthropic API key has no remaining credits.",
NO_DATA_FOR_THIS_ROUND: "There is no data available for this round.",
ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "We couldn't add the PR to the summarizer todo list.",
}
export const actionMessage = {
ISSUE_FAILED_TO_BE_SUMMARIZED: "We couldn't summarize this issue. Please try again later.",
ISSUE_SUCCESSFULLY_SUMMARIZED: "The issue was successfully summarized.",
NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "There are no issues waiting to be summarized at this time.",
ROUND_LESS_THAN_OR_EQUAL_TO_1: "This operation requires a round number greater than 1.",
NO_ORCA_CLIENT: "Please click Orca icon to connect your Orca Pod.",
NO_CHOSEN_AS_ISSUE_SUMMARIZER: "You haven't been selected as an issue summarizer.",
UNKNOWN_ERROR: "An unexpected error occurred. Please try again later.",
STAR_ISSUE_FAILED: "We couldn't star the issue. Please try again later.",
GITHUB_CHECK_FAILED: "Please go to the env variable page to update your GitHub Key.",
ANTHROPIC_API_KEY_INVALID: "Please follow the guide under task description page to set up your Anthropic API key correctly.",
ANTHROPIC_API_KEY_NO_CREDIT: "Please add credits to continue.",
NO_DATA_FOR_THIS_ROUND: "There is no data available for this round.",
ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "We couldn't add the PR to the summarizer todo list. Please try again later.",
}
/*********************THE CONSTANTS THAT PROD/TEST ARE DIFFERENT *********************/
export const defaultBountyMarkdownFile = "https://raw.githubusercontent.com/koii-network/prometheus-swarm-bounties/master/README.md"
export const customReward = 400*10**9 // This should be in ROE!
export const middleServerUrl = "https://ooww84kco0s0cs808w8cg804.dev.koii.network"

View File

@ -0,0 +1,96 @@
import { namespaceWrapper } from "@_koii/namespace-wrapper";
import { getFile } from "./ipfs";
/**
* Filter out ineligible nodes from the distribution list
* @param distributionList Raw distribution list from namespace
* @param submissions List of submissions for the round
* @returns Filtered distribution list containing only eligible nodes
*/
async function filterIneligibleNodes(
distributionList: Record<string, number>,
roundNumber: number,
): Promise<Record<string, any>> {
const filteredDistributionList: Record<string, any> = {};
if (Object.keys(distributionList).length === 0) {
console.log("Distribution list is empty, skipping filterIneligibleNodes");
return filteredDistributionList;
}
const taskSubmissionInfo = await namespaceWrapper.getTaskSubmissionInfo(roundNumber);
if (!taskSubmissionInfo) {
console.log("Task submission info is null, skipping filterIneligibleNodes");
return filteredDistributionList;
}
const submissions = taskSubmissionInfo.submissions;
for (const [stakingKey, amount] of Object.entries(distributionList)) {
const numericAmount = amount as number;
// Skip if amount is zero or negative (failed audit)
if (numericAmount <= 0) {
console.log("Skipping staking key:", stakingKey, "Amount:", numericAmount);
continue;
}
// Find corresponding submission
const submissionCID = submissions[roundNumber][stakingKey]["submission_value"];
const submission = await getFile(submissionCID);
// Skip if no submission found
if (!submission) {
console.log("No submission found, skipping staking key:", stakingKey);
continue;
}
const submissionData = JSON.parse(submission);
console.log("Staking key:", stakingKey, "Submission data:", submissionData);
const payload = await namespaceWrapper.verifySignature(submissionData.signature, stakingKey);
console.log("Payload:", payload);
const payloadData = JSON.parse(payload.data || "{}");
// Skip if submission has no PR URL or is a dummy submission
if (!payloadData.prUrl || payloadData.prUrl === "none") {
continue;
}
// Node is eligible, include in filtered list
filteredDistributionList[stakingKey] = payloadData;
}
console.log("Filtered distribution list:", filteredDistributionList);
return filteredDistributionList;
}
export async function getDistributionList(roundNumber: number): Promise<Record<string, any> | null> {
try {
const taskDistributionInfo = await namespaceWrapper.getTaskDistributionInfo(roundNumber);
if (!taskDistributionInfo) {
console.log("Task distribution info is null, skipping task");
return null;
}
const distribution = taskDistributionInfo.distribution_rewards_submission[roundNumber];
const leaderStakingKey = Object.keys(distribution)[0];
console.log("Fetching distribution list for round", roundNumber, "with leader staking key", leaderStakingKey);
const distributionList = await namespaceWrapper.getDistributionList(leaderStakingKey, roundNumber);
if (!distributionList) {
console.log("Distribution list is null, skipping task");
return null;
}
console.log("Raw distribution list:", distributionList);
const parsedDistributionList: Record<string, number> = JSON.parse(distributionList);
return await filterIneligibleNodes(parsedDistributionList, roundNumber);
} catch (error) {
console.error("Error fetching distribution list:", error);
return null;
}
}

View File

@ -0,0 +1,161 @@
import { defaultBountyMarkdownFile } from "./constant";
interface BountyIssue {
githubUrl: string;
projectName: string;
bountyTask: string;
description: string;
bountyAmount: string;
bountyType: string;
transactionHash: string;
status: string;
}
export async function getExistingIssues(): Promise<BountyIssue[]> {
try {
// read from the bounty markdown file
// console.log('Fetching markdown file from:', defaultBountyMarkdownFile);
const bountyMarkdownFile = await fetch(defaultBountyMarkdownFile);
const bountyMarkdownFileText = await bountyMarkdownFile.text();
// console.log('Raw markdown content:', bountyMarkdownFileText);
const bountyMarkdownFileLines = bountyMarkdownFileText.split("\n");
// console.log('Number of lines:', bountyMarkdownFileLines.length);
const issues: BountyIssue[] = [];
let isTableStarted = false;
for (const line of bountyMarkdownFileLines) {
// Skip empty lines
if (line.trim() === '') {
// console.log('Skipping empty line');
continue;
}
// console.log('Processing line:', line);
// Skip the title line starting with #
if (line.startsWith('#')) {
// console.log('Found title line:', line);
continue;
}
// Skip the header and separator lines
if (line.startsWith('|') && line.includes('GitHub URL')) {
//console.log('Found header line');
continue;
}
if (line.startsWith('|') && line.includes('-----')) {
// console.log('Found separator line');
continue;
}
// Process table rows
if (line.startsWith('|')) {
isTableStarted = true;
// Remove first and last | and split by |
const cells = line.slice(1, -1).split('|').map(cell => cell.trim());
// console.log('Parsed cells:', cells);
// Extract GitHub URL and name from markdown link format [name](url)
const githubUrlMatch = cells[0].match(/\[(.*?)\]\((.*?)\)/);
// console.log('GitHub URL match:', githubUrlMatch);
const projectName = githubUrlMatch ? githubUrlMatch[1] : '';
const githubUrl = githubUrlMatch ? githubUrlMatch[2] : '';
const issue: BountyIssue = {
githubUrl,
projectName,
bountyTask: cells[1],
description: cells[3],
bountyAmount: cells[4],
bountyType: cells[5],
transactionHash: cells[6],
status: cells[7]
};
// console.log('Created issue object:', issue);
issues.push(issue);
}
}
// Filter all issues with status "Initialized" && Bounty Task is Document & Summarize
console.log('Final parsed issues number:', issues.length);
return issues
} catch (error) {
// console.error('Error processing markdown:', error);
throw error;
}
}
export async function getInitializedDocumentSummarizeIssues(issues: BountyIssue[]) {
return issues.filter(issue => issue.status === "Initialized" && issue.bountyTask === "Document & Summarize");
}
// async function main(){
// const existingIssues = await getExistingIssues();
// const transactionHashs = [
// "51680569890c40efa0f1f891044db219",
// "21a7021da88a4092af014702da7638cb",
// "befcf8d281074e3e934d8947c02ecb6f",
// "a1db701bbda24a45b573e58840d9b31c",
// "4ab503566a1142b1a3a9b406849839c9",
// "7f6fb74e4b6a41b0af805ca3f6c9ea15",
// "878af0d284c7460394b6d6e1090119be",
// "64d90b6f891d4ea385c8f6ad81808103",
// "6f7522b2e2374d4ca4f92bcf1f694bec",
// "e85201ae9ed9417e8c56216bb44cd78b",
// "d2ca259ef6ce4129a786677d919aad24",
// "6ce684318aab4356b76ba64e87b31be7",
// "d94d07647b1b42819d9bf629f5624ae1",
// "60aa8f04dd314c14b30e5ac2957bd9f8",
// "b7e21455e41b4626b5015b7bf39ff190",
// "5e7109ed4dd94373958eda2416337ad3",
// "2d647d3ab2c5465890939315ada47fd7",
// "51ade1ba2f6341e99aa6ec56b1a00f27",
// "a74f5e80238a4582aa444c18e9d5d66f",
// "8390a3143a8445f196a124605e524f3d",
// "26b712f341ca457d86db67ecd841c438",
// "0ec98ba1e7174eef87772df8356bab0d",
// "2737c33bff8c4490b7e5f53a5f5da580",
// "e5b9b714d5694680a56cfa77361f3477",
// "afb1bbbf1c074d28bef5fa216008cd6b",
// "b40da8c53a644a6e898e3314e08c10ea",
// "6a2f743c0497427ea4cd3cadb785b166",
// "ce390111854b4a4b980b5e1e3f7c2f0e",
// "c1b54e7a8dfd40be873051dd64bae5c4",
// "7dcda8e5969c45e08f9a8887d8c39d10",
// "fc11382529644d55b95fc2264e40436f",
// "7c145db039b64edba719e81dd398b37e",
// "c92b4920b25540a692c3b8e12215f0e0",
// "cebbf4e2310d4a11ac44321823ddb373",
// "5ae707005d0e413cb9feb9bdadc1e987",
// "d28f92643c2548338d3e49144bc66afc",
// "bd18484224c24fc786a5171e9d06cd50",
// "f0605ea0f9524572bbe5bf4e72597476",
// "62e6303c57334f72ada393bfa9e7aacc",
// "f4ee9168804c4b01932ac76cc32d1f13",
// "d4a95e2d35db47d28a208309019b1925",
// "014425adc1b8447ab34d7d8104e91cf0"
// ]
// const initializedDocumentSummarizeIssues = existingIssues.filter((issue) => transactionHashs.includes(issue.transactionHash));
// if (initializedDocumentSummarizeIssues.length == 0) {
// console.log("No issues pending to be summarized");
// return;
// }
// console.log("Initialized Document & Summarize issues number:", initializedDocumentSummarizeIssues.length);
// }
// async function main() {
// try {
// const existingIssues = await getInitializedDocumentSummarizeIssues();
// console.log('Initialized Document & Summarize issues number:', existingIssues.length);
// } catch (error) {
// console.error('Error in main:', error);
// }
// }
// main();

View File

@ -0,0 +1,36 @@
export async function checkGitHub(username: string, token: string) {
// 1. Check username
const userRes = await fetch(`https://api.github.com/users/${username}`);
const isUsernameValid = userRes.status === 200;
// 2. Check token
const tokenRes = await fetch('https://api.github.com/user', {
headers: {
Authorization: `token ${token}`,
},
});
const isTokenValid = tokenRes.status === 200;
const isIdentityValid = await checkGitHubIdentity(username, token);
return isIdentityValid&&isUsernameValid&&isTokenValid
}
async function checkGitHubIdentity(username: string, token: string) {
const res = await fetch('https://api.github.com/user', {
headers: {
Authorization: `token ${token}`,
Accept: 'application/vnd.github.v3+json',
},
});
if (res.status !== 200) {
return false
}
const data = await res.json();
if (data.login.toLowerCase() !== username.toLowerCase()) {
return false
}
return true
}

34
worker/src/utils/ipfs.ts Normal file
View File

@ -0,0 +1,34 @@
import { namespaceWrapper } from "@_koii/namespace-wrapper";
import { KoiiStorageClient } from "@_koii/storage-task-sdk";
import fs from "fs";
export async function storeFile(data: any, filename: string = "submission.json"): Promise<string> {
// Create a new instance of the Koii Storage Client
const client = KoiiStorageClient.getInstance({});
const basePath = await namespaceWrapper.getBasePath();
try {
// Write the data to a temp file
fs.writeFileSync(`${basePath}/${filename}`, typeof data === "string" ? data : JSON.stringify(data));
// Get the user staking account, to be used for signing the upload request
const userStaking = await namespaceWrapper.getSubmitterAccount();
if (!userStaking) {
throw new Error("No staking keypair found");
}
// Upload the file to IPFS and get the CID
const { cid } = await client.uploadFile(`${basePath}/${filename}`, userStaking);
return cid;
} catch (error) {
throw error;
} finally {
// Delete the temp file
fs.unlinkSync(`${basePath}/${filename}`);
}
}
export async function getFile(cid: string, filename: string = "submission.json"): Promise<string> {
const storageClient = KoiiStorageClient.getInstance({});
const fileBlob = await storageClient.getFile(cid, filename);
return await fileBlob.text();
}

265
worker/src/utils/leader.ts Normal file
View File

@ -0,0 +1,265 @@
import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper";
import { getFile } from "./ipfs";
import seedrandom from "seedrandom";
export async function fetchRoundSubmissionGitHubRepoOwner(
roundNumber: number,
submitterPublicKey: string,
): Promise<string | null> {
try {
const taskSubmissionInfo = await namespaceWrapper.getTaskSubmissionInfo(roundNumber);
if (!taskSubmissionInfo) {
console.error("NO TASK SUBMISSION INFO");
return null;
}
const submissions = taskSubmissionInfo.submissions;
// This should only have one round
const lastRound = Object.keys(submissions).pop();
if (!lastRound) {
return null;
}
const lastRoundSubmissions = submissions[lastRound];
const lastRoundSubmitterSubmission = lastRoundSubmissions[submitterPublicKey];
console.log("lastRoundSubmitterSubmission", { lastRoundSubmitterSubmission });
if (!lastRoundSubmitterSubmission) {
return null;
}
const cid = lastRoundSubmitterSubmission.submission_value;
const submissionString = await getFile(cid);
const submission = JSON.parse(submissionString);
console.log({ submission });
// verify the signature of the submission
const signaturePayload = await namespaceWrapper.verifySignature(submission.signature, submitterPublicKey);
console.log({ signaturePayload });
// verify the signature payload
if (signaturePayload.error || !signaturePayload.data) {
console.error("INVALID SIGNATURE");
return null;
}
const data = JSON.parse(signaturePayload.data);
if (data.taskId !== TASK_ID || data.stakingKey !== submitterPublicKey) {
console.error("INVALID SIGNATURE DATA");
return null;
}
if (!data.githubUsername) {
console.error("NO GITHUB USERNAME");
console.log("data", { data });
return null;
}
return data.githubUsername;
} catch (error) {
console.error("FETCH LAST ROUND SUBMISSION GITHUB REPO OWNER ERROR:", error);
return null;
}
}
export async function selectShortestDistance(keys: string[], submitterPublicKey: string): Promise<string> {
let shortestDistance = Infinity;
let closestKey = "";
for (const key of keys) {
const distance = knnDistance(submitterPublicKey, key);
if (distance < shortestDistance) {
shortestDistance = distance;
closestKey = key;
}
}
return closestKey;
}
async function getSubmissionInfo(roundNumber: number): Promise<any> {
try {
return await namespaceWrapper.getTaskSubmissionInfo(roundNumber);
} catch (error) {
console.error("GET SUBMISSION INFO ERROR:", error);
return null;
}
}
function calculatePublicKeyFrequency(submissions: any): Record<string, number> {
const frequency: Record<string, number> = {};
for (const round in submissions) {
for (const publicKey in submissions[round]) {
if (frequency[publicKey]) {
frequency[publicKey]++;
} else {
frequency[publicKey] = 1;
}
}
}
return frequency;
}
function handleAuditTrigger(submissionAuditTrigger: any): Set<string> {
const auditTriggerKeys = new Set<string>();
for (const round in submissionAuditTrigger) {
for (const publicKey in submissionAuditTrigger[round]) {
auditTriggerKeys.add(publicKey);
}
}
return auditTriggerKeys;
}
async function selectLeaderKey(
sortedKeys: string[],
leaderNumber: number,
submitterPublicKey: string,
submissionPublicKeysFrequency: Record<string, number>,
): Promise<string> {
const topValue = sortedKeys[leaderNumber - 1];
const count = sortedKeys.filter(
(key) => submissionPublicKeysFrequency[key] >= submissionPublicKeysFrequency[topValue],
).length;
if (count >= leaderNumber) {
const rng = seedrandom(String(TASK_ID));
const guaranteedKeys = sortedKeys.filter(
(key) => submissionPublicKeysFrequency[key] > submissionPublicKeysFrequency[topValue],
);
const randomKeys = sortedKeys
.filter((key) => submissionPublicKeysFrequency[key] === submissionPublicKeysFrequency[topValue])
.sort(() => rng() - 0.5)
.slice(0, leaderNumber - guaranteedKeys.length);
const keys = [...guaranteedKeys, ...randomKeys];
return await selectShortestDistance(keys, submitterPublicKey);
} else {
const keys = sortedKeys.slice(0, leaderNumber);
return await selectShortestDistance(keys, submitterPublicKey);
}
}
export async function getRandomNodes(roundNumber: number, numberOfNodes: number): Promise<string[]> {
console.log("Getting random nodes for round:", roundNumber, "with number of nodes:", numberOfNodes);
const lastRoundSubmission = await getSubmissionInfo(roundNumber - 1);
console.log("Last round submission:", lastRoundSubmission);
if (!lastRoundSubmission) {
return [];
}
const lastRoundSubmissions = lastRoundSubmission.submissions;
console.log("Last round submissions:", lastRoundSubmissions);
// Get the last round number
const lastRound = Object.keys(lastRoundSubmissions).pop();
if (!lastRound) {
return [];
}
// Get the submissions for that round
const submissions = lastRoundSubmissions[lastRound];
console.log("Submissions:", submissions);
const availableKeys = Object.keys(submissions);
console.log("Available keys:", availableKeys);
// If we have fewer submissions than requested nodes, return all available submissions
if (availableKeys.length <= numberOfNodes) {
return availableKeys;
}
const seed = TASK_ID + roundNumber.toString() || "default" + roundNumber;
const rng = seedrandom(seed);
// Use the keys from the submissions object
const randomKeys = availableKeys.sort(() => rng() - 0.5).slice(0, numberOfNodes);
console.log("Random keys:", randomKeys);
return randomKeys;
}
// Helper function that finds the leader for a specific round
async function getLeaderForRound(
roundNumber: number,
maxLeaderNumber: number,
submitterPublicKey: string,
): Promise<{ chosenKey: string | null; leaderNode: string | null }> {
if (roundNumber <= 0) {
return { chosenKey: null, leaderNode: null };
}
const submissionPublicKeysFrequency: Record<string, number> = {};
const submissionAuditTriggerKeys = new Set<string>();
for (let i = 1; i < 5; i++) {
const taskSubmissionInfo = await getSubmissionInfo(roundNumber - i);
console.log({ taskSubmissionInfo });
if (taskSubmissionInfo) {
const submissions = taskSubmissionInfo.submissions;
const frequency = calculatePublicKeyFrequency(submissions);
Object.assign(submissionPublicKeysFrequency, frequency);
const auditTriggerKeys = handleAuditTrigger(taskSubmissionInfo.submissions_audit_trigger);
auditTriggerKeys.forEach((key) => submissionAuditTriggerKeys.add(key));
}
}
const keysNotInAuditTrigger = Object.keys(submissionPublicKeysFrequency).filter(
(key) => !submissionAuditTriggerKeys.has(key),
);
const sortedKeys = keysNotInAuditTrigger.sort(
(a, b) => submissionPublicKeysFrequency[b] - submissionPublicKeysFrequency[a],
);
console.log({ sortedKeys });
let chosenKey = null;
const leaderNumber = sortedKeys.length < maxLeaderNumber ? sortedKeys.length : maxLeaderNumber;
chosenKey = await selectLeaderKey(sortedKeys, leaderNumber, submitterPublicKey, submissionPublicKeysFrequency);
// Find GitHub username for the chosen key
for (let i = 1; i < 5; i++) {
const githubUsername = await fetchRoundSubmissionGitHubRepoOwner(roundNumber - i, chosenKey);
if (githubUsername) {
return { chosenKey, leaderNode: githubUsername };
}
}
return { chosenKey, leaderNode: null };
}
export async function getLeaderNode({
roundNumber,
leaderNumber = 5,
submitterPublicKey,
}: {
roundNumber: number;
leaderNumber?: number;
submitterPublicKey: string;
}): Promise<{ isLeader: boolean; leaderNode: string | null }> {
// Find leader for current round
const currentLeader = await getLeaderForRound(roundNumber, leaderNumber, submitterPublicKey);
console.log({ currentLeader });
if (currentLeader.chosenKey === submitterPublicKey) {
// If we're the leader, get the leader from 3 rounds ago
const previousLeader = await getLeaderForRound(roundNumber - 3, leaderNumber, submitterPublicKey);
console.log({ previousLeader });
return { isLeader: true, leaderNode: previousLeader.leaderNode };
}
// Not the leader, return the current leader's info
return { isLeader: false, leaderNode: currentLeader.leaderNode };
}
function base58ToNumber(char: string): number {
const base58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
return base58Chars.indexOf(char);
}
function knnDistance(a: string, b: string): number {
if (a.length !== b.length) {
throw new Error("Strings must be of the same length for KNN distance calculation.");
}
const truncatedA = a.slice(0, 30);
const truncatedB = b.slice(0, 30);
let distance = 0;
for (let i = 0; i < truncatedA.length; i++) {
const numA = base58ToNumber(truncatedA[i]);
const numB = base58ToNumber(truncatedB[i]);
distance += Math.abs(numA - numB);
}
return distance;
}

View File

@ -0,0 +1,40 @@
import { TASK_ID } from "@_koii/namespace-wrapper";
import { getFile } from "./ipfs";
import { Submission } from "@_koii/namespace-wrapper/dist/types";
import { Submitter } from "@_koii/task-manager/dist/types/global";
import { namespaceWrapper } from "@_koii/namespace-wrapper";
export async function submissionJSONSignatureDecode({submission_value, submitterPublicKey, roundNumber}: {submission_value: string, submitterPublicKey: string, roundNumber: number}) {
let submissionString;
try {
console.log("Getting file from IPFS", submission_value);
submissionString = await getFile(submission_value);
console.log("submissionString", submissionString);
} catch (error) {
console.log("error", error);
console.error("INVALID SIGNATURE DATA");
return null;
}
// verify the signature of the submission
const submission = JSON.parse(submissionString);
console.log("submission", submission);
const signaturePayload = await namespaceWrapper.verifySignature(submission.signature, submitterPublicKey);
if (!signaturePayload.data) {
console.error("INVALID SIGNATURE");
return null;
}
const data = JSON.parse(signaturePayload.data);
console.log("signaturePayload", signaturePayload);
console.log("data", data);
if (
data.taskId !== TASK_ID ||
data.roundNumber !== roundNumber ||
data.stakingKey !== submitterPublicKey ||
!data.pubKey ||
!data.prUrl
) {
console.error("INVALID SIGNATURE DATA");
return null;
}
return data;
}