Skip to content

Commit

Permalink
feat: use ipfs for metadata upload and fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrlc03 committed Jul 5, 2024
1 parent b747b50 commit e052459
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 16 deletions.
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# Storage for metadata
# Create a Blob database and get token here: https://vercel.com/dashboard/stores?type=blob
BLOB_READ_WRITE_TOKEN=

# ---------------------
# NETWORK CONFIGURATION
# ---------------------
Expand Down Expand Up @@ -84,3 +80,8 @@ NEXT_PUBLIC_TALLY_URL=https://upblxu2duoxmkobt.public.blob.vercel-storage.com
NEXT_PUBLIC_POLL_MODE="non-qv"

NEXT_PUBLIC_ROUND_LOGO="round-logo.png"

NEXT_PUBLIC_IPFS_URL=""
NEXT_PUBLIC_IPFS_FETCHING_URL=""
IPFS_API_KEY=""
IPFS_SECRET=""
4 changes: 4 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ env:
NEXT_PUBLIC_VOTING_END_DATE: ${{ vars.NEXT_PUBLIC_VOTING_END_DATE }}
NEXT_PUBLIC_RESULTS_DATE: ${{ vars.NEXT_PUBLIC_RESULTS_DATE }}
NEXT_PUBLIC_POLL_MODE: ${{ vars.NEXT_PUBLIC_POLL_MODE }}
NEXT_PUBLIC_IPFS_URL: ${{ vars.NEXT_PUBLIC_IPFS_URL }}
NEXT_PUBLIC_IPFS_FETCHING_URL: ${{ vars.NEXT_PUBLIC_IPFS_FETCHING_URL }}
IPFS_API_KEY: ${{ secrets.IPFS_API_KEY }}
IPFS_SECRET: ${{ secrets.IPFS_SECRET }}
TEST_MNEMONIC: ${{ secrets.TEST_MNEMONIC }}
WALLET_PRIVATE_KEY: ""

Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ export const config = {
roundLogo: process.env.NEXT_PUBLIC_ROUND_LOGO,
};

export const ipfs = {
url: process.env.NEXT_PUBLIC_IPFS_URL ?? "",
apiKey: process.env.IPFS_API_KEY ?? "",
secret: process.env.IPFS_SECRET ?? "",
fetchingUrl: process.env.NEXT_PUBLIC_IPFS_FETCHING_URL ?? "",
};

export const theme = {
colorMode: "light",
};
Expand Down
6 changes: 6 additions & 0 deletions src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ module.exports = createEnv({

NEXT_PUBLIC_POLL_MODE: z.enum(["qv", "non-qv"]).default("non-qv"),
NEXT_PUBLIC_ROUND_LOGO: z.string().optional(),

NEXT_PUBLIC_IPFS_URL: z.string().url(),
NEXT_PUBLIC_IPFS_FETCHING_URL: z.string().url(),
},

/**
Expand Down Expand Up @@ -108,6 +111,9 @@ module.exports = createEnv({

NEXT_PUBLIC_POLL_MODE: process.env.NEXT_PUBLIC_POLL_MODE,
NEXT_PUBLIC_ROUND_LOGO: process.env.NEXT_PUBLIC_ROUND_LOGO,

NEXT_PUBLIC_IPFS_URL: process.env.NEXT_PUBLIC_IPFS_URL,
NEXT_PUBLIC_IPFS_FETCHING_URL: process.env.NEXT_PUBLIC_IPFS_FETCHING_URL,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
Expand Down
11 changes: 10 additions & 1 deletion src/hooks/useMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export function useMetadata<T>(metadataPtr?: string): UseTRPCQueryResult<T, unkn
return api.metadata.get.useQuery({ metadataPtr: String(metadataPtr) }, { enabled: Boolean(metadataPtr) });
}

// based on API documentation of infura and pinata the hash could come
// with different property names
interface UploadResponse {
Hash?: string;
IpfsHash?: string;
}

export function useUploadMetadata(): UseMutationResult<{ url: string }, DefaultError, Record<string, unknown> | File> {
return useMutation({
mutationFn: async (data: Record<string, unknown> | File) => {
Expand All @@ -30,7 +37,9 @@ export function useUploadMetadata(): UseMutationResult<{ url: string }, DefaultE
if (!r.ok) {
throw new Error("Network error");
}
return (await r.json()) as { url: string };
const res = (await r.json()) as unknown as UploadResponse;
const hash = res.Hash ?? res.IpfsHash;
return { url: hash! };
});
},
});
Expand Down
38 changes: 33 additions & 5 deletions src/pages/api/blob.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
import { put } from "@vercel/blob";
import { IncomingMessage } from "http";

import { ipfs } from "~/config";

import type { NextApiResponse, NextApiRequest, PageConfig } from "next";

export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const blob = await put(req.query.filename as string, req, {
access: "public",
function bufferize(req: IncomingMessage): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve(Buffer.concat(chunks));
});
req.on("error", reject);
});
}

export default async function blob(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const buffer = await bufferize(req);
const formData = new FormData();
formData.append("file", new Blob([buffer]), req.query.filename as string);

const response = await fetch(`${ipfs.url}/api/v0/add`, {
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${ipfs.apiKey}:${ipfs.secret}`).toString("base64")}`,
},
body: formData,
});

if (!response.ok) {
res.status(response.status).json({ error: "Failed to upload to IPFS" });
return;
}

const data: unknown = await response.json();

res.status(200).json(blob);
res.status(200).json(data);
}

export const config: PageConfig = {
Expand Down
9 changes: 6 additions & 3 deletions src/utils/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ export interface ICreateCachedFetchArgs {

export type TCachedFetch = <T>(
url: string,
opts?: { method: "POST" | "GET"; body?: string },
opts?: { method: "POST" | "GET"; body?: string; headers?: Record<string, string> },
) => Promise<{ data: T; error: Error }>;

export function createCachedFetch({ ttl = 1000 * 60 }: ICreateCachedFetchArgs): TCachedFetch {
const cachedFetch = NodeFetchCache.create({ cache: new MemoryCache({ ttl }) });

return async <T>(url: string, opts?: { method: "POST" | "GET"; body?: string }): Promise<{ data: T; error: Error }> =>
return async <T>(
url: string,
opts?: { method: "POST" | "GET"; body?: string; headers?: Record<string, string> },
): Promise<{ data: T; error: Error }> =>
cachedFetch(url, {
method: opts?.method ?? "GET",
body: opts?.body,
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", ...opts?.headers },
}).then(async (r) => {
if (!r.ok) {
await r.ejectFromCache();
Expand Down
11 changes: 8 additions & 3 deletions src/utils/fetchMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { ipfs } from "~/config";

import { createCachedFetch } from "./fetch";

// ipfs data never changes
const ttl = 2147483647;
const cachedFetch = createCachedFetch({ ttl });

export async function fetchMetadata<T>(url: string): Promise<{ data: T; error: Error }> {
const ipfsGateway = process.env.NEXT_PUBLIC_IPFS_GATEWAY ?? "https://dweb.link/ipfs/";

if (!url.startsWith("http")) {
return cachedFetch<T>(`${ipfsGateway}${url}`);
return cachedFetch<T>(`${ipfs.fetchingUrl}${url}`, {
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${ipfs.apiKey}:${ipfs.secret}`).toString("base64")}`,
},
});
}

return cachedFetch<T>(url);
Expand Down

0 comments on commit e052459

Please sign in to comment.