Skip to content

Commit

Permalink
Add txChainId to grants (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
technophile-04 authored Mar 13, 2024
1 parent 911336e commit 0d7bcf8
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 36 deletions.
15 changes: 14 additions & 1 deletion packages/nextjs/app/admin/_components/ActionModal.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,6 +14,9 @@ type ActionModalProps = {
export const ActionModal = forwardRef<HTMLDialogElement, ActionModalProps>(({ grant, initialTxLink }, ref) => {
const inputRef = useRef<HTMLInputElement>(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;
Expand All @@ -22,7 +28,14 @@ export const ActionModal = forwardRef<HTMLDialogElement, ActionModalProps>(({ gr
{/* if there is a button in form, it will close the modal */}
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<p className="font-bold text-lg m-0">{acceptLabel} this grant</p>
<div className="flex justify-between items-center">
<p className="font-bold text-lg m-0">{acceptLabel} this grant</p>
{chainWithExtraAttributes && (
<p className="m-0 text-sm" style={{ color: getNetworkColor(chainWithExtraAttributes, true) }}>
{chainWithExtraAttributes.name}
</p>
)}
</div>
<input
type="text"
ref={inputRef}
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/app/admin/_components/BatchActionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { forwardRef, useRef } from "react";
import { useBatchReviewGrants } from "../hooks/useBatchReviewGrants";
import { useNetwork } from "wagmi";
import { getNetworkColor } from "~~/hooks/scaffold-eth";
import { PROPOSAL_STATUS } from "~~/utils/grants";
import { NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth";

type BatchActionModalProps = {
selectedGrants: string[];
Expand All @@ -13,6 +16,9 @@ export const BatchActionModal = forwardRef<HTMLDialogElement, BatchActionModalPr
({ selectedGrants, initialTxLink, closeModal, btnLabel }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);

const { chain } = useNetwork();
const chainWithExtraAttributes = chain ? { ...chain, ...NETWORKS_EXTRA_DATA[chain.id] } : undefined;

const { handleBatchReview, isLoading } = useBatchReviewGrants();

return (
Expand All @@ -22,7 +28,14 @@ export const BatchActionModal = forwardRef<HTMLDialogElement, BatchActionModalPr
{/* if there is a button in form, it will close the modal */}
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<p className="font-bold text-lg m-0">{btnLabel} this grant</p>
<div className="flex justify-between items-center">
<p className="font-bold text-lg m-0">{btnLabel} this grant</p>
{chainWithExtraAttributes && (
<p className="m-0 text-sm" style={{ color: getNetworkColor(chainWithExtraAttributes, true) }}>
{chainWithExtraAttributes.name}
</p>
)}
</div>
<input
type="text"
ref={inputRef}
Expand Down
31 changes: 25 additions & 6 deletions packages/nextjs/app/admin/_components/GrantReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Image from "next/image";
import { useReviewGrant } from "../hooks/useReviewGrant";
import { ActionModal } from "./ActionModal";
import { parseEther } from "viem";
import { useSendTransaction } from "wagmi";
import { useNetwork, useSendTransaction } from "wagmi";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
import TelegramIcon from "~~/components/assets/TelegramIcon";
import TwitterIcon from "~~/components/assets/TwitterIcon";
Expand Down Expand Up @@ -48,6 +48,7 @@ type GrantReviewProps = {
};
export const GrantReview = ({ grant, selected, toggleSelection }: GrantReviewProps) => {
const modalRef = useRef<HTMLDialogElement>(null);
const { chain: connectedChain } = useNetwork();

const { data: txnHash, sendTransactionAsync } = useSendTransaction({
to: grant.builder,
Expand All @@ -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 (
<div className="border p-4 my-4">
<div className="flex justify-between mb-2">
Expand All @@ -71,7 +81,14 @@ export const GrantReview = ({ grant, selected, toggleSelection }: GrantReviewPro
</a>
)}
</div>
<input type="checkbox" className="checkbox checkbox-primary" checked={selected} onChange={toggleSelection} />
<input
type="checkbox"
className={`checkbox checkbox-primary ${completeActionDisableClassName}`}
data-tip={completeActionDisableToolTip}
disabled={isCompleteActionDisabled}
checked={selected}
onChange={toggleSelection}
/>
</div>
<div className="flex mb-2 items-center">
<Image src="/assets/eth-completed-grant.png" alt="ETH Icon" width={10} height={10} />
Expand All @@ -94,22 +111,24 @@ export const GrantReview = ({ grant, selected, toggleSelection }: GrantReviewPro
</button>
<div className="flex gap-2 lg:gap-4">
<button
className={`btn btn-sm btn-neutral ${isLoading ? "opacity-50" : ""}`}
className={`btn btn-sm btn-neutral ${isLoading ? "opacity-50" : ""} ${completeActionDisableClassName}`}
data-tip={completeActionDisableToolTip}
onClick={async () => {
const resHash = await sendTx(sendTransactionAsync);
// Transactor eats the error, so we need to handle by checking resHash
if (resHash && modalRef.current) modalRef.current.showModal();
}}
disabled={isLoading}
disabled={isLoading || isCompleteActionDisabled}
>
Send 50%
</button>
<button
className={`btn btn-sm btn-success ${isLoading ? "opacity-50" : ""}`}
className={`btn btn-sm btn-success ${isLoading ? "opacity-50" : ""} ${completeActionDisableClassName}`}
data-tip={completeActionDisableToolTip}
onClick={() => {
if (modalRef.current) modalRef.current.showModal();
}}
disabled={isLoading}
disabled={isLoading || isCompleteActionDisabled}
>
{acceptLabel}
</button>
Expand Down
9 changes: 6 additions & 3 deletions packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,20 +13,22 @@ type BatchReqBody = {
grantId: string;
action: ProposalStatusType;
txHash: string;
txChainId: string;
}[];
};

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<BatchReqBody>,
);

const handleBatchReview = async (selectedGrants: string[], action: ProposalStatusType, txHash = "") => {
if (!connectedAddress) {
if (!connectedAddress || !connectedChain) {
notification.error("No connected address");
return;
}
Expand All @@ -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,
Expand Down
17 changes: 13 additions & 4 deletions packages/nextjs/app/admin/hooks/useReviewGrant.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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`,
Expand All @@ -26,21 +28,22 @@ 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;
}

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) {
Expand All @@ -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}`);
Expand Down
13 changes: 10 additions & 3 deletions packages/nextjs/app/api/grants/[grantId]/review/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
});
Expand All @@ -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 });
Expand Down
13 changes: 11 additions & 2 deletions packages/nextjs/app/api/grants/review/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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))) {
Expand All @@ -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 },
Expand All @@ -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);
Expand Down
32 changes: 21 additions & 11 deletions packages/nextjs/services/database/grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {};
// Prepare the data to update based on the action
const updateData: Record<string, any> = { 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;
}
};
Expand Down
Loading

0 comments on commit 0d7bcf8

Please sign in to comment.