Skip to content

Commit e1b6104

Browse files
committed
feat: task submittion
1 parent e2e9495 commit e1b6104

24 files changed

Lines changed: 998 additions & 157 deletions

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ deactivate-task-local:
4141
solve-task-local:
4242
forge script script/SolveTask.s.sol:SolveTask --rpc-url http://127.0.0.1:8545 --private-key ${PRIVATE_KEY_SOLVER} --broadcast
4343

44+
get-task:
45+
forge script script/GetTasks.s.sol:GetTasks --rpc-url http://127.0.0.1:8545 --private-key ${PRIVATE_KEY_SOLVER} --broadcast
46+
4447
# Run smart contract tests
4548
test:
4649
forge test

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"license": "ISC",
1313
"description": "",
1414
"dependencies": {
15+
"@fastify/multipart": "^9.0.3",
1516
"@fastify/swagger": "^9.5.1",
1617
"@fastify/swagger-ui": "^5.2.3",
1718
"@helia/http": "^2.2.1",

backend/src/app.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import Fastify from "fastify"
2+
import multipart from "@fastify/multipart"
23
import fastifySwagger from '@fastify/swagger'
34
import fastifySwaggerUi from '@fastify/swagger-ui'
45

56
import Db from './db/Db.ts'
67
import TaskBoardIPFS from './ipfs/TaskBoardIPFS.ts'
78
import EventListener from './listener/Listener.ts'
8-
import taskRoutes from './routes/taskRoutes.ts';
9+
import taskRoutes from './routes/taskRoutes.ts'
910

1011

1112
async function buildApp() {
1213
const app = Fastify({ logger: true })
1314

15+
// enable multipart
16+
await app.register(multipart, {
17+
attachFieldsToBody: false, // keep stream interface (needed for files)
18+
limits: {
19+
fileSize: 50 * 1024 * 1024 // optional: 50MB max per file
20+
}
21+
})
22+
1423
// Swagger for API documentation
1524
app.register(fastifySwagger, {
1625
swagger: {

backend/src/controllers/taskController.ts

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,78 @@
77
*/
88

99
import { type FastifyReply, type FastifyRequest } from 'fastify'
10-
import { type Static } from '@sinclair/typebox'
11-
import { TokenMetadataSchema } from '../schemas/taskSchema.ts'
1210
import TaskBoardIPFS from '../ipfs/TaskBoardIPFS.ts'
11+
import { Task } from '../../../objects/BoardTask.ts'
1312
import { TaskToken } from '../../../objects/TaskToken.ts'
13+
import dotenv from 'dotenv'
1414

15-
type TokenBody = Static<typeof TokenMetadataSchema>
16-
15+
dotenv.config()
1716

1817
export function createTaskController(IPFS: TaskBoardIPFS) {
19-
return async (request: FastifyRequest<{ Body: TokenBody }>, reply: FastifyReply) => {
18+
return async (request: FastifyRequest, reply: FastifyReply) => {
2019
try {
21-
const token = TaskToken.fromJSON(request.body)
22-
const cid = await IPFS.addTask(token)
23-
return reply.code(201).send({ ...token.toJSON(), createdAt: token.task.createdAt, ipfsCid: cid })
20+
// Fastify multipart: use await request.parts() for async iterator
21+
if (!request.isMultipart()) {
22+
return reply.code(400).send({ error: "Expected multipart/form-data" });
23+
}
24+
const parts = request.parts();
25+
let fields: any = {};
26+
let resourcesBuffer: any[] = [];
27+
let imageBuffer: any = null;
28+
29+
for await (const part of parts) {
30+
if (part.type === 'file') {
31+
if (part.fieldname === 'image') {
32+
imageBuffer = part.toBuffer();
33+
} else if (part.fieldname === 'resources') {
34+
resourcesBuffer.push(part.toBuffer());
35+
}
36+
} else {
37+
fields[part.fieldname] = part.value;
38+
}
39+
}
40+
41+
if (!imageBuffer) {
42+
return reply.code(400).send({ error: "Required token image", message: "Missing token image." });
43+
}
44+
45+
// Parse JSON fields
46+
const data = JSON.parse(fields.data);
47+
48+
// Upload resources to IPFS
49+
const resourceUris: string[] = [];
50+
for (const buffer of resourcesBuffer) {
51+
const cid = await IPFS.uploadBytes(new Uint8Array(buffer));
52+
resourceUris.push(`ipfs://${cid}`);
53+
}
54+
55+
// Upload image to IPFS
56+
let imageUri: string = "";
57+
const imageCid = await IPFS.uploadBytes(new Uint8Array(imageBuffer));
58+
imageUri = `ipfs://${imageCid}`;
59+
60+
61+
// Build Task and TaskToken
62+
const task = new Task({
63+
...data.task,
64+
resources: resourceUris,
65+
});
66+
const token = new TaskToken({
67+
...data.token,
68+
image: imageUri,
69+
task,
70+
});
71+
72+
// Upload TaskToken to IPFS
73+
const cid = await IPFS.addTask(token);
74+
console.log("Task uploaded: ", cid);
75+
return reply.code(201).send({ cid });
2476
} catch (error) {
25-
request.log.error(error)
26-
return reply.code(500).send({
77+
request.log.error(error);
78+
return reply.code(500).send({
2779
error: 'Failed to create task',
2880
message: 'Could not create task. Please try again later.'
29-
})
81+
});
3082
}
3183
}
3284
}
@@ -78,4 +130,12 @@ export function deleteTaskController(IPFS: TaskBoardIPFS) {
78130
})
79131
}
80132
}
133+
}
134+
135+
export function getContractAddressController() {
136+
return async (request: FastifyRequest, reply: FastifyReply) => {
137+
const address = process.env.CONTRACT_ADDRESS;
138+
if (!address) return reply.code(500).send({ error: "Contract address not set" });
139+
return { address };
140+
}
81141
}

backend/src/ipfs/TaskBoardIPFS.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,9 @@ export default class TaskBoardIPFS {
9696
// TODO: DB adjustments
9797
await this.ipfs.delete(cid)
9898
}
99+
100+
// Public wrapper for uploadBytes
101+
public async uploadBytes(data: Uint8Array): Promise<string> {
102+
return this.ipfs.uploadBytes(data)
103+
}
99104
}

backend/src/routes/taskRoutes.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
*/
88

99
import { type FastifyPluginAsync } from 'fastify';
10-
import { TokenMetadataSchema, TokenMetadataResponseSchema } from '../schemas/taskSchema.ts'
11-
import { createTaskController, deleteTaskController, getTasksController } from '../controllers/taskController.ts'
10+
import { TokenMetadataSchema, TokenCidResponseSchema, ContractAddressResponseSchema } from '../schemas/taskSchema.ts'
11+
import { createTaskController, deleteTaskController, getTasksController, getContractAddressController } from '../controllers/taskController.ts'
1212
import TaskBoardIPFS from '../ipfs/TaskBoardIPFS.ts'
13+
import dotenv from 'dotenv'
14+
15+
dotenv.config()
1316

1417
const taskRoutes: FastifyPluginAsync<{ ipfs: TaskBoardIPFS }> = async (app, opts) => {
1518
const { ipfs } = opts
@@ -39,10 +42,11 @@ const taskRoutes: FastifyPluginAsync<{ ipfs: TaskBoardIPFS }> = async (app, opts
3942
})
4043

4144
app.post('/tasks', {
45+
// Accept multipart/form-data, no schema here
4246
schema: {
43-
body: TokenMetadataSchema,
47+
consumes: ['multipart/form-data'],
4448
response: {
45-
201: TokenMetadataResponseSchema
49+
201: TokenCidResponseSchema
4650
}
4751
},
4852
handler: createTaskController(ipfs)
@@ -58,6 +62,14 @@ const taskRoutes: FastifyPluginAsync<{ ipfs: TaskBoardIPFS }> = async (app, opts
5862
},
5963
handler: deleteTaskController(ipfs)
6064
})
65+
66+
// Endpoint to get contract address
67+
app.get('/contract-address', {
68+
schema: {
69+
response: { 200: ContractAddressResponseSchema }
70+
},
71+
handler: getContractAddressController()
72+
})
6173
}
6274

6375
export default taskRoutes;

backend/src/schemas/taskSchema.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,18 @@ export const TokenMetadataResponseSchema = Type.Object({
4747
description: Type.String({ minLength: 0 }),
4848
image: Type.String({ minLength: 1 }), // e.g. "ipfs://..."
4949
task: TaskResponseSchema
50+
})
51+
52+
// ----------------------
53+
// Contract Address Schema
54+
// ----------------------
55+
export const TokenCidResponseSchema = Type.Object({
56+
cid: Type.String({ minLength: 1 })
57+
})
58+
59+
// ----------------------
60+
// Contract Address Schema
61+
// ----------------------
62+
export const ContractAddressResponseSchema = Type.Object({
63+
address: Type.String({ minLength: 1 })
5064
})

frontend/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,27 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"@fastify/multipart": "^9.0.3",
1314
"@fortawesome/fontawesome-svg-core": "^7.0.0",
1415
"@fortawesome/free-solid-svg-icons": "^7.0.0",
1516
"@fortawesome/react-fontawesome": "^3.0.1",
1617
"@helia/http": "^2.2.1",
1718
"@libp2p/interfaces": "^3.3.2",
1819
"@tanstack/react-query": "^5.85.5",
20+
"@types/circomlibjs": "^0.1.6",
21+
"buffer": "^6.0.3",
22+
"circomlibjs": "^0.1.2",
23+
"ethers": "^6.15.0",
24+
"events": "^3.3.0",
1925
"kubo-rpc-client": "^5.2.0",
26+
"process": "^0.11.10",
2027
"react": "^19.1.0",
21-
"react-dom": "^19.1.0"
28+
"react-dom": "^19.1.0",
29+
"util": "^0.12.5"
2230
},
2331
"devDependencies": {
32+
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
33+
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
2434
"@eslint/js": "^9.30.1",
2535
"@types/react": "^19.1.8",
2636
"@types/react-dom": "^19.1.6",

frontend/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import ThemeProvider from './context/ThemeContext';
22
import LoadingScreenProvider from './context/LoadingScreenContext';
33
import MainPage from './pages/MainPage';
44
import AlertProvider from './context/AlertContext';
5+
import WalletProvider from './context/WalletContext';
56

67
function App() {
78

89
return (
910
<ThemeProvider>
1011
<LoadingScreenProvider>
1112
<AlertProvider>
12-
<MainPage/>
13+
<WalletProvider>
14+
<MainPage/>
15+
</WalletProvider>
1316
</AlertProvider>
1417
</LoadingScreenProvider>
1518
</ThemeProvider>

frontend/src/components/Buttons.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type ButtonProps = {
99
export function FilledButton({ children, onClick, disabled }: ButtonProps) {
1010
return (
1111
<button
12+
type="button"
1213
className="button-filled"
1314
onClick={onClick}
1415
disabled={disabled}
@@ -21,6 +22,7 @@ export function FilledButton({ children, onClick, disabled }: ButtonProps) {
2122
export function EmptyButton({ children, onClick, disabled }: ButtonProps) {
2223
return (
2324
<button
25+
type="button"
2426
className="button-empty"
2527
onClick={onClick}
2628
disabled={disabled}

0 commit comments

Comments
 (0)