Skip to content

Commit 4fcce37

Browse files
committed
Implement payout justification upload
1 parent 4404219 commit 4fcce37

File tree

11 files changed

+149
-183
lines changed

11 files changed

+149
-183
lines changed

package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@
8585
"cmdk": "^1.0.0",
8686
"date-fns": "^3.6.0",
8787
"embla-carousel-react": "^8.3.0",
88-
"formidable": "^3.5.2",
8988
"immer": "^9.0.21",
9089
"lucide-react": "^0.378.0",
9190
"markdown-truncate": "^1.1.1",
@@ -110,7 +109,6 @@
110109
"remark-gfm": "^4.0.0",
111110
"remeda": "^2.10.1",
112111
"sass": "^1.77.2",
113-
"server-only": "^0.0.1",
114112
"styled-components": "^6.1.11",
115113
"swr": "^2.2.5",
116114
"tailwind-merge": "^2.3.0",
@@ -161,4 +159,4 @@
161159
"vite-tsconfig-paths": "^4.3.2",
162160
"vitest": "^1.6.0"
163161
}
164-
}
162+
}

src/app/api/files/route.ts

Whitespace-only changes.
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextResponse } from "next/server";
2+
3+
import { pinataClient } from "@/common/services/pinata";
4+
5+
export const dynamic = "force-dynamic";
6+
7+
/**
8+
* @link https://docs.pinata.cloud/frameworks/next-js-ipfs#create-api-route-2
9+
*/
10+
export async function GET() {
11+
try {
12+
const uuid = crypto.randomUUID();
13+
14+
const keyData = await pinataClient.sdk.keys.create({
15+
keyName: uuid.toString(),
16+
maxUses: 1,
17+
permissions: { endpoints: { pinning: { pinFileToIPFS: true } } },
18+
});
19+
20+
return NextResponse.json(keyData, { status: 200 });
21+
} catch (error) {
22+
console.log(error);
23+
return NextResponse.json({ text: "Error creating API Key:" }, { status: 500 });
24+
}
25+
}

src/common/services/pinata/client.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"server-only";
2+
3+
import { type KeyResponse, type PinResponse, PinataSDK } from "pinata-web3";
4+
import { identity } from "remeda";
5+
6+
import { IPFS_GATEWAY_URL } from "@/common/_config";
7+
8+
export const sdk = new PinataSDK({
9+
pinataJwt: process.env.PINATA_JWT,
10+
pinataGateway: IPFS_GATEWAY_URL,
11+
});
12+
13+
export type FileUploadParams = {
14+
file: File;
15+
};
16+
17+
export const upload = ({ file }: FileUploadParams): Promise<PinResponse> =>
18+
fetch("/api/pinata/get-auth-key")
19+
.then((response) => response.json())
20+
.then(({ JWT }: KeyResponse) => sdk.upload.file(file).key(JWT).then(identity()))
21+
.catch((error) => {
22+
throw new Error(error.message || "Unable to retrieve Pinata auth key");
23+
});

src/common/services/pinata/hooks.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { useCallback, useState } from "react";
4+
5+
import type { PinResponse } from "pinata-web3";
6+
7+
import * as client from "./client";
8+
9+
// TODO: QA, built-in `const ipfsUrl = await pinata.gateways.convert(upload.IpfsHash)`
10+
export const useFileUpload = () => {
11+
const [file, setFile] = useState<File>();
12+
const [isPending, setIsPending] = useState(false);
13+
const [response, setResponse] = useState<PinResponse | null>(null);
14+
const [error, setError] = useState<Error | null>(null);
15+
16+
const upload = useCallback(() => {
17+
if (file) {
18+
setIsPending(true);
19+
20+
client
21+
.upload({ file })
22+
.then(setResponse)
23+
.catch(setError)
24+
.finally(() => setIsPending(false));
25+
} else setError(new Error("No file selected"));
26+
}, [file]);
27+
28+
const handleFileInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
29+
setFile(e.target?.files?.[0]);
30+
}, []);
31+
32+
return {
33+
isPending,
34+
handleFileInputChange,
35+
upload,
36+
data: response ?? undefined,
37+
error: error ?? undefined,
38+
};
39+
};

src/common/services/pinata/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
export { pinataClient as pinata } from "./singleton.client";
1+
import * as pinataClient from "./client";
2+
import * as pinataHooks from "./hooks";
3+
4+
export type * from "./hooks";
5+
6+
export { pinataClient, pinataHooks };

src/common/services/pinata/singleton.client.ts

-10
This file was deleted.

src/features/proportional-funding/model/effects.ts

+47-49
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import type { ByPotId } from "@/common/api/indexer";
55
import { FULL_TGAS } from "@/common/constants";
66
import { potContractClient } from "@/common/contracts/core/pot";
77
import { parseNearAmount } from "@/common/lib";
8-
import type { AccountId } from "@/common/types";
9-
import type { VotingRoundElectionResult, VotingRoundParticipants } from "@/entities/voting-round";
8+
import { pinataClient } from "@/common/services/pinata";
9+
import type { VotingRoundParticipants } from "@/entities/voting-round";
10+
11+
import type { PFPayoutJustificationInputs } from "./types";
1012

1113
export type PayoutSubmitInputs = ByPotId & {
1214
recipients: VotingRoundParticipants["winners"];
@@ -30,51 +32,47 @@ export const publishPayoutJustification = async ({
3032
potId,
3133
votingRoundResult,
3234
challengerAccountId,
33-
}: ByPotId & { votingRoundResult: VotingRoundElectionResult; challengerAccountId: AccountId }) => {
34-
const uploadRequestBody = new FormData();
35-
36-
uploadRequestBody.set(
37-
"file",
38-
39-
new File(
40-
[new Blob([JSON.stringify(votingRoundResult)], { type: "application/json" })],
41-
`${potId}_payout-justification.json`,
42-
),
43-
);
44-
45-
const uploadRequest = await fetch("/api/files", { method: "POST", body: uploadRequestBody });
46-
47-
console.log(uploadRequest);
48-
49-
const args = {
50-
challenge_payouts: {
51-
reason: JSON.stringify({
52-
PayoutJustification: "", // TODO: Pass URL from the upload request response
35+
}: PFPayoutJustificationInputs) =>
36+
pinataClient
37+
.upload({
38+
file: new File(
39+
[new Blob([JSON.stringify(votingRoundResult)], { type: "application/json" })],
40+
`${potId}_payout-justification.json`,
41+
),
42+
})
43+
.then(({ IpfsHash }) =>
44+
pinataClient.sdk.gateways.convert(IpfsHash).then((ipfsUrl) => {
45+
const args = {
46+
challenge_payouts: {
47+
reason: JSON.stringify({
48+
PayoutJustification: ipfsUrl,
49+
}),
50+
},
51+
52+
admin_update_payouts_challenge: {
53+
challenger_id: challengerAccountId,
54+
resolve_challenge: true,
55+
},
56+
};
57+
58+
return potContractClient.contractApi(potId).callMultiple([
59+
{
60+
method: "challenge_payouts",
61+
args: args.challenge_payouts,
62+
gas: FULL_TGAS,
63+
64+
deposit: parseNearAmount(calculateDepositByDataSize(args.challenge_payouts)) ?? "0",
65+
} as Transaction<potContractClient.ChallengePayoutsArgs>,
66+
67+
{
68+
method: "admin_update_payouts_challenge",
69+
args: args.admin_update_payouts_challenge,
70+
gas: FULL_TGAS,
71+
72+
deposit:
73+
parseNearAmount(calculateDepositByDataSize(args.admin_update_payouts_challenge)) ??
74+
"0",
75+
} as Transaction<potContractClient.PayoutChallengeUpdateArgs>,
76+
] as Transaction<object>[]);
5377
}),
54-
},
55-
56-
admin_update_payouts_challenge: {
57-
challenger_id: challengerAccountId,
58-
resolve_challenge: true,
59-
},
60-
};
61-
62-
return potContractClient.contractApi(potId).callMultiple([
63-
{
64-
method: "challenge_payouts",
65-
args: args.challenge_payouts,
66-
gas: FULL_TGAS,
67-
68-
deposit: parseNearAmount(calculateDepositByDataSize(args.challenge_payouts)) ?? "0",
69-
} as Transaction<potContractClient.ChallengePayoutsArgs>,
70-
71-
{
72-
method: "admin_update_payouts_challenge",
73-
args: args.admin_update_payouts_challenge,
74-
gas: FULL_TGAS,
75-
76-
deposit:
77-
parseNearAmount(calculateDepositByDataSize(args.admin_update_payouts_challenge)) ?? "0",
78-
} as Transaction<potContractClient.PayoutChallengeUpdateArgs>,
79-
] as Transaction<object>[]);
80-
};
78+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { ByPotId } from "@/common/api/indexer";
2+
import type { AccountId } from "@/common/types";
3+
import type { VotingRoundElectionResult } from "@/entities/voting-round";
4+
5+
export type PFPayoutJustificationInputs = ByPotId & {
6+
votingRoundResult: VotingRoundElectionResult;
7+
challengerAccountId: AccountId;
8+
};

src/pages/api/files.ts

-88
This file was deleted.

0 commit comments

Comments
 (0)