Skip to content

Commit

Permalink
Admin page actions (approve / reject) + EIP712
Browse files Browse the repository at this point in the history
  • Loading branch information
carletex committed Feb 18, 2024
1 parent f2f7d5c commit 63ba761
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 10 deletions.
61 changes: 60 additions & 1 deletion packages/nextjs/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
"use client";

import { useEffect, useState } from "react";
import { useAccount, useSignTypedData } from "wagmi";
import { GrantData } from "~~/services/database/schema";
import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT } from "~~/utils/eip712";
import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants";
import { notification } from "~~/utils/scaffold-eth";

// ToDo. "Protect" with address header or PROTECT with signing the read.
// ToDo. Submitted grants
// ToDo. Loading states (initial, actions, etc)
// ToDo. Refresh list after action
const AdminPage = () => {
const { address } = useAccount();
const [grants, setGrants] = useState<GrantData[]>([]);
const { signTypedDataAsync } = useSignTypedData();

useEffect(() => {
const getGrants = async () => {
Expand All @@ -22,6 +30,38 @@ const AdminPage = () => {
getGrants();
}, []);

const reviewGrant = async (grant: GrantData, action: ProposalStatusType) => {
let signature;
try {
signature = await signTypedDataAsync({
domain: EIP_712_DOMAIN,
types: EIP_712_TYPES__REVIEW_GRANT,
primaryType: "Message",
message: {
grantId: grant.id,
action: action,
},
});
} catch (e) {
console.error("Error signing message", e);
notification.error("Error signing message");
return;
}

try {
await fetch(`/api/grants/${grant.id}/review`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ signature, signer: address, action }),
});
notification.success(`Grant reviewed: ${action}`);
} catch (error) {
notification.error("Error reviewing grant");
}
};

return (
<div className="container mx-auto max-w-screen-md mt-12">
<h1 className="text-4xl font-bold">Admin page</h1>
Expand All @@ -30,8 +70,27 @@ const AdminPage = () => {
<h2 className="font-bold mt-8">All grants that need review:</h2>
{grants.map(grant => (
<div key={grant.id} className="border p-4 my-4">
<h3 className="font-bold">{grant.title}</h3>
<h3 className="font-bold">
{grant.title}
<span className="text-sm text-gray-500 ml-2">({grant.id})</span>
</h3>
<p>{grant.description}</p>
{grant.status === PROPOSAL_STATUS.PROPOSED && (
<div className="mt-4">
<button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
onClick={() => reviewGrant(grant, PROPOSAL_STATUS.APPROVED)}
>
Approve
</button>
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded ml-4"
onClick={() => reviewGrant(grant, PROPOSAL_STATUS.REJECTED)}
>
Reject
</button>
</div>
)}
</div>
))}
</>
Expand Down
35 changes: 35 additions & 0 deletions packages/nextjs/app/api/grants/[grantId]/review/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { recoverTypedDataAddress } from "viem";
import { reviewGrant } from "~~/services/database/grants";
import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT } from "~~/utils/eip712";

export async function POST(req: NextRequest, { params }: { params: { grantId: string } }) {
const { grantId } = params;
const { signature, signer, action } = await req.json();

// Validate Signature
const recoveredAddress = await recoverTypedDataAddress({
domain: EIP_712_DOMAIN,
types: EIP_712_TYPES__REVIEW_GRANT,
primaryType: "Message",
message: {
grantId: grantId,
action: action,
},
signature,
});

if (recoveredAddress !== signer) {
console.error("Signature error", recoveredAddress, signer);
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

try {
await reviewGrant(grantId, action);
} catch (error) {
console.error("Error approving grant", error);
return NextResponse.json({ error: "Error approving grant" }, { status: 500 });
}

return NextResponse.json({ success: true });
}
2 changes: 1 addition & 1 deletion packages/nextjs/scaffold.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type ScaffoldConfig = {

const scaffoldConfig = {
// The networks on which your DApp is live
targetNetworks: [chains.optimism],
targetNetworks: [chains.mainnet],

// The interval at which your front-end polls the RPC servers for new data
// it has no effect if you only target the local network (default is 4000)
Expand Down
18 changes: 10 additions & 8 deletions packages/nextjs/services/database/grants.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { getFirestoreConnector } from "./firestoreDB";
import { GrantData } from "./schema";

export const PROPOSAL_STATUS = {
PROPOSED: "proposed",
APPROVED: "approved",
SUBMITTED: "submitted",
COMPLETED: "completed",
REJECTED: "rejected",
} as const;
import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants";

const firestoreDB = getFirestoreConnector();
const grantsCollection = firestoreDB.collection("grants");
Expand Down Expand Up @@ -73,6 +66,15 @@ export const getAllCompletedGrants = async () => {
}
};

export const reviewGrant = async (grantId: string, action: ProposalStatusType) => {
try {
await grantsCollection.doc(grantId).update({ status: action });
} catch (error) {
console.error("Error approving the grant:", error);
throw error;
}
};

export const getGrantsStats = async () => {
// Summation of askAmount for completed grants: total_eth_granted
// Total number of completed grants : total_completed_grants
Expand Down
13 changes: 13 additions & 0 deletions packages/nextjs/utils/eip712.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const EIP_712_DOMAIN = {
name: "BuidlGuidl Grants",
version: "1",
chainId: 1,
} as const;

// ToDo. We could add more fields (grant title, builder, etc)
export const EIP_712_TYPES__REVIEW_GRANT = {
Message: [
{ name: "grantId", type: "string" },
{ name: "action", type: "string" },
],
} as const;
9 changes: 9 additions & 0 deletions packages/nextjs/utils/grants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const PROPOSAL_STATUS = {
PROPOSED: "proposed",
APPROVED: "approved",
SUBMITTED: "submitted",
COMPLETED: "completed",
REJECTED: "rejected",
} as const;

export type ProposalStatusType = (typeof PROPOSAL_STATUS)[keyof typeof PROPOSAL_STATUS];

0 comments on commit 63ba761

Please sign in to comment.