Skip to content

Commit 2a86251

Browse files
authored
Merge pull request #971 from kleros/feat(web)/update-upload-to-ipfs-function
Feat(web): update uploadToIPFS netlify function
2 parents 45e26c2 + 5c0aa1d commit 2a86251

File tree

6 files changed

+1825
-51
lines changed

6 files changed

+1825
-51
lines changed

.github/workflows/contracts-testing.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
nodejs.org:443
3030
objects.githubusercontent.com:443
3131
registry.yarnpkg.com:443
32+
registry.npmjs.org:443
3233
3334
- name: Setup Node.js environment
3435
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"nanoid^3.3.1": "^3.3.4",
5656
"node-fetch": "^2.6.7",
5757
"underscore@npm^3.0.4": "^1.12.1",
58-
"eth-sig-util@npm:^1.4.2": "3.0.0"
58+
"eth-sig-util@npm:^1.4.2": "3.0.0",
59+
"fast-xml-parser": "^4.2.5"
5960
},
6061
"scripts": {
6162
"check-prerequisites": "scripts/check-prerequisites.sh",

web/netlify.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ YARN_ENABLE_GLOBAL_CACHE = "true"
99

1010
[functions]
1111
directory = "web/netlify/functions/"
12+
13+
[dev]
14+
framework = "parcel"

web/netlify/functions/uploadToIPFS.ts

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,109 @@
11
import { Handler } from "@netlify/functions";
2-
import fetch from "node-fetch";
3-
4-
const ESTUARI_API_KEY = process.env["ESTUARY_API_KEY"];
5-
const ESTUARI_URL = process.env["ESTUARY_GATEWAY"];
6-
7-
export const handler: Handler = async (event, context) => {
8-
context.callbackWaitsForEmptyEventLoop = false;
9-
if (event.body) {
10-
const newHeaders = event.headers;
11-
delete newHeaders.host;
12-
const response = await fetch(ESTUARI_URL, {
13-
method: "POST",
14-
headers: {
15-
Authorization: `Bearer ${ESTUARI_API_KEY}`,
16-
...newHeaders,
17-
},
18-
body: Buffer.from(event.body, "base64"),
19-
});
20-
21-
const parsedResponse = await response.json();
2+
import { File, FilebaseClient } from "@filebase/client";
3+
import amqp, { Connection } from "amqplib";
4+
import busboy from "busboy";
5+
6+
const { FILEBASE_TOKEN, RABBITMQ_URL, FILEBASE_API_WRAPPER } = process.env;
7+
const filebase = new FilebaseClient({ token: FILEBASE_TOKEN ?? "" });
8+
9+
type FormElement =
10+
| { isFile: true; filename: string; mimeType: string; content: Buffer }
11+
| { isFile: false; content: string };
12+
type FormData = { [key: string]: FormElement };
13+
14+
const emitRabbitMQLog = async (cid: string, operation: string) => {
15+
let connection: Connection | undefined;
16+
try {
17+
connection = await amqp.connect(RABBITMQ_URL ?? "");
18+
const channel = await connection.createChannel();
19+
20+
await channel.assertExchange("ipfs", "topic");
21+
channel.publish("ipfs", operation, Buffer.from(cid));
22+
23+
//eslint-disable-next-line no-console
24+
console.log(`Sent IPFS CID '${cid}' to exchange 'ipfs'`);
25+
} catch (err) {
26+
console.warn(err);
27+
} finally {
28+
if (typeof connection !== "undefined") await connection.close();
29+
}
30+
};
31+
32+
const parseMultipart = ({ headers, body, isBase64Encoded }) =>
33+
new Promise<FormData>((resolve, reject) => {
34+
const fields: FormData = {};
35+
36+
const bb = busboy({ headers });
37+
38+
bb.on("file", (name, file, { filename, mimeType }) =>
39+
file.on("data", (content) => {
40+
fields[name] = { isFile: true, filename, mimeType, content };
41+
})
42+
)
43+
.on("field", (name, value) => {
44+
if (value) fields[name] = { isFile: false, content: value };
45+
})
46+
.on("close", () => resolve(fields))
47+
.on("error", (err) => reject(err));
48+
49+
bb.write(body, isBase64Encoded ? "base64" : "binary");
50+
bb.end();
51+
});
52+
53+
const pinToFilebase = async (data: FormData, dapp: string, operation: string): Promise<Array<string>> => {
54+
const cids = new Array<string>();
55+
for (const [_, dataElement] of Object.entries(data)) {
56+
if (dataElement.isFile) {
57+
const { filename, mimeType, content } = dataElement;
58+
const path = `${filename}`;
59+
const cid = await filebase.storeDirectory([new File([content], path, { type: mimeType })]);
60+
await emitRabbitMQLog(cid, operation);
61+
cids.push(cid);
62+
}
63+
}
64+
65+
return cids;
66+
};
67+
68+
export const handler: Handler = async (event) => {
69+
const { queryStringParameters } = event;
70+
71+
if (
72+
!queryStringParameters ||
73+
!queryStringParameters.dapp ||
74+
!queryStringParameters.key ||
75+
!queryStringParameters.operation
76+
) {
77+
return {
78+
statusCode: 400,
79+
body: JSON.stringify({ message: "Invalid query parameters" }),
80+
};
81+
}
82+
83+
const { dapp, key, operation } = queryStringParameters;
84+
85+
if (key !== FILEBASE_API_WRAPPER) {
86+
return {
87+
statusCode: 403,
88+
body: JSON.stringify({ message: "Invalid API key" }),
89+
};
90+
}
91+
92+
try {
93+
const parsed = await parseMultipart(event);
94+
const cids = await pinToFilebase(parsed, dapp, operation);
95+
96+
return {
97+
statusCode: 200,
98+
body: JSON.stringify({
99+
message: "File has been stored successfully",
100+
cids,
101+
}),
102+
};
103+
} catch (err: any) {
22104
return {
23-
statusCode: response.status,
24-
body: JSON.stringify(parsedResponse),
105+
statusCode: 500,
106+
body: JSON.stringify({ message: err.message }),
25107
};
26108
}
27-
return {
28-
statusCode: 500,
29-
body: JSON.stringify({ message: "Invalid body format" }),
30-
};
31109
};

web/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"@netlify/functions": "^1.6.0",
4444
"@parcel/transformer-svg-react": "~2.8.0",
4545
"@parcel/watcher": "~2.1.0",
46+
"@types/amqplib": "^0.10.1",
47+
"@types/busboy": "^1.5.0",
4648
"@types/react": "^18.2.12",
4749
"@types/react-dom": "^18.2.5",
4850
"@types/styled-components": "^5.1.26",
@@ -59,13 +61,15 @@
5961
"typescript": "^4.9.5"
6062
},
6163
"dependencies": {
64+
"@filebase/client": "^0.0.4",
6265
"@kleros/kleros-v2-contracts": "workspace:^",
6366
"@kleros/ui-components-library": "^2.5.2",
6467
"@sentry/react": "^7.55.2",
6568
"@sentry/tracing": "^7.55.2",
6669
"@types/react-modal": "^3.16.0",
6770
"@web3modal/ethereum": "^2.2.2",
6871
"@web3modal/react": "^2.2.2",
72+
"amqplib": "^0.10.3",
6973
"chart.js": "^3.9.1",
7074
"chartjs-adapter-moment": "^1.0.1",
7175
"core-js": "^3.31.0",

0 commit comments

Comments
 (0)