Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add txChainId to grants #72

Merged
merged 9 commits into from
Mar 13, 2024
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
Loading