diff --git a/packages/nextjs/app/admin/_components/ActionModal.tsx b/packages/nextjs/app/admin/_components/ActionModal.tsx index ad4fbfc..78f6e18 100644 --- a/packages/nextjs/app/admin/_components/ActionModal.tsx +++ b/packages/nextjs/app/admin/_components/ActionModal.tsx @@ -1,7 +1,10 @@ import { forwardRef, useRef } from "react"; import { useReviewGrant } from "../hooks/useReviewGrant"; +import { useNetwork } from "wagmi"; +import { getNetworkColor } from "~~/hooks/scaffold-eth"; import { GrantData } from "~~/services/database/schema"; import { PROPOSAL_STATUS } from "~~/utils/grants"; +import { NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth"; type ActionModalProps = { grant: GrantData; @@ -11,6 +14,9 @@ type ActionModalProps = { export const ActionModal = forwardRef(({ grant, initialTxLink }, ref) => { const inputRef = useRef(null); + const { chain } = useNetwork(); + const chainWithExtraAttributes = chain ? { ...chain, ...NETWORKS_EXTRA_DATA[chain.id] } : undefined; + const { handleReviewGrant, isLoading } = useReviewGrant(grant); const acceptStatus = grant.status === PROPOSAL_STATUS.PROPOSED ? PROPOSAL_STATUS.APPROVED : PROPOSAL_STATUS.COMPLETED; @@ -22,7 +28,14 @@ export const ActionModal = forwardRef(({ gr {/* if there is a button in form, it will close the modal */} -

{acceptLabel} this grant

+
+

{acceptLabel} this grant

+ {chainWithExtraAttributes && ( +

+ {chainWithExtraAttributes.name} +

+ )} +
{ const inputRef = useRef(null); + const { chain } = useNetwork(); + const chainWithExtraAttributes = chain ? { ...chain, ...NETWORKS_EXTRA_DATA[chain.id] } : undefined; + const { handleBatchReview, isLoading } = useBatchReviewGrants(); return ( @@ -22,7 +28,14 @@ export const BatchActionModal = forwardRef✕ -

{btnLabel} this grant

+
+

{btnLabel} this grant

+ {chainWithExtraAttributes && ( +

+ {chainWithExtraAttributes.name} +

+ )} +
{ const modalRef = useRef(null); + const { chain: connectedChain } = useNetwork(); const { data: txnHash, sendTransactionAsync } = useSendTransaction({ to: grant.builder, @@ -59,6 +60,15 @@ export const GrantReview = ({ grant, selected, toggleSelection }: GrantReviewPro if (grant.status !== PROPOSAL_STATUS.PROPOSED && grant.status !== PROPOSAL_STATUS.SUBMITTED) return null; const acceptLabel = grant.status === PROPOSAL_STATUS.PROPOSED ? "Approve" : "Complete"; + + // Disable complete action if chain is mismatch + const isCompleteAction = grant.status === PROPOSAL_STATUS.SUBMITTED; + const isChainMismatch = !connectedChain || connectedChain.id.toString() !== grant.txChainId; + // Boolean(grant.txChainId) => We want to enable btn in this case nd allow admin to send txn on any chain + const isCompleteActionDisabled = Boolean(grant.txChainId) && isCompleteAction && isChainMismatch; + const completeActionDisableClassName = isCompleteActionDisabled ? "tooltip !pointer-events-auto" : ""; + const completeActionDisableToolTip = isCompleteActionDisabled && `Please switch to chain: ${grant.txChainId}`; + return (
@@ -71,7 +81,14 @@ export const GrantReview = ({ grant, selected, toggleSelection }: GrantReviewPro )}
- +
ETH Icon @@ -94,22 +111,24 @@ export const GrantReview = ({ grant, selected, toggleSelection }: GrantReviewPro
diff --git a/packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts b/packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts index 4325d94..7b3b54d 100644 --- a/packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts +++ b/packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts @@ -1,6 +1,6 @@ import { useSWRConfig } from "swr"; import useSWRMutation from "swr/mutation"; -import { useAccount, useSignTypedData } from "wagmi"; +import { useAccount, useNetwork, useSignTypedData } from "wagmi"; import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT_BATCH } from "~~/utils/eip712"; import { ProposalStatusType } from "~~/utils/grants"; import { getParsedError, notification } from "~~/utils/scaffold-eth"; @@ -13,6 +13,7 @@ type BatchReqBody = { grantId: string; action: ProposalStatusType; txHash: string; + txChainId: string; }[]; }; @@ -20,13 +21,14 @@ export const useBatchReviewGrants = () => { const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData(); const { mutate } = useSWRConfig(); const { address: connectedAddress } = useAccount(); + const { chain: connectedChain } = useNetwork(); const { trigger: postBatchReviewGrant, isMutating: isPostingBatchReviewGrant } = useSWRMutation( `/api/grants/review`, postMutationFetcher, ); const handleBatchReview = async (selectedGrants: string[], action: ProposalStatusType, txHash = "") => { - if (!connectedAddress) { + if (!connectedAddress || !connectedChain) { notification.error("No connected address"); return; } @@ -36,13 +38,14 @@ export const useBatchReviewGrants = () => { grantId, action, txHash, + txChainId: connectedChain.id.toString(), }; }); try { const message = { reviews: grantReviews }; const signature = await signTypedDataAsync({ - domain: EIP_712_DOMAIN, + domain: { ...EIP_712_DOMAIN, chainId: connectedChain.id }, types: EIP_712_TYPES__REVIEW_GRANT_BATCH, primaryType: "Message", message: message, diff --git a/packages/nextjs/app/admin/hooks/useReviewGrant.ts b/packages/nextjs/app/admin/hooks/useReviewGrant.ts index 15fbc71..daca633 100644 --- a/packages/nextjs/app/admin/hooks/useReviewGrant.ts +++ b/packages/nextjs/app/admin/hooks/useReviewGrant.ts @@ -1,6 +1,6 @@ import { useSWRConfig } from "swr"; import useSWRMutation from "swr/mutation"; -import { useAccount, useSignTypedData } from "wagmi"; +import { useAccount, useNetwork, 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"; @@ -12,10 +12,12 @@ type ReqBody = { signature: `0x${string}`; action: ProposalStatusType; txHash: string; + txChainId: string; }; export const useReviewGrant = (grant: GrantData) => { const { address } = useAccount(); + const { chain: connectedChain } = useNetwork(); const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData(); const { trigger: postReviewGrant, isMutating: isPostingNewGrant } = useSWRMutation( `/api/grants/${grant.id}/review`, @@ -26,7 +28,7 @@ export const useReviewGrant = (grant: GrantData) => { const isLoading = isSigningMessage || isPostingNewGrant; const handleReviewGrant = async (action: ProposalStatusType, txnHash = "") => { - if (!address) { + if (!address || !connectedChain) { notification.error("Please connect your wallet"); return; } @@ -34,13 +36,14 @@ export const useReviewGrant = (grant: GrantData) => { let signature; try { signature = await signTypedDataAsync({ - domain: EIP_712_DOMAIN, + domain: { ...EIP_712_DOMAIN, chainId: connectedChain.id }, types: EIP_712_TYPES__REVIEW_GRANT, primaryType: "Message", message: { grantId: grant.id, action: action, txHash: txnHash, + txChainId: connectedChain.id.toString(), }, }); } catch (e) { @@ -52,7 +55,13 @@ export const useReviewGrant = (grant: GrantData) => { let notificationId; try { notificationId = notification.loading("Submitting review"); - await postReviewGrant({ signer: address, signature, action, txHash: txnHash }); + await postReviewGrant({ + signer: address, + signature, + action, + txHash: txnHash, + txChainId: connectedChain.id.toString(), + }); await mutate("/api/grants/review"); notification.remove(notificationId); notification.success(`Grant reviewed: ${action}`); diff --git a/packages/nextjs/app/api/grants/[grantId]/review/route.tsx b/packages/nextjs/app/api/grants/[grantId]/review/route.tsx index d52bcd9..9a790e7 100644 --- a/packages/nextjs/app/api/grants/[grantId]/review/route.tsx +++ b/packages/nextjs/app/api/grants/[grantId]/review/route.tsx @@ -10,11 +10,12 @@ type ReqBody = { signature: `0x${string}`; action: ProposalStatusType; txHash: string; + txChainId: string; }; export async function POST(req: NextRequest, { params }: { params: { grantId: string } }) { const { grantId } = params; - const { signature, signer, action, txHash } = (await req.json()) as ReqBody; + const { signature, signer, action, txHash, txChainId } = (await req.json()) as ReqBody; // Validate action is valid const validActions = Object.values(PROPOSAL_STATUS); @@ -25,13 +26,14 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st // Validate Signature const recoveredAddress = await recoverTypedDataAddress({ - domain: EIP_712_DOMAIN, + domain: { ...EIP_712_DOMAIN, chainId: Number(txChainId) }, types: EIP_712_TYPES__REVIEW_GRANT, primaryType: "Message", message: { grantId: grantId, action: action, txHash, + txChainId, }, signature, }); @@ -50,7 +52,12 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st } try { - await reviewGrant(grantId, action, txHash); + await reviewGrant({ + grantId, + action, + txHash, + txChainId, + }); } catch (error) { console.error("Error approving grant", error); return NextResponse.json({ error: "Error approving grant" }, { status: 500 }); diff --git a/packages/nextjs/app/api/grants/review/route.tsx b/packages/nextjs/app/api/grants/review/route.tsx index ac586e1..377d043 100644 --- a/packages/nextjs/app/api/grants/review/route.tsx +++ b/packages/nextjs/app/api/grants/review/route.tsx @@ -28,12 +28,21 @@ type BatchReqBody = { grantId: string; action: ProposalStatusType; txHash: string; + txChainId: string; }[]; }; export async function POST(req: NextRequest) { const { signer, signature, reviews } = (await req.json()) as BatchReqBody; + if (!reviews.length) { + console.error("No reviews in batch"); + return NextResponse.json({ error: "No reviews in batch" }, { status: 400 }); + } + + // Assuming all reviews are for the same chain + const txChainId = reviews[0].txChainId; + // Ensure all actions are valid const validActions = Object.values(PROPOSAL_STATUS); if (!reviews.every(review => validActions.includes(review.action))) { @@ -49,7 +58,7 @@ export async function POST(req: NextRequest) { } const recoveredAddress = await recoverTypedDataAddress({ - domain: EIP_712_DOMAIN, + domain: { ...EIP_712_DOMAIN, chainId: Number(txChainId) }, types: EIP_712_TYPES__REVIEW_GRANT_BATCH, primaryType: "Message", message: { reviews }, @@ -63,7 +72,7 @@ export async function POST(req: NextRequest) { try { for (const review of reviews) { - await reviewGrant(review.grantId, review.action, review.txHash); + await reviewGrant(review); } } catch (error) { console.error("Error processing batch grant review", error); diff --git a/packages/nextjs/services/database/grants.ts b/packages/nextjs/services/database/grants.ts index 720b599..4610efb 100644 --- a/packages/nextjs/services/database/grants.ts +++ b/packages/nextjs/services/database/grants.ts @@ -109,28 +109,38 @@ export const getAllActiveGrants = async () => { } }; -export const reviewGrant = async (grantId: string, action: ProposalStatusType, txHash: string) => { +type ReviewGrantParams = { + grantId: string; + action: ProposalStatusType; + txHash: string; + txChainId: string; +}; +export const reviewGrant = async ({ grantId, action, txHash, txChainId }: ReviewGrantParams) => { try { const validActions = Object.values(PROPOSAL_STATUS); if (!validActions.includes(action)) { throw new Error(`Invalid action: ${action}`); } - const updateTxHash: Record = {}; + // Prepare the data to update based on the action + const updateData: Record = { status: action }; + + // Add/update the transaction hash based on the action if (action === PROPOSAL_STATUS.APPROVED) { - updateTxHash["approvedTx"] = txHash; - } - if (action === PROPOSAL_STATUS.COMPLETED) { - updateTxHash["completedTx"] = txHash; + updateData["approvedTx"] = txHash; + updateData["txChainId"] = txChainId; // Add txChainId when the grant is approved + } else if (action === PROPOSAL_STATUS.COMPLETED) { + updateData["completedTx"] = txHash; } + // Update timestamp based on the action const grantActionTimeStamp = new Date().getTime(); - const grantActionTimeStampKey = (action + "At") as `${typeof action}At`; - await grantsCollection - .doc(grantId) - .update({ status: action, [grantActionTimeStampKey]: grantActionTimeStamp, ...updateTxHash }); + const grantActionTimeStampKey = `${action}At`; + updateData[grantActionTimeStampKey] = grantActionTimeStamp; + + await grantsCollection.doc(grantId).update(updateData); } catch (error) { - console.error("Error approving the grant:", error); + console.error("Error processing the grant:", error); throw error; } }; diff --git a/packages/nextjs/services/database/schema.ts b/packages/nextjs/services/database/schema.ts index 91c19a3..183b0c5 100644 --- a/packages/nextjs/services/database/schema.ts +++ b/packages/nextjs/services/database/schema.ts @@ -50,6 +50,7 @@ export type GrantWithoutTimestamps = { status: "proposed" | "approved" | "submitted" | "completed" | "rejected"; approvedTx?: string; completedTx?: string; + txChainId?: string; }; export type GrantData = Simplify< diff --git a/packages/nextjs/utils/eip712.ts b/packages/nextjs/utils/eip712.ts index ecccd25..7d03877 100644 --- a/packages/nextjs/utils/eip712.ts +++ b/packages/nextjs/utils/eip712.ts @@ -19,6 +19,7 @@ export const EIP_712_TYPES__REVIEW_GRANT = { { name: "grantId", type: "string" }, { name: "action", type: "string" }, { name: "txHash", type: "string" }, + { name: "txChainId", type: "string" }, ], } as const; @@ -31,10 +32,6 @@ export const EIP_712_TYPES__SUBMIT_GRANT = { } as const; export const EIP_712_TYPES__REVIEW_GRANT_BATCH = { - GrantReview: [ - { name: "grantId", type: "string" }, - { name: "action", type: "string" }, - { name: "txHash", type: "string" }, - ], + GrantReview: [...EIP_712_TYPES__REVIEW_GRANT.Message], Message: [{ name: "reviews", type: "GrantReview[]" }], } as const;