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

allow safe signature #135

Merged
merged 5 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/nextjs/app/admin/_components/EditGrantModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ChangeEvent, forwardRef, useState } from "react";
import { useSWRConfig } from "swr";
import useSWRMutation from "swr/mutation";
import { useAccount, useNetwork, useSignTypedData } from "wagmi";
import { useAccount, useNetwork, usePublicClient, useSignTypedData } from "wagmi";
import { GrantDataWithPrivateNote } from "~~/services/database/schema";
import { EIP_712_DOMAIN, EIP_712_TYPES__EDIT_GRANT } from "~~/utils/eip712";
import { isSafeContext } from "~~/utils/safe-signature";
import { getParsedError, notification } from "~~/utils/scaffold-eth";
import { patchMutationFetcher } from "~~/utils/swr";

Expand All @@ -19,6 +20,8 @@ type ReqBody = {
signature?: `0x${string}`;
signer?: string;
private_note?: string;
isSafeSignature?: boolean;
chainId?: number;
};

export const EditGrantModal = forwardRef<HTMLDialogElement, EditGrantModalProps>(({ grant, closeModal }, ref) => {
Expand All @@ -31,6 +34,7 @@ export const EditGrantModal = forwardRef<HTMLDialogElement, EditGrantModalProps>

const { address } = useAccount();
const { chain: connectedChain } = useNetwork();
const publiClient = usePublicClient({ chainId: connectedChain?.id });
const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData();

const { trigger: editGrant, isMutating } = useSWRMutation(`/api/grants/${grant.id}`, patchMutationFetcher<ReqBody>);
Expand Down Expand Up @@ -69,11 +73,15 @@ export const EditGrantModal = forwardRef<HTMLDialogElement, EditGrantModalProps>
},
});
notificationId = notification.loading("Updating grant");

const isSafeSignature = await isSafeContext(publiClient, address);
await editGrant({
signer: address,
signature,
...formData,
askAmount: parseFloat(formData.askAmount),
isSafeSignature,
chainId: connectedChain.id,
});
await mutate("/api/grants/review");
notification.remove(notificationId);
Expand Down
8 changes: 7 additions & 1 deletion packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useSWRConfig } from "swr";
import useSWRMutation from "swr/mutation";
import { useAccount, useNetwork, useSignTypedData } from "wagmi";
import { useAccount, useNetwork, usePublicClient, useSignTypedData } from "wagmi";
import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT_BATCH } from "~~/utils/eip712";
import { ProposalStatusType } from "~~/utils/grants";
import { isSafeContext } from "~~/utils/safe-signature";
import { getParsedError, notification } from "~~/utils/scaffold-eth";
import { postMutationFetcher } from "~~/utils/swr";

Expand All @@ -15,13 +16,15 @@ type BatchReqBody = {
txHash: string;
txChainId: string;
}[];
isSafeSignature?: boolean;
};

export const useBatchReviewGrants = () => {
const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData();
const { mutate } = useSWRConfig();
const { address: connectedAddress } = useAccount();
const { chain: connectedChain } = useNetwork();
const publicClient = usePublicClient({ chainId: connectedChain?.id });
const { trigger: postBatchReviewGrant, isMutating: isPostingBatchReviewGrant } = useSWRMutation(
`/api/grants/review`,
postMutationFetcher<BatchReqBody>,
Expand Down Expand Up @@ -51,10 +54,13 @@ export const useBatchReviewGrants = () => {
message: message,
});

const isSafeSignature = await isSafeContext(publicClient, connectedAddress);

await postBatchReviewGrant({
signature: signature,
reviews: grantReviews,
signer: connectedAddress,
isSafeSignature: isSafeSignature,
});
await mutate("/api/grants/review");
notification.success(`Grants reviews successfully submitted!`);
Expand Down
7 changes: 6 additions & 1 deletion packages/nextjs/app/admin/hooks/useReviewGrant.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useSWRConfig } from "swr";
import useSWRMutation from "swr/mutation";
import { useAccount, useNetwork, useSignTypedData } from "wagmi";
import { useAccount, useNetwork, usePublicClient, useSignTypedData } from "wagmi";
import { GrantData } from "~~/services/database/schema";
import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT, EIP_712_TYPES__REVIEW_GRANT_WITH_NOTE } from "~~/utils/eip712";
import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants";
import { isSafeContext } from "~~/utils/safe-signature";
import { notification } from "~~/utils/scaffold-eth";
import { postMutationFetcher } from "~~/utils/swr";

Expand All @@ -14,11 +15,13 @@ type ReqBody = {
txHash: string;
txChainId: string;
note?: string;
isSafeSignature?: boolean;
};

export const useReviewGrant = (grant: GrantData) => {
const { address } = useAccount();
const { chain: connectedChain } = useNetwork();
const publicClient = usePublicClient({ chainId: connectedChain?.id });
const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData();
const { trigger: postReviewGrant, isMutating: isPostingNewGrant } = useSWRMutation(
`/api/grants/${grant.id}/review`,
Expand Down Expand Up @@ -71,13 +74,15 @@ export const useReviewGrant = (grant: GrantData) => {
let notificationId;
try {
notificationId = notification.loading("Submitting review");
const isSafeSignature = await isSafeContext(publicClient, address);
await postReviewGrant({
signer: address,
signature,
action,
txHash: txnHash,
txChainId: connectedChain.id.toString(),
note,
isSafeSignature,
});
await mutate("/api/grants/review");
notification.remove(notificationId);
Expand Down
18 changes: 16 additions & 2 deletions packages/nextjs/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import useSWR from "swr";
import useSWRMutation from "swr/mutation";
import { useLocalStorage } from "usehooks-ts";
import { parseEther } from "viem";
import { useAccount, useSignTypedData } from "wagmi";
import { useAccount, useNetwork, usePublicClient, useSignTypedData } from "wagmi";
import { useScaffoldContractWrite } from "~~/hooks/scaffold-eth";
import { GrantDataWithBuilder } from "~~/services/database/schema";
import { EIP_712_DOMAIN, EIP_712_TYPES__ADMIN_SIGN_IN } from "~~/utils/eip712";
import { PROPOSAL_STATUS } from "~~/utils/grants";
import { isSafeContext } from "~~/utils/safe-signature";
import { getParsedError, notification } from "~~/utils/scaffold-eth";
import { postMutationFetcher } from "~~/utils/swr";

Expand All @@ -38,6 +39,8 @@ const fetcherWithHeader = async (url: string, headers: { address: string; apiKey
const AdminPage = () => {
const { address } = useAccount();
const [selectedApproveGrants, setSelectedApproveGrants] = useState<string[]>([]);
const { chain: connectedChain } = useNetwork();
const publicClient = usePublicClient({ chainId: connectedChain?.id });
const [selectedCompleteGrants, setSelectedCompleteGrants] = useState<string[]>([]);
const [modalBtnLabel, setModalBtnLabel] = useState<"Approve" | "Complete">("Approve");
const modalRef = useRef<HTMLDialogElement>(null);
Expand All @@ -48,6 +51,8 @@ const AdminPage = () => {
postMutationFetcher<{
signer?: string;
signature?: `0x${string}`;
isSafeSignature?: boolean;
chainId?: number;
}>,
);

Expand Down Expand Up @@ -130,7 +135,16 @@ const AdminPage = () => {
message: { action: "Sign In", description: "I authorize myself as admin" },
});

const resData = (await postAdminSignIn({ signer: address, signature })) as { data: { apiKey: string } };
const isSafeSignature = await isSafeContext(publicClient, address);

const resData = (await postAdminSignIn({
signer: address,
signature,
isSafeSignature,
chainId: connectedChain?.id,
})) as {
data: { apiKey: string };
};
setApiKey(resData.data.apiKey);
} catch (error) {
console.error("Error signing in", error);
Expand Down
24 changes: 19 additions & 5 deletions packages/nextjs/app/api/admin/signin/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { NextResponse } from "next/server";
import { recoverTypedDataAddress } from "viem";
import { findUserByAddress } from "~~/services/database/users";
import { EIP_712_DOMAIN, EIP_712_TYPES__ADMIN_SIGN_IN } from "~~/utils/eip712";
import { validateSafeSignature } from "~~/utils/safe-signature";

type AdminSignInBody = {
signer?: string;
signature?: `0x${string}`;
isSafeSignature?: boolean;
chainId?: number;
};
export async function POST(req: Request) {
try {
const { signer, signature } = (await req.json()) as AdminSignInBody;
const { signer, signature, isSafeSignature, chainId } = (await req.json()) as AdminSignInBody;

if (!signer || !signature) {
return new Response("Missing signer or signature", { status: 400 });
Expand All @@ -21,15 +24,26 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const recoveredAddress = await recoverTypedDataAddress({
let isValidSignature = false;

const typedData = {
domain: EIP_712_DOMAIN,
types: EIP_712_TYPES__ADMIN_SIGN_IN,
primaryType: "Message",
message: { action: "Sign In", description: "I authorize myself as admin" },
signature,
});
if (recoveredAddress !== signer) {
console.error("Signer and Recovered address does not match", recoveredAddress, signer);
} as const;

if (isSafeSignature) {
if (!chainId) return new Response("Missing chainId", { status: 400 });
isValidSignature = await validateSafeSignature({ chainId, typedData, safeAddress: signer, signature });
} else {
const recoveredAddress = await recoverTypedDataAddress(typedData);
isValidSignature = recoveredAddress === signer;
}

if (!isValidSignature) {
console.error("Signer and Recovered address does not match");
return NextResponse.json({ error: "Unauthorized in batch" }, { status: 401 });
}

Expand Down
41 changes: 33 additions & 8 deletions packages/nextjs/app/api/grants/[grantId]/review/route.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { EIP712TypedData } from "@safe-global/safe-core-sdk-types";
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, EIP_712_TYPES__REVIEW_GRANT_WITH_NOTE } from "~~/utils/eip712";
import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants";
import { validateSafeSignature } from "~~/utils/safe-signature";

type ReqBody = {
signer: string;
Expand All @@ -12,11 +14,12 @@ type ReqBody = {
txHash: string;
txChainId: string;
note?: string;
isSafeSignature?: boolean;
};

export async function POST(req: NextRequest, { params }: { params: { grantId: string } }) {
const { grantId } = params;
const { signature, signer, action, txHash, txChainId, note } = (await req.json()) as ReqBody;
const { signature, signer, action, txHash, txChainId, note, isSafeSignature } = (await req.json()) as ReqBody;

// Validate action is valid
const validActions = Object.values(PROPOSAL_STATUS);
Expand All @@ -25,11 +28,11 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
}

let recoveredAddress: string;
let isValidSignature: boolean;

// If action is approved or rejected, include note in signature
if (action === PROPOSAL_STATUS.APPROVED || action === PROPOSAL_STATUS.REJECTED) {
recoveredAddress = await recoverTypedDataAddress({
const typedData = {
domain: { ...EIP_712_DOMAIN, chainId: Number(txChainId) },
types: EIP_712_TYPES__REVIEW_GRANT_WITH_NOTE,
primaryType: "Message",
Expand All @@ -41,10 +44,21 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st
note: note ?? "",
},
signature,
});
} as const;
if (isSafeSignature) {
isValidSignature = await validateSafeSignature({
chainId: Number(txChainId),
typedData: typedData as unknown as EIP712TypedData,
signature,
safeAddress: signer,
});
} else {
const recoveredAddress = await recoverTypedDataAddress(typedData);
isValidSignature = recoveredAddress === signer;
}
} else {
// Validate Signature
recoveredAddress = await recoverTypedDataAddress({
const typedData = {
domain: { ...EIP_712_DOMAIN, chainId: Number(txChainId) },
types: EIP_712_TYPES__REVIEW_GRANT,
primaryType: "Message",
Expand All @@ -55,11 +69,22 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st
txChainId,
},
signature,
});
} as const;
if (isSafeSignature) {
isValidSignature = await validateSafeSignature({
chainId: Number(txChainId),
typedData: typedData as unknown as EIP712TypedData,
signature,
safeAddress: signer,
});
} else {
const recoveredAddress = await recoverTypedDataAddress(typedData);
isValidSignature = recoveredAddress === signer;
}
}

if (recoveredAddress !== signer) {
console.error("Signature error", recoveredAddress, signer);
if (!isValidSignature) {
console.error("Invalid signature", signer);
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

Expand Down
29 changes: 25 additions & 4 deletions packages/nextjs/app/api/grants/[grantId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { NextRequest, NextResponse } from "next/server";
import { EIP712TypedData } from "@safe-global/safe-core-sdk-types";
import { recoverTypedDataAddress } from "viem";
import { updateGrant } from "~~/services/database/grants";
import { findUserByAddress } from "~~/services/database/users";
import { EIP_712_DOMAIN, EIP_712_TYPES__EDIT_GRANT } from "~~/utils/eip712";
import { validateSafeSignature } from "~~/utils/safe-signature";

type ReqBody = {
title?: string;
Expand All @@ -11,25 +13,44 @@ type ReqBody = {
signature?: `0x${string}`;
signer?: string;
private_note?: string;
isSafeSignature?: boolean;
chainId?: number;
};

export async function PATCH(req: NextRequest, { params }: { params: { grantId: string } }) {
try {
const { grantId } = params;
const { title, description, signature, signer, askAmount, private_note } = (await req.json()) as ReqBody;
const { title, description, signature, signer, askAmount, private_note, isSafeSignature, chainId } =
(await req.json()) as ReqBody;

if (!title || !description || !askAmount || typeof askAmount !== "number" || !signature || !signer) {
return NextResponse.json({ error: "Invalid form details submited" }, { status: 400 });
}

const recoveredAddress = await recoverTypedDataAddress({
let isValidSignature: boolean;

const typedData = {
domain: EIP_712_DOMAIN,
types: EIP_712_TYPES__EDIT_GRANT,
primaryType: "Message",
message: { title, description, askAmount: askAmount.toString(), grantId, private_note: private_note ?? "" },
signature: signature,
});
if (recoveredAddress !== signer) {
} as const;

if (isSafeSignature) {
if (!chainId) return new Response("Missing chainId", { status: 400 });
isValidSignature = await validateSafeSignature({
chainId: Number(chainId),
typedData: typedData as unknown as EIP712TypedData,
safeAddress: signer,
signature,
});
} else {
const recoveredAddress = await recoverTypedDataAddress(typedData);
isValidSignature = recoveredAddress === signer;
}

if (!isValidSignature) {
return NextResponse.json({ error: "Recovered address did not match signer" }, { status: 401 });
}

Expand Down
Loading
Loading