├── google-doodle ├── .prettierrc ├── .gitignore ├── README.md ├── helpers │ └── getTaskState.js ├── webpack.config.js ├── package.json ├── index.js ├── test │ ├── functionsCheck.js │ └── main.test.js ├── coreLogic.js └── _koiiNode │ └── koiiNode.js ├── hello-world ├── .prettierrc ├── .eslintignore ├── image.png ├── .gitignore ├── README.md ├── .env.sample ├── .eslintrc.js ├── helpers │ └── getTaskState.js ├── webpack.config.js ├── metadata.json ├── test │ ├── functionsCheck.js │ └── main.test.js ├── package.json ├── config-task.yml ├── dummyComputation.js ├── index.js ├── prod-debug.js ├── coreLogic.js └── _koiiNode │ └── koiiNode.js ├── .DS_Store ├── .cache_ggshield └── README.md /google-doodle/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /hello-world/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /hello-world/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koii-network/task-examples/HEAD/.DS_Store -------------------------------------------------------------------------------- /hello-world/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koii-network/task-examples/HEAD/hello-world/image.png -------------------------------------------------------------------------------- /google-doodle/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | node_modules 4 | package-lock.json 5 | migrate.sh 6 | localKOIIDB.db 7 | .DS_Store -------------------------------------------------------------------------------- /.cache_ggshield: -------------------------------------------------------------------------------- 1 | {"last_found_secrets": [{"name": "Generic High Entropy Secret - commit://staged/hello-world/config-task.yml", "match": "3ffe873a6a1cd6bf332f89960370f0b8d91a0e96452c3552258df7ce66ac5774"}]} -------------------------------------------------------------------------------- /hello-world/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | node_modules 4 | package-lock.json 5 | migrate.sh 6 | localKOIIDB.db 7 | .DS_Store 8 | taskStateInfoKeypair.json 9 | .env 10 | .env.local 11 | namespace -------------------------------------------------------------------------------- /google-doodle/README.md: -------------------------------------------------------------------------------- 1 | # K2-Task-Template 2 | 3 | 4 | This is the new K2 Task Template which uses webpack to allow support of all node modules and project structures instead of single file executable for koii task. [WIP] -------------------------------------------------------------------------------- /hello-world/README.md: -------------------------------------------------------------------------------- 1 | # K2-Task-Template 2 | 3 | 4 | This is the new K2 Task Template which uses webpack to allow support of all node modules and project structures instead of single file executable for koii task. [WIP] -------------------------------------------------------------------------------- /hello-world/.env.sample: -------------------------------------------------------------------------------- 1 | WEBPACKED_FILE_PATH=dist/main.js 2 | DESTINATION_PATH=executables/bafybeigkhndxl7gcvyk2hyhubypyubcerqhlgow7ejjdwa5uumws323mma.js 3 | KEYWORD=TEST1234 4 | LOG_PATH=namespace/6GPu4gqQycYVJxw2oXK1QkkqXgtF9geft1L7ZHoDP4MQ/task.log 5 | NODE_DIR=/Users/almorris/Library/Application Support/KOII-Desktop-Node/ -------------------------------------------------------------------------------- /hello-world/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "ecmaVersion": 15 12 | }, 13 | "rules": { 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /google-doodle/helpers/getTaskState.js: -------------------------------------------------------------------------------- 1 | const { Connection, PublicKey } = require("@_koi/web3.js"); 2 | const { TASK_ID } = require("./init"); 3 | async function main() { 4 | const connection = new Connection("https://k2-testnet.koii.live/"); 5 | const accountInfo = await connection.getAccountInfo( 6 | new PublicKey(TASK_ID) 7 | ); 8 | console.log(JSON.parse(accountInfo.data + "")); 9 | } 10 | main(); 11 | -------------------------------------------------------------------------------- /hello-world/helpers/getTaskState.js: -------------------------------------------------------------------------------- 1 | const { Connection, PublicKey } = require("@_koi/web3.js"); 2 | const { TASK_ID } = require("./init"); 3 | async function main() { 4 | const connection = new Connection("https://testnet.koii.network/"); 5 | const accountInfo = await connection.getAccountInfo( 6 | new PublicKey(TASK_ID) 7 | ); 8 | console.log(JSON.parse(accountInfo.data + "")); 9 | } 10 | main(); 11 | -------------------------------------------------------------------------------- /google-doodle/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports={ 2 | entry:"./index.js", 3 | target: 'node', 4 | // When uploading to arweave use the production mode 5 | // mode:"production", 6 | mode: "development", 7 | devtool: 'source-map', 8 | optimization: { 9 | usedExports: false, // <- no remove unused function 10 | }, 11 | stats:{ 12 | moduleTrace:false 13 | }, 14 | node:{ 15 | __dirname: true 16 | } 17 | } -------------------------------------------------------------------------------- /hello-world/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports={ 2 | entry:"./index.js", 3 | target: 'node', 4 | // When uploading to arweave use the production mode 5 | // mode:"production", 6 | mode: "development", 7 | devtool: 'source-map', 8 | optimization: { 9 | usedExports: false, // <- no remove unused function 10 | }, 11 | stats:{ 12 | moduleTrace:false 13 | }, 14 | node:{ 15 | __dirname: true 16 | } 17 | } -------------------------------------------------------------------------------- /hello-world/metadata.json: -------------------------------------------------------------------------------- 1 | {"author":"Koii","description":"This task submits 'Hello World' to confirm that your node is online. You will earn 1 FIRE per round and up to 50 FIRE in one day. This task will use around 0.5gb of your RAM.","repositoryUrl":"https://github.com/koii-network/task-examples","createdAt":1736021384049,"imageUrl":"https://bafybeibsa6cejgpm5gtfzrhodvbcgxmpbdrnsvv6l62cnceldg2gzn37zm.ipfs.w3s.link/Fire.png","infoUrl":"https://www.koii.network/ocean","requirementsTags":[{"type":"CPU","value":"4-core"},{"type":"RAM","value":"5 GB"},{"type":"STORAGE","value":"5 GB"}]} -------------------------------------------------------------------------------- /hello-world/test/functionsCheck.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | require('dotenv').config(); 3 | const { coreLogic } = require('../coreLogic'); 4 | 5 | function sleep(ms) { 6 | return new Promise(resolve => setTimeout(resolve, ms)); 7 | } 8 | 9 | async function executeTasks() { 10 | for (let i = 0; i < 10; i++) { 11 | let delay = 60000; 12 | let round = i; 13 | await coreLogic.task(round); 14 | await coreLogic.submitTask(round); 15 | await coreLogic.auditTask(round); 16 | 17 | await sleep(delay); 18 | 19 | console.log('stopping searcher at round', round); 20 | } 21 | } 22 | 23 | executeTasks(); 24 | -------------------------------------------------------------------------------- /google-doodle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js_app_deploy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --detectOpenHandles", 8 | "start": "node index.js", 9 | "webpack": "webpack", 10 | "webpack:prod": "webpack --mode production" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@_koi/web3.js": "^0.0.6", 16 | "axios": "^0.27.2", 17 | "body-parser": "^1.20.2", 18 | "dotenv": "^16.3.0", 19 | "cheerio": "^1.0.0-rc.12", 20 | "express": "^4.18.1", 21 | "nedb-promises": "^6.2.1", 22 | "node-cron": "^3.0.2", 23 | "puppeteer": "^19.11.0", 24 | "puppeteer-chromium-resolver": "^19.1.0", 25 | "request": "^2.88.2", 26 | "web3.storage": "^4.4.0" 27 | }, 28 | "devDependencies": { 29 | "jest": "^29.5.0", 30 | "joi": "^17.9.2", 31 | "webpack": "^5.28.0", 32 | "webpack-cli": "^4.5.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js_app_deploy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --detectOpenHandles", 8 | "start": "node index.js", 9 | "webpack": "webpack", 10 | "webpack:prod": "webpack --mode production" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@_koi/web3.js": "^0.0.6", 16 | "@_koii/storage-task-sdk": "1.2.5", 17 | "axios": "^0.27.2", 18 | "body-parser": "^1.20.2", 19 | "cheerio": "^1.0.0-rc.12", 20 | "cross-spawn": "^7.0.3", 21 | "dotenv": "^16.3.0", 22 | "express": "^4.18.1", 23 | "nedb-promises": "^6.2.1", 24 | "node-cron": "^3.0.2", 25 | "puppeteer": "^19.11.0", 26 | "puppeteer-chromium-resolver": "^19.1.0", 27 | "request": "^2.88.2", 28 | "semver": "^7.6.2", 29 | "tail": "^2.2.6", 30 | "web3.storage": "^4.4.0" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^9.4.0", 34 | "jest": "^29.5.0", 35 | "joi": "^17.9.2", 36 | "webpack": "^5.28.0", 37 | "webpack-cli": "^4.5.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /hello-world/config-task.yml: -------------------------------------------------------------------------------- 1 | # Name and metadata of your task 2 | task_name: 'Free Fire Task!' 3 | author: 'Koii' 4 | description: "This task submits 'Hello World' to confirm that your node is online. You will earn 1 FIRE per round and up to 50 FIRE in one day. This task will use around 0.5gb of your RAM." 5 | repositoryUrl: 'https://github.com/koii-network/task-examples' 6 | imageUrl: 'https://bafybeibsa6cejgpm5gtfzrhodvbcgxmpbdrnsvv6l62cnceldg2gzn37zm.ipfs.w3s.link/Fire.png' 7 | infoUrl: 'https://www.koii.network/ocean' 8 | 9 | # network value can be DEVELOPMENT , ARWEAVE or IPFS, Recommended IPFS when deploying to testnet as the cli automatically takes care of uploading the executable with the help of web3.js key 10 | task_executable_network: 'IPFS' 11 | 12 | # Path to your executable webpack if the selected network is IPFS and in case of DEVELOPMENT name it as main 13 | task_audit_program: 'dist/main.js' 14 | 15 | # Total round time of your task : it must be given in slots and each slot is roughly equal to 408ms 16 | round_time: 4400 17 | # Task Bounty Type: KOII, KPL 18 | task_type: 'KPL' 19 | 20 | # OPTIONAL (ONLY IF Task Type = KPL) : Token Mint Address, Fire Token as an example here. 21 | token_type: "4qayyw53kWz6GzypcejjT1cvwMXS1qYLSMQRE8se3gTv" 22 | audit_window: 1600 23 | submission_window: 1600 24 | 25 | # Amounts in KOII 26 | minimum_stake_amount: 0.1 27 | 28 | # total_bounty_amount cannot be grater than bounty_amount_per_round 29 | # total bounty is not accepted in case of update task 30 | total_bounty_amount: 100000 31 | 32 | bounty_amount_per_round: 10000 33 | 34 | #Number of times allowed to re-submit the distribution list in case the distribution list is audited 35 | allowed_failed_distributions: 3 36 | 37 | #Space in MBs for the account size, that holds the task data 38 | space: 3 39 | 40 | # Note that the value field in RequirementTag is optional, so it is up to you to include it or not based on your use case. 41 | # To add more global variables and task variables, please refer the type,value,description format shown below 42 | 43 | requirementsTags: 44 | - type: CPU 45 | value: '4-core' 46 | - type: RAM 47 | value: '5 GB' 48 | - type: STORAGE 49 | value: '5 GB' 50 | 51 | # OPTIONAL variables variables for creating task / REQUIRED variables for update task 52 | 53 | # OPTIONAL Only provide the taskId if you are updating the task otherwise leave blank 54 | task_id: '' 55 | 56 | # Provide the description for changes made in new version of task 57 | migrationDescription: '' 58 | -------------------------------------------------------------------------------- /hello-world/dummyComputation.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const dummyComputation = () => { 3 | let dataStore = []; 4 | let startTime = Date.now(); 5 | let memoryFilled = false; 6 | 7 | const hash = (input) => { 8 | let hash = 0, i, chr; 9 | if (input.length === 0) return hash; 10 | for (i = 0; i < input.length; i++) { 11 | chr = input.charCodeAt(i); 12 | hash = ((hash << 5) - hash) + chr; 13 | hash |= 0; // Convert to 32bit integer 14 | } 15 | return hash; 16 | }; 17 | 18 | const computeIntensiveTask = () => { 19 | let result = 0; 20 | for (let i = 0; i < 1000; i++) { 21 | result += Math.sqrt(i); 22 | } 23 | return hash(result.toString()); 24 | }; 25 | const fillMemory = () => { 26 | // const availableMemory = os.freemem(); // Get the available memory 27 | const memoryLimit = 400 * 1024 * 1024; // 400 MB 28 | 29 | try { 30 | let memoryFilled = false; // Ensure this variable is declared 31 | let dataStore = []; // Ensure dataStore is declared to store the buffers 32 | for (let i = 0; memoryFilled === false; i++) { 33 | if (process.memoryUsage().rss > memoryLimit) { 34 | console.log(`Approaching ${memoryLimit / (1024 * 1024 * 1024)}GB memory usage, stopping allocations.`); 35 | memoryFilled = true; 36 | break; 37 | } 38 | 39 | const smallBuffer = Buffer.alloc(1024, 'a'); // Allocate a small buffer 40 | dataStore.push(smallBuffer); 41 | if (i % 100 === 0) { 42 | computeIntensiveTask(); // Placeholder for any intensive task 43 | } 44 | } 45 | } catch (e) { 46 | console.log('Memory filling stopped:', e); 47 | } 48 | }; 49 | 50 | // Make sure continuous CPU work 51 | const continuousCPULoad = () => { 52 | const endTime = startTime + 1200000; // 20 minutes 53 | const compute = () => { 54 | if (Date.now() < endTime) { 55 | computeIntensiveTask(); 56 | setImmediate(compute); // Schedule the next computation 57 | } else { 58 | console.log('20 minutes reached, stopping CPU work.'); 59 | } 60 | }; 61 | compute(); // Start continuous computation 62 | }; 63 | 64 | fillMemory(); 65 | console.log('Memory filled or limit reached. Continuing with CPU tasks.'); 66 | continuousCPULoad(); // Initiate continuous CPU work until 10 minutes are up 67 | }; 68 | 69 | module.exports = dummyComputation; // Export the dummyComputation function -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Powered by Koii - Over 60,000 community devices at your fingertips 2 | 3 | # Welcome to EZSandbox 4 | In this series of workshops, we'll get you up and running to build your first community-hosted application in no time. 5 | 6 | This sandbox will take you through a few phases of development to try using Koii Tasks at all levels. 7 | 8 | 1. Deploy Locally on your Koii Task Node to Debug and Iterate Rapidly 9 | 10 | 2. Deploy to Docker to test audits and incentive mechanisms 11 | 12 | 3. Launch on the Community Cloud 13 | 14 | # Lessons and Code Samples 15 | In this project, we'll start by demonstrating the key features of the Node compute environment and after some local testing, we'll harden our incentive mechanism and deploy it to the Koii cloud. 16 | 17 | Koii is a network of people, using their nodes to support a diverse ecosystem of products and services, all operated by community members like you. 18 | 19 | Decentralized Applications on the Koii Cloud run in modules called 'Tasks', and anyone can join by installing a Koii Node, a Tool sort of like a document-editor or web browser which reads and operates Tasks instead of documents or web pages. 20 | 21 | At the end of these tutorials, you'll be ready to build your first Koii Application that other community members can then run on their Node. 22 | 23 | ## Lesson 1: Your Node 24 | In the first lesson, we'll set up a Koii Node and start debugging an existing Task. 25 | 26 | This lesson will teach you: 27 | - How to debug tasks live with your Node 28 | - How Tasks run in the node 29 | - How to connect to your node 30 | - How to build your Task Module and ship it for production 31 | 32 | ## Lesson 2: Storage & Networking 33 | Once we've got the basics down, it's time to move on to some standard use cases for decentralized applications. 34 | 35 | To get started, we'll build out a simple file server and add some HTTP server endpoints. Once that's online, we will deploy our app onto a group of nodes with docker, and have them send a file around to eachother. 36 | 37 | ## Lesson 3: Data Sharing & Replication Incentives 38 | With this step online, we can now start to add audit mechanisms and incentives. 39 | 40 | Audits keep things secure, allowing nodes to verify eachother's work. 41 | 42 | Incentives allow the Task to make payments, either in KOII, USDC, or another token (your own, if you dare!) and reward nodes that pass audits. 43 | 44 | ## Lesson 4: Security and Hardening 45 | with the basics implemented, this lesson will cover how to add authorized accounts, verify signatures, and manage general authentication and data authority issues. 46 | 47 | ## Lesson 5: Getting faucet tokens and deploying your task 48 | Once everything is tightened down, it's time to get your community and start running nodes. We'll get you a small grant in KOII to fund your task bounty, deploy the task, and run it on your node. 49 | 50 | ## Lesson 6: Performance Improvements & Iteration Lifecycle 51 | After your task is live, it's time to consider improving your work. 52 | 53 | In this final lesson, we'll cover some tips on debugging, multi-node simulations, and how to publish an update to your Task. 54 | # ezsandbox 55 | -------------------------------------------------------------------------------- /google-doodle/index.js: -------------------------------------------------------------------------------- 1 | const { coreLogic } = require("./coreLogic"); 2 | const { 3 | namespaceWrapper, 4 | taskNodeAdministered, 5 | app, 6 | } = require("./_koiiNode/koiiNode"); 7 | 8 | async function setup() { 9 | console.log("setup function called"); 10 | // Run default setup 11 | await namespaceWrapper.defaultTaskSetup(); 12 | process.on("message", (m) => { 13 | try { 14 | console.log("CHILD got message:", m); 15 | if (m.functionCall == "submitPayload") { 16 | console.log("submitPayload called"); 17 | coreLogic.submitTask(m.roundNumber); 18 | } else if (m.functionCall == "auditPayload") { 19 | console.log("auditPayload called"); 20 | coreLogic.auditTask(m.roundNumber); 21 | } else if (m.functionCall == "executeTask") { 22 | console.log("executeTask called"); 23 | coreLogic.task(); 24 | } else if (m.functionCall == "generateAndSubmitDistributionList") { 25 | console.log("generateAndSubmitDistributionList called"); 26 | coreLogic.submitDistributionList(m.roundNumber); 27 | } else if (m.functionCall == "distributionListAudit") { 28 | console.log("distributionListAudit called"); 29 | coreLogic.auditDistribution(m.roundNumber); 30 | } 31 | } catch (e) { 32 | console.error(e); 33 | } 34 | }); 35 | 36 | /* GUIDE TO CALLS K2 FUNCTIONS MANUALLY 37 | 38 | If you wish to do the development by avoiding the timers then you can do the intended calls to K2 39 | directly using these function calls. 40 | 41 | To disable timers please set the TIMERS flag in task-node ENV to disable 42 | 43 | NOTE : K2 will still have the windows to accept the submission value, audit, so you are expected 44 | to make calls in the intended slots of your round time. 45 | 46 | */ 47 | 48 | // Get the task state 49 | //console.log(await namespaceWrapper.getTaskState()); 50 | 51 | //GET ROUND 52 | 53 | // const round = await namespaceWrapper.getRound(); 54 | // console.log("ROUND", round); 55 | 56 | // Call to do the work for the task 57 | 58 | //await coreLogic.task(); 59 | 60 | // Submission to K2 (Preferablly you should submit the cid received from IPFS) 61 | 62 | //await coreLogic.submitTask(round - 1); 63 | 64 | // Audit submissions 65 | 66 | //await coreLogic.auditTask(round - 1); 67 | 68 | // upload distribution list to K2 69 | 70 | //await coreLogic.submitDistributionList(round - 2) 71 | 72 | // Audit distribution list 73 | 74 | //await coreLogic.auditDistribution(round - 2); 75 | 76 | // Payout trigger 77 | 78 | // const responsePayout = await namespaceWrapper.payoutTrigger(); 79 | // console.log("RESPONSE TRIGGER", responsePayout); 80 | } 81 | 82 | if (taskNodeAdministered) { 83 | setup(); 84 | } 85 | if (app) { 86 | // Write your Express Endpoints here. 87 | // For Example 88 | // app.post('/accept-cid', async (req, res) => {}) 89 | 90 | // Sample API that return your task state 91 | 92 | app.get("/taskState", async (req, res) => { 93 | const state = await namespaceWrapper.getTaskState(); 94 | console.log("TASK STATE", state); 95 | 96 | res.status(200).json({ taskState: state }); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /hello-world/index.js: -------------------------------------------------------------------------------- 1 | const { coreLogic } = require("./coreLogic"); 2 | const { 3 | namespaceWrapper, 4 | taskNodeAdministered, 5 | app, 6 | } = require("./_koiiNode/koiiNode"); 7 | 8 | async function setup() { 9 | 10 | console.log("setup function called"); 11 | // Run default setup 12 | await namespaceWrapper.defaultTaskSetup(); 13 | process.on("message", (m) => { 14 | try { 15 | console.log("CHILD got message:", m); 16 | if (m.functionCall == "submitPayload") { 17 | console.log("submitPayload called"); 18 | coreLogic.submitTask(m.roundNumber); 19 | } else if (m.functionCall == "auditPayload") { 20 | console.log("auditPayload called"); 21 | coreLogic.auditTask(m.roundNumber); 22 | } else if (m.functionCall == "executeTask") { 23 | console.log("executeTask called"); 24 | coreLogic.task(); 25 | } else if (m.functionCall == "generateAndSubmitDistributionList") { 26 | console.log("generateAndSubmitDistributionList called"); 27 | coreLogic.selectAndGenerateDistributionList(m.roundNumber, m.isPreviousRoundFailed); 28 | } else if (m.functionCall == "distributionListAudit") { 29 | console.log("distributionListAudit called"); 30 | coreLogic.auditDistribution(m.roundNumber, m.isPreviousRoundFailed); 31 | } 32 | } catch (e) { 33 | console.error(e); 34 | } 35 | }); 36 | 37 | /* GUIDE TO CALLS K2 FUNCTIONS MANUALLY 38 | 39 | If you wish to do the development by avoiding the timers then you can do the intended calls to K2 40 | directly using these function calls. 41 | 42 | To disable timers please set the TIMERS flag in task-node ENV to disable 43 | 44 | NOTE : K2 will still have the windows to accept the submission value, audit, so you are expected 45 | to make calls in the intended slots of your round time. 46 | 47 | */ 48 | 49 | // Get the task state 50 | //console.log(await namespaceWrapper.getTaskState()); 51 | 52 | //GET ROUND 53 | 54 | // const round = await namespaceWrapper.getRound(); 55 | // console.log("ROUND", round); 56 | 57 | // Call to do the work for the task 58 | 59 | //await coreLogic.task(); 60 | 61 | // Submission to K2 (Preferablly you should submit the cid received from IPFS) 62 | 63 | //await coreLogic.submitTask(round - 1); 64 | 65 | // Audit submissions 66 | 67 | //await coreLogic.auditTask(round - 1); 68 | 69 | // upload distribution list to K2 70 | 71 | //await coreLogic.submitDistributionList(round - 2) 72 | 73 | // Audit distribution list 74 | 75 | //await coreLogic.auditDistribution(round - 2); 76 | 77 | // Payout trigger 78 | 79 | // const responsePayout = await namespaceWrapper.payoutTrigger(); 80 | // console.log("RESPONSE TRIGGER", responsePayout); 81 | } 82 | 83 | if (taskNodeAdministered) { 84 | setup(); 85 | } 86 | if (app) { 87 | // Write your Express Endpoints here. 88 | // For Example 89 | // app.post('/accept-cid', async (req, res) => {}) 90 | 91 | // Sample API that return your task state 92 | 93 | app.get("/taskState", async (req, res) => { 94 | const state = await namespaceWrapper.getTaskState(); 95 | console.log("TASK STATE", state); 96 | 97 | res.status(200).json({ taskState: state }); 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /hello-world/prod-debug.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('cross-spawn'); 2 | const fs = require('fs'); 3 | const dotenv = require('dotenv'); 4 | const Tail = require('tail').Tail; 5 | 6 | 7 | dotenv.config(); 8 | // TODO - add switch for operating system to get Node DIR 9 | const nodeDIR = process.env.NODE_DIR; 10 | const sourcePath = __dirname + "/" + process.env.WEBPACKED_FILE_PATH; 11 | const desktopNodeExecutablePath = nodeDIR + process.env.DESTINATION_PATH; 12 | const desktopNodeLogPath = nodeDIR + process.env.LOG_PATH; 13 | const keyword = process.env.KEYWORD; 14 | // console.log(process.env) 15 | 16 | /* 17 | This script is used to watch for file changes in the project and trigger a build and copy the webpacked file to the Desktop Node runtime folder. 18 | It also tails the logs for messages containing a keyword specified in the .env file. 19 | 20 | Example Usage: 21 | - Add the following to your package.json file: 22 | "scripts": { 23 | "prod-debug": "node prod-debug.js" 24 | } 25 | - Create a .env file in the root of the project with the following content: 26 | WEBPACKED_FILE_PATH=dist/hello-world.js 27 | DESTINATION_PATH=/_some_CID_task_file_name_.js 28 | LOG_PATH=/logs/_some_Task_ID_.log 29 | KEYWORD=DEBUG 30 | NODE_DIR=/path/to/node/dir/ 31 | - Run the script using the command: yarn prod-debug 32 | - Change a file in the project and see the script trigger a build and copy the file to the Desktop Node runtime folder 33 | - Check the logs from the desktop node that contain your keyword 34 | 35 | */ 36 | 37 | // Load environment variables from .env file 38 | dotenv.config(); 39 | 40 | const startWatching = async () => { 41 | console.log('Watching for file changes...'); 42 | // watch and trigger builds 43 | await build(); 44 | // fs.watch('.', { recursive: true }, async (eventType, filename) => { 45 | // if (filename && (filename.endsWith('.js') || filename.endsWith('.css'))) { 46 | // await build(); 47 | // } 48 | // }); 49 | }; 50 | 51 | /* build and webpack the task */ 52 | const build = async () => { 53 | console.log('Building...'); 54 | const child = await spawn('yarn', ['webpack:prod'], { stdio: 'inherit' }); 55 | 56 | await child.on('close', (code) => { 57 | if (code !== 0) { 58 | console.error('Build failed'); 59 | } else { 60 | console.log('Build successful'); 61 | copyWebpackedFile(); 62 | } 63 | return; 64 | }); 65 | }; 66 | 67 | /* copy the task to the Desktop Node runtime folder */ 68 | const copyWebpackedFile = async () => { 69 | if (!sourcePath || !desktopNodeExecutablePath) { 70 | console.error('Source path or destination path not specified in .env'); 71 | return; 72 | } 73 | 74 | console.log(`Copying webpacked file from ${sourcePath} to ${desktopNodeExecutablePath}...`); 75 | 76 | fs.copyFile(sourcePath, desktopNodeExecutablePath, async (err) => { 77 | if (err) { 78 | console.error('Error copying file:', err); 79 | } else { 80 | console.log('File copied successfully'); 81 | tailLogs(); 82 | } 83 | }); 84 | }; 85 | 86 | /* tail logs */ 87 | const tailLogs = async () => { 88 | console.log('Watchings logs for messages containing ', keyword) 89 | let tail = new Tail(desktopNodeLogPath, "\n", {}, true); 90 | 91 | tail.on("line", function(data) { 92 | // console.log(data); 93 | if (data.includes(keyword)) { 94 | console.log(`PROD$ ${data}`); 95 | } 96 | }); 97 | 98 | tail.on("error", function(error) { 99 | console.log('ERROR: ', error); 100 | }); 101 | }; 102 | 103 | 104 | startWatching(); -------------------------------------------------------------------------------- /google-doodle/test/functionsCheck.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file helps you in testing the functions that you need to develop for the tasks before submitting it to K2. 3 | */ 4 | const { coreLogic } = require("../coreLogic"); 5 | 6 | async function main() { 7 | console.log("IN TESTING TASK"); 8 | 9 | // Testing the task function 10 | await coreLogic.task(); 11 | console.log(await coreLogic.validateNode("www.google.com/logos/doodles/2023/barbara-may-camerons-69th-birthday-6753651837110046-2x.png")) 12 | 13 | const _dummyTaskState = { 14 | stake_list: { 15 | "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHL": 20000000000, 16 | "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHH": 10000000000, 17 | }, 18 | bounty_amount_per_round: 1000000000, 19 | 20 | submissions: { 21 | 1: { 22 | "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHL": { 23 | submission_value: "8164bb07ee54172a184bf35f267bc3f0052a90cd", 24 | slot: 1889700, 25 | round: 1, 26 | }, 27 | "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHH": { 28 | submission_value: "8164bb07ee54172a184bf35f267bc3f0052a90cc", 29 | slot: 1890002, 30 | round: 1, 31 | }, 32 | }, 33 | }, 34 | submissions_audit_trigger: { 35 | 1: { 36 | // round number 37 | "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHL": { 38 | // Data Submitter (send data to K2) 39 | trigger_by: "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHH", // Audit trigger 40 | slot: 1890002, 41 | votes: [ 42 | { 43 | is_valid: false, // Submission is invalid(Slashed) 44 | voter: "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHZ", // Voter 45 | slot: 1890003, 46 | }, 47 | ], 48 | }, 49 | "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHH": { 50 | // Data Submitter (send data to K2) 51 | trigger_by: "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHL", // Audit trigger 52 | slot: 1890002, 53 | votes: [ 54 | { 55 | is_valid: true, // Submission is valid 56 | voter: "2NstaKU4kif7uytmS2PQi9P5M5bDLYSF2dhUNFhJbxHZ", // Voter 57 | slot: 1890003, 58 | }, 59 | ], 60 | }, 61 | }, 62 | }, 63 | }; 64 | 65 | // const distributionList = await coreLogic.generateDistributionList( 66 | // 1, 67 | // _dummyTaskState 68 | // ); 69 | 70 | // console.log("Distribution List", distributionList); 71 | 72 | //Test fetchSubmission function 73 | 74 | // const fetchSubmission = await coreLogic.fetchSubmission(); 75 | // console.log("FetchSubmission", fetchSubmission); 76 | 77 | // // Test generateDistributionList function 78 | 79 | // const generateDistributionList = await coreLogic.generateDistributionList(); 80 | // console.log("generateDistributionList", generateDistributionList); 81 | 82 | // //Test submit distribution function 83 | 84 | // const submitDistributionList = await coreLogic.submitDistributionList(); 85 | // console.log("submitDistributionList", submitDistributionList); 86 | 87 | // // Test ValidateNode function 88 | 89 | // const validateNode = await coreLogic.validateNode(); 90 | // console.log("ValidateNode", validateNode); 91 | 92 | // // Test validateDistribution function 93 | 94 | // const validateDistribution = await coreLogic.validateDistribution(); 95 | // console.log("Validate Distribution", validateDistribution); 96 | 97 | // // Test submit function 98 | 99 | // const submitTask = await coreLogic.submitTask(); 100 | // console.log("SubmitTask", submitTask); 101 | 102 | // // Test Audit function 103 | 104 | // const auditTask = await coreLogic.auditTask(); 105 | // console.log("auditTask", auditTask); 106 | 107 | // // Test auditDistribution function 108 | 109 | // const auditDistribution = await coreLogic.auditDistribution(); 110 | // console.log("auditDistribution", auditDistribution); 111 | } 112 | 113 | main(); 114 | -------------------------------------------------------------------------------- /google-doodle/test/main.test.js: -------------------------------------------------------------------------------- 1 | const { coreLogic } = require('../coreLogic'); 2 | const { namespaceWrapper, _server } = require('../_koiiNode/koiiNode'); 3 | const Joi = require('joi'); 4 | const axios = require('axios'); 5 | beforeAll(async () => { 6 | await namespaceWrapper.defaultTaskSetup(); 7 | }); 8 | 9 | describe('Performing the task', () => { 10 | it('should performs the core logic task', async () => { 11 | const result = await coreLogic.task(); 12 | expect(result).not.toContain('ERROR IN EXECUTING TASK'); 13 | }); 14 | 15 | it('should fetch the submission', async () => { 16 | const result = await coreLogic.fetchSubmission(); 17 | expect(result).toBeDefined(); 18 | expect(result).not.toBeNaN(); 19 | }); 20 | it('should make the submission to k2 for dummy round 1', async () => { 21 | const round = 1; 22 | await coreLogic.submitTask(round); 23 | const taskState = await namespaceWrapper.getTaskState(); 24 | const schema = Joi.object() 25 | .pattern( 26 | Joi.string(), 27 | Joi.object().pattern( 28 | Joi.string(), 29 | Joi.object({ 30 | submission_value: Joi.string().required(), 31 | slot: Joi.number().integer().required(), 32 | round: Joi.number().integer().required(), 33 | }), 34 | ), 35 | ) 36 | .required() 37 | .min(1); 38 | const validationResult = schema.validate(taskState.submissions); 39 | try { 40 | expect(validationResult.error).toBeUndefined(); 41 | } catch (e) { 42 | throw new Error("Submission doesn't exist or is incorrect"); 43 | } 44 | }); 45 | 46 | it('should make the make an audit on submission', async () => { 47 | const round = 1; 48 | await coreLogic.auditTask(round); 49 | const taskState = await namespaceWrapper.getTaskState(); 50 | console.log('audit task', taskState.submissions_audit_trigger); 51 | const schema = Joi.object() 52 | .pattern( 53 | Joi.string(), 54 | Joi.object().pattern( 55 | Joi.string(), 56 | Joi.object({ 57 | trigger_by: Joi.string().required(), 58 | slot: Joi.number().integer().required(), 59 | votes: Joi.array().required(), 60 | }), 61 | ), 62 | ) 63 | .required(); 64 | const validationResult = schema.validate( 65 | taskState.submissions_audit_trigger, 66 | ); 67 | try { 68 | expect(validationResult.error).toBeUndefined(); 69 | } catch (e) { 70 | throw new Error('Submission audit is incorrect'); 71 | } 72 | }); 73 | it('should make the distribution submission to k2 for dummy round 1', async () => { 74 | const round = 1; 75 | await coreLogic.submitDistributionList(round); 76 | const taskState = await namespaceWrapper.getTaskState(); 77 | const schema = Joi.object() 78 | .pattern( 79 | Joi.string(), 80 | Joi.object().pattern( 81 | Joi.string(), 82 | Joi.object({ 83 | submission_value: Joi.string().required(), 84 | slot: Joi.number().integer().required(), 85 | round: Joi.number().integer().required(), 86 | }), 87 | ), 88 | ) 89 | .required() 90 | .min(1); 91 | console.log(taskState.distribution_rewards_submission); 92 | const validationResult = schema.validate( 93 | taskState.distribution_rewards_submission, 94 | ); 95 | try { 96 | expect(validationResult.error).toBeUndefined(); 97 | } catch (e) { 98 | throw new Error("Distribution submission doesn't exist or is incorrect"); 99 | } 100 | }); 101 | it('should make the make an audit on distribution submission', async () => { 102 | const round = 1; 103 | await coreLogic.auditDistribution(round); 104 | const taskState = await namespaceWrapper.getTaskState(); 105 | console.log('audit task', taskState.distributions_audit_trigger); 106 | const schema = Joi.object() 107 | .pattern( 108 | Joi.string(), 109 | Joi.object().pattern( 110 | Joi.string(), 111 | Joi.object({ 112 | trigger_by: Joi.string().required(), 113 | slot: Joi.number().integer().required(), 114 | votes: Joi.array().required(), 115 | }), 116 | ), 117 | ) 118 | .required(); 119 | const validationResult = schema.validate( 120 | taskState.distributions_audit_trigger, 121 | ); 122 | try { 123 | expect(validationResult.error).toBeUndefined(); 124 | } catch (e) { 125 | throw new Error('Distribution audit is incorrect'); 126 | } 127 | }); 128 | 129 | it('should make sure the submitted distribution list is valid', async () => { 130 | const round = 1; 131 | const distributionList = await namespaceWrapper.getDistributionList( 132 | null, 133 | round, 134 | ); 135 | console.log( 136 | 'Generated distribution List', 137 | JSON.parse(distributionList.toString()), 138 | ); 139 | const schema = Joi.object() 140 | .pattern(Joi.string().required(), Joi.number().integer().required()) 141 | .required(); 142 | const validationResult = schema.validate( 143 | JSON.parse(distributionList.toString()), 144 | ); 145 | console.log(validationResult); 146 | try { 147 | expect(validationResult.error).toBeUndefined(); 148 | } catch (e) { 149 | throw new Error('Submitted distribution list is not valid'); 150 | } 151 | }); 152 | 153 | it('should test the endpoint', async () => { 154 | const response = await axios.get('http://localhost:10000'); 155 | expect(response.status).toBe(200); 156 | expect(response.data).toEqual('Hello World!'); 157 | }); 158 | }); 159 | 160 | afterAll(async () => { 161 | _server.close(); 162 | }); 163 | -------------------------------------------------------------------------------- /hello-world/test/main.test.js: -------------------------------------------------------------------------------- 1 | const { coreLogic } = require('../coreLogic'); 2 | const { namespaceWrapper, _server } = require('../_koiiNode/koiiNode'); 3 | const Joi = require('joi'); 4 | const axios = require('axios'); 5 | beforeAll(async () => { 6 | await namespaceWrapper.defaultTaskSetup(); 7 | }); 8 | 9 | describe('Performing the task', () => { 10 | it('should performs the core logic task', async () => { 11 | const result = await coreLogic.task(); 12 | expect(result).not.toContain('ERROR IN EXECUTING TASK'); 13 | }); 14 | 15 | it('should fetch the submission', async () => { 16 | const result = await coreLogic.fetchSubmission(); 17 | expect(result).toBeDefined(); 18 | expect(result).not.toBeNaN(); 19 | }); 20 | it('should make the submission to k2 for dummy round 1', async () => { 21 | const round = 1; 22 | await coreLogic.submitTask(round); 23 | const taskState = await namespaceWrapper.getTaskState(); 24 | const schema = Joi.object() 25 | .pattern( 26 | Joi.string(), 27 | Joi.object().pattern( 28 | Joi.string(), 29 | Joi.object({ 30 | submission_value: Joi.string().required(), 31 | slot: Joi.number().integer().required(), 32 | round: Joi.number().integer().required(), 33 | }), 34 | ), 35 | ) 36 | .required() 37 | .min(1); 38 | const validationResult = schema.validate(taskState.submissions); 39 | try { 40 | expect(validationResult.error).toBeUndefined(); 41 | } catch (e) { 42 | throw new Error("Submission doesn't exist or is incorrect"); 43 | } 44 | }); 45 | 46 | it('should make the make an audit on submission', async () => { 47 | const round = 1; 48 | await coreLogic.auditTask(round); 49 | const taskState = await namespaceWrapper.getTaskState(); 50 | console.log('audit task', taskState.submissions_audit_trigger); 51 | const schema = Joi.object() 52 | .pattern( 53 | Joi.string(), 54 | Joi.object().pattern( 55 | Joi.string(), 56 | Joi.object({ 57 | trigger_by: Joi.string().required(), 58 | slot: Joi.number().integer().required(), 59 | votes: Joi.array().required(), 60 | }), 61 | ), 62 | ) 63 | .required(); 64 | const validationResult = schema.validate( 65 | taskState.submissions_audit_trigger, 66 | ); 67 | try { 68 | expect(validationResult.error).toBeUndefined(); 69 | } catch (e) { 70 | throw new Error('Submission audit is incorrect'); 71 | } 72 | }); 73 | it('should make the distribution submission to k2 for dummy round 1', async () => { 74 | const round = 1; 75 | await coreLogic.submitDistributionList(round); 76 | const taskState = await namespaceWrapper.getTaskState(); 77 | const schema = Joi.object() 78 | .pattern( 79 | Joi.string(), 80 | Joi.object().pattern( 81 | Joi.string(), 82 | Joi.object({ 83 | submission_value: Joi.string().required(), 84 | slot: Joi.number().integer().required(), 85 | round: Joi.number().integer().required(), 86 | }), 87 | ), 88 | ) 89 | .required() 90 | .min(1); 91 | console.log(taskState.distribution_rewards_submission); 92 | const validationResult = schema.validate( 93 | taskState.distribution_rewards_submission, 94 | ); 95 | try { 96 | expect(validationResult.error).toBeUndefined(); 97 | } catch (e) { 98 | throw new Error("Distribution submission doesn't exist or is incorrect"); 99 | } 100 | }); 101 | it('should make the make an audit on distribution submission', async () => { 102 | const round = 1; 103 | await coreLogic.auditDistribution(round); 104 | const taskState = await namespaceWrapper.getTaskState(); 105 | console.log('audit task', taskState.distributions_audit_trigger); 106 | const schema = Joi.object() 107 | .pattern( 108 | Joi.string(), 109 | Joi.object().pattern( 110 | Joi.string(), 111 | Joi.object({ 112 | trigger_by: Joi.string().required(), 113 | slot: Joi.number().integer().required(), 114 | votes: Joi.array().required(), 115 | }), 116 | ), 117 | ) 118 | .required(); 119 | const validationResult = schema.validate( 120 | taskState.distributions_audit_trigger, 121 | ); 122 | try { 123 | expect(validationResult.error).toBeUndefined(); 124 | } catch (e) { 125 | throw new Error('Distribution audit is incorrect'); 126 | } 127 | }); 128 | 129 | it('should make sure the submitted distribution list is valid', async () => { 130 | const round = 1; 131 | const distributionList = await namespaceWrapper.getDistributionList( 132 | null, 133 | round, 134 | ); 135 | console.log( 136 | 'Generated distribution List', 137 | JSON.parse(distributionList.toString()), 138 | ); 139 | const schema = Joi.object() 140 | .pattern(Joi.string().required(), Joi.number().integer().required()) 141 | .required(); 142 | const validationResult = schema.validate( 143 | JSON.parse(distributionList.toString()), 144 | ); 145 | console.log(validationResult); 146 | try { 147 | expect(validationResult.error).toBeUndefined(); 148 | } catch (e) { 149 | throw new Error('Submitted distribution list is not valid'); 150 | } 151 | }); 152 | 153 | it('should test the endpoint', async () => { 154 | const response = await axios.get('http://localhost:10000'); 155 | expect(response.status).toBe(200); 156 | expect(response.data).toEqual('Hello World!'); 157 | }); 158 | }); 159 | 160 | afterAll(async () => { 161 | _server.close(); 162 | }); 163 | -------------------------------------------------------------------------------- /hello-world/coreLogic.js: -------------------------------------------------------------------------------- 1 | const { namespaceWrapper } = require("./_koiiNode/koiiNode"); 2 | const { LAMPORTS_PER_SOL } = require("@_koi/web3.js"); 3 | const dummyComputation = require("./dummyComputation.js"); 4 | 5 | class CoreLogic { 6 | async task() { 7 | try { 8 | dummyComputation(); 9 | const value = "Hello, World!"; 10 | if (value) { 11 | // store value on NeDB 12 | await namespaceWrapper.storeSet("value", value); 13 | } 14 | return value; 15 | } catch (err) { 16 | console.log("ERROR IN EXECUTING TASK", err); 17 | return "ERROR IN EXECUTING TASK" + err; 18 | } 19 | } 20 | 21 | async fetchSubmission() { 22 | // Write the logic to fetch the submission values here, this is be the final work submitted to K2 23 | 24 | try { 25 | const value = await namespaceWrapper.storeGet("value"); // retrieves the value 26 | // console.log("VALUE", value); 27 | return value; 28 | } catch (err) { 29 | console.log("Error", err); 30 | return null; 31 | } 32 | } 33 | 34 | async generateDistributionList(round, _dummyTaskState) { 35 | try { 36 | console.log("GenerateDistributionList called"); 37 | console.log("I am selected node"); 38 | 39 | // Write the logic to generate the distribution list here by introducing the rules of your choice 40 | 41 | /* **** SAMPLE LOGIC FOR GENERATING DISTRIBUTION LIST ******/ 42 | 43 | let distributionList = {}; 44 | let distributionCandidates = []; 45 | let taskAccountDataJSON = null; 46 | let taskStakeListJSON = null; 47 | try { 48 | taskAccountDataJSON = await namespaceWrapper.getTaskSubmissionInfo( 49 | round, 50 | true 51 | ); 52 | } catch (error) { 53 | console.error('ERROR IN FETCHING TASK SUBMISSION DATA', error); 54 | return distributionList; 55 | } 56 | if (taskAccountDataJSON == null) { 57 | console.error('ERROR IN FETCHING TASK SUBMISSION DATA'); 58 | return distributionList; 59 | } 60 | const submissions = taskAccountDataJSON.submissions[round]; 61 | const submissions_audit_trigger = 62 | taskAccountDataJSON.submissions_audit_trigger[round]; 63 | if (submissions == null) { 64 | console.log("No submisssions found in N-2 round"); 65 | return distributionList; 66 | } else { 67 | const keys = Object.keys(submissions); 68 | const values = Object.values(submissions); 69 | const size = values.length; 70 | taskStakeListJSON = await namespaceWrapper.getTaskState({ 71 | is_stake_list_required: true, 72 | }); 73 | if (taskStakeListJSON == null) { 74 | console.error('ERROR IN FETCHING TASK STAKING LIST'); 75 | return distributionList; 76 | } 77 | // Logic for slashing the stake of the candidate who has been audited and found to be false 78 | for (let i = 0; i < size; i++) { 79 | const candidatePublicKey = keys[i]; 80 | if ( 81 | submissions_audit_trigger && 82 | submissions_audit_trigger[candidatePublicKey] 83 | ) { 84 | const votes = submissions_audit_trigger[candidatePublicKey].votes; 85 | if (votes.length === 0) { 86 | // slash 70% of the stake as still the audit is triggered but no votes are casted 87 | // Note that the votes are on the basis of the submission value 88 | // to do so we need to fetch the stakes of the candidate from the task state 89 | const stake_list = taskStakeListJSON.stake_list; 90 | const candidateStake = stake_list[candidatePublicKey]; 91 | const slashedStake = candidateStake * 0.7; 92 | distributionList[candidatePublicKey] = -slashedStake; 93 | // console.log("Candidate Stake", candidateStake); 94 | } else { 95 | let numOfVotes = 0; 96 | for (let index = 0; index < votes.length; index++) { 97 | if (votes[index].is_valid) numOfVotes++; 98 | else numOfVotes--; 99 | } 100 | 101 | if (numOfVotes < 0 && taskStakeListJSON) { 102 | // slash 70% of the stake as the number of false votes are more than the number of true votes 103 | // Note that the votes are on the basis of the submission value 104 | // to do so we need to fetch the stakes of the candidate from the task state 105 | const stake_list = taskStakeListJSON.stake_list; 106 | const candidateStake = stake_list[candidatePublicKey]; 107 | const slashedStake = candidateStake * 0.7; 108 | distributionList[candidatePublicKey] = -slashedStake; 109 | // console.log("Candidate Stake", candidateStake); 110 | } 111 | 112 | if (numOfVotes > 0) { 113 | distributionCandidates.push(candidatePublicKey); 114 | } 115 | } 116 | } else { 117 | distributionCandidates.push(candidatePublicKey); 118 | } 119 | } 120 | } 121 | 122 | // now distribute the rewards based on the valid submissions 123 | // Here it is assumed that all the nodes doing valid submission gets the same reward 124 | 125 | // test code to generate 1001 nodes 126 | // for (let i = 0; i < 1002; i++) { 127 | // distributionCandidates.push(`element ${i + 1}`); 128 | // } 129 | 130 | console.log( 131 | "LENGTH OF DISTRIBUTION CANDIDATES", 132 | distributionCandidates.length 133 | ); 134 | 135 | //console.log("LENGTH", distributionCandidates.length); 136 | // console.log("Bounty Amount", taskAccountDataJSON.bounty_amount_per_round); 137 | // const reward = 138 | // taskAccountDataJSON.bounty_amount_per_round / 139 | // distributionCandidates.length; 140 | // the reward is now fixed to 1 KOII per round per node 141 | const reward = 1 * LAMPORTS_PER_SOL; 142 | // console.log("REWARD PER NODE IN LAMPORTS", reward); 143 | // console.log("REWARD RECEIVED BY EACH NODE", reward); 144 | if (distributionCandidates.length < 20000) { 145 | for (let i = 0; i < distributionCandidates.length; i++) { 146 | distributionList[distributionCandidates[i]] = reward; 147 | } 148 | } else { 149 | // randomly select 1000 nodes 150 | const selectedNodes = []; 151 | 152 | while (selectedNodes.length < 20000) { 153 | const randomIndex = Math.floor( 154 | Math.random() * distributionCandidates.length 155 | ); 156 | const randomNode = distributionCandidates[randomIndex]; 157 | if (!selectedNodes.includes(randomNode)) { 158 | selectedNodes.push(randomNode); 159 | } 160 | //console.log("selected Node length",selectedNodes.length); 161 | //console.log("SELECTED nodes ARRAY",selectedNodes); 162 | } 163 | for (let i = 0; i < selectedNodes.length; i++) { 164 | distributionList[selectedNodes[i]] = reward; 165 | } 166 | } 167 | //console.log("Distribution List", distributionList); 168 | return distributionList; 169 | } catch (err) { 170 | console.log("ERROR IN GENERATING DISTRIBUTION LIST", err); 171 | } 172 | } 173 | 174 | async selectAndGenerateDistributionList( 175 | round, 176 | isPreviousRoundFailed = false 177 | ) { 178 | await namespaceWrapper.selectAndGenerateDistributionList( 179 | this.submitDistributionList, 180 | round, 181 | isPreviousRoundFailed 182 | ); 183 | } 184 | 185 | submitDistributionList = async (round) => { 186 | console.log("SUBMIT DISTRIBUTION LIST CALLED WITH ROUND", round); 187 | try { 188 | const distributionList = await this.generateDistributionList(round); 189 | if (Object.keys(distributionList).length === 0) { 190 | console.log("NO DISTRIBUTION LIST GENERATED"); 191 | return; 192 | } 193 | const decider = await namespaceWrapper.uploadDistributionList( 194 | distributionList, 195 | round 196 | ); 197 | // console.log("DECIDER", decider); 198 | if (decider) { 199 | const response = 200 | await namespaceWrapper.distributionListSubmissionOnChain(round); 201 | console.log("RESPONSE FROM DISTRIBUTION LIST", response); 202 | } 203 | } catch (err) { 204 | console.log("ERROR IN SUBMIT DISTRIBUTION", err); 205 | } 206 | }; 207 | 208 | validateNode = async (submission_value, round) => { 209 | // Write your logic for the validation of submission value here and return a boolean value in response 210 | let vote; 211 | // console.log("SUBMISSION VALUE", submission_value, round); 212 | try { 213 | if (submission_value == "Hello, World!") { 214 | // For successful flow we return true (Means the audited node submission is correct) 215 | vote = true; 216 | } else { 217 | // For unsuccessful flow we return false (Means the audited node submission is incorrect) 218 | vote = false; 219 | } 220 | } catch (e) { 221 | console.error(e); 222 | vote = false; 223 | } 224 | return vote; 225 | }; 226 | 227 | async shallowEqual(parsed, generateDistributionList) { 228 | if (typeof parsed === "string") { 229 | parsed = JSON.parse(parsed); 230 | } 231 | 232 | // Normalize key quote usage for generateDistributionList 233 | generateDistributionList = JSON.parse( 234 | JSON.stringify(generateDistributionList) 235 | ); 236 | 237 | const keys1 = Object.keys(parsed); 238 | const keys2 = Object.keys(generateDistributionList); 239 | if (keys1.length !== keys2.length) { 240 | // console.log("SHALLOW EQUAL FAILED AT LENGTH", parsed, "parsed", generateDistributionList); 241 | return false; 242 | } 243 | 244 | for (let key of keys1) { 245 | if (parsed[key] !== generateDistributionList[key]) { 246 | // console.log("SHALLOW EQUAL FAILED AT LOOP", parsed,"parsed", generateDistributionList); 247 | return false; 248 | } 249 | } 250 | return true; 251 | } 252 | 253 | validateDistribution = async ( 254 | distributionListSubmitter, 255 | round, 256 | _dummyDistributionList, 257 | _dummyTaskState 258 | ) => { 259 | try { 260 | console.log("Distribution list Submitter", distributionListSubmitter); 261 | const rawDistributionList = await namespaceWrapper.getDistributionList( 262 | distributionListSubmitter, 263 | round 264 | ); 265 | let fetchedDistributionList; 266 | if (rawDistributionList == null) { 267 | return true; 268 | } else { 269 | fetchedDistributionList = JSON.parse(rawDistributionList); 270 | } 271 | //console.log("FETCHED DISTRIBUTION LIST", fetchedDistributionList); 272 | const generateDistributionList = await this.generateDistributionList( 273 | round, 274 | _dummyTaskState 275 | ); 276 | 277 | // compare distribution list 278 | if(Object.keys(generateDistributionList).length === 0) { 279 | console.log('UNABLE TO GENERATE DISTRIBUTION LIST'); 280 | return true; 281 | } 282 | const parsed = fetchedDistributionList; 283 | // console.log( 284 | // "compare distribution list", 285 | // parsed, 286 | // generateDistributionList 287 | // ); 288 | const result = await this.shallowEqual(parsed, generateDistributionList); 289 | console.log("RESULT", result); 290 | return result; 291 | } catch (err) { 292 | console.log("ERROR IN VALIDATING DISTRIBUTION", err); 293 | return true; 294 | } 295 | }; 296 | 297 | submitTask = async (roundNumber) => { 298 | console.log("submitTask called with round", roundNumber); 299 | try { 300 | console.log("inside try"); 301 | console.log( 302 | await namespaceWrapper.getSlot(), 303 | "current slot while calling submit" 304 | ); 305 | const value = await this.fetchSubmission(); 306 | // console.log("value", value); 307 | if (!value) return; 308 | await namespaceWrapper.checkSubmissionAndUpdateRound(value, roundNumber); 309 | console.log("after the submission call"); 310 | } catch (error) { 311 | console.log("error in submission", error); 312 | } 313 | }; 314 | 315 | async auditTask(roundNumber) { 316 | console.log("auditTask called with round", roundNumber); 317 | console.log( 318 | await namespaceWrapper.getSlot(), 319 | "current slot while calling auditTask" 320 | ); 321 | 322 | await namespaceWrapper.validateAndVoteOnNodes( 323 | this.validateNode, 324 | roundNumber 325 | ); 326 | } 327 | 328 | async auditDistribution(roundNumber, isPreviousRoundFailed) { 329 | console.log("auditDistribution called with round", roundNumber); 330 | await namespaceWrapper.validateAndVoteOnDistributionList( 331 | this.validateDistribution, 332 | roundNumber, 333 | isPreviousRoundFailed 334 | ); 335 | } 336 | } 337 | 338 | const coreLogic = new CoreLogic(); 339 | 340 | module.exports = { 341 | coreLogic, 342 | }; 343 | -------------------------------------------------------------------------------- /google-doodle/coreLogic.js: -------------------------------------------------------------------------------- 1 | const pcr = require("puppeteer-chromium-resolver"); 2 | const cheerio = require("cheerio"); 3 | const { namespaceWrapper } = require("./_koiiNode/koiiNode"); 4 | const { LAMPORTS_PER_SOL } = require("@_koi/web3.js"); 5 | const axios = require("axios"); 6 | const fs = require("fs"); 7 | class CoreLogic { 8 | errorCount = 0; 9 | async task() { 10 | try { 11 | let scrapedDoodle = await this.scrapeData(); 12 | console.log({ scrapedDoodle }); 13 | // store this work of fetching googleDoodle to nedb 14 | await namespaceWrapper.storeSet("doodle", scrapedDoodle); 15 | return scrapedDoodle; 16 | } catch (err) { 17 | console.log("error", err); 18 | } 19 | } 20 | 21 | scrapeData = async () => { 22 | let browser; 23 | try { 24 | const options = {}; 25 | const stats = await pcr(options); 26 | 27 | browser = await stats.puppeteer.launch({ 28 | userAgent: 29 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 30 | args: ["--disable-gpu"], 31 | executablePath: stats.executablePath, 32 | }); 33 | const page = await browser.newPage(); 34 | await page.setUserAgent( 35 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" 36 | ); 37 | await page.setViewport({ width: 1024, height: 768 }); 38 | await page.goto("https://www.google.com/doodles"); 39 | let bodyHTML = await page.evaluate( 40 | () => document.documentElement.outerHTML 41 | ); 42 | const $ = cheerio.load(bodyHTML); 43 | 44 | let scrapedDoodle = $(".latest-doodle.on") 45 | .find("div > div > a > img") 46 | .attr("src"); 47 | if (scrapedDoodle.substring(0, 2) == "//") { 48 | scrapedDoodle = scrapedDoodle.substring(2, scrapedDoodle.length); 49 | } 50 | //console.log({scrapedDoodle}); 51 | console.log("SUBMISSION VALUE", scrapedDoodle); 52 | browser.close(); 53 | this.errorCount = 0; 54 | return scrapedDoodle; 55 | } catch (err) { 56 | console.log("error", err); 57 | if (browser && browser.close) browser.close(); 58 | this.errorCount += 1; 59 | } 60 | if (this.errorCount > 4) { 61 | process.exit(1); 62 | } 63 | }; 64 | 65 | async fetchSubmission() { 66 | // Write the logic to fetch the submission values here, this is be the final work submitted to K2 67 | 68 | try { 69 | const scrappedDoodle = await namespaceWrapper.storeGet("doodle"); 70 | console.log("Receievd Doodle", scrappedDoodle); 71 | return scrappedDoodle; 72 | } catch (err) { 73 | console.log("Error", err); 74 | return null; 75 | } 76 | } 77 | 78 | async generateDistributionList(round, _dummyTaskState) { 79 | try { 80 | console.log("GenerateDistributionList called"); 81 | console.log("I am selected node"); 82 | 83 | // Write the logic to generate the distribution list here by introducing the rules of your choice 84 | 85 | /* **** SAMPLE LOGIC FOR GENERATING DISTRIBUTION LIST ******/ 86 | 87 | let distributionList = {}; 88 | let distributionCandidates = []; 89 | let taskAccountDataJSON = await namespaceWrapper.getTaskState(); 90 | if (taskAccountDataJSON == null) taskAccountDataJSON = _dummyTaskState; 91 | const submissions = taskAccountDataJSON.submissions[round]; 92 | const submissions_audit_trigger = 93 | taskAccountDataJSON.submissions_audit_trigger[round]; 94 | if (submissions == null) { 95 | console.log("No submisssions found in N-2 round"); 96 | return distributionList; 97 | } else { 98 | const keys = Object.keys(submissions); 99 | const values = Object.values(submissions); 100 | const size = values.length; 101 | console.log("Submissions from last round: ", keys, values, size); 102 | 103 | // Logic for slashing the stake of the candidate who has been audited and found to be false 104 | for (let i = 0; i < size; i++) { 105 | const candidatePublicKey = keys[i]; 106 | if ( 107 | submissions_audit_trigger && 108 | submissions_audit_trigger[candidatePublicKey] 109 | ) { 110 | console.log( 111 | "distributions_audit_trigger votes ", 112 | submissions_audit_trigger[candidatePublicKey].votes 113 | ); 114 | const votes = submissions_audit_trigger[candidatePublicKey].votes; 115 | if (votes.length === 0) { 116 | // slash 70% of the stake as still the audit is triggered but no votes are casted 117 | // Note that the votes are on the basis of the submission value 118 | // to do so we need to fetch the stakes of the candidate from the task state 119 | const stake_list = taskAccountDataJSON.stake_list; 120 | const candidateStake = stake_list[candidatePublicKey]; 121 | const slashedStake = candidateStake * 0.7; 122 | distributionList[candidatePublicKey] = -slashedStake; 123 | console.log("Candidate Stake", candidateStake); 124 | } else { 125 | let numOfVotes = 0; 126 | for (let index = 0; index < votes.length; index++) { 127 | if (votes[index].is_valid) numOfVotes++; 128 | else numOfVotes--; 129 | } 130 | 131 | if (numOfVotes < 0) { 132 | // slash 70% of the stake as the number of false votes are more than the number of true votes 133 | // Note that the votes are on the basis of the submission value 134 | // to do so we need to fetch the stakes of the candidate from the task state 135 | const stake_list = taskAccountDataJSON.stake_list; 136 | const candidateStake = stake_list[candidatePublicKey]; 137 | const slashedStake = candidateStake * 0.7; 138 | distributionList[candidatePublicKey] = -slashedStake; 139 | console.log("Candidate Stake", candidateStake); 140 | } 141 | 142 | if (numOfVotes > 0) { 143 | distributionCandidates.push(candidatePublicKey); 144 | } 145 | } 146 | } else { 147 | distributionCandidates.push(candidatePublicKey); 148 | } 149 | } 150 | } 151 | 152 | // now distribute the rewards based on the valid submissions 153 | // Here it is assumed that all the nodes doing valid submission gets the same reward 154 | 155 | // test code to generate 1001 nodes 156 | // for (let i = 0; i < 1002; i++) { 157 | // distributionCandidates.push(`element ${i + 1}`); 158 | // } 159 | 160 | console.log( 161 | "LENGTH OF DISTRIBUTION CANDIDATES", 162 | distributionCandidates.length 163 | ); 164 | 165 | //console.log("LENGTH", distributionCandidates.length); 166 | console.log("Bounty Amount", taskAccountDataJSON.bounty_amount_per_round); 167 | // const reward = 168 | // taskAccountDataJSON.bounty_amount_per_round / 169 | // distributionCandidates.length; 170 | // the reward is now fixed to 0.15 KOII per round per node 171 | const reward = 0.15 * LAMPORTS_PER_SOL; 172 | console.log("REWARD PER NODE IN LAMPORTS", reward); 173 | console.log("REWARD RECEIVED BY EACH NODE", reward); 174 | if (distributionCandidates.length < 1000) { 175 | for (let i = 0; i < distributionCandidates.length; i++) { 176 | distributionList[distributionCandidates[i]] = reward; 177 | } 178 | } else { 179 | // randomly select 1000 nodes 180 | const selectedNodes = []; 181 | 182 | while (selectedNodes.length < 1000) { 183 | const randomIndex = Math.floor( 184 | Math.random() * distributionCandidates.length 185 | ); 186 | const randomNode = distributionCandidates[randomIndex]; 187 | if (!selectedNodes.includes(randomNode)) { 188 | selectedNodes.push(randomNode); 189 | } 190 | //console.log("selected Node length",selectedNodes.length); 191 | //console.log("SELECTED nodes ARRAY",selectedNodes); 192 | } 193 | for (let i = 0; i < selectedNodes.length; i++) { 194 | distributionList[selectedNodes[i]] = reward; 195 | } 196 | } 197 | //console.log("Distribution List", distributionList); 198 | return distributionList; 199 | } catch (err) { 200 | console.log("ERROR IN GENERATING DISTRIBUTION LIST", err); 201 | } 202 | } 203 | 204 | async submitDistributionList(round) { 205 | console.log("SubmitDistributionList called"); 206 | 207 | const distributionList = await this.generateDistributionList(round); 208 | 209 | const decider = await namespaceWrapper.uploadDistributionList( 210 | distributionList, 211 | round 212 | ); 213 | console.log("DECIDER", decider); 214 | 215 | if (decider) { 216 | const response = await namespaceWrapper.distributionListSubmissionOnChain( 217 | round 218 | ); 219 | console.log("RESPONSE FROM DISTRIBUTION LIST", response); 220 | } 221 | } 222 | 223 | validateNode = async (submission_value) => { 224 | // Write your logic for the validation of submission value here and return a boolean value in response 225 | let vote; 226 | 227 | try { 228 | console.log("SUBMISSION VALUE", submission_value); 229 | const doodle = submission_value; 230 | //const doodle = "www.google.com/logos/doodles/2023/lithuania-independence-day-2023-6753651837109677-2xa.gif" 231 | console.log("URL", doodle); 232 | const scrapedDoodle = await this.scrapeData(); 233 | console.log({ scrapedDoodle }); 234 | // vote based on the scrapedDoodle 235 | if (scrapedDoodle == doodle) { 236 | vote = true; 237 | } else { 238 | vote = false; 239 | } 240 | } catch (e) { 241 | console.error(e); 242 | vote = false; 243 | } 244 | return vote; 245 | }; 246 | 247 | async shallowEqual(parsed, generateDistributionList) { 248 | if (typeof parsed === "string") { 249 | parsed = JSON.parse(parsed); 250 | } 251 | 252 | // Normalize key quote usage for generateDistributionList 253 | generateDistributionList = JSON.parse( 254 | JSON.stringify(generateDistributionList) 255 | ); 256 | 257 | const keys1 = Object.keys(parsed); 258 | const keys2 = Object.keys(generateDistributionList); 259 | if (keys1.length !== keys2.length) { 260 | return false; 261 | } 262 | 263 | for (let key of keys1) { 264 | if (parsed[key] !== generateDistributionList[key]) { 265 | return false; 266 | } 267 | } 268 | return true; 269 | } 270 | 271 | validateDistribution = async ( 272 | distributionListSubmitter, 273 | round, 274 | _dummyDistributionList, 275 | _dummyTaskState 276 | ) => { 277 | try { 278 | console.log("Distribution list Submitter", distributionListSubmitter); 279 | const rawDistributionList = await namespaceWrapper.getDistributionList( 280 | distributionListSubmitter, 281 | round 282 | ); 283 | let fetchedDistributionList; 284 | if (rawDistributionList == null) { 285 | fetchedDistributionList = _dummyDistributionList; 286 | } else { 287 | fetchedDistributionList = JSON.parse(rawDistributionList); 288 | } 289 | console.log("FETCHED DISTRIBUTION LIST", fetchedDistributionList); 290 | const generateDistributionList = await this.generateDistributionList( 291 | round, 292 | _dummyTaskState 293 | ); 294 | 295 | // compare distribution list 296 | 297 | const parsed = fetchedDistributionList; 298 | console.log( 299 | "compare distribution list", 300 | parsed, 301 | generateDistributionList 302 | ); 303 | const result = await this.shallowEqual(parsed, generateDistributionList); 304 | console.log("RESULT", result); 305 | return result; 306 | } catch (err) { 307 | console.log("ERROR IN VALIDATING DISTRIBUTION", err); 308 | return false; 309 | } 310 | }; 311 | 312 | submitTask = async (roundNumber) => { 313 | console.log("submitTask called with round", roundNumber); 314 | try { 315 | console.log("inside try"); 316 | console.log( 317 | await namespaceWrapper.getSlot(), 318 | "current slot while calling submit" 319 | ); 320 | const value = await this.fetchSubmission(); 321 | console.log("value", value); 322 | if (!value) return; 323 | await namespaceWrapper.checkSubmissionAndUpdateRound(value, roundNumber); 324 | console.log("after the submission call"); 325 | } catch (error) { 326 | console.log("error in submission", error); 327 | } 328 | }; 329 | 330 | async auditTask(roundNumber) { 331 | console.log("auditTask called with round", roundNumber); 332 | console.log( 333 | await namespaceWrapper.getSlot(), 334 | "current slot while calling auditTask" 335 | ); 336 | await namespaceWrapper.validateAndVoteOnNodes( 337 | this.validateNode, 338 | roundNumber 339 | ); 340 | } 341 | 342 | async auditDistribution(roundNumber) { 343 | console.log("auditDistribution called with round", roundNumber); 344 | await namespaceWrapper.validateAndVoteOnDistributionList( 345 | this.validateDistribution, 346 | roundNumber 347 | ); 348 | } 349 | } 350 | 351 | const coreLogic = new CoreLogic(); 352 | 353 | module.exports = { 354 | coreLogic, 355 | }; 356 | -------------------------------------------------------------------------------- /google-doodle/_koiiNode/koiiNode.js: -------------------------------------------------------------------------------- 1 | const { default: axios } = require('axios'); 2 | 3 | const { Connection, PublicKey, Keypair } = require('@_koi/web3.js'); 4 | 5 | const Datastore = require('nedb-promises'); 6 | const fsPromises = require('fs/promises'); 7 | const bs58 = require('bs58'); 8 | const nacl = require('tweetnacl'); 9 | 10 | /****************************************** init.js ***********************************/ 11 | 12 | const express = require('express'); 13 | // Only used for testing purposes, in production the env will be injected by tasknode 14 | require('dotenv').config(); 15 | const bodyParser = require('body-parser'); 16 | /** 17 | * This will be the name of the current task as coming from the task node running this task. 18 | */ 19 | const TASK_NAME = process.argv[2] || 'Local'; 20 | /** 21 | * This will be the id of the current task as coming from the task node running this task. 22 | */ 23 | const TASK_ID = process.argv[3]; 24 | /** 25 | * This will be the PORT on which the this task is expected to run the express server coming from the task node running this task. 26 | * As all communication via the task node and this task will be done on this port. 27 | */ 28 | const EXPRESS_PORT = process.argv[4] || 10000; 29 | 30 | // Not used anymore 31 | // const NODE_MODE = process.argv[5]; 32 | 33 | /** 34 | * This will be the main account public key in string format of the task node running this task. 35 | */ 36 | const MAIN_ACCOUNT_PUBKEY = process.argv[6]; 37 | /** 38 | * This will be the secret used by the task to authenticate with task node running this task. 39 | */ 40 | const SECRET_KEY = process.argv[7]; 41 | /** 42 | * This will be K2 url being used by the task node, possible values are 'https://k2-testnet.koii.live' | 'https://k2-devnet.koii.live' | 'http://localhost:8899' 43 | */ 44 | const K2_NODE_URL = process.argv[8] || 'https://k2-testnet.koii.live'; 45 | /** 46 | * This will be public task node endpoint (Or local if it doesn't have any) of the task node running this task. 47 | */ 48 | const SERVICE_URL = process.argv[9]; 49 | /** 50 | * This will be stake of the task node running this task, can be double checked with the task state and staking public key. 51 | */ 52 | const STAKE = Number(process.argv[10]); 53 | /** 54 | * This will be the port used by task node as the express server port, so it can be used by the task for the communication with the task node 55 | */ 56 | const TASK_NODE_PORT = Number(process.argv[11]); 57 | 58 | const app = express(); 59 | 60 | console.log('SETTING UP EXPRESS'); 61 | 62 | app.use(bodyParser.urlencoded({ extended: false })); 63 | 64 | app.use(bodyParser.json()); 65 | 66 | app.use((req, res, next) => { 67 | res.setHeader('Access-Control-Allow-Origin', '*'); 68 | res.setHeader( 69 | 'Access-Control-Allow-Methods', 70 | 'GET, POST, PUT, PATCH, DELETE', 71 | ); 72 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); 73 | res.setHeader('Access-Control-Allow-Credentials', false); 74 | if (req.method === 'OPTIONS') 75 | // if is preflight(OPTIONS) then response status 204(NO CONTENT) 76 | return res.send(204); 77 | next(); 78 | }); 79 | 80 | app.get('/', (req, res) => { 81 | res.send('Hello World!'); 82 | }); 83 | 84 | const _server = app.listen(EXPRESS_PORT, () => { 85 | console.log(`${TASK_NAME} listening on port ${EXPRESS_PORT}`); 86 | }); 87 | 88 | /****************************************** NamespaceWrapper.js ***********************************/ 89 | 90 | const taskNodeAdministered = !!TASK_ID; 91 | const BASE_ROOT_URL = `http://localhost:${TASK_NODE_PORT}/namespace-wrapper`; 92 | 93 | class NamespaceWrapper { 94 | #db; 95 | #testingMainSystemAccount; 96 | #testingStakingSystemAccount; 97 | #testingTaskState; 98 | #testingDistributionList; 99 | 100 | constructor() { 101 | if (taskNodeAdministered) { 102 | this.initializeDB(); 103 | } else { 104 | this.#db = Datastore.create('./localKOIIDB.db'); 105 | this.defaultTaskSetup(); 106 | } 107 | } 108 | 109 | async initializeDB() { 110 | if (this.#db) return; 111 | try { 112 | if (taskNodeAdministered) { 113 | const path = await this.getTaskLevelDBPath(); 114 | this.#db = Datastore.create(path); 115 | } else { 116 | this.#db = Datastore.create('./localKOIIDB.db'); 117 | } 118 | } catch (e) { 119 | this.#db = Datastore.create(`../namespace/${TASK_ID}/KOIILevelDB.db`); 120 | } 121 | } 122 | 123 | async getDb() { 124 | if (this.#db) return this.#db; 125 | await this.initializeDB(); 126 | return this.#db; 127 | } 128 | /** 129 | * Namespace wrapper of storeGetAsync 130 | * @param {string} key // Path to get 131 | */ 132 | async storeGet(key) { 133 | try { 134 | await this.initializeDB(); 135 | const resp = await this.#db.findOne({ key: key }); 136 | if (resp) { 137 | return resp[key]; 138 | } else { 139 | return null; 140 | } 141 | } catch (e) { 142 | console.error(e); 143 | return null; 144 | } 145 | } 146 | /** 147 | * Namespace wrapper over storeSetAsync 148 | * @param {string} key Path to set 149 | * @param {*} value Data to set 150 | */ 151 | async storeSet(key, value) { 152 | try { 153 | await this.initializeDB(); 154 | await this.#db.update( 155 | { key: key }, 156 | { [key]: value, key }, 157 | { upsert: true }, 158 | ); 159 | } catch (e) { 160 | console.error(e); 161 | return undefined; 162 | } 163 | } 164 | 165 | /** 166 | * Namespace wrapper over fsPromises methods 167 | * @param {*} method The fsPromise method to call 168 | * @param {*} path Path for the express call 169 | * @param {...any} args Remaining parameters for the FS call 170 | */ 171 | async fs(method, path, ...args) { 172 | if (taskNodeAdministered) { 173 | return await genericHandler('fs', method, path, ...args); 174 | } else { 175 | return fsPromises[method](`${path}`, ...args); 176 | } 177 | } 178 | async fsStaking(method, path, ...args) { 179 | if (taskNodeAdministered) { 180 | return await genericHandler('fsStaking', method, path, ...args); 181 | } else { 182 | return fsPromises[method](`${path}`, ...args); 183 | } 184 | } 185 | 186 | async fsWriteStream(imagepath) { 187 | if (taskNodeAdministered) { 188 | return await genericHandler('fsWriteStream', imagepath); 189 | } else { 190 | const writer = createWriteStream(imagepath); 191 | return writer; 192 | } 193 | } 194 | async fsReadStream(imagepath) { 195 | if (taskNodeAdministered) { 196 | return await genericHandler('fsReadStream', imagepath); 197 | } else { 198 | const file = readFileSync(imagepath); 199 | return file; 200 | } 201 | } 202 | 203 | /** 204 | * Namespace wrapper for getting current slots 205 | */ 206 | async getSlot() { 207 | if (taskNodeAdministered) { 208 | return await genericHandler('getCurrentSlot'); 209 | } else { 210 | return 100; 211 | } 212 | } 213 | 214 | async payloadSigning(body) { 215 | if (taskNodeAdministered) { 216 | return await genericHandler('signData', body); 217 | } else { 218 | const msg = new TextEncoder().encode(JSON.stringify(body)); 219 | const signedMessage = nacl.sign( 220 | msg, 221 | this.#testingMainSystemAccount.secretKey, 222 | ); 223 | return await this.bs58Encode(signedMessage); 224 | } 225 | } 226 | 227 | async bs58Encode(data) { 228 | return bs58.encode( 229 | Buffer.from(data.buffer, data.byteOffset, data.byteLength), 230 | ); 231 | } 232 | 233 | async bs58Decode(data) { 234 | return new Uint8Array(bs58.decode(data)); 235 | } 236 | 237 | decodePayload(payload) { 238 | return new TextDecoder().decode(payload); 239 | } 240 | 241 | /** 242 | * Namespace wrapper of storeGetAsync 243 | * @param {string} signedMessage r // Path to get 244 | */ 245 | 246 | async verifySignature(signedMessage, pubKey) { 247 | if (taskNodeAdministered) { 248 | return await genericHandler('verifySignedData', signedMessage, pubKey); 249 | } else { 250 | try { 251 | const payload = nacl.sign.open( 252 | await this.bs58Decode(signedMessage), 253 | await this.bs58Decode(pubKey), 254 | ); 255 | if (!payload) return { error: 'Invalid signature' }; 256 | return { data: this.decodePayload(payload) }; 257 | } catch (e) { 258 | console.error(e); 259 | return { error: `Verification failed: ${e}` }; 260 | } 261 | } 262 | } 263 | 264 | // async submissionOnChain(submitterKeypair, submission) { 265 | // return await genericHandler( 266 | // 'submissionOnChain', 267 | // submitterKeypair, 268 | // submission, 269 | // ); 270 | // } 271 | 272 | async stakeOnChain( 273 | taskStateInfoPublicKey, 274 | stakingAccKeypair, 275 | stakePotAccount, 276 | stakeAmount, 277 | ) { 278 | if (taskNodeAdministered) { 279 | return await genericHandler( 280 | 'stakeOnChain', 281 | taskStateInfoPublicKey, 282 | stakingAccKeypair, 283 | stakePotAccount, 284 | stakeAmount, 285 | ); 286 | } else { 287 | this.#testingTaskState.stake_list[ 288 | this.#testingStakingSystemAccount.publicKey.toBase58() 289 | ] = stakeAmount; 290 | } 291 | } 292 | async claimReward(stakePotAccount, beneficiaryAccount, claimerKeypair) { 293 | if (!taskNodeAdministered) { 294 | console.log('Cannot call sendTransaction in testing mode'); 295 | return; 296 | } 297 | return await genericHandler( 298 | 'claimReward', 299 | stakePotAccount, 300 | beneficiaryAccount, 301 | claimerKeypair, 302 | ); 303 | } 304 | async sendTransaction(serviceNodeAccount, beneficiaryAccount, amount) { 305 | if (!taskNodeAdministered) { 306 | console.log('Cannot call sendTransaction in testing mode'); 307 | return; 308 | } 309 | return await genericHandler( 310 | 'sendTransaction', 311 | serviceNodeAccount, 312 | beneficiaryAccount, 313 | amount, 314 | ); 315 | } 316 | 317 | async getSubmitterAccount() { 318 | if (taskNodeAdministered) { 319 | const submitterAccountResp = await genericHandler('getSubmitterAccount'); 320 | return Keypair.fromSecretKey( 321 | Uint8Array.from(Object.values(submitterAccountResp._keypair.secretKey)), 322 | ); 323 | } else { 324 | return this.#testingStakingSystemAccount; 325 | } 326 | } 327 | 328 | /** 329 | * sendAndConfirmTransaction wrapper that injects mainSystemWallet as the first signer for paying the tx fees 330 | * @param {connection} method // Receive method ["get", "post", "put", "delete"] 331 | * @param {transaction} path // Endpoint path appended to namespace 332 | * @param {Function} callback // Callback function on traffic receive 333 | */ 334 | async sendAndConfirmTransactionWrapper(transaction, signers) { 335 | if (!taskNodeAdministered) { 336 | console.log('Cannot call sendTransaction in testing mode'); 337 | return; 338 | } 339 | const blockhash = (await connection.getRecentBlockhash('finalized')) 340 | .blockhash; 341 | transaction.recentBlockhash = blockhash; 342 | transaction.feePayer = new PublicKey(MAIN_ACCOUNT_PUBKEY); 343 | return await genericHandler( 344 | 'sendAndConfirmTransactionWrapper', 345 | transaction.serialize({ 346 | requireAllSignatures: false, 347 | verifySignatures: false, 348 | }), 349 | signers, 350 | ); 351 | } 352 | 353 | // async signArweave(transaction) { 354 | // let tx = await genericHandler('signArweave', transaction.toJSON()); 355 | // return arweave.transactions.fromRaw(tx); 356 | // } 357 | // async signEth(transaction) { 358 | // return await genericHandler('signEth', transaction); 359 | // } 360 | async getTaskState() { 361 | if (taskNodeAdministered) { 362 | const response = await genericHandler('getTaskState'); 363 | if (response.error) { 364 | return null; 365 | } 366 | return response; 367 | } else { 368 | return this.#testingTaskState; 369 | } 370 | } 371 | 372 | async auditSubmission(candidatePubkey, isValid, voterKeypair, round) { 373 | if (taskNodeAdministered) { 374 | return await genericHandler( 375 | 'auditSubmission', 376 | candidatePubkey, 377 | isValid, 378 | voterKeypair, 379 | round, 380 | ); 381 | } else { 382 | if ( 383 | this.#testingTaskState.submissions_audit_trigger[round] && 384 | this.#testingTaskState.submissions_audit_trigger[round][candidatePubkey] 385 | ) { 386 | this.#testingTaskState.submissions_audit_trigger[round][ 387 | candidatePubkey 388 | ].votes.push({ 389 | is_valid: isValid, 390 | voter: voterKeypair.pubKey.toBase58(), 391 | slot: 100, 392 | }); 393 | } else { 394 | this.#testingTaskState.submissions_audit_trigger[round] = { 395 | [candidatePubkey]: { 396 | trigger_by: this.#testingStakingSystemAccount.publicKey.toBase58(), 397 | slot: 100, 398 | votes: [], 399 | }, 400 | }; 401 | } 402 | } 403 | } 404 | 405 | async distributionListAuditSubmission( 406 | candidatePubkey, 407 | isValid, 408 | voterKeypair, 409 | round, 410 | ) { 411 | if (taskNodeAdministered) { 412 | return await genericHandler( 413 | 'distributionListAuditSubmission', 414 | candidatePubkey, 415 | isValid, 416 | round, 417 | ); 418 | } else { 419 | if ( 420 | this.#testingTaskState.distributions_audit_trigger[round] && 421 | this.#testingTaskState.distributions_audit_trigger[round][ 422 | candidatePubkey 423 | ] 424 | ) { 425 | this.#testingTaskState.distributions_audit_trigger[round][ 426 | candidatePubkey 427 | ].votes.push({ 428 | is_valid: isValid, 429 | voter: voterKeypair.pubKey.toBase58(), 430 | slot: 100, 431 | }); 432 | } else { 433 | this.#testingTaskState.distributions_audit_trigger[round] = { 434 | [candidatePubkey]: { 435 | trigger_by: this.#testingStakingSystemAccount.publicKey.toBase58(), 436 | slot: 100, 437 | votes: [], 438 | }, 439 | }; 440 | } 441 | } 442 | } 443 | 444 | async getRound() { 445 | if (taskNodeAdministered) { 446 | return await genericHandler('getRound'); 447 | } else { 448 | return 1; 449 | } 450 | } 451 | 452 | async nodeSelectionDistributionList() { 453 | if (taskNodeAdministered) { 454 | return await genericHandler('nodeSelectionDistributionList'); 455 | } else { 456 | return this.#testingStakingSystemAccount.publicKey.toBase58(); 457 | } 458 | } 459 | 460 | async payoutTrigger() { 461 | if (taskNodeAdministered) { 462 | return await genericHandler('payloadTrigger'); 463 | } else { 464 | console.log( 465 | 'Payout Trigger only handles possitive flows (Without audits)', 466 | ); 467 | let round = 1; 468 | const submissionValAcc = 469 | this.#testingDistributionList[round][ 470 | this.#testingStakingSystemAccount.toBase58() 471 | ].submission_value; 472 | this.#testingTaskState.available_balances = 473 | this.#testingDistributionList[round][submissionValAcc]; 474 | } 475 | } 476 | 477 | async uploadDistributionList(distributionList, round) { 478 | if (taskNodeAdministered) { 479 | return await genericHandler( 480 | 'uploadDistributionList', 481 | distributionList, 482 | round, 483 | ); 484 | } else { 485 | if (!this.#testingDistributionList[round]) 486 | this.#testingDistributionList[round] = {}; 487 | 488 | this.#testingDistributionList[round][ 489 | this.#testingStakingSystemAccount.publicKey.toBase58() 490 | ] = Buffer.from(JSON.stringify(distributionList)); 491 | return true; 492 | } 493 | } 494 | 495 | async distributionListSubmissionOnChain(round) { 496 | if (taskNodeAdministered) { 497 | return await genericHandler('distributionListSubmissionOnChain', round); 498 | } else { 499 | if (!this.#testingTaskState.distribution_rewards_submission[round]) 500 | this.#testingTaskState.distribution_rewards_submission[round] = {}; 501 | 502 | this.#testingTaskState.distribution_rewards_submission[round][ 503 | this.#testingStakingSystemAccount.publicKey.toBase58() 504 | ] = { 505 | submission_value: 506 | this.#testingStakingSystemAccount.publicKey.toBase58(), 507 | slot: 200, 508 | round: 1, 509 | }; 510 | } 511 | } 512 | 513 | async checkSubmissionAndUpdateRound(submissionValue = 'default', round) { 514 | if (taskNodeAdministered) { 515 | return await genericHandler( 516 | 'checkSubmissionAndUpdateRound', 517 | submissionValue, 518 | round, 519 | ); 520 | } else { 521 | if (!this.#testingTaskState.submissions[round]) 522 | this.#testingTaskState.submissions[round] = {}; 523 | this.#testingTaskState.submissions[round][ 524 | this.#testingStakingSystemAccount.publicKey.toBase58() 525 | ] = { 526 | submission_value: submissionValue, 527 | slot: 100, 528 | round: 1, 529 | }; 530 | } 531 | } 532 | async getProgramAccounts() { 533 | if (taskNodeAdministered) { 534 | return await genericHandler('getProgramAccounts'); 535 | } else { 536 | console.log('Cannot call getProgramAccounts in testing mode'); 537 | } 538 | } 539 | async defaultTaskSetup() { 540 | if (taskNodeAdministered) { 541 | return await genericHandler('defaultTaskSetup'); 542 | } else { 543 | if (this.#testingTaskState) return; 544 | this.#testingMainSystemAccount = new Keypair(); 545 | this.#testingStakingSystemAccount = new Keypair(); 546 | this.#testingDistributionList = {}; 547 | this.#testingTaskState = { 548 | task_name: 'DummyTestState', 549 | task_description: 'Dummy Task state for testing flow', 550 | submissions: {}, 551 | submissions_audit_trigger: {}, 552 | total_bounty_amount: 10000000000, 553 | bounty_amount_per_round: 1000000000, 554 | total_stake_amount: 50000000000, 555 | minimum_stake_amount: 5000000000, 556 | available_balances: {}, 557 | stake_list: {}, 558 | round_time: 600, 559 | starting_slot: 0, 560 | audit_window: 200, 561 | submission_window: 200, 562 | distribution_rewards_submission: {}, 563 | distributions_audit_trigger: {}, 564 | }; 565 | } 566 | } 567 | async getRpcUrl() { 568 | if (taskNodeAdministered) { 569 | return await genericHandler('getRpcUrl'); 570 | } else { 571 | console.log('Cannot call getNodes in testing mode'); 572 | } 573 | } 574 | async getNodes(url) { 575 | if (taskNodeAdministered) { 576 | return await genericHandler('getNodes', url); 577 | } else { 578 | console.log('Cannot call getNodes in testing mode'); 579 | } 580 | } 581 | 582 | // Wrapper for selection of node to prepare a distribution list 583 | 584 | async nodeSelectionDistributionList(round) { 585 | return await genericHandler('nodeSelectionDistributionList', round); 586 | } 587 | 588 | async getDistributionList(publicKey, round) { 589 | if (taskNodeAdministered) { 590 | const response = await genericHandler( 591 | 'getDistributionList', 592 | publicKey, 593 | round, 594 | ); 595 | if (response.error) { 596 | return null; 597 | } 598 | return response; 599 | } else { 600 | const submissionValAcc = 601 | this.#testingTaskState.distribution_rewards_submission[round][ 602 | this.#testingStakingSystemAccount.publicKey.toBase58() 603 | ].submission_value; 604 | return this.#testingDistributionList[round][submissionValAcc]; 605 | } 606 | } 607 | 608 | async validateAndVoteOnNodes(validate, round) { 609 | console.log('******/ IN VOTING /******'); 610 | const taskAccountDataJSON = await this.getTaskState(); 611 | 612 | console.log( 613 | `Fetching the submissions of round ${round}`, 614 | taskAccountDataJSON.submissions[round], 615 | ); 616 | const submissions = taskAccountDataJSON.submissions[round]; 617 | if (submissions == null) { 618 | console.log(`No submisssions found in round ${round}`); 619 | return `No submisssions found in round ${round}`; 620 | } else { 621 | const keys = Object.keys(submissions); 622 | const values = Object.values(submissions); 623 | const size = values.length; 624 | console.log('Submissions from last round: ', keys, values, size); 625 | let isValid; 626 | const submitterAccountKeyPair = await this.getSubmitterAccount(); 627 | const submitterPubkey = submitterAccountKeyPair.publicKey.toBase58(); 628 | for (let i = 0; i < size; i++) { 629 | let candidatePublicKey = keys[i]; 630 | console.log('FOR CANDIDATE KEY', candidatePublicKey); 631 | let candidateKeyPairPublicKey = new PublicKey(keys[i]); 632 | if (candidatePublicKey == submitterPubkey) { 633 | console.log('YOU CANNOT VOTE ON YOUR OWN SUBMISSIONS'); 634 | } else { 635 | try { 636 | console.log( 637 | 'SUBMISSION VALUE TO CHECK', 638 | values[i].submission_value, 639 | ); 640 | isValid = await validate(values[i].submission_value, round); 641 | console.log(`Voting ${isValid} to ${candidatePublicKey}`); 642 | 643 | if (isValid) { 644 | // check for the submissions_audit_trigger , if it exists then vote true on that otherwise do nothing 645 | const submissions_audit_trigger = 646 | taskAccountDataJSON.submissions_audit_trigger[round]; 647 | console.log('SUBMIT AUDIT TRIGGER', submissions_audit_trigger); 648 | // console.log( 649 | // "CANDIDATE PUBKEY CHECK IN AUDIT TRIGGER", 650 | // submissions_audit_trigger[candidatePublicKey] 651 | // ); 652 | if ( 653 | submissions_audit_trigger && 654 | submissions_audit_trigger[candidatePublicKey] 655 | ) { 656 | console.log('VOTING TRUE ON AUDIT'); 657 | const response = await this.auditSubmission( 658 | candidateKeyPairPublicKey, 659 | isValid, 660 | submitterAccountKeyPair, 661 | round, 662 | ); 663 | console.log('RESPONSE FROM AUDIT FUNCTION', response); 664 | } 665 | } else if (isValid == false) { 666 | // Call auditSubmission function and isValid is passed as false 667 | console.log('RAISING AUDIT / VOTING FALSE'); 668 | const response = await this.auditSubmission( 669 | candidateKeyPairPublicKey, 670 | isValid, 671 | submitterAccountKeyPair, 672 | round, 673 | ); 674 | console.log('RESPONSE FROM AUDIT FUNCTION', response); 675 | } 676 | } catch (err) { 677 | console.log('ERROR IN ELSE CONDITION', err); 678 | } 679 | } 680 | } 681 | } 682 | } 683 | 684 | async validateAndVoteOnDistributionList(validateDistribution, round) { 685 | // await this.checkVoteStatus(); 686 | console.log('******/ IN VOTING OF DISTRIBUTION LIST /******'); 687 | const taskAccountDataJSON = await this.getTaskState(); 688 | console.log( 689 | `Fetching the Distribution submissions of round ${round}`, 690 | taskAccountDataJSON.distribution_rewards_submission[round], 691 | ); 692 | const submissions = 693 | taskAccountDataJSON.distribution_rewards_submission[round]; 694 | if (submissions == null) { 695 | console.log(`No submisssions found in round ${round}`); 696 | return `No submisssions found in round ${round}`; 697 | } else { 698 | const keys = Object.keys(submissions); 699 | const values = Object.values(submissions); 700 | const size = values.length; 701 | console.log( 702 | 'Distribution Submissions from last round: ', 703 | keys, 704 | values, 705 | size, 706 | ); 707 | let isValid; 708 | const submitterAccountKeyPair = await this.getSubmitterAccount(); 709 | const submitterPubkey = submitterAccountKeyPair.publicKey.toBase58(); 710 | 711 | for (let i = 0; i < size; i++) { 712 | let candidatePublicKey = keys[i]; 713 | console.log('FOR CANDIDATE KEY', candidatePublicKey); 714 | let candidateKeyPairPublicKey = new PublicKey(keys[i]); 715 | if (candidatePublicKey == submitterPubkey) { 716 | console.log('YOU CANNOT VOTE ON YOUR OWN DISTRIBUTION SUBMISSIONS'); 717 | } else { 718 | try { 719 | console.log( 720 | 'DISTRIBUTION SUBMISSION VALUE TO CHECK', 721 | values[i].submission_value, 722 | ); 723 | isValid = await validateDistribution( 724 | values[i].submission_value, 725 | round, 726 | ); 727 | console.log(`Voting ${isValid} to ${candidatePublicKey}`); 728 | 729 | if (isValid) { 730 | // check for the submissions_audit_trigger , if it exists then vote true on that otherwise do nothing 731 | const distributions_audit_trigger = 732 | taskAccountDataJSON.distributions_audit_trigger[round]; 733 | console.log( 734 | 'SUBMIT DISTRIBUTION AUDIT TRIGGER', 735 | distributions_audit_trigger, 736 | ); 737 | // console.log( 738 | // "CANDIDATE PUBKEY CHECK IN AUDIT TRIGGER", 739 | // distributions_audit_trigger[candidatePublicKey] 740 | // ); 741 | if ( 742 | distributions_audit_trigger && 743 | distributions_audit_trigger[candidatePublicKey] 744 | ) { 745 | console.log('VOTING TRUE ON DISTRIBUTION AUDIT'); 746 | const response = await this.distributionListAuditSubmission( 747 | candidateKeyPairPublicKey, 748 | isValid, 749 | submitterAccountKeyPair, 750 | round, 751 | ); 752 | console.log( 753 | 'RESPONSE FROM DISTRIBUTION AUDIT FUNCTION', 754 | response, 755 | ); 756 | } 757 | } else if (isValid == false) { 758 | // Call auditSubmission function and isValid is passed as false 759 | console.log('RAISING AUDIT / VOTING FALSE ON DISTRIBUTION'); 760 | const response = await this.distributionListAuditSubmission( 761 | candidateKeyPairPublicKey, 762 | isValid, 763 | submitterAccountKeyPair, 764 | round, 765 | ); 766 | console.log( 767 | 'RESPONSE FROM DISTRIBUTION AUDIT FUNCTION', 768 | response, 769 | ); 770 | } 771 | } catch (err) { 772 | console.log('ERROR IN ELSE CONDITION FOR DISTRIBUTION', err); 773 | } 774 | } 775 | } 776 | } 777 | } 778 | async getTaskLevelDBPath() { 779 | if (taskNodeAdministered) { 780 | return await genericHandler('getTaskLevelDBPath'); 781 | } else { 782 | return './KOIIDB'; 783 | } 784 | } 785 | async getBasePath() { 786 | if (taskNodeAdministered) { 787 | const basePath = (await namespaceWrapper.getTaskLevelDBPath()).replace( 788 | '/KOIIDB', 789 | '', 790 | ); 791 | return basePath; 792 | } else { 793 | return './'; 794 | } 795 | } 796 | getMainAccountPubkey() { 797 | if (taskNodeAdministered) { 798 | return MAIN_ACCOUNT_PUBKEY; 799 | } else { 800 | return this.#testingMainSystemAccount.publicKey.toBase58(); 801 | } 802 | } 803 | } 804 | 805 | async function genericHandler(...args) { 806 | try { 807 | let response = await axios.post(BASE_ROOT_URL, { 808 | args, 809 | taskId: TASK_ID, 810 | secret: SECRET_KEY, 811 | }); 812 | if (response.status == 200) return response.data.response; 813 | else { 814 | console.error(response.status, response.data); 815 | return null; 816 | } 817 | } catch (err) { 818 | console.error(`Error in genericHandler: "${args[0]}"`, err.message); 819 | console.error(err?.response?.data); 820 | return { error: err }; 821 | } 822 | } 823 | let connection; 824 | const namespaceWrapper = new NamespaceWrapper(); 825 | if (taskNodeAdministered) { 826 | namespaceWrapper.getRpcUrl().then(rpcUrl => { 827 | console.log(rpcUrl, 'RPC URL'); 828 | connection = new Connection(rpcUrl, 'confirmed'); 829 | }); 830 | } 831 | module.exports = { 832 | namespaceWrapper, 833 | taskNodeAdministered, // Boolean flag indicating that the task is being ran in active mode (Task node supervised), or development (testing) mode 834 | app, // The initialized express app to be used to register endpoints 835 | TASK_ID, // This will be the PORT on which the this task is expected to run the express server coming from the task node running this task. As all communication via the task node and this task will be done on this port. 836 | MAIN_ACCOUNT_PUBKEY, // This will be the secret used to authenticate with task node running this task. 837 | SECRET_KEY, // This will be the secret used by the task to authenticate with task node running this task. 838 | K2_NODE_URL, // This will be K2 url being used by the task node, possible values are 'https://k2-testnet.koii.live' | 'https://k2-devnet.koii.live' | 'http://localhost:8899' 839 | SERVICE_URL, // This will be public task node endpoint (Or local if it doesn't have any) of the task node running this task. 840 | STAKE, // This will be stake of the task node running this task, can be double checked with the task state and staking public key. 841 | TASK_NODE_PORT, // This will be the port used by task node as the express server port, so it can be used by the task for the communication with the task node 842 | _server, // Express server object 843 | }; 844 | -------------------------------------------------------------------------------- /hello-world/_koiiNode/koiiNode.js: -------------------------------------------------------------------------------- 1 | const { default: axios } = require("axios"); 2 | const { createHash } = require("crypto"); 3 | 4 | const { Connection, PublicKey, Keypair } = require("@_koi/web3.js"); 5 | 6 | const { KoiiStorageClient } = require("@_koii/storage-task-sdk"); 7 | const Datastore = require("nedb-promises"); 8 | const fsPromises = require("fs/promises"); 9 | const bs58 = require("bs58"); 10 | const nacl = require("tweetnacl"); 11 | const semver = require('semver'); 12 | /****************************************** init.js ***********************************/ 13 | 14 | const express = require("express"); 15 | // Only used for testing purposes, in production the env will be injected by tasknode 16 | require("dotenv").config(); 17 | const bodyParser = require("body-parser"); 18 | /** 19 | * This will be the name of the current task as coming from the task node running this task. 20 | */ 21 | const TASK_NAME = process.argv[2] || "Local"; 22 | /** 23 | * This will be the id of the current task as coming from the task node running this task. 24 | */ 25 | const TASK_ID = process.argv[3]; 26 | /** 27 | * This will be the PORT on which the this task is expected to run the express server coming from the task node running this task. 28 | * As all communication via the task node and this task will be done on this port. 29 | */ 30 | const EXPRESS_PORT = process.argv[4] || 10000; 31 | 32 | const LogLevel = { 33 | Log: "log", 34 | Warn: "warn", 35 | Error: "error", 36 | }; 37 | 38 | // Not used anymore 39 | // const NODE_MODE = process.argv[5]; 40 | 41 | /** 42 | * This will be the main account public key in string format of the task node running this task. 43 | */ 44 | const MAIN_ACCOUNT_PUBKEY = process.argv[6]; 45 | /** 46 | * This will be the secret used by the task to authenticate with task node running this task. 47 | */ 48 | const SECRET_KEY = process.argv[7]; 49 | /** 50 | * This will be K2 url being used by the task node, possible values are 'https://testnet.koii.network' | 'https://k2-devnet.koii.live' | 'http://localhost:8899' 51 | */ 52 | const K2_NODE_URL = process.argv[8] || "https://testnet.koii.network"; 53 | /** 54 | * This will be public task node endpoint (Or local if it doesn't have any) of the task node running this task. 55 | */ 56 | const SERVICE_URL = process.argv[9]; 57 | /** 58 | * This will be stake of the task node running this task, can be double checked with the task state and staking public key. 59 | */ 60 | const STAKE = Number(process.argv[10]); 61 | /** 62 | * This will be the port used by task node as the express server port, so it can be used by the task for the communication with the task node 63 | */ 64 | const TASK_NODE_PORT = Number(process.argv[11]); 65 | 66 | const app = express(); 67 | 68 | console.log("SETTING UP EXPRESS"); 69 | 70 | app.use(bodyParser.urlencoded({ extended: false })); 71 | 72 | app.use(bodyParser.json()); 73 | 74 | app.use((req, res, next) => { 75 | res.setHeader("Access-Control-Allow-Origin", "*"); 76 | res.setHeader( 77 | "Access-Control-Allow-Methods", 78 | "GET, POST, PUT, PATCH, DELETE" 79 | ); 80 | res.setHeader("Access-Control-Allow-Headers", "Content-Type"); 81 | res.setHeader("Access-Control-Allow-Credentials", false); 82 | if (req.method === "OPTIONS") 83 | // if is preflight(OPTIONS) then response status 204(NO CONTENT) 84 | return res.send(204); 85 | next(); 86 | }); 87 | 88 | app.get("/", (req, res) => { 89 | res.send("Hello World!"); 90 | }); 91 | 92 | const _server = app.listen(EXPRESS_PORT, () => { 93 | console.log(`${TASK_NAME} listening on port ${EXPRESS_PORT}`); 94 | }); 95 | 96 | /****************************************** NamespaceWrapper.js ***********************************/ 97 | 98 | const taskNodeAdministered = !!TASK_ID; 99 | const BASE_ROOT_URL = `http://localhost:${TASK_NODE_PORT}/namespace-wrapper`; 100 | let connection; 101 | 102 | class NamespaceWrapper { 103 | #db; 104 | #testingMainSystemAccount; 105 | #testingStakingSystemAccount; 106 | #testingTaskState; 107 | #testingDistributionList; 108 | 109 | constructor() { 110 | if (taskNodeAdministered) { 111 | this.initializeDB(); 112 | } else { 113 | this.#db = Datastore.create("./localKOIIDB.db"); 114 | this.defaultTaskSetup(); 115 | } 116 | } 117 | 118 | async initializeDB() { 119 | if (this.#db) return; 120 | try { 121 | if (taskNodeAdministered) { 122 | const path = await this.getTaskLevelDBPath(); 123 | this.#db = Datastore.create(path); 124 | } else { 125 | this.#db = Datastore.create("./localKOIIDB.db"); 126 | } 127 | } catch (e) { 128 | this.#db = Datastore.create(`../namespace/${TASK_ID}/KOIILevelDB.db`); 129 | } 130 | } 131 | 132 | async getDb() { 133 | if (this.#db) return this.#db; 134 | await this.initializeDB(); 135 | return this.#db; 136 | } 137 | /** 138 | * Namespace wrapper of storeGetAsync 139 | * @param {string} key // Path to get 140 | */ 141 | async storeGet(key) { 142 | try { 143 | await this.initializeDB(); 144 | const resp = await this.#db.findOne({ key: key }); 145 | if (resp) { 146 | return resp[key]; 147 | } else { 148 | return null; 149 | } 150 | } catch (e) { 151 | console.error(e); 152 | return null; 153 | } 154 | } 155 | /** 156 | * Namespace wrapper over storeSetAsync 157 | * @param {string} key Path to set 158 | * @param {*} value Data to set 159 | */ 160 | async storeSet(key, value) { 161 | try { 162 | await this.initializeDB(); 163 | await this.#db.update( 164 | { key: key }, 165 | { [key]: value, key }, 166 | { upsert: true } 167 | ); 168 | } catch (e) { 169 | console.error(e); 170 | return undefined; 171 | } 172 | } 173 | 174 | /** 175 | * Namespace wrapper over fsPromises methods 176 | * @param {*} method The fsPromise method to call 177 | * @param {*} path Path for the express call 178 | * @param {...any} args Remaining parameters for the FS call 179 | */ 180 | async fs(method, path, ...args) { 181 | if (taskNodeAdministered) { 182 | return await genericHandler("fs", method, path, ...args); 183 | } else { 184 | return fsPromises[method](`${path}`, ...args); 185 | } 186 | } 187 | async fsStaking(method, path, ...args) { 188 | if (taskNodeAdministered) { 189 | return await genericHandler("fsStaking", method, path, ...args); 190 | } else { 191 | return fsPromises[method](`${path}`, ...args); 192 | } 193 | } 194 | 195 | async fsWriteStream(imagepath) { 196 | if (taskNodeAdministered) { 197 | return await genericHandler("fsWriteStream", imagepath); 198 | } else { 199 | const writer = createWriteStream(imagepath); 200 | return writer; 201 | } 202 | } 203 | async fsReadStream(imagepath) { 204 | if (taskNodeAdministered) { 205 | return await genericHandler("fsReadStream", imagepath); 206 | } else { 207 | const file = readFileSync(imagepath); 208 | return file; 209 | } 210 | } 211 | 212 | /** 213 | * Namespace wrapper for getting current slots 214 | */ 215 | async getSlot() { 216 | if (taskNodeAdministered) { 217 | return await genericHandler("getCurrentSlot"); 218 | } else { 219 | return 100; 220 | } 221 | } 222 | 223 | async payloadSigning(body) { 224 | if (taskNodeAdministered) { 225 | return await genericHandler("signData", body); 226 | } else { 227 | const msg = new TextEncoder().encode(JSON.stringify(body)); 228 | const signedMessage = nacl.sign( 229 | msg, 230 | this.#testingMainSystemAccount.secretKey 231 | ); 232 | return await this.bs58Encode(signedMessage); 233 | } 234 | } 235 | 236 | async bs58Encode(data) { 237 | return bs58.encode( 238 | Buffer.from(data.buffer, data.byteOffset, data.byteLength) 239 | ); 240 | } 241 | 242 | async bs58Decode(data) { 243 | return new Uint8Array(bs58.decode(data)); 244 | } 245 | 246 | decodePayload(payload) { 247 | return new TextDecoder().decode(payload); 248 | } 249 | 250 | /** 251 | * Namespace wrapper of storeGetAsync 252 | * @param {string} signedMessage r // Path to get 253 | */ 254 | 255 | async verifySignature(signedMessage, pubKey) { 256 | if (taskNodeAdministered) { 257 | return await genericHandler("verifySignedData", signedMessage, pubKey); 258 | } else { 259 | try { 260 | const payload = nacl.sign.open( 261 | await this.bs58Decode(signedMessage), 262 | await this.bs58Decode(pubKey) 263 | ); 264 | if (!payload) return { error: "Invalid signature" }; 265 | return { data: this.decodePayload(payload) }; 266 | } catch (e) { 267 | console.error(e); 268 | return { error: `Verification failed: ${e}` }; 269 | } 270 | } 271 | } 272 | 273 | // async submissionOnChain(submitterKeypair, submission) { 274 | // return await genericHandler( 275 | // 'submissionOnChain', 276 | // submitterKeypair, 277 | // submission, 278 | // ); 279 | // } 280 | 281 | async stakeOnChain( 282 | taskStateInfoPublicKey, 283 | stakingAccKeypair, 284 | stakePotAccount, 285 | stakeAmount 286 | ) { 287 | if (taskNodeAdministered) { 288 | return await genericHandler( 289 | "stakeOnChain", 290 | taskStateInfoPublicKey, 291 | stakingAccKeypair, 292 | stakePotAccount, 293 | stakeAmount 294 | ); 295 | } else { 296 | this.#testingTaskState.stake_list[ 297 | this.#testingStakingSystemAccount.publicKey.toBase58() 298 | ] = stakeAmount; 299 | } 300 | } 301 | async claimReward(stakePotAccount, beneficiaryAccount, claimerKeypair) { 302 | if (!taskNodeAdministered) { 303 | console.log("Cannot call sendTransaction in testing mode"); 304 | return; 305 | } 306 | return await genericHandler( 307 | "claimReward", 308 | stakePotAccount, 309 | beneficiaryAccount, 310 | claimerKeypair 311 | ); 312 | } 313 | async sendTransaction(serviceNodeAccount, beneficiaryAccount, amount) { 314 | if (!taskNodeAdministered) { 315 | console.log("Cannot call sendTransaction in testing mode"); 316 | return; 317 | } 318 | return await genericHandler( 319 | "sendTransaction", 320 | serviceNodeAccount, 321 | beneficiaryAccount, 322 | amount 323 | ); 324 | } 325 | 326 | async getSubmitterAccount() { 327 | if (taskNodeAdministered) { 328 | const submitterAccountResp = await genericHandler("getSubmitterAccount"); 329 | return Keypair.fromSecretKey( 330 | Uint8Array.from(Object.values(submitterAccountResp._keypair.secretKey)) 331 | ); 332 | } else { 333 | return this.#testingStakingSystemAccount; 334 | } 335 | } 336 | 337 | /** 338 | * sendAndConfirmTransaction wrapper that injects mainSystemWallet as the first signer for paying the tx fees 339 | * @param {connection} method // Receive method ["get", "post", "put", "delete"] 340 | * @param {transaction} path // Endpoint path appended to namespace 341 | * @param {Function} callback // Callback function on traffic receive 342 | */ 343 | async sendAndConfirmTransactionWrapper(transaction, signers) { 344 | if (!taskNodeAdministered) { 345 | console.log("Cannot call sendTransaction in testing mode"); 346 | return; 347 | } 348 | const blockhash = (await connection.getRecentBlockhash("finalized")) 349 | .blockhash; 350 | transaction.recentBlockhash = blockhash; 351 | transaction.feePayer = new PublicKey(MAIN_ACCOUNT_PUBKEY); 352 | return await genericHandler( 353 | "sendAndConfirmTransactionWrapper", 354 | transaction.serialize({ 355 | requireAllSignatures: false, 356 | verifySignatures: false, 357 | }), 358 | signers 359 | ); 360 | } 361 | 362 | // async signArweave(transaction) { 363 | // let tx = await genericHandler('signArweave', transaction.toJSON()); 364 | // return arweave.transactions.fromRaw(tx); 365 | // } 366 | // async signEth(transaction) { 367 | // return await genericHandler('signEth', transaction); 368 | // } 369 | async getTaskState(options) { 370 | if (taskNodeAdministered) { 371 | const response = await genericHandler("getTaskState", options); 372 | if (response?.error) { 373 | console.log("Error in getting task state", response.error); 374 | return null; 375 | } 376 | return response; 377 | } else { 378 | return this.#testingTaskState; 379 | } 380 | } 381 | 382 | async logMessage(level, message) { 383 | switch (level) { 384 | case LogLevel.Log: 385 | console.log(message); 386 | break; 387 | case LogLevel.Warn: 388 | console.warn(message); 389 | break; 390 | case LogLevel.Error: 391 | console.error(message); 392 | break; 393 | default: 394 | console.log( 395 | `Invalid log level: ${level}. The log levels can be log, warn or error` 396 | ); 397 | return false; 398 | } 399 | return true; 400 | } 401 | 402 | /** 403 | * This logger function is used to log the task erros , warnings and logs on desktop-node 404 | * @param {level} enum // Receive method ["Log", "Warn", "Error"] 405 | enum LogLevel { 406 | Log = 'log', 407 | Warn = 'warn', 408 | Error = 'error', 409 | } 410 | * @param {message} string // log, error or warning message 411 | * @returns {boolean} // true if the message is logged successfully otherwise false 412 | */ 413 | 414 | async logger(level, message) { 415 | if (taskNodeAdministered) { 416 | return await genericHandler("logger", level, message); 417 | } else { 418 | return await this.logMessage(level, message); 419 | } 420 | } 421 | 422 | async auditSubmission(candidatePubkey, isValid, voterKeypair, round) { 423 | if (taskNodeAdministered) { 424 | return await genericHandler( 425 | "auditSubmission", 426 | candidatePubkey, 427 | isValid, 428 | round 429 | ); 430 | } else { 431 | if ( 432 | this.#testingTaskState.submissions_audit_trigger[round] && 433 | this.#testingTaskState.submissions_audit_trigger[round][candidatePubkey] 434 | ) { 435 | this.#testingTaskState.submissions_audit_trigger[round][ 436 | candidatePubkey 437 | ].votes.push({ 438 | is_valid: isValid, 439 | voter: voterKeypair.pubKey.toBase58(), 440 | slot: 100, 441 | }); 442 | } else { 443 | this.#testingTaskState.submissions_audit_trigger[round] = { 444 | [candidatePubkey]: { 445 | trigger_by: this.#testingStakingSystemAccount.publicKey.toBase58(), 446 | slot: 100, 447 | votes: [], 448 | }, 449 | }; 450 | } 451 | } 452 | } 453 | 454 | async distributionListAuditSubmission( 455 | candidatePubkey, 456 | isValid, 457 | voterKeypair, 458 | round 459 | ) { 460 | if (taskNodeAdministered) { 461 | return await genericHandler( 462 | "distributionListAuditSubmission", 463 | candidatePubkey, 464 | isValid, 465 | round 466 | ); 467 | } else { 468 | if ( 469 | this.#testingTaskState.distributions_audit_trigger[round] && 470 | this.#testingTaskState.distributions_audit_trigger[round][ 471 | candidatePubkey 472 | ] 473 | ) { 474 | this.#testingTaskState.distributions_audit_trigger[round][ 475 | candidatePubkey 476 | ].votes.push({ 477 | is_valid: isValid, 478 | voter: voterKeypair.pubKey.toBase58(), 479 | slot: 100, 480 | }); 481 | } else { 482 | this.#testingTaskState.distributions_audit_trigger[round] = { 483 | [candidatePubkey]: { 484 | trigger_by: this.#testingStakingSystemAccount.publicKey.toBase58(), 485 | slot: 100, 486 | votes: [], 487 | }, 488 | }; 489 | } 490 | } 491 | } 492 | 493 | async getRound() { 494 | if (taskNodeAdministered) { 495 | return await genericHandler("getRound"); 496 | } else { 497 | return 1; 498 | } 499 | } 500 | 501 | async payoutTrigger(round) { 502 | if (taskNodeAdministered) { 503 | return await genericHandler("payloadTrigger", round); 504 | } else { 505 | console.log( 506 | "Payout Trigger only handles possitive flows (Without audits)" 507 | ); 508 | let round = 1; 509 | const submissionValAcc = 510 | this.#testingDistributionList[round][ 511 | this.#testingStakingSystemAccount.toBase58() 512 | ].submission_value; 513 | this.#testingTaskState.available_balances = 514 | this.#testingDistributionList[round][submissionValAcc]; 515 | } 516 | } 517 | 518 | async uploadDistributionList(distributionList, round) { 519 | if (taskNodeAdministered) { 520 | return await genericHandler( 521 | "uploadDistributionList", 522 | distributionList, 523 | round 524 | ); 525 | } else { 526 | if (!this.#testingDistributionList[round]) 527 | this.#testingDistributionList[round] = {}; 528 | 529 | this.#testingDistributionList[round][ 530 | this.#testingStakingSystemAccount.publicKey.toBase58() 531 | ] = Buffer.from(JSON.stringify(distributionList)); 532 | return true; 533 | } 534 | } 535 | 536 | async distributionListSubmissionOnChain(round) { 537 | if (taskNodeAdministered) { 538 | return await genericHandler("distributionListSubmissionOnChain", round); 539 | } else { 540 | if (!this.#testingTaskState.distribution_rewards_submission[round]) 541 | this.#testingTaskState.distribution_rewards_submission[round] = {}; 542 | 543 | this.#testingTaskState.distribution_rewards_submission[round][ 544 | this.#testingStakingSystemAccount.publicKey.toBase58() 545 | ] = { 546 | submission_value: 547 | this.#testingStakingSystemAccount.publicKey.toBase58(), 548 | slot: 200, 549 | round: 1, 550 | }; 551 | } 552 | } 553 | 554 | async checkSubmissionAndUpdateRound(submissionValue = "default", round) { 555 | if (taskNodeAdministered) { 556 | return await genericHandler( 557 | "checkSubmissionAndUpdateRound", 558 | submissionValue, 559 | round 560 | ); 561 | } else { 562 | if (!this.#testingTaskState.submissions[round]) 563 | this.#testingTaskState.submissions[round] = {}; 564 | this.#testingTaskState.submissions[round][ 565 | this.#testingStakingSystemAccount.publicKey.toBase58() 566 | ] = { 567 | submission_value: submissionValue, 568 | slot: 100, 569 | round: 1, 570 | }; 571 | } 572 | } 573 | async getProgramAccounts() { 574 | if (taskNodeAdministered) { 575 | return await genericHandler("getProgramAccounts"); 576 | } else { 577 | console.log("Cannot call getProgramAccounts in testing mode"); 578 | } 579 | } 580 | async defaultTaskSetup() { 581 | if (taskNodeAdministered) { 582 | return await genericHandler("defaultTaskSetup"); 583 | } else { 584 | if (this.#testingTaskState) return; 585 | this.#testingMainSystemAccount = new Keypair(); 586 | this.#testingStakingSystemAccount = new Keypair(); 587 | this.#testingDistributionList = {}; 588 | this.#testingTaskState = { 589 | task_name: "DummyTestState", 590 | task_description: "Dummy Task state for testing flow", 591 | submissions: {}, 592 | submissions_audit_trigger: {}, 593 | total_bounty_amount: 10000000000, 594 | bounty_amount_per_round: 1000000000, 595 | total_stake_amount: 50000000000, 596 | minimum_stake_amount: 5000000000, 597 | available_balances: {}, 598 | stake_list: {}, 599 | round_time: 600, 600 | starting_slot: 0, 601 | audit_window: 200, 602 | submission_window: 200, 603 | distribution_rewards_submission: {}, 604 | distributions_audit_trigger: {}, 605 | }; 606 | } 607 | } 608 | async getRpcUrl() { 609 | if (taskNodeAdministered) { 610 | return await genericHandler("getRpcUrl"); 611 | } else { 612 | console.log("Cannot call getNodes in testing mode"); 613 | } 614 | } 615 | async getNodes(url) { 616 | if (taskNodeAdministered) { 617 | return await genericHandler("getNodes", url); 618 | } else { 619 | console.log("Cannot call getNodes in testing mode"); 620 | } 621 | } 622 | 623 | async getDistributionList(publicKey, round) { 624 | if (taskNodeAdministered) { 625 | const response = await genericHandler( 626 | "getDistributionList", 627 | publicKey, 628 | round 629 | ); 630 | if (response?.error) { 631 | return null; 632 | } 633 | return response; 634 | } else { 635 | const submissionValAcc = 636 | this.#testingTaskState.distribution_rewards_submission[round][ 637 | this.#testingStakingSystemAccount.publicKey.toBase58() 638 | ].submission_value; 639 | return this.#testingDistributionList[round][submissionValAcc]; 640 | } 641 | } 642 | 643 | async getTaskSubmissionInfo(round, forcefetch = false) { 644 | if (taskNodeAdministered) { 645 | const taskSubmissionInfo = await genericHandler( 646 | "getTaskSubmissionInfo", 647 | round, 648 | forcefetch, 649 | ); 650 | if (!taskSubmissionInfo || taskSubmissionInfo.error) { 651 | return null; 652 | } 653 | return taskSubmissionInfo; 654 | } else { 655 | return this.#testingTaskState; 656 | } 657 | } 658 | 659 | async validateAndVoteOnNodes(validate, round) { 660 | console.log("******/ IN VOTING /******"); 661 | 662 | let taskAccountDataJSON = null; 663 | try { 664 | taskAccountDataJSON = await this.getTaskSubmissionInfo(round); 665 | } catch (error) { 666 | console.error("Error in getting submissions for the round", error); 667 | } 668 | if (taskAccountDataJSON == null) { 669 | console.log("No submissions found for the round", round); 670 | return; 671 | } 672 | 673 | // console.log( 674 | // `Fetching the submissions of round ${round}`, 675 | // taskAccountDataJSON.submissions[round], 676 | // ); 677 | const submissions = taskAccountDataJSON.submissions[round]; 678 | if (submissions == null) { 679 | console.log(`No submisssions found in round ${round}`); 680 | return `No submisssions found in round ${round}`; 681 | } else { 682 | const keys = Object.keys(submissions); 683 | const values = Object.values(submissions); 684 | const size = values.length; 685 | // console.log('Submissions from last round: ', keys, values, size); 686 | let isValid; 687 | const submitterAccountKeyPair = await this.getSubmitterAccount(); 688 | const submitterPubkey = submitterAccountKeyPair.publicKey.toBase58(); 689 | for (let i = 0; i < size; i++) { 690 | let candidatePublicKey = keys[i]; 691 | // console.log('FOR CANDIDATE KEY', candidatePublicKey); 692 | let candidateKeyPairPublicKey = new PublicKey(keys[i]); 693 | if (candidatePublicKey == submitterPubkey) { 694 | console.log("YOU CANNOT VOTE ON YOUR OWN SUBMISSIONS"); 695 | } else { 696 | try { 697 | // console.log( 698 | // 'SUBMISSION VALUE TO CHECK', 699 | // values[i].submission_value, 700 | // ); 701 | isValid = await validate(values[i].submission_value, round); 702 | // console.log(`Voting ${isValid} to ${candidatePublicKey}`); 703 | 704 | if (isValid) { 705 | // check for the submissions_audit_trigger , if it exists then vote true on that otherwise do nothing 706 | const submissions_audit_trigger = 707 | taskAccountDataJSON.submissions_audit_trigger[round]; 708 | // console.log('SUBMIT AUDIT TRIGGER', submissions_audit_trigger); 709 | // console.log( 710 | // "CANDIDATE PUBKEY CHECK IN AUDIT TRIGGER", 711 | // submissions_audit_trigger[candidatePublicKey] 712 | // ); 713 | if ( 714 | submissions_audit_trigger && 715 | submissions_audit_trigger[candidatePublicKey] 716 | ) { 717 | console.log("VOTING TRUE ON AUDIT"); 718 | const response = await this.auditSubmission( 719 | candidateKeyPairPublicKey, 720 | isValid, 721 | submitterAccountKeyPair, 722 | round 723 | ); 724 | console.log("RESPONSE FROM AUDIT FUNCTION", response); 725 | } 726 | } else if (isValid == false) { 727 | // Call auditSubmission function and isValid is passed as false 728 | console.log("RAISING AUDIT / VOTING FALSE"); 729 | const response = await this.auditSubmission( 730 | candidateKeyPairPublicKey, 731 | isValid, 732 | submitterAccountKeyPair, 733 | round 734 | ); 735 | console.log("RESPONSE FROM AUDIT FUNCTION", response); 736 | } 737 | } catch (err) { 738 | console.log("ERROR IN ELSE CONDITION", err); 739 | } 740 | } 741 | } 742 | } 743 | } 744 | 745 | async getTaskDistributionInfo(round) { 746 | if (taskNodeAdministered) { 747 | const taskDistributionInfo = await genericHandler( 748 | "getTaskDistributionInfo", 749 | round 750 | ); 751 | if (!taskDistributionInfo || taskDistributionInfo.error) { 752 | return null; 753 | } 754 | return taskDistributionInfo; 755 | } else { 756 | return this.#testingTaskState; 757 | } 758 | } 759 | 760 | async validateAndVoteOnDistributionList( 761 | validateDistribution, 762 | round, 763 | isPreviousRoundFailed = false 764 | ) { 765 | // await this.checkVoteStatus(); 766 | console.log("******/ IN VOTING OF DISTRIBUTION LIST /******"); 767 | let tasknodeVersionSatisfied = false; 768 | const taskNodeVersion = await this.getTaskNodeVersion(); 769 | if (semver.gte(taskNodeVersion, "1.11.19")) { 770 | tasknodeVersionSatisfied = true; 771 | } 772 | let taskAccountDataJSON = null; 773 | try { 774 | taskAccountDataJSON = await this.getTaskDistributionInfo(round); 775 | } catch (error) { 776 | console.error("Error in getting distributions for the round", error); 777 | } 778 | if (taskAccountDataJSON == null) { 779 | console.log("No distribution submissions found for the round", round); 780 | return; 781 | } 782 | // console.log( 783 | // `Fetching the Distribution submissions of round ${round}`, 784 | // taskAccountDataJSON.distribution_rewards_submission[round], 785 | // ); 786 | const submissions = 787 | taskAccountDataJSON?.distribution_rewards_submission[round]; 788 | if ( 789 | submissions == null || 790 | submissions == undefined || 791 | submissions.length == 0 792 | ) { 793 | console.log(`No submisssions found in round ${round}`); 794 | return `No submisssions found in round ${round}`; 795 | } else { 796 | const keys = Object.keys(submissions); 797 | const values = Object.values(submissions); 798 | const size = values.length; 799 | // console.log( 800 | // 'Distribution Submissions from last round: ', 801 | // keys, 802 | // values, 803 | // size, 804 | // ); 805 | let isValid; 806 | const submitterAccountKeyPair = await this.getSubmitterAccount(); 807 | const submitterPubkey = submitterAccountKeyPair.publicKey.toBase58(); 808 | const selectedNode = await this.nodeSelectionDistributionList( 809 | round, 810 | isPreviousRoundFailed 811 | ); 812 | console.log("SELECTED NODE FOR AUDIT", selectedNode); 813 | if (selectedNode == submitterPubkey) { 814 | console.log("YOU CANNOT VOTE ON YOUR OWN DISTRIBUTION SUBMISSIONS"); 815 | return; 816 | } 817 | for (let i = 0; i < size; i++) { 818 | let candidatePublicKey = keys[i]; 819 | // console.log('FOR CANDIDATE KEY', candidatePublicKey); 820 | let candidateKeyPairPublicKey = new PublicKey(keys[i]); 821 | try { 822 | // console.log( 823 | // 'DISTRIBUTION SUBMISSION VALUE TO CHECK', 824 | // values[i].submission_value, 825 | // ); 826 | console.log("VOTING ON DISTRIBUTION LIST"); 827 | isValid = await validateDistribution( 828 | values[i].submission_value, 829 | round 830 | ); 831 | 832 | // console.log(`Voting ${isValid} to ${candidatePublicKey}`); 833 | 834 | if (isValid) { 835 | // check for the submissions_audit_trigger , if it exists then vote true on that otherwise do nothing 836 | const distributions_audit_trigger = 837 | taskAccountDataJSON.distributions_audit_trigger[round]; 838 | // console.log( 839 | // 'SUBMIT DISTRIBUTION AUDIT TRIGGER', 840 | // distributions_audit_trigger, 841 | // ); 842 | // console.log( 843 | // "CANDIDATE PUBKEY CHECK IN AUDIT TRIGGER", 844 | // distributions_audit_trigger[candidatePublicKey] 845 | // ); 846 | if ( 847 | distributions_audit_trigger && 848 | distributions_audit_trigger[candidatePublicKey] 849 | ) { 850 | console.log("VOTING TRUE ON DISTRIBUTION AUDIT"); 851 | const response = await this.distributionListAuditSubmission( 852 | candidateKeyPairPublicKey, 853 | isValid, 854 | submitterAccountKeyPair, 855 | round 856 | ); 857 | console.log( 858 | "RESPONSE FROM DISTRIBUTION AUDIT FUNCTION", 859 | response 860 | ); 861 | } 862 | } else if (isValid == false && tasknodeVersionSatisfied) { 863 | // Call auditSubmission function and isValid is passed as false 864 | console.log("RAISING AUDIT / VOTING FALSE ON DISTRIBUTION"); 865 | const response = await this.distributionListAuditSubmission( 866 | candidateKeyPairPublicKey, 867 | isValid, 868 | submitterAccountKeyPair, 869 | round 870 | ); 871 | console.log("RESPONSE FROM DISTRIBUTION AUDIT FUNCTION", response); 872 | // get logs 873 | const basepath = await this.getBasePath(); 874 | const logPath = `${basepath}/task.log`; 875 | 876 | const webhookLink = "https://hooks.slack.com/services/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"; 877 | // 878 | try{ 879 | const client = new KoiiStorageClient(undefined, undefined, true); 880 | const userStaking = await namespaceWrapper.getSubmitterAccount(); 881 | const fileUploadResponse = await client.uploadFile(`${logPath}`,userStaking); 882 | const cid_returned = fileUploadResponse.cid; 883 | const message = { 884 | text: `Audit raised on distribution list submission by ${submitterPubkey} for round ${round}. CID: ${cid_returned}`, 885 | }; 886 | axios.post(webhookLink, message); 887 | } catch (error) { 888 | console.log("Error in sending slack message", error); 889 | } 890 | } 891 | } catch (err) { 892 | console.log("ERROR IN ELSE CONDITION FOR DISTRIBUTION", err); 893 | } 894 | } 895 | } 896 | } 897 | async getTaskLevelDBPath() { 898 | if (taskNodeAdministered) { 899 | return await genericHandler("getTaskLevelDBPath"); 900 | } else { 901 | return "./KOIIDB"; 902 | } 903 | } 904 | async getBasePath() { 905 | if (taskNodeAdministered) { 906 | const basePath = (await namespaceWrapper.getTaskLevelDBPath()).replace( 907 | "/KOIIDB", 908 | "" 909 | ); 910 | return basePath; 911 | } else { 912 | return "./"; 913 | } 914 | } 915 | 916 | async getAverageSlotTime() { 917 | if (taskNodeAdministered) { 918 | try { 919 | return await genericHandler("getAverageSlotTime"); 920 | } catch (error) { 921 | console.error("Error getting average slot time", error); 922 | return 400; 923 | } 924 | } else { 925 | return 400; 926 | } 927 | } 928 | 929 | async getTaskNodeVersion() { 930 | if (taskNodeAdministered) { 931 | try { 932 | return await genericHandler("getTaskNodeVersion"); 933 | } catch (error) { 934 | console.error("Error getting task node version", error); 935 | return; 936 | } 937 | } else { 938 | return "1.11.19"; 939 | } 940 | } 941 | 942 | async nodeSelectionDistributionList(round, isPreviousFailed) { 943 | let taskAccountDataJSON = null; 944 | try { 945 | taskAccountDataJSON = await this.getTaskSubmissionInfo(round, true); 946 | } catch (error) { 947 | console.error("Task submission not found", error); 948 | return; 949 | } 950 | 951 | if (taskAccountDataJSON == null) { 952 | console.error("Task state not found"); 953 | return; 954 | } 955 | console.log("EXPECTED ROUND", round); 956 | 957 | const submissions = taskAccountDataJSON.submissions[round]; 958 | if (submissions == null) { 959 | console.log("No submisssions found in N-1 round"); 960 | return "No submisssions found in N-1 round"; 961 | } else { 962 | // getting last 3 submissions for the rounds 963 | let keys; 964 | const latestRounds = [round, round - 1, round - 2].filter((r) => r >= 0); 965 | 966 | const promises = latestRounds.map(async (r) => { 967 | if (r == round) { 968 | return new Set(Object.keys(submissions)); 969 | } else { 970 | let roundSubmissions = null; 971 | try { 972 | roundSubmissions = await this.getTaskSubmissionInfo(r, true); 973 | if (roundSubmissions && roundSubmissions.submissions[r]) { 974 | return new Set(Object.keys(roundSubmissions.submissions[r])); 975 | } 976 | } catch (error) { 977 | console.error("Error in getting submissions for the round", error); 978 | } 979 | return new Set(); 980 | } 981 | }); 982 | const keySets = await Promise.all(promises); 983 | 984 | // Find the keys present in all the rounds 985 | keys = 986 | keySets.length > 0 987 | ? [...keySets[0]].filter((key) => 988 | keySets.every((set) => set.has(key)) 989 | ) 990 | : []; 991 | if (keys.length == 0) { 992 | console.log("No common keys found in last 3 rounds"); 993 | keys = Object.keys(submissions); 994 | } 995 | const values = keys.map((key) => submissions[key]); 996 | let size = keys.length; 997 | console.log("Submissions from N-2 round: ", size); 998 | 999 | // Check the keys i.e if the submitter shall be excluded or not 1000 | 1001 | 1002 | try { 1003 | const distributionData = await this.getTaskDistributionInfo(round); 1004 | const audit_record = distributionData?.distributions_audit_record; 1005 | if (audit_record && audit_record[round] == 'PayoutFailed') { 1006 | console.log('ROUND DATA', audit_record[round]); 1007 | console.log( 1008 | 'SUBMITTER LIST', 1009 | distributionData.distribution_rewards_submission[round], 1010 | ); 1011 | const submitterList = 1012 | distributionData.distribution_rewards_submission[round]; 1013 | const submitterKeys = Object.keys(submitterList); 1014 | console.log('SUBMITTER KEYS', submitterKeys); 1015 | const submitterSize = submitterKeys.length; 1016 | console.log('SUBMITTER SIZE', submitterSize); 1017 | 1018 | for (let j = 0; j < submitterSize; j++) { 1019 | console.log('SUBMITTER KEY CANDIDATE', submitterKeys[j]); 1020 | const id = keys.indexOf(submitterKeys[j]); 1021 | console.log('ID', id); 1022 | if (id != -1) { 1023 | keys.splice(id, 1); 1024 | values.splice(id, 1); 1025 | size--; 1026 | } 1027 | } 1028 | 1029 | 1030 | console.log('KEYS FOR HASH CALC', keys.length); 1031 | } 1032 | } catch (error) { 1033 | console.log('Error in getting distribution data', error); 1034 | } 1035 | 1036 | // calculating the digest 1037 | 1038 | const ValuesString = JSON.stringify(values); 1039 | 1040 | const hashDigest = createHash("sha256") 1041 | .update(ValuesString) 1042 | .digest("hex"); 1043 | 1044 | // console.log('HASH DIGEST', hashDigest); 1045 | 1046 | // function to calculate the score 1047 | const calculateScore = (str = "") => { 1048 | return str.split("").reduce((acc, val) => { 1049 | return acc + val.charCodeAt(0); 1050 | }, 0); 1051 | }; 1052 | 1053 | // function to compare the ASCII values 1054 | 1055 | const compareASCII = (str1, str2) => { 1056 | const firstScore = calculateScore(str1); 1057 | const secondScore = calculateScore(str2); 1058 | return Math.abs(firstScore - secondScore); 1059 | }; 1060 | 1061 | // loop through the keys and select the one with higest score 1062 | 1063 | const selectedNode = { 1064 | score: 0, 1065 | pubkey: "", 1066 | }; 1067 | let score = 0; 1068 | if (isPreviousFailed) { 1069 | let leastScore = -Infinity; 1070 | let secondLeastScore = -Infinity; 1071 | for (let i = 0; i < size; i++) { 1072 | const candidateSubmissionJson = {}; 1073 | candidateSubmissionJson[keys[i]] = values[i]; 1074 | const candidateSubmissionString = JSON.stringify( 1075 | candidateSubmissionJson 1076 | ); 1077 | const candidateSubmissionHash = createHash("sha256") 1078 | .update(candidateSubmissionString) 1079 | .digest("hex"); 1080 | const candidateScore = compareASCII( 1081 | hashDigest, 1082 | candidateSubmissionHash 1083 | ); 1084 | if (candidateScore > leastScore) { 1085 | secondLeastScore = leastScore; 1086 | leastScore = candidateScore; 1087 | } else if (candidateScore > secondLeastScore) { 1088 | secondLeastScore = candidateScore; 1089 | selectedNode.score = candidateScore; 1090 | selectedNode.pubkey = keys[i]; 1091 | } 1092 | } 1093 | } else { 1094 | for (let i = 0; i < size; i++) { 1095 | const candidateSubmissionJson = {}; 1096 | candidateSubmissionJson[keys[i]] = values[i]; 1097 | const candidateSubmissionString = JSON.stringify( 1098 | candidateSubmissionJson 1099 | ); 1100 | const candidateSubmissionHash = createHash("sha256") 1101 | .update(candidateSubmissionString) 1102 | .digest("hex"); 1103 | const candidateScore = compareASCII( 1104 | hashDigest, 1105 | candidateSubmissionHash 1106 | ); 1107 | // console.log('CANDIDATE SCORE', candidateScore); 1108 | if (candidateScore > score) { 1109 | score = candidateScore; 1110 | selectedNode.score = candidateScore; 1111 | selectedNode.pubkey = keys[i]; 1112 | } 1113 | } 1114 | } 1115 | 1116 | // console.log('SELECTED NODE OBJECT', selectedNode); 1117 | return selectedNode.pubkey; 1118 | } 1119 | } 1120 | 1121 | async selectAndGenerateDistributionList( 1122 | submitDistributionList, 1123 | round, 1124 | isPreviousRoundFailed 1125 | ) { 1126 | console.log("SelectAndGenerateDistributionList called"); 1127 | const selectedNode = await this.nodeSelectionDistributionList( 1128 | round, 1129 | isPreviousRoundFailed 1130 | ); 1131 | console.log("Selected Node", selectedNode); 1132 | const submitPubKey = await this.getSubmitterAccount(); 1133 | if ( 1134 | selectedNode == undefined || 1135 | selectedNode == '' || 1136 | submitPubKey == undefined 1137 | ) 1138 | return; 1139 | if (selectedNode == submitPubKey?.publicKey.toBase58()) { 1140 | await submitDistributionList(round); 1141 | const taskState = await this.getTaskState({}); 1142 | if (taskState == null) { 1143 | console.error("Task state not found"); 1144 | return; 1145 | } 1146 | const avgSlotTime = await this.getAverageSlotTime(); 1147 | if (avgSlotTime == null) { 1148 | console.error("Avg slot time not found"); 1149 | return; 1150 | } 1151 | setTimeout(async () => { 1152 | await this.payoutTrigger(round); 1153 | }, (taskState.audit_window + taskState.submission_window) * avgSlotTime); 1154 | } 1155 | } 1156 | 1157 | getMainAccountPubkey() { 1158 | if (taskNodeAdministered) { 1159 | return MAIN_ACCOUNT_PUBKEY; 1160 | } else { 1161 | return this.#testingMainSystemAccount.publicKey.toBase58(); 1162 | } 1163 | } 1164 | } 1165 | 1166 | async function genericHandler(...args) { 1167 | try { 1168 | let response = await axios.post(BASE_ROOT_URL, { 1169 | args, 1170 | taskId: TASK_ID, 1171 | secret: SECRET_KEY, 1172 | }); 1173 | if (response.status == 200) return response.data.response; 1174 | else { 1175 | console.error(response.status, response.data); 1176 | return null; 1177 | } 1178 | } catch (err) { 1179 | const responseData = err?.response?.data?.message; 1180 | if ((args[0] === 'getTaskSubmissionInfo' || args[0] === 'getTaskDistributionInfo') && 1181 | responseData && typeof responseData === 'string' && responseData.includes('Task does not have any')) { 1182 | console.log(`Error in genericHandler: "${args[0]}"`, err.message); 1183 | console.log(err?.response?.data); 1184 | }else{ 1185 | console.error(`Error in genericHandler: "${args[0]}"`, err.message); 1186 | console.error(err?.response?.data); 1187 | return { error: err }; 1188 | } 1189 | } 1190 | } 1191 | 1192 | const namespaceWrapper = new NamespaceWrapper(); 1193 | if (taskNodeAdministered) { 1194 | namespaceWrapper.getRpcUrl().then((rpcUrl) => { 1195 | console.log(rpcUrl, "RPC URL"); 1196 | connection = new Connection(rpcUrl, "confirmed"); 1197 | }); 1198 | } 1199 | module.exports = { 1200 | namespaceWrapper, 1201 | taskNodeAdministered, // Boolean flag indicating that the task is being ran in active mode (Task node supervised), or development (testing) mode 1202 | app, // The initialized express app to be used to register endpoints 1203 | TASK_ID, // This will be the PORT on which the this task is expected to run the express server coming from the task node running this task. As all communication via the task node and this task will be done on this port. 1204 | MAIN_ACCOUNT_PUBKEY, // This will be the secret used to authenticate with task node running this task. 1205 | SECRET_KEY, // This will be the secret used by the task to authenticate with task node running this task. 1206 | K2_NODE_URL, // This will be K2 url being used by the task node, possible values are 'https://testnet.koii.network' | 'https://k2-devnet.koii.live' | 'http://localhost:8899' 1207 | SERVICE_URL, // This will be public task node endpoint (Or local if it doesn't have any) of the task node running this task. 1208 | STAKE, // This will be stake of the task node running this task, can be double checked with the task state and staking public key. 1209 | TASK_NODE_PORT, // This will be the port used by task node as the express server port, so it can be used by the task for the communication with the task node 1210 | _server, // Express server object 1211 | }; 1212 | --------------------------------------------------------------------------------