Skip to content

Feat(web): update uploadToIPFS netlify function #971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 27, 2023
1 change: 1 addition & 0 deletions .github/workflows/contracts-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
nodejs.org:443
objects.githubusercontent.com:443
registry.yarnpkg.com:443
registry.npmjs.org:443

- name: Setup Node.js environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"nanoid^3.3.1": "^3.3.4",
"node-fetch": "^2.6.7",
"underscore@npm^3.0.4": "^1.12.1",
"eth-sig-util@npm:^1.4.2": "3.0.0"
"eth-sig-util@npm:^1.4.2": "3.0.0",
"fast-xml-parser": "^4.2.5"
},
"scripts": {
"check-prerequisites": "scripts/check-prerequisites.sh",
Expand Down
3 changes: 3 additions & 0 deletions web/netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ YARN_ENABLE_GLOBAL_CACHE = "true"

[functions]
directory = "web/netlify/functions/"

[dev]
framework = "parcel"
130 changes: 104 additions & 26 deletions web/netlify/functions/uploadToIPFS.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,109 @@
import { Handler } from "@netlify/functions";
import fetch from "node-fetch";

const ESTUARI_API_KEY = process.env["ESTUARY_API_KEY"];
const ESTUARI_URL = process.env["ESTUARY_GATEWAY"];

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();
import { File, FilebaseClient } from "@filebase/client";
import amqp, { Connection } from "amqplib";
import busboy from "busboy";

const { FILEBASE_TOKEN, RABBITMQ_URL, FILEBASE_API_WRAPPER } = 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<FormData>((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, dapp: string, operation: string): Promise<Array<string>> => {
const cids = new Array<string>();
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(cid);
}
}

return cids;
};

export const handler: Handler = async (event) => {
const { queryStringParameters } = event;

if (
!queryStringParameters ||
!queryStringParameters.dapp ||
!queryStringParameters.key ||
!queryStringParameters.operation
) {
return {
statusCode: 400,
body: JSON.stringify({ message: "Invalid query parameters" }),
};
}

const { dapp, key, operation } = queryStringParameters;

if (key !== FILEBASE_API_WRAPPER) {
return {
statusCode: 403,
body: JSON.stringify({ message: "Invalid API key" }),
};
}

try {
const parsed = await parseMultipart(event);
const cids = await pinToFilebase(parsed, dapp, operation);

return {
statusCode: 200,
body: JSON.stringify({
message: "File has been stored successfully",
cids,
}),
};
} catch (err: any) {
return {
statusCode: response.status,
body: JSON.stringify(parsedResponse),
statusCode: 500,
body: JSON.stringify({ message: err.message }),
};
}
return {
statusCode: 500,
body: JSON.stringify({ message: "Invalid body format" }),
};
};
4 changes: 4 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
"@netlify/functions": "^1.6.0",
"@parcel/transformer-svg-react": "~2.8.0",
"@parcel/watcher": "~2.1.0",
"@types/amqplib": "^0.10.1",
"@types/busboy": "^1.5.0",
"@types/react": "^18.2.12",
"@types/react-dom": "^18.2.5",
"@types/styled-components": "^5.1.26",
Expand All @@ -59,13 +61,15 @@
"typescript": "^4.9.5"
},
"dependencies": {
"@filebase/client": "^0.0.4",
"@kleros/kleros-v2-contracts": "workspace:^",
"@kleros/ui-components-library": "^2.5.2",
"@sentry/react": "^7.55.2",
"@sentry/tracing": "^7.55.2",
"@types/react-modal": "^3.16.0",
"@web3modal/ethereum": "^2.2.2",
"@web3modal/react": "^2.2.2",
"amqplib": "^0.10.3",
"chart.js": "^3.9.1",
"chartjs-adapter-moment": "^1.0.1",
"core-js": "^3.31.0",
Expand Down
Loading