Skip to content

Commit 3059ab3

Browse files
authored
Merge pull request #539 from kleros/feat(web)/estuary-netlify-function
Feat(web): use netlify function to upload to IPFS
2 parents befde6c + e01c1a0 commit 3059ab3

File tree

7 files changed

+113
-5
lines changed

7 files changed

+113
-5
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,6 @@ tags
200200
# subgraph
201201
subgraph/generated/*
202202
subgraph/build/*
203+
204+
# Local Netlify folder
205+
.netlify

web/netlify/functions/uploadToIPFS.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
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 = "https://api.estuary.tech/content/add";
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();
22+
return {
23+
statusCode: response.status,
24+
body: JSON.stringify(parsedResponse),
25+
};
26+
}
27+
return {
28+
statusCode: 500,
29+
body: JSON.stringify({ message: "Invalid body format" }),
30+
};
31+
};

web/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"generate": "graphql-codegen"
3333
},
3434
"devDependencies": {
35+
"@netlify/functions": "^1.4.0",
3536
"@parcel/transformer-svg-react": "~2.7.0",
3637
"@parcel/watcher": "~2.0.0",
3738
"@types/react": "^18.0.25",
@@ -40,6 +41,7 @@
4041
"@typescript-eslint/eslint-plugin": "^5.43.0",
4142
"@typescript-eslint/parser": "^5.43.0",
4243
"@typescript-eslint/utils": "^5.43.0",
44+
"dotenv": "^16.0.3",
4345
"eslint": "^8.27.0",
4446
"eslint-config-prettier": "^8.3.0",
4547
"eslint-import-resolver-parcel": "^1.10.6",

web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx

+26-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Textarea, Button } from "@kleros/ui-components-library";
55
import { DisputeKitClassic } from "@kleros/kleros-v2-contracts/typechain-types/src/arbitration/dispute-kits/DisputeKitClassic";
66
import { wrapWithToast } from "utils/wrapWithToast";
77
import { useConnectedContract } from "hooks/useConnectedContract";
8+
import { uploadFormDataToIPFS } from "utils/uploadFormDataToIPFS";
89

910
const SubmitEvidenceModal: React.FC<{
1011
isOpen: boolean;
@@ -37,11 +38,21 @@ const SubmitEvidenceModal: React.FC<{
3738
disabled={isSending}
3839
onClick={() => {
3940
setIsSending(true);
40-
wrapWithToast(disputeKit.submitEvidence(evidenceGroup, message))
41-
.then(() => {
42-
setMessage("");
43-
close();
41+
const formData = constructEvidence(message);
42+
uploadFormDataToIPFS(formData)
43+
.then(async (res) => {
44+
const response = await res.json();
45+
if (res.status === 200) {
46+
const cid = "/ipfs/" + response["cid"];
47+
await wrapWithToast(
48+
disputeKit.submitEvidence(evidenceGroup, cid)
49+
).then(() => {
50+
setMessage("");
51+
close();
52+
});
53+
}
4454
})
55+
.catch()
4556
.finally(() => setIsSending(false));
4657
}}
4758
/>
@@ -50,6 +61,17 @@ const SubmitEvidenceModal: React.FC<{
5061
);
5162
};
5263

64+
const constructEvidence = (msg: string) => {
65+
const formData = new FormData();
66+
const file = new File(
67+
[JSON.stringify({ name: "Evidence", description: msg })],
68+
"evidence.json",
69+
{ type: "text/plain" }
70+
);
71+
formData.append("data", file, file.name);
72+
return formData;
73+
};
74+
5375
const StyledModal = styled(Modal)`
5476
position: absolute;
5577
top: 50%;

web/src/utils/uploadFormDataToIPFS.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { toast, ToastContentProps } from "react-toastify";
2+
import { OPTIONS } from "utils/wrapWithToast";
3+
import { FetchError } from "node-fetch";
4+
5+
interface RenderError extends ToastContentProps {
6+
data: FetchError;
7+
}
8+
9+
export function uploadFormDataToIPFS(formData: FormData): Promise<Response> {
10+
return toast.promise(
11+
new Promise((resolve, reject) =>
12+
fetch("/.netlify/functions/uploadToIPFS", {
13+
method: "POST",
14+
body: formData,
15+
}).then(async (response) =>
16+
response.status === 200
17+
? resolve(response)
18+
: reject({ message: (await response.json()).error.reason })
19+
)
20+
),
21+
{
22+
pending: "Uploading evidence to IPFS...",
23+
success: "Uploaded successfully!",
24+
error: {
25+
render({ data }: RenderError) {
26+
return `Upload failed: ${data.message}`;
27+
},
28+
},
29+
},
30+
OPTIONS
31+
);
32+
}

web/src/utils/wrapWithToast.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { toast, ToastPosition, Theme } from "react-toastify";
22
import { ContractTransaction } from "ethers";
33

4-
const OPTIONS = {
4+
export const OPTIONS = {
55
position: "top-center" as ToastPosition,
66
autoClose: 5000,
77
hideProgressBar: false,

yarn.lock

+18
Original file line numberDiff line numberDiff line change
@@ -2312,6 +2312,7 @@ __metadata:
23122312
"@graphql-codegen/typescript-operations": ^2.5.6
23132313
"@kleros/kleros-v2-contracts": "workspace:^"
23142314
"@kleros/ui-components-library": ^1.9.0
2315+
"@netlify/functions": ^1.4.0
23152316
"@parcel/transformer-svg-react": ~2.7.0
23162317
"@parcel/watcher": ~2.0.0
23172318
"@types/react": ^18.0.25
@@ -2327,6 +2328,7 @@ __metadata:
23272328
chart.js: ^3.9.1
23282329
chartjs-adapter-moment: ^1.0.0
23292330
core-js: ^3.21.1
2331+
dotenv: ^16.0.3
23302332
eslint: ^8.27.0
23312333
eslint-config-prettier: ^8.3.0
23322334
eslint-import-resolver-parcel: ^1.10.6
@@ -2516,6 +2518,15 @@ __metadata:
25162518
languageName: node
25172519
linkType: hard
25182520

2521+
"@netlify/functions@npm:^1.4.0":
2522+
version: 1.4.0
2523+
resolution: "@netlify/functions@npm:1.4.0"
2524+
dependencies:
2525+
is-promise: ^4.0.0
2526+
checksum: 0adcd967689d647bcc98f5b20eda858e765cdef7caf4c56e11845edad03e070640c372865a1b846f0d7de786a9a87784bab099972bee7157e7771ccf0df503fb
2527+
languageName: node
2528+
linkType: hard
2529+
25192530
"@noble/hashes@npm:1.1.2":
25202531
version: 1.1.2
25212532
resolution: "@noble/hashes@npm:1.1.2"
@@ -14400,6 +14411,13 @@ __metadata:
1440014411
languageName: node
1440114412
linkType: hard
1440214413

14414+
"is-promise@npm:^4.0.0":
14415+
version: 4.0.0
14416+
resolution: "is-promise@npm:4.0.0"
14417+
checksum: 0b46517ad47b00b6358fd6553c83ec1f6ba9acd7ffb3d30a0bf519c5c69e7147c132430452351b8a9fc198f8dd6c4f76f8e6f5a7f100f8c77d57d9e0f4261a8a
14418+
languageName: node
14419+
linkType: hard
14420+
1440314421
"is-promise@npm:~1, is-promise@npm:~1.0.0":
1440414422
version: 1.0.1
1440514423
resolution: "is-promise@npm:1.0.1"

0 commit comments

Comments
 (0)