diff --git a/packages/nextjs/app/admin/_components/ActionModal.tsx b/packages/nextjs/app/admin/_components/ActionModal.tsx new file mode 100644 index 0000000..ad4fbfc --- /dev/null +++ b/packages/nextjs/app/admin/_components/ActionModal.tsx @@ -0,0 +1,46 @@ +import { forwardRef, useRef } from "react"; +import { useReviewGrant } from "../hooks/useReviewGrant"; +import { GrantData } from "~~/services/database/schema"; +import { PROPOSAL_STATUS } from "~~/utils/grants"; + +type ActionModalProps = { + grant: GrantData; + initialTxLink?: string; +}; + +export const ActionModal = forwardRef(({ grant, initialTxLink }, ref) => { + const inputRef = useRef(null); + + const { handleReviewGrant, isLoading } = useReviewGrant(grant); + + const acceptStatus = grant.status === PROPOSAL_STATUS.PROPOSED ? PROPOSAL_STATUS.APPROVED : PROPOSAL_STATUS.COMPLETED; + const acceptLabel = grant.status === PROPOSAL_STATUS.PROPOSED ? "Approve" : "Complete"; + return ( + +
+
+ {/* if there is a button in form, it will close the modal */} + +
+

{acceptLabel} this grant

+ + +
+
+ ); +}); + +ActionModal.displayName = "ActionModal"; diff --git a/packages/nextjs/app/admin/_components/GrantReview.tsx b/packages/nextjs/app/admin/_components/GrantReview.tsx index b46ddbf..3b7f27a 100644 --- a/packages/nextjs/app/admin/_components/GrantReview.tsx +++ b/packages/nextjs/app/admin/_components/GrantReview.tsx @@ -1,20 +1,14 @@ -import { useSWRConfig } from "swr"; -import useSWRMutation from "swr/mutation"; -import { useAccount, useSignTypedData } from "wagmi"; +import { useRef } from "react"; +import { useReviewGrant } from "../hooks/useReviewGrant"; +import { ActionModal } from "./ActionModal"; +import { parseEther } from "viem"; +import { useSendTransaction } from "wagmi"; import TelegramIcon from "~~/components/assets/TelegramIcon"; import TwitterIcon from "~~/components/assets/TwitterIcon"; import { Address } from "~~/components/scaffold-eth"; -import { GrantData, GrantDataWithBuilder, SocialLinks } 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"; -import { postMutationFetcher } from "~~/utils/swr"; - -type ReqBody = { - signer: string; - signature: `0x${string}`; - action: ProposalStatusType; -}; +import { useTransactor } from "~~/hooks/scaffold-eth"; +import { GrantDataWithBuilder, SocialLinks } from "~~/services/database/schema"; +import { PROPOSAL_STATUS } from "~~/utils/grants"; const BuilderSocials = ({ socialLinks }: { socialLinks?: SocialLinks }) => { if (!socialLinks) return null; @@ -46,55 +40,18 @@ const BuilderSocials = ({ socialLinks }: { socialLinks?: SocialLinks }) => { }; export const GrantReview = ({ grant }: { grant: GrantDataWithBuilder }) => { - const { address } = useAccount(); - const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData(); - const { trigger: postReviewGrant, isMutating: isPostingNewGrant } = useSWRMutation( - `/api/grants/${grant.id}/review`, - postMutationFetcher, - ); - const { mutate } = useSWRConfig(); - const isLoading = isSigningMessage || isPostingNewGrant; - - const handleReviewGrant = async (grant: GrantData, action: ProposalStatusType) => { - if (!address) { - notification.error("Please connect your wallet"); - return; - } + const modalRef = useRef(null); - 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; - } + const { data: txnHash, sendTransactionAsync } = useSendTransaction({ + to: grant.builder, + value: parseEther((grant.askAmount / 2).toString()), + }); + const sendTx = useTransactor(); - let notificationId; - try { - notificationId = notification.loading("Submitting review"); - await postReviewGrant({ signer: address, signature, action }); - await mutate("/api/grants/review"); - notification.remove(notificationId); - notification.success(`Grant reviewed: ${action}`); - } catch (error) { - notification.error("Error reviewing grant"); - } finally { - if (notificationId) notification.remove(notificationId); - } - }; + const { handleReviewGrant, isLoading } = useReviewGrant(grant); if (grant.status !== PROPOSAL_STATUS.PROPOSED && grant.status !== PROPOSAL_STATUS.SUBMITTED) return null; - const acceptStatus = grant.status === PROPOSAL_STATUS.PROPOSED ? PROPOSAL_STATUS.APPROVED : PROPOSAL_STATUS.COMPLETED; const acceptLabel = grant.status === PROPOSAL_STATUS.PROPOSED ? "Approve" : "Complete"; return (
@@ -112,22 +69,38 @@ export const GrantReview = ({ grant }: { grant: GrantDataWithBuilder }) => {

{grant.description}

-
+
- +
+ + +
+
); }; diff --git a/packages/nextjs/app/admin/hooks/useReviewGrant.ts b/packages/nextjs/app/admin/hooks/useReviewGrant.ts new file mode 100644 index 0000000..15fbc71 --- /dev/null +++ b/packages/nextjs/app/admin/hooks/useReviewGrant.ts @@ -0,0 +1,67 @@ +import { useSWRConfig } from "swr"; +import useSWRMutation from "swr/mutation"; +import { useAccount, useSignTypedData } from "wagmi"; +import { GrantData } from "~~/services/database/schema"; +import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT } from "~~/utils/eip712"; +import { ProposalStatusType } from "~~/utils/grants"; +import { notification } from "~~/utils/scaffold-eth"; +import { postMutationFetcher } from "~~/utils/swr"; + +type ReqBody = { + signer: string; + signature: `0x${string}`; + action: ProposalStatusType; + txHash: string; +}; + +export const useReviewGrant = (grant: GrantData) => { + const { address } = useAccount(); + const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData(); + const { trigger: postReviewGrant, isMutating: isPostingNewGrant } = useSWRMutation( + `/api/grants/${grant.id}/review`, + postMutationFetcher, + ); + const { mutate } = useSWRConfig(); + + const isLoading = isSigningMessage || isPostingNewGrant; + + const handleReviewGrant = async (action: ProposalStatusType, txnHash = "") => { + if (!address) { + notification.error("Please connect your wallet"); + return; + } + + let signature; + try { + signature = await signTypedDataAsync({ + domain: EIP_712_DOMAIN, + types: EIP_712_TYPES__REVIEW_GRANT, + primaryType: "Message", + message: { + grantId: grant.id, + action: action, + txHash: txnHash, + }, + }); + } catch (e) { + console.error("Error signing message", e); + notification.error("Error signing message"); + return; + } + + let notificationId; + try { + notificationId = notification.loading("Submitting review"); + await postReviewGrant({ signer: address, signature, action, txHash: txnHash }); + await mutate("/api/grants/review"); + notification.remove(notificationId); + notification.success(`Grant reviewed: ${action}`); + } catch (error) { + notification.error("Error reviewing grant"); + } finally { + if (notificationId) notification.remove(notificationId); + } + }; + + return { handleReviewGrant, isLoading }; +}; diff --git a/packages/nextjs/app/api/grants/[grantId]/review/route.tsx b/packages/nextjs/app/api/grants/[grantId]/review/route.tsx index 8a42533..d52bcd9 100644 --- a/packages/nextjs/app/api/grants/[grantId]/review/route.tsx +++ b/packages/nextjs/app/api/grants/[grantId]/review/route.tsx @@ -3,11 +3,18 @@ import { recoverTypedDataAddress } from "viem"; import { reviewGrant } from "~~/services/database/grants"; import { findUserByAddress } from "~~/services/database/users"; import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT } from "~~/utils/eip712"; -import { PROPOSAL_STATUS } from "~~/utils/grants"; +import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants"; + +type ReqBody = { + signer: string; + signature: `0x${string}`; + action: ProposalStatusType; + txHash: string; +}; export async function POST(req: NextRequest, { params }: { params: { grantId: string } }) { const { grantId } = params; - const { signature, signer, action } = await req.json(); + const { signature, signer, action, txHash } = (await req.json()) as ReqBody; // Validate action is valid const validActions = Object.values(PROPOSAL_STATUS); @@ -24,6 +31,7 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st message: { grantId: grantId, action: action, + txHash, }, signature, }); @@ -42,7 +50,7 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st } try { - await reviewGrant(grantId, action); + await reviewGrant(grantId, action, txHash); } catch (error) { console.error("Error approving grant", error); return NextResponse.json({ error: "Error approving grant" }, { status: 500 }); diff --git a/packages/nextjs/services/database/grants.ts b/packages/nextjs/services/database/grants.ts index 73dacfa..720b599 100644 --- a/packages/nextjs/services/database/grants.ts +++ b/packages/nextjs/services/database/grants.ts @@ -109,15 +109,26 @@ export const getAllActiveGrants = async () => { } }; -export const reviewGrant = async (grantId: string, action: ProposalStatusType) => { +export const reviewGrant = async (grantId: string, action: ProposalStatusType, txHash: string) => { try { const validActions = Object.values(PROPOSAL_STATUS); if (!validActions.includes(action)) { throw new Error(`Invalid action: ${action}`); } + + const updateTxHash: Record = {}; + if (action === PROPOSAL_STATUS.APPROVED) { + updateTxHash["approvedTx"] = txHash; + } + if (action === PROPOSAL_STATUS.COMPLETED) { + updateTxHash["completedTx"] = txHash; + } + const grantActionTimeStamp = new Date().getTime(); const grantActionTimeStampKey = (action + "At") as `${typeof action}At`; - await grantsCollection.doc(grantId).update({ status: action, [grantActionTimeStampKey]: grantActionTimeStamp }); + await grantsCollection + .doc(grantId) + .update({ status: action, [grantActionTimeStampKey]: grantActionTimeStamp, ...updateTxHash }); } catch (error) { console.error("Error approving the grant:", error); throw error; diff --git a/packages/nextjs/services/database/schema.ts b/packages/nextjs/services/database/schema.ts index 74d0793..91c19a3 100644 --- a/packages/nextjs/services/database/schema.ts +++ b/packages/nextjs/services/database/schema.ts @@ -48,6 +48,8 @@ export type GrantWithoutTimestamps = { builder: string; link?: string; status: "proposed" | "approved" | "submitted" | "completed" | "rejected"; + approvedTx?: string; + completedTx?: string; }; export type GrantData = Simplify< diff --git a/packages/nextjs/utils/eip712.ts b/packages/nextjs/utils/eip712.ts index 319ecee..012abe7 100644 --- a/packages/nextjs/utils/eip712.ts +++ b/packages/nextjs/utils/eip712.ts @@ -1,7 +1,9 @@ +import scaffoldConfig from "~~/scaffold.config"; + export const EIP_712_DOMAIN = { name: "BuidlGuidl Grants", version: "1", - chainId: 10, + chainId: scaffoldConfig.targetNetworks[0].id, } as const; export const EIP_712_TYPES__APPLY_FOR_GRANT = { @@ -17,6 +19,7 @@ export const EIP_712_TYPES__REVIEW_GRANT = { Message: [ { name: "grantId", type: "string" }, { name: "action", type: "string" }, + { name: "txHash", type: "string" }, ], } as const;