diff --git a/.gitignore b/.gitignore index c90683599..a775b0544 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,6 @@ tags # subgraph subgraph/generated/* subgraph/build/* + +# Local Netlify folder +.netlify diff --git a/web/netlify/functions/uploadToIPFS.ts b/web/netlify/functions/uploadToIPFS.ts new file mode 100644 index 000000000..e9b101545 --- /dev/null +++ b/web/netlify/functions/uploadToIPFS.ts @@ -0,0 +1,31 @@ +import { Handler } from "@netlify/functions"; +import fetch from "node-fetch"; + +const ESTUARI_API_KEY = process.env["ESTUARY_API_KEY"]; +const ESTUARI_URL = "https://api.estuary.tech/content/add"; + +export const handler: Handler = async (event, context) => { + context.callbackWaitsForEmptyEventLoop = false; + if (event.body) { + const newHeaders = event.headers; + delete newHeaders.host; + const response = await fetch(ESTUARI_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${ESTUARI_API_KEY}`, + ...newHeaders, + }, + body: Buffer.from(event.body, "base64"), + }); + + const parsedResponse = await response.json(); + return { + statusCode: response.status, + body: JSON.stringify(parsedResponse), + }; + } + return { + statusCode: 500, + body: JSON.stringify({ message: "Invalid body format" }), + }; +}; diff --git a/web/package.json b/web/package.json index a50c09147..61575fda8 100644 --- a/web/package.json +++ b/web/package.json @@ -32,6 +32,7 @@ "generate": "graphql-codegen" }, "devDependencies": { + "@netlify/functions": "^1.4.0", "@parcel/transformer-svg-react": "~2.7.0", "@parcel/watcher": "~2.0.0", "@types/react": "^18.0.25", @@ -40,6 +41,7 @@ "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "@typescript-eslint/utils": "^5.43.0", + "dotenv": "^16.0.3", "eslint": "^8.27.0", "eslint-config-prettier": "^8.3.0", "eslint-import-resolver-parcel": "^1.10.6", diff --git a/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx b/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx index 23527b9ff..64b22a583 100644 --- a/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx +++ b/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx @@ -5,6 +5,7 @@ import { Textarea, Button } from "@kleros/ui-components-library"; import { DisputeKitClassic } from "@kleros/kleros-v2-contracts/typechain-types/src/arbitration/dispute-kits/DisputeKitClassic"; import { wrapWithToast } from "utils/wrapWithToast"; import { useConnectedContract } from "hooks/useConnectedContract"; +import { uploadFormDataToIPFS } from "utils/uploadFormDataToIPFS"; const SubmitEvidenceModal: React.FC<{ isOpen: boolean; @@ -37,11 +38,21 @@ const SubmitEvidenceModal: React.FC<{ disabled={isSending} onClick={() => { setIsSending(true); - wrapWithToast(disputeKit.submitEvidence(evidenceGroup, message)) - .then(() => { - setMessage(""); - close(); + const formData = constructEvidence(message); + uploadFormDataToIPFS(formData) + .then(async (res) => { + const response = await res.json(); + if (res.status === 200) { + const cid = "/ipfs/" + response["cid"]; + await wrapWithToast( + disputeKit.submitEvidence(evidenceGroup, cid) + ).then(() => { + setMessage(""); + close(); + }); + } }) + .catch() .finally(() => setIsSending(false)); }} /> @@ -50,6 +61,17 @@ const SubmitEvidenceModal: React.FC<{ ); }; +const constructEvidence = (msg: string) => { + const formData = new FormData(); + const file = new File( + [JSON.stringify({ name: "Evidence", description: msg })], + "evidence.json", + { type: "text/plain" } + ); + formData.append("data", file, file.name); + return formData; +}; + const StyledModal = styled(Modal)` position: absolute; top: 50%; diff --git a/web/src/utils/uploadFormDataToIPFS.ts b/web/src/utils/uploadFormDataToIPFS.ts new file mode 100644 index 000000000..9432bfa26 --- /dev/null +++ b/web/src/utils/uploadFormDataToIPFS.ts @@ -0,0 +1,32 @@ +import { toast, ToastContentProps } from "react-toastify"; +import { OPTIONS } from "utils/wrapWithToast"; +import { FetchError } from "node-fetch"; + +interface RenderError extends ToastContentProps { + data: FetchError; +} + +export function uploadFormDataToIPFS(formData: FormData): Promise { + return toast.promise( + new Promise((resolve, reject) => + fetch("/.netlify/functions/uploadToIPFS", { + method: "POST", + body: formData, + }).then(async (response) => + response.status === 200 + ? resolve(response) + : reject({ message: (await response.json()).error.reason }) + ) + ), + { + pending: "Uploading evidence to IPFS...", + success: "Uploaded successfully!", + error: { + render({ data }: RenderError) { + return `Upload failed: ${data.message}`; + }, + }, + }, + OPTIONS + ); +} diff --git a/web/src/utils/wrapWithToast.ts b/web/src/utils/wrapWithToast.ts index ef6df0e11..da5aab494 100644 --- a/web/src/utils/wrapWithToast.ts +++ b/web/src/utils/wrapWithToast.ts @@ -1,7 +1,7 @@ import { toast, ToastPosition, Theme } from "react-toastify"; import { ContractTransaction } from "ethers"; -const OPTIONS = { +export const OPTIONS = { position: "top-center" as ToastPosition, autoClose: 5000, hideProgressBar: false, diff --git a/yarn.lock b/yarn.lock index 21904f29f..69d42b7db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2312,6 +2312,7 @@ __metadata: "@graphql-codegen/typescript-operations": ^2.5.6 "@kleros/kleros-v2-contracts": "workspace:^" "@kleros/ui-components-library": ^1.9.0 + "@netlify/functions": ^1.4.0 "@parcel/transformer-svg-react": ~2.7.0 "@parcel/watcher": ~2.0.0 "@types/react": ^18.0.25 @@ -2327,6 +2328,7 @@ __metadata: chart.js: ^3.9.1 chartjs-adapter-moment: ^1.0.0 core-js: ^3.21.1 + dotenv: ^16.0.3 eslint: ^8.27.0 eslint-config-prettier: ^8.3.0 eslint-import-resolver-parcel: ^1.10.6 @@ -2516,6 +2518,15 @@ __metadata: languageName: node linkType: hard +"@netlify/functions@npm:^1.4.0": + version: 1.4.0 + resolution: "@netlify/functions@npm:1.4.0" + dependencies: + is-promise: ^4.0.0 + checksum: 0adcd967689d647bcc98f5b20eda858e765cdef7caf4c56e11845edad03e070640c372865a1b846f0d7de786a9a87784bab099972bee7157e7771ccf0df503fb + languageName: node + linkType: hard + "@noble/hashes@npm:1.1.2": version: 1.1.2 resolution: "@noble/hashes@npm:1.1.2" @@ -14400,6 +14411,13 @@ __metadata: languageName: node linkType: hard +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 0b46517ad47b00b6358fd6553c83ec1f6ba9acd7ffb3d30a0bf519c5c69e7147c132430452351b8a9fc198f8dd6c4f76f8e6f5a7f100f8c77d57d9e0f4261a8a + languageName: node + linkType: hard + "is-promise@npm:~1, is-promise@npm:~1.0.0": version: 1.0.1 resolution: "is-promise@npm:1.0.1"