From 3d217dafe6280b3beaab32264ddabbc6073c4ee9 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Thu, 12 Sep 2024 16:53:47 +0530 Subject: [PATCH 1/2] feat(web): atlas-upload-to-ipfs-integration --- web/netlify/functions/uploadToIPFS.ts | 101 ------------------ web/netlify/middleware/authMiddleware.ts | 37 ------- web/src/context/AtlasProvider.tsx | 36 ++++++- .../Evidence/SubmitEvidenceModal.tsx | 49 ++++----- web/src/pages/Resolver/Policy/index.tsx | 15 ++- web/src/utils/atlas/index.ts | 1 + web/src/utils/atlas/uploadToIpfs.ts | 70 ++++++++++++ 7 files changed, 137 insertions(+), 172 deletions(-) delete mode 100644 web/netlify/functions/uploadToIPFS.ts delete mode 100644 web/netlify/middleware/authMiddleware.ts create mode 100644 web/src/utils/atlas/uploadToIpfs.ts diff --git a/web/netlify/functions/uploadToIPFS.ts b/web/netlify/functions/uploadToIPFS.ts deleted file mode 100644 index e59ccfcb7..000000000 --- a/web/netlify/functions/uploadToIPFS.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { File, FilebaseClient } from "@filebase/client"; -import middy from "@middy/core"; -import amqp, { Connection } from "amqplib"; -import busboy from "busboy"; - -import { authMiddleware } from "../middleware/authMiddleware"; - -const { FILEBASE_TOKEN, RABBITMQ_URL } = process.env; -const filebase = new FilebaseClient({ token: FILEBASE_TOKEN ?? "" }); - -type FormElement = - | { isFile: true; filename: string; mimeType: string; content: Buffer } - | { isFile: false; content: string }; -type FormData = { [key: string]: FormElement }; - -const emitRabbitMQLog = async (cid: string, operation: string) => { - let connection: Connection | undefined; - try { - connection = await amqp.connect(RABBITMQ_URL ?? ""); - const channel = await connection.createChannel(); - - await channel.assertExchange("ipfs", "topic"); - channel.publish("ipfs", operation, Buffer.from(cid)); - - //eslint-disable-next-line no-console - console.log(`Sent IPFS CID '${cid}' to exchange 'ipfs'`); - } catch (err) { - console.warn(err); - } finally { - if (typeof connection !== "undefined") await connection.close(); - } -}; - -const parseMultipart = ({ headers, body, isBase64Encoded }) => - new Promise((resolve, reject) => { - const fields: FormData = {}; - - const bb = busboy({ headers }); - - bb.on("file", (name, file, { filename, mimeType }) => - file.on("data", (content) => { - fields[name] = { isFile: true, filename, mimeType, content }; - }) - ) - .on("field", (name, value) => { - if (value) fields[name] = { isFile: false, content: value }; - }) - .on("close", () => resolve(fields)) - .on("error", (err) => reject(err)); - - bb.write(body, isBase64Encoded ? "base64" : "binary"); - bb.end(); - }); - -const pinToFilebase = async (data: FormData, operation: string): Promise> => { - const cids = new Array(); - for (const [_, dataElement] of Object.entries(data)) { - if (dataElement.isFile) { - const { filename, mimeType, content } = dataElement; - const path = `${filename}`; - const cid = await filebase.storeDirectory([new File([content], path, { type: mimeType })]); - await emitRabbitMQLog(cid, operation); - cids.push(`ipfs://${cid}/${path}`); - } - } - - return cids; -}; - -export const uploadToIPFS = async (event) => { - const { queryStringParameters } = event; - - if (!queryStringParameters?.operation) { - return { - statusCode: 400, - body: JSON.stringify({ message: "Invalid query parameters" }), - }; - } - - const { operation } = queryStringParameters; - - try { - const parsed = await parseMultipart(event); - const cids = await pinToFilebase(parsed, operation); - - return { - statusCode: 200, - body: JSON.stringify({ - message: "File has been stored successfully", - cids, - }), - }; - } catch (err: any) { - return { - statusCode: 500, - body: JSON.stringify({ message: err.message }), - }; - } -}; - -export const handler = middy(uploadToIPFS).use(authMiddleware()); diff --git a/web/netlify/middleware/authMiddleware.ts b/web/netlify/middleware/authMiddleware.ts deleted file mode 100644 index d54d7f9bd..000000000 --- a/web/netlify/middleware/authMiddleware.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as jwt from "jose"; - -export const authMiddleware = () => { - return { - before: async (request) => { - const { event } = request; - - const authToken = event?.headers?.["x-auth-token"]; - if (!authToken) { - return { - statusCode: 400, - body: JSON.stringify({ message: `Error : Missing x-auth-token in Header}` }), - }; - } - - try { - const secret = process.env.JWT_SECRET; - - if (!secret) { - throw new Error("Secret not set in environment"); - } - - const encodedSecret = new TextEncoder().encode(secret); - - const { payload } = await jwt.jwtVerify(authToken, encodedSecret); - - // add auth details to event - request.event.auth = payload; - } catch (err) { - return { - statusCode: 401, - body: JSON.stringify({ message: `Error : ${err?.message ?? "Not Authorised"}` }), - }; - } - }, - }; -}; diff --git a/web/src/context/AtlasProvider.tsx b/web/src/context/AtlasProvider.tsx index b4b707f3c..caf6f8708 100644 --- a/web/src/context/AtlasProvider.tsx +++ b/web/src/context/AtlasProvider.tsx @@ -13,6 +13,7 @@ import { addUser as addUserToAtlas, fetchUser, updateUser as updateUserInAtlas, + uploadToIpfs, type User, type AddUserData, type UpdateUserData, @@ -26,11 +27,13 @@ interface IAtlasProvider { isAddingUser: boolean; isFetchingUser: boolean; isUpdatingUser: boolean; + isUploadingFile: boolean; user: User | undefined; userExists: boolean; authoriseUser: () => void; addUser: (userSettings: AddUserData) => Promise; updateUser: (userSettings: UpdateUserData) => Promise; + uploadFile: (file: File) => Promise; } const Context = createContext(undefined); @@ -49,6 +52,7 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) = const [isAddingUser, setIsAddingUser] = useState(false); const [isUpdatingUser, setIsUpdatingUser] = useState(false); const [isVerified, setIsVerified] = useState(false); + const [isUploadingFile, setIsUploadingFile] = useState(false); const { signMessageAsync } = useSignMessage(); const atlasGqlClient = useMemo(() => { @@ -123,7 +127,7 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) = // this would change based on the fields we have and what defines a user to be existing const userExists = useMemo(() => { if (!user) return false; - return user.email ? true : false; + return !isUndefined(user.email); }, [user]); /** @@ -200,6 +204,32 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) = [address, isVerified, setIsUpdatingUser, atlasGqlClient, refetchUser] ); + /** + * @description upload file to ipfs + * @param {File} file - file to be uploaded + * @returns {Promise} A promise that resolves to the ipfs cid if file was uploaded successfully else + * null + */ + const uploadFile = useCallback( + async (file: File) => { + try { + if (!address || !isVerified) return null; + setIsUploadingFile(true); + + const hash = await uploadToIpfs(atlasGqlClient, file); + + return hash ? `/ipfs/${hash}` : null; + } catch (err: any) { + // eslint-disable-next-line + console.log("Upload File Error : ", err?.message); + return null; + } finally { + setIsUploadingFile(false); + } + }, + [address, isVerified, setIsUploadingFile, atlasGqlClient] + ); + return ( = ({ children }) = updateUser, isUpdatingUser, userExists, + isUploadingFile, + uploadFile, }), [ isVerified, @@ -226,6 +258,8 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) = updateUser, isUpdatingUser, userExists, + isUploadingFile, + uploadFile, ] )} > diff --git a/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx b/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx index 24e74f74d..c70e6addf 100644 --- a/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx +++ b/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx @@ -7,8 +7,8 @@ import { useWalletClient, usePublicClient, useConfig } from "wagmi"; import { Textarea, Button, FileUploader } from "@kleros/ui-components-library"; +import { useAtlasProvider } from "context/AtlasProvider"; import { simulateEvidenceModuleSubmitEvidence } from "hooks/contracts/generated"; -import { uploadFormDataToIPFS } from "utils/uploadFormDataToIPFS"; import { wrapWithToast, OPTIONS as toastOptions } from "utils/wrapWithToast"; import EnsureAuth from "components/EnsureAuth"; @@ -61,23 +61,28 @@ const SubmitEvidenceModal: React.FC<{ const [isSending, setIsSending] = useState(false); const [message, setMessage] = useState(""); const [file, setFile] = useState(); + const { uploadFile } = useAtlasProvider(); const submitEvidence = useCallback(async () => { - setIsSending(true); - const evidenceJSON = await constructEvidence(message, file); - - const { request } = await simulateEvidenceModuleSubmitEvidence(wagmiConfig, { - args: [BigInt(evidenceGroup), JSON.stringify(evidenceJSON)], - }); - - if (!walletClient) return; - await wrapWithToast(async () => await walletClient.writeContract(request), publicClient) - .then(() => { - setMessage(""); - close(); - }) - .finally(() => setIsSending(false)); - }, [publicClient, wagmiConfig, walletClient, close, evidenceGroup, file, message, setIsSending]); + try { + setIsSending(true); + const evidenceJSON = await constructEvidence(uploadFile, message, file); + + const { request } = await simulateEvidenceModuleSubmitEvidence(wagmiConfig, { + args: [BigInt(evidenceGroup), JSON.stringify(evidenceJSON)], + }); + + if (!walletClient || !publicClient) return; + await wrapWithToast(async () => await walletClient.writeContract(request), publicClient) + .then(() => { + setMessage(""); + close(); + }) + .finally(() => setIsSending(false)); + } catch { + setIsSending(false); + } + }, [publicClient, wagmiConfig, walletClient, close, evidenceGroup, file, message, setIsSending, uploadFile]); return ( @@ -96,16 +101,12 @@ const SubmitEvidenceModal: React.FC<{ ); }; -const constructEvidence = async (msg: string, file?: File) => { - let fileURI: string | undefined = undefined; +const constructEvidence = async (uploadFile: (file: File) => Promise, msg: string, file?: File) => { + let fileURI: string | null = null; if (file) { toast.info("Uploading to IPFS", toastOptions); - const fileFormData = new FormData(); - fileFormData.append("data", file, file.name); - fileURI = await uploadFormDataToIPFS(fileFormData).then(async (res) => { - const response = await res.json(); - return response["cids"][0]; - }); + fileURI = await uploadFile(file); + if (!fileURI) throw new Error("Error uploading evidence file"); } return { name: "Evidence", description: msg, fileURI }; }; diff --git a/web/src/pages/Resolver/Policy/index.tsx b/web/src/pages/Resolver/Policy/index.tsx index 49b54bcd1..bd7dcd272 100644 --- a/web/src/pages/Resolver/Policy/index.tsx +++ b/web/src/pages/Resolver/Policy/index.tsx @@ -5,8 +5,8 @@ import { toast } from "react-toastify"; import { FileUploader } from "@kleros/ui-components-library"; +import { useAtlasProvider } from "context/AtlasProvider"; import { useNewDisputeContext } from "context/NewDisputeContext"; -import { uploadFormDataToIPFS } from "utils/uploadFormDataToIPFS"; import { OPTIONS as toastOptions } from "utils/wrapWithToast"; import { landscapeStyle } from "styles/landscapeStyle"; @@ -51,19 +51,16 @@ const StyledFileUploader = styled(FileUploader)` const Policy: React.FC = () => { const { disputeData, setDisputeData, setIsPolicyUploading } = useNewDisputeContext(); + const { uploadFile } = useAtlasProvider(); const handleFileUpload = (file: File) => { setIsPolicyUploading(true); toast.info("Uploading Policy to IPFS", toastOptions); - const fileFormData = new FormData(); - fileFormData.append("data", file, file.name); - - uploadFormDataToIPFS(fileFormData, "policy") - .then(async (res) => { - const response = await res.json(); - const policyURI = response["cids"][0]; - setDisputeData({ ...disputeData, policyURI }); + uploadFile(file) + .then(async (cid) => { + if (!cid) return; + setDisputeData({ ...disputeData, policyURI: cid }); }) .catch((err) => console.log(err)) .finally(() => setIsPolicyUploading(false)); diff --git a/web/src/utils/atlas/index.ts b/web/src/utils/atlas/index.ts index c98225989..7c9b5b0c8 100644 --- a/web/src/utils/atlas/index.ts +++ b/web/src/utils/atlas/index.ts @@ -4,3 +4,4 @@ export * from "./createMessage"; export * from "./addUser"; export * from "./fetchUser"; export * from "./updateUser"; +export * from "./uploadToIpfs"; diff --git a/web/src/utils/atlas/uploadToIpfs.ts b/web/src/utils/atlas/uploadToIpfs.ts new file mode 100644 index 000000000..d3caefb3a --- /dev/null +++ b/web/src/utils/atlas/uploadToIpfs.ts @@ -0,0 +1,70 @@ +import { gql, type GraphQLClient } from "graphql-request"; +import { toast } from "react-toastify"; + +import { OPTIONS } from "utils/wrapWithToast"; + +export async function uploadToIpfs(client: GraphQLClient, file: File): Promise { + const presignedUrl = await getPreSignedUrl(client, file.name); + + return toast.promise( + fetch(presignedUrl, { + method: "PUT", + body: file, + }).then(async (response) => { + if (response.status !== 200) { + const error = await response.json().catch(() => ({ message: "Error uploading to IPFS" })); + throw new Error(error.message); + } + return response.headers.get("x-amz-meta-cid"); + }), + { + pending: `Uploading to IPFS...`, + success: "Uploaded successfully!", + error: { + render({ data: error }) { + return `Upload failed: ${error?.message}`; + }, + }, + }, + OPTIONS + ); +} + +const presignedUrlQuery = gql` + mutation GetPresignedUrl($filename: String!) { + getPresignedUrl(filename: $filename, appname: KlerosCourt) + } +`; + +type GetPresignedUrlResponse = { + getPresignedUrl: string; +}; + +const getPreSignedUrl = (client: GraphQLClient, filename: string) => { + const variables = { + filename, + }; + + return toast.promise( + client + .request(presignedUrlQuery, variables) + .then(async (response) => response.getPresignedUrl) + .catch((errors) => { + // eslint-disable-next-line no-console + console.log("Get Presigned Url error:", { errors }); + + const errorMessage = Array.isArray(errors?.response?.errors) + ? errors.response.errors[0]?.message + : "Unknown error"; + throw new Error(errorMessage); + }), + { + error: { + render({ data: error }) { + return `Getting Presigned Url failed: ${error?.message}`; + }, + }, + }, + OPTIONS + ); +}; From 96a49ccc89abbe91a1385494ede3d1eaebb49718 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 15 Oct 2024 17:14:02 +0100 Subject: [PATCH 2/2] chore(web): new-ipfs-upload-flow --- web/.env.devnet-neo.public | 2 +- web/.env.devnet-university.public | 2 +- web/.env.devnet.public | 2 +- web/.env.local.public | 2 +- web/.env.mainnet-neo.public | 2 +- web/.env.testnet.public | 2 +- web/src/context/AtlasProvider.tsx | 19 +++-- .../Evidence/SubmitEvidenceModal.tsx | 9 +- web/src/pages/Resolver/Policy/index.tsx | 3 +- web/src/utils/atlas/uploadToIpfs.ts | 85 +++++++++---------- 10 files changed, 65 insertions(+), 63 deletions(-) diff --git a/web/.env.devnet-neo.public b/web/.env.devnet-neo.public index ccf47427b..f9068f7db 100644 --- a/web/.env.devnet-neo.public +++ b/web/.env.devnet-neo.public @@ -5,7 +5,7 @@ export REACT_APP_DRT_ARBSEPOLIA_SUBGRAPH=https://api.studio.thegraph.com/query/6 export REACT_APP_STATUS_URL=https://kleros-v2-devnet.betteruptime.com/badge export REACT_APP_GENESIS_BLOCK_ARBSEPOLIA=3084598 export REACT_APP_ARBITRATOR_TYPE=neo -export REACT_APP_ATLAS_URI=http://localhost:3000/graphql +export REACT_APP_ATLAS_URI=http://localhost:3000 export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= export NODE_OPTIONS='--max-old-space-size=7680' diff --git a/web/.env.devnet-university.public b/web/.env.devnet-university.public index 2ae062b0a..1e167ec4b 100644 --- a/web/.env.devnet-university.public +++ b/web/.env.devnet-university.public @@ -5,7 +5,7 @@ export REACT_APP_DRT_ARBSEPOLIA_SUBGRAPH=https://api.studio.thegraph.com/query/6 export REACT_APP_STATUS_URL=https://kleros-v2-devnet.betteruptime.com/badge export REACT_APP_GENESIS_BLOCK_ARBSEPOLIA=3084598 export REACT_APP_ARBITRATOR_TYPE=university -export REACT_APP_ATLAS_URI=http://localhost:3000/graphql +export REACT_APP_ATLAS_URI=http://localhost:3000 export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= export NODE_OPTIONS='--max-old-space-size=7680' diff --git a/web/.env.devnet.public b/web/.env.devnet.public index 943c2148d..ebb4831a6 100644 --- a/web/.env.devnet.public +++ b/web/.env.devnet.public @@ -4,7 +4,7 @@ export REACT_APP_CORE_SUBGRAPH=https://api.studio.thegraph.com/query/61738/klero export REACT_APP_DRT_ARBSEPOLIA_SUBGRAPH=https://api.studio.thegraph.com/query/61738/kleros-v2-drt-arbisep-devnet/version/latest export REACT_APP_STATUS_URL=https://kleros-v2-devnet.betteruptime.com/badge export REACT_APP_GENESIS_BLOCK_ARBSEPOLIA=3084598 -export REACT_APP_ATLAS_URI=http://localhost:3000/graphql +export REACT_APP_ATLAS_URI=http://localhost:3000 export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= export NODE_OPTIONS='--max-old-space-size=7680' diff --git a/web/.env.local.public b/web/.env.local.public index b71f31e93..b5a5cdd43 100644 --- a/web/.env.local.public +++ b/web/.env.local.public @@ -2,7 +2,7 @@ export REACT_APP_DEPLOYMENT=devnet export REACT_APP_CORE_SUBGRAPH=http://localhost:8000/subgraphs/name/kleros/kleros-v2-core-local export REACT_APP_DRT_ARBSEPOLIA_SUBGRAPH=https://api.thegraph.com/subgraphs/name/alcercu/templateregistrydevnet -export REACT_APP_ATLAS_URI=http://localhost:3000/graphql +export REACT_APP_ATLAS_URI=http://localhost:3000 export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= export NODE_OPTIONS='--max-old-space-size=7680' diff --git a/web/.env.mainnet-neo.public b/web/.env.mainnet-neo.public index a0eb52416..a32745c02 100644 --- a/web/.env.mainnet-neo.public +++ b/web/.env.mainnet-neo.public @@ -5,7 +5,7 @@ export REACT_APP_DRT_ARBMAINNET_SUBGRAPH=https://api.studio.thegraph.com/query/6 export REACT_APP_STATUS_URL=https://kleros-v2-devnet.betteruptime.com/badge export REACT_APP_GENESIS_BLOCK_ARBMAINNET=190274403 export REACT_APP_ARBITRATOR_TYPE=neo -export REACT_APP_ATLAS_URI=http://localhost:3000/graphql +export REACT_APP_ATLAS_URI=http://localhost:3000 export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= export NODE_OPTIONS='--max-old-space-size=7680' diff --git a/web/.env.testnet.public b/web/.env.testnet.public index ab5c07779..43c83fd02 100644 --- a/web/.env.testnet.public +++ b/web/.env.testnet.public @@ -3,7 +3,7 @@ export REACT_APP_DEPLOYMENT=testnet export REACT_APP_CORE_SUBGRAPH=https://api.studio.thegraph.com/query/61738/kleros-v2-core-testnet/version/latest export REACT_APP_DRT_ARBSEPOLIA_SUBGRAPH=https://api.studio.thegraph.com/query/61738/kleros-v2-drt-arbisep-devnet/version/latest export REACT_APP_STATUS_URL=https://kleros-v2.betteruptime.com/badge -export REACT_APP_ATLAS_URI=http://localhost:3000/graphql +export REACT_APP_ATLAS_URI=http://localhost:3000 export REACT_APP_GENESIS_BLOCK_ARBSEPOLIA=3842783 export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= diff --git a/web/src/context/AtlasProvider.tsx b/web/src/context/AtlasProvider.tsx index 84d05109e..b34dbe3f2 100644 --- a/web/src/context/AtlasProvider.tsx +++ b/web/src/context/AtlasProvider.tsx @@ -20,6 +20,8 @@ import { type UpdateEmailData, type ConfirmEmailData, type ConfirmEmailResponse, + Roles, + Products, } from "utils/atlas"; import { isUndefined } from "src/utils"; @@ -36,7 +38,7 @@ interface IAtlasProvider { authoriseUser: () => void; addUser: (userSettings: AddUserData) => Promise; updateEmail: (userSettings: UpdateEmailData) => Promise; - uploadFile: (file: File) => Promise; + uploadFile: (file: File, role: Roles) => Promise; confirmEmail: (userSettings: ConfirmEmailData) => Promise< ConfirmEmailResponse & { isError: boolean; @@ -69,7 +71,7 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) = authorization: `Bearer ${authToken}`, } : undefined; - return new GraphQLClient(atlasUri, { headers }); + return new GraphQLClient(`${atlasUri}/graphql`, { headers }); }, [authToken]); /** @@ -215,17 +217,20 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) = /** * @description upload file to ipfs * @param {File} file - file to be uploaded + * @param {Roles} role - role for which file is being uploaded * @returns {Promise} A promise that resolves to the ipfs cid if file was uploaded successfully else * null */ const uploadFile = useCallback( - async (file: File) => { + async (file: File, role: Roles) => { try { - if (!address || !isVerified) return null; + if (!address || !isVerified || !atlasUri || !authToken) return null; setIsUploadingFile(true); - const hash = await uploadToIpfs(atlasGqlClient, file); - + const hash = await uploadToIpfs( + { baseUrl: atlasUri, authToken }, + { file, name: file.name, role, product: Products.CourtV2 } + ); return hash ? `/ipfs/${hash}` : null; } catch (err: any) { // eslint-disable-next-line @@ -235,7 +240,7 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) = setIsUploadingFile(false); } }, - [address, isVerified, setIsUploadingFile, atlasGqlClient] + [address, isVerified, setIsUploadingFile, authToken] ); /** diff --git a/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx b/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx index c70e6addf..2b7caab8a 100644 --- a/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx +++ b/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx @@ -9,6 +9,7 @@ import { Textarea, Button, FileUploader } from "@kleros/ui-components-library"; import { useAtlasProvider } from "context/AtlasProvider"; import { simulateEvidenceModuleSubmitEvidence } from "hooks/contracts/generated"; +import { Roles } from "utils/atlas"; import { wrapWithToast, OPTIONS as toastOptions } from "utils/wrapWithToast"; import EnsureAuth from "components/EnsureAuth"; @@ -101,11 +102,15 @@ const SubmitEvidenceModal: React.FC<{ ); }; -const constructEvidence = async (uploadFile: (file: File) => Promise, msg: string, file?: File) => { +const constructEvidence = async ( + uploadFile: (file: File, role: Roles) => Promise, + msg: string, + file?: File +) => { let fileURI: string | null = null; if (file) { toast.info("Uploading to IPFS", toastOptions); - fileURI = await uploadFile(file); + fileURI = await uploadFile(file, Roles.Evidence); if (!fileURI) throw new Error("Error uploading evidence file"); } return { name: "Evidence", description: msg, fileURI }; diff --git a/web/src/pages/Resolver/Policy/index.tsx b/web/src/pages/Resolver/Policy/index.tsx index bd7dcd272..036317846 100644 --- a/web/src/pages/Resolver/Policy/index.tsx +++ b/web/src/pages/Resolver/Policy/index.tsx @@ -7,6 +7,7 @@ import { FileUploader } from "@kleros/ui-components-library"; import { useAtlasProvider } from "context/AtlasProvider"; import { useNewDisputeContext } from "context/NewDisputeContext"; +import { Roles } from "utils/atlas"; import { OPTIONS as toastOptions } from "utils/wrapWithToast"; import { landscapeStyle } from "styles/landscapeStyle"; @@ -57,7 +58,7 @@ const Policy: React.FC = () => { setIsPolicyUploading(true); toast.info("Uploading Policy to IPFS", toastOptions); - uploadFile(file) + uploadFile(file, Roles.Policy) .then(async (cid) => { if (!cid) return; setDisputeData({ ...disputeData, policyURI: cid }); diff --git a/web/src/utils/atlas/uploadToIpfs.ts b/web/src/utils/atlas/uploadToIpfs.ts index d3caefb3a..dfbbf8211 100644 --- a/web/src/utils/atlas/uploadToIpfs.ts +++ b/web/src/utils/atlas/uploadToIpfs.ts @@ -1,21 +1,51 @@ -import { gql, type GraphQLClient } from "graphql-request"; import { toast } from "react-toastify"; import { OPTIONS } from "utils/wrapWithToast"; -export async function uploadToIpfs(client: GraphQLClient, file: File): Promise { - const presignedUrl = await getPreSignedUrl(client, file.name); +export enum Products { + CourtV2 = "CourtV2", +} + +export enum Roles { + Photo = "photo", + Evidence = "evidence", + Policy = "policy", + Generic = "generic", +} + +export type IpfsUploadPayload = { + file: File; + name: string; + product: Products; + role: Roles; +}; + +type Config = { + baseUrl: string; + authToken: string; +}; + +export async function uploadToIpfs(config: Config, payload: IpfsUploadPayload): Promise { + const formData = new FormData(); + formData.append("file", payload.file, payload.name); + formData.append("name", payload.name); + formData.append("product", payload.product); + formData.append("role", payload.role); return toast.promise( - fetch(presignedUrl, { - method: "PUT", - body: file, + fetch(`${config.baseUrl}/ipfs/file`, { + method: "POST", + headers: { + authorization: `Bearer ${config.authToken}`, + }, + body: formData, }).then(async (response) => { - if (response.status !== 200) { + if (!response.ok) { const error = await response.json().catch(() => ({ message: "Error uploading to IPFS" })); throw new Error(error.message); } - return response.headers.get("x-amz-meta-cid"); + + return await response.text(); }), { pending: `Uploading to IPFS...`, @@ -29,42 +59,3 @@ export async function uploadToIpfs(client: GraphQLClient, file: File): Promise { - const variables = { - filename, - }; - - return toast.promise( - client - .request(presignedUrlQuery, variables) - .then(async (response) => response.getPresignedUrl) - .catch((errors) => { - // eslint-disable-next-line no-console - console.log("Get Presigned Url error:", { errors }); - - const errorMessage = Array.isArray(errors?.response?.errors) - ? errors.response.errors[0]?.message - : "Unknown error"; - throw new Error(errorMessage); - }), - { - error: { - render({ data: error }) { - return `Getting Presigned Url failed: ${error?.message}`; - }, - }, - }, - OPTIONS - ); -};