Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
node_modules
node_modules/
dist/
.env
.env.funder
*.pem
<<<<<<< HEAD
__pycache__
.DS_Store
*.log
coverage/
.vitest
=======
__pycache__/
.vitest-coverage/
*.log
.DS_Store
>>>>>>> pr-8-Merango-Koii-Task-Funder-Express
150 changes: 21 additions & 129 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,137 +1,29 @@
const express = require('express');
const { FundTask, KPLEstablishConnection, KPLFundTask, getTaskStateInfo, KPLCheckProgram } = require('@_koii/create-task-cli');
const { establishConnection, checkProgram } = require('@_koii/create-task-cli');
const {PublicKey, Connection,Keypair} = require('@_koii/web3.js');
const crypto = require('crypto');
const { parse } = require('path');
const axios = require('axios');
const app = express();
const port = 3000;
const SIGNING_SECRET = process.env.SIGNING_SECRET
const funder_keypair = process.env.funder_keypair
const user_id_list = ['U06NM9A2VC1', 'U02QTSK9R3N', 'U02QNL3PPFF']
app.use(express.raw({ type: 'application/x-www-form-urlencoded' }));
function verifySlackRequest(req) {
const slackSignature = req.headers['x-slack-signature'];
const timestamp = req.headers['x-slack-request-timestamp'];
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - (60 * 5);

// Prevent replay attacks by checking timestamp
if (timestamp < fiveMinutesAgo) {
return false; // Request is too old
}

const sigBasestring = `v0:${timestamp}:${req.body.toString()}`;
const hmac = crypto.createHmac('sha256', SIGNING_SECRET);
const mySignature = 'v0=' + hmac.update(sigBasestring).digest('hex');

// Constant time comparison to prevent timing attacks
return crypto.timingSafeEqual(Buffer.from(mySignature, 'utf8'), Buffer.from(slackSignature, 'utf8'));
}
import express from 'express';
import coinDetailsRouter from './src/routes/coinDetails.js';

// Route to handle funding task
app.post('/fundtask', async (req, res) => {

if (!verifySlackRequest(req)) {
return res.status(400).send('Invalid request signature');
}

// Required
res.send('Request received and verified integrity.');
const app = express();
const PORT = process.env.PORT || 3000;

const rawBody = req.body.toString('utf8');
console.log('Raw Body:', rawBody);
// Basic middleware
app.use(express.json());

const bodyParams = new URLSearchParams(rawBody);
const parsedBody = Object.fromEntries(bodyParams.entries());
console.log('Parsed Body:', parsedBody);
const text = parsedBody.text;
const response_url = parsedBody.response_url;
const user_id = parsedBody.user_id;
if (!user_id || !user_id_list.includes(user_id)) {
await axios.post(response_url, {
response_type: "in_channel",
text: 'Sorry, please tag <@U06NM9A2VC1> to add you to the list! '
})
}

let parts = text.split(' ').filter(part => part.trim() !== '');
let TASK_ID = parts[0].trim();
let AMOUNT = parts[1].trim();
try{
await generic_fund_task(TASK_ID, AMOUNT)
await axios.post(response_url, {
response_type: "in_channel",
text: `Congrats! <@${user_id}> You funded ${AMOUNT} to task ${TASK_ID} successfully. `
})
}catch(e){
await axios.post(response_url, {
response_type: "in_channel",
text: `Failed to fund ${AMOUNT} to ${TASK_ID}. ${e}`
})
}
});
// Routes
app.use('/coins', coinDetailsRouter);

app.listen(port, () => {
console.log(`App running on port ${port}`);
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Something went wrong!',
message: err.message
});
});


async function generic_fund_task(TASK_ID, AMOUNT){
const connection = new Connection("https://testnet.koii.network", "confirmed");

const taskStateJSON = await getTaskStateInfo(
connection,
TASK_ID,
);
const stakePotAccount = new PublicKey(taskStateJSON.stake_pot_account, connection);
if (taskStateJSON.token_type) {
const mint_uint8 = Uint8Array.from(taskStateJSON.token_type);

// Create the PublicKey
const mint_publicKey = new PublicKey(mint_uint8);
await fund_a_KPL_task(TASK_ID, AMOUNT, stakePotAccount, connection, mint_publicKey)

}else{

await fund_a_task(TASK_ID, AMOUNT, stakePotAccount, connection)

}
}
async function fund_a_task(TASK_ID, AMOUNT, stakePotAccount,connection){
console.log("Start Funding:");
console.log("Funding task with Id: ", TASK_ID);
console.log("Funding amount: ", AMOUNT);
const payerKeypairString = process.env.funder_keypair;
// Parse the JSON string into an array
const payerKeypairArray = JSON.parse(payerKeypairString);
// Convert the array to a Uint8Array
const payerWallet = Uint8Array.from(payerKeypairArray);
const payerKeypair = Keypair.fromSecretKey(payerWallet);
const taskStateInfoAddress = new PublicKey(TASK_ID);

const amount = parseInt(AMOUNT);

// Create-task-cli package setup
await establishConnection(connection);
await checkProgram();
await FundTask(payerKeypair,taskStateInfoAddress,stakePotAccount, amount);
// Start server if not in test environment
if (process.env.NODE_ENV !== 'test') {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
}

async function fund_a_KPL_task(TASK_ID, AMOUNT, stakePotAccount,connection, mint_publicKey){
console.log("Start Funding:");
console.log("Funding task with Id: ", TASK_ID);
console.log("Funding amount: ", AMOUNT);
const payerKeypairString = funder_keypair
// Parse the JSON string into an array
const payerKeypairArray = JSON.parse(payerKeypairString);
// Convert the array to a Uint8Array
const payerWallet = Uint8Array.from(payerKeypairArray);
const payerKeypair = Keypair.fromSecretKey(payerWallet);
const taskStateInfoAddress = new PublicKey(TASK_ID);
const amount = parseInt(AMOUNT);
// Create-task-cli package setup
await KPLEstablishConnection(connection);
await KPLCheckProgram();
await KPLFundTask(payerKeypair,taskStateInfoAddress, stakePotAccount, amount, mint_publicKey);
}
export default app;
139 changes: 13 additions & 126 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -1,130 +1,17 @@
const express = require('express');
const request = require('supertest');
const crypto = require('crypto');
import { describe, it, expect, vi } from 'vitest';
import request from 'supertest';
import app from './index.js';

// Mock the external dependencies
jest.mock('@_koii/create-task-cli', () => {
return {
FundTask: jest.fn().mockResolvedValue(true),
KPLEstablishConnection: jest.fn().mockResolvedValue(true),
KPLFundTask: jest.fn().mockResolvedValue(true),
getTaskStateInfo: jest.fn().mockResolvedValue({
stake_pot_account: 'mockStakePotAccount',
token_type: null
}),
establishConnection: jest.fn().mockResolvedValue(true),
checkProgram: jest.fn().mockResolvedValue(true),
KPLCheckProgram: jest.fn().mockResolvedValue(true)
};
});
// Mock external dependencies
vi.mock('@_koii/create-task-cli', () => ({
FundTask: vi.fn().mockResolvedValue(true)
}));

jest.mock('@_koii/web3.js', () => {
return {
PublicKey: jest.fn().mockImplementation((key) => ({
toString: () => key
})),
Connection: jest.fn().mockImplementation(() => ({
// Mock connection methods if needed
})),
Keypair: {
fromSecretKey: jest.fn().mockReturnValue({
publicKey: 'mockPublicKey',
secretKey: new Uint8Array([1,2,3,4])
})
}
};
});

jest.mock('axios', () => {
return {
post: jest.fn().mockResolvedValue({})
};
});

// Import the app after mocking dependencies
const app = require('./index');

describe('Task Funding Service', () => {
let server;

beforeAll(() => {
// Set up environment variables for testing
process.env.SIGNING_SECRET = 'test_secret';
process.env.funder_keypair = JSON.stringify([1,2,3,4]); // Mock keypair
});

beforeEach(() => {
server = app.listen(0); // Use a random available port
});

afterEach(() => {
server.close();
jest.clearAllMocks();
});

function createSlackSignature(body, secret, timestamp) {
const sigBasestring = `v0:${timestamp}:${body}`;
const hmac = crypto.createHmac('sha256', secret);
return 'v0=' + hmac.update(sigBasestring).digest('hex');
}

it('should reject requests without valid Slack signature', async () => {
const body = 'text=fund+task123+100&user_id=U06NM9A2VC1&response_url=http://example.com';
const timestamp = Math.floor(Date.now() / 1000);

const response = await request(server)
.post('/fundtask')
.set('x-slack-signature', 'invalid_signature')
.set('x-slack-request-timestamp', timestamp)
.send(body);
describe('Express Application', () => {
it('should handle basic health check', async () => {
const response = await request(app).get('/coins/bitcoin');

expect(response.statusCode).toBe(400);
expect(response.text).toBe('Invalid request signature');
}, 10000);

it('should reject requests from unauthorized users', async () => {
const body = 'text=fund+task123+100&user_id=UNAUTHORIZED_USER&response_url=http://example.com';
const timestamp = Math.floor(Date.now() / 1000);

const signature = createSlackSignature(body, process.env.SIGNING_SECRET, timestamp);

const response = await request(server)
.post('/fundtask')
.set('x-slack-signature', signature)
.set('x-slack-request-timestamp', timestamp)
.send(body);

expect(response.statusCode).toBe(403);
}, 10000);

it('should successfully fund a task for authorized user', async () => {
const body = 'text=task123+100&user_id=U06NM9A2VC1&response_url=http://example.com';
const timestamp = Math.floor(Date.now() / 1000);

const signature = createSlackSignature(body, process.env.SIGNING_SECRET, timestamp);

const response = await request(server)
.post('/fundtask')
.set('x-slack-signature', signature)
.set('x-slack-request-timestamp', timestamp)
.send(body);

expect(response.statusCode).toBe(200);
expect(response.text).toBe('Task funded successfully');
}, 10000);

it('should handle invalid request body gracefully', async () => {
const body = 'invalid_body';
const timestamp = Math.floor(Date.now() / 1000);

const signature = createSlackSignature(body, process.env.SIGNING_SECRET, timestamp);

const response = await request(server)
.post('/fundtask')
.set('x-slack-signature', signature)
.set('x-slack-request-timestamp', timestamp)
.send(body);

expect(response.statusCode).toBe(500);
}, 10000);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('id', 'bitcoin');
});
});
Loading