diff --git a/packages/nextjs/app/admin/page.tsx b/packages/nextjs/app/admin/page.tsx index c7d1efe..77e86ae 100644 --- a/packages/nextjs/app/admin/page.tsx +++ b/packages/nextjs/app/admin/page.tsx @@ -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([]); + const { signTypedDataAsync } = useSignTypedData(); useEffect(() => { const getGrants = async () => { @@ -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 (

Admin page

@@ -30,8 +70,27 @@ const AdminPage = () => {

All grants that need review:

{grants.map(grant => (
-

{grant.title}

+

+ {grant.title} + ({grant.id}) +

{grant.description}

+ {grant.status === PROPOSAL_STATUS.PROPOSED && ( +
+ + +
+ )}
))} diff --git a/packages/nextjs/app/api/grants/[grantId]/review/route.tsx b/packages/nextjs/app/api/grants/[grantId]/review/route.tsx new file mode 100644 index 0000000..b285075 --- /dev/null +++ b/packages/nextjs/app/api/grants/[grantId]/review/route.tsx @@ -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 }); +} diff --git a/packages/nextjs/scaffold.config.ts b/packages/nextjs/scaffold.config.ts index bee782e..ca452a5 100644 --- a/packages/nextjs/scaffold.config.ts +++ b/packages/nextjs/scaffold.config.ts @@ -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) diff --git a/packages/nextjs/services/database/grants.ts b/packages/nextjs/services/database/grants.ts index cf24ab2..5c42539 100644 --- a/packages/nextjs/services/database/grants.ts +++ b/packages/nextjs/services/database/grants.ts @@ -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"); @@ -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 diff --git a/packages/nextjs/utils/eip712.ts b/packages/nextjs/utils/eip712.ts new file mode 100644 index 0000000..d4316d6 --- /dev/null +++ b/packages/nextjs/utils/eip712.ts @@ -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; diff --git a/packages/nextjs/utils/grants.ts b/packages/nextjs/utils/grants.ts new file mode 100644 index 0000000..758dbda --- /dev/null +++ b/packages/nextjs/utils/grants.ts @@ -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];