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

submit application & use useSmartAccount hook #17

Merged
merged 2 commits into from
Aug 28, 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
1 change: 1 addition & 0 deletions packages/interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test:e2e": "playwright test --project=chromium"
},
"dependencies": {
"@ethereum-attestation-service/eas-contracts": "^1.7.1",
"@ethereum-attestation-service/eas-sdk": "^1.5.0",
"@hatsprotocol/sdk-v1-core": "^0.10.0",
"@hookform/resolvers": "^3.3.4",
Expand Down
14 changes: 3 additions & 11 deletions packages/interface/src/components/EligibilityDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { useRouter } from "next/router";
import { useState, useCallback, useEffect } from "react";
import { toast } from "sonner";
import { useAccount, useDisconnect } from "wagmi";

import { useMaci } from "~/contexts/Maci";
import { useAppState } from "~/utils/state";
import { EAppState } from "~/utils/types";

import { Dialog } from "./ui/Dialog";
import useSmartAccount from "~/hooks/useSmartAccount";

export const EligibilityDialog = (): JSX.Element | null => {
const { address } = useAccount();
const { disconnect } = useDisconnect();
const { address } = useSmartAccount();

const [openDialog, setOpenDialog] = useState<boolean>(!!address);
const { onSignup, isEligibleToVote, isRegistered } = useMaci();
Expand All @@ -34,10 +33,6 @@ export const EligibilityDialog = (): JSX.Element | null => {
setOpenDialog(false);
}, [setOpenDialog]);

const handleDisconnect = useCallback(() => {
disconnect();
}, [disconnect]);

const handleGoToProjects = useCallback(() => {
router.push("/projects");
}, [router]);
Expand All @@ -46,7 +41,7 @@ export const EligibilityDialog = (): JSX.Element | null => {
router.push("/applications/new");
}, [router]);

if (appState === EAppState.APPLICATION) {
if (appState === EAppState.APPLICATION && isEligibleToVote) {
return (
<Dialog
button="secondary"
Expand Down Expand Up @@ -123,9 +118,6 @@ export const EligibilityDialog = (): JSX.Element | null => {
if (appState === EAppState.VOTING && !isEligibleToVote) {
return (
<Dialog
button="secondary"
buttonAction={handleDisconnect}
buttonName="Disconnect"
description="To participate in this round, you must be in the voter's registry. Contact the round organizers to get access as a voter."
isOpen={openDialog}
size="sm"
Expand Down
4 changes: 2 additions & 2 deletions packages/interface/src/components/JoinButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const JoinButton = (): JSX.Element => {
<Button variant="disabled">You are not allowed to vote</Button>
)}

{appState !== EAppState.TALLYING && !isEligibleToVote && !isRegistered && (
<Button variant="primary">
{(appState === EAppState.APPLICATION || appState === EAppState.VOTING) && !isEligibleToVote && !isRegistered && (
<Button variant={isRegistered === undefined || isLoading ? "disabled" : "primary"}>
<Link href="/signup/registerEmail">Register</Link>
</Button>
)}
Expand Down
3 changes: 2 additions & 1 deletion packages/interface/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as wagmiChains from "wagmi/chains";
import { Config } from "./utils/types";
import { Address } from "viem";

export const metadata = {
title: "MACI PLATFORM",
Expand Down Expand Up @@ -104,7 +105,7 @@ export const config: Config = {
tokenName: process.env.NEXT_PUBLIC_TOKEN_NAME!,
eventName: process.env.NEXT_PUBLIC_EVENT_NAME ?? "MACI-PLATFORM",
roundId: process.env.NEXT_PUBLIC_ROUND_ID!,
admin: (process.env.NEXT_PUBLIC_ADMIN_ADDRESS ?? "") as `0x${string}`,
admin: (process.env.NEXT_PUBLIC_ADMIN_ADDRESS ?? "") as Address,
network: wagmiChains[process.env.NEXT_PUBLIC_CHAIN_NAME as keyof typeof wagmiChains],
maciAddress: process.env.NEXT_PUBLIC_MACI_ADDRESS,
maciStartBlock: Number(process.env.NEXT_PUBLIC_MACI_START_BLOCK ?? 0),
Expand Down
9 changes: 0 additions & 9 deletions packages/interface/src/contexts/Ballot.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from "react";
import { useAccount } from "wagmi";

import { config } from "~/config";

Expand All @@ -14,8 +13,6 @@ export const BallotProvider: React.FC<BallotProviderProps> = ({ children }: Ball
const [ballot, setBallot] = useState<Ballot>(defaultBallot);
const [isLoading, setLoading] = useState<boolean>(true);

const { isDisconnected } = useAccount();

// when summing the ballot we take the individual vote and square it
// if the mode is quadratic voting, otherwise we just add the amount
const sumBallot = useCallback(
Expand Down Expand Up @@ -102,12 +99,6 @@ export const BallotProvider: React.FC<BallotProviderProps> = ({ children }: Ball
}
}, [ballot, ballot.votes, ballot.published, saveBallot]);

useEffect(() => {
if (isDisconnected) {
deleteBallot();
}
}, [isDisconnected, deleteBallot]);

const value = useMemo(
() => ({
ballot,
Expand Down
6 changes: 3 additions & 3 deletions packages/interface/src/contexts/Maci.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getHatsSingleGatekeeperData,
} from "maci-cli/sdk";
import React, { createContext, useContext, useCallback, useEffect, useMemo, useState } from "react";
import { Address, type EIP1193Provider } from "viem";

import { config } from "~/config";
import { useEthersSigner } from "~/hooks/useEthersSigner";
Expand All @@ -23,7 +24,6 @@ import { getHatsClient } from "~/utils/hatsProtocol";
import { getSemaphoreProof } from "~/utils/semaphore";

import type { IVoteArgs, MaciContextType, MaciProviderProps } from "./types";
import { type EIP1193Provider } from "viem";
import type { Attestation } from "~/utils/types";
import signUp from "~/utils/signUp";

Expand Down Expand Up @@ -159,11 +159,11 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
updateEligibility(sgData, address);
}, [sgData, address])

const updateEligibility = (_sgData: string | undefined, _address: `0x${string}` | undefined) => {
const updateEligibility = (_sgData: string | undefined, _address: Address | undefined) => {
setIsEligibleToVote(checkEligibility(_sgData, _address));
}

function checkEligibility(sgData: string | undefined, address: `0x${string}` | undefined): boolean {
function checkEligibility(sgData: string | undefined, address: Address | undefined): boolean {
return (gatekeeperTrait && (gatekeeperTrait === GatekeeperTrait.FreeForAll || Boolean(sgData)) && Boolean(address)) ?? false;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useMemo, useCallback, useState } from "react";
import { useFormContext } from "react-hook-form";
import { useAccount } from "wagmi";

import { Button, IconButton } from "~/components/ui/Button";
import { Dialog } from "~/components/ui/Dialog";
Expand All @@ -9,6 +8,7 @@ import { useIsCorrectNetwork } from "~/hooks/useIsCorrectNetwork";

import type { Application } from "../types";
import type { ImpactMetrix, ContributionLink, FundingSource } from "~/features/projects/types";
import useSmartAccount from "~/hooks/useSmartAccount";

export enum EApplicationStep {
PROFILE,
Expand All @@ -33,7 +33,7 @@ export const ApplicationButtons = ({
}: IApplicationButtonsProps): JSX.Element => {
const { isCorrectNetwork } = useIsCorrectNetwork();

const { address } = useAccount();
const { address } = useSmartAccount();

const [showDialog, setShowDialog] = useState<boolean>(false);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Transaction } from "@ethereum-attestation-service/eas-sdk";
import { useRouter } from "next/router";
import { useState, useCallback } from "react";
import { useLocalStorage } from "react-use";
import { toast } from "sonner";
import { useAccount } from "wagmi";
import { Hex } from "viem";

import { ImageUpload } from "~/components/ImageUpload";
import { FieldArray, Form, FormControl, FormSection, Select, Textarea } from "~/components/ui/Form";
Expand All @@ -17,13 +16,14 @@ import { ApplicationButtons, EApplicationStep } from "./ApplicationButtons";
import { ApplicationSteps } from "./ApplicationSteps";
import { ImpactTags } from "./ImpactTags";
import { ReviewApplicationDetails } from "./ReviewApplicationDetails";
import useSmartAccount from "~/hooks/useSmartAccount";

export const ApplicationForm = (): JSX.Element => {
const clearDraft = useLocalStorage("application-draft")[2];

const { isCorrectNetwork, correctNetwork } = useIsCorrectNetwork();

const { address } = useAccount();
const { address } = useSmartAccount();

const router = useRouter();

Expand Down Expand Up @@ -52,14 +52,14 @@ export const ApplicationForm = (): JSX.Element => {
}, [step, setStep]);

const create = useCreateApplication({
onSuccess: (data: Transaction<string[]>) => {
onSuccess: (hash: Hex) => {
clearDraft();
router.push(`/applications/confirmation?txHash=${data.tx.hash}`);
router.push(`/applications/confirmation?txHash=${hash}`);
},
onError: (err: { reason?: string; data?: { message: string } }) =>
onError: (err: { reason?: string; data?: { message: string } }) => {
toast.error("Application create error", {
description: err.reason ?? err.data?.message,
}),
})},
});

const { error: createError } = create;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { type Transaction } from "@ethereum-attestation-service/eas-sdk";
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { Hex } from "viem";

import { config, eas } from "~/config";
import { type TransactionError } from "~/features/voters/hooks/useApproveVoters";
import { useAttest } from "~/hooks/useEAS";
import { useEthersSigner } from "~/hooks/useEthersSigner";
import useSmartAccount from "~/hooks/useSmartAccount";
import { createAttestation } from "~/lib/eas/createAttestation";

export function useApproveApplication(opts?: {
onSuccess?: () => void;
}): UseMutationResult<Transaction<string[]>, Error | TransactionError, string[]> {
}): UseMutationResult<Hex, Error | TransactionError, string[]> {
const attest = useAttest();
const signer = useEthersSigner();
const { smartAccountClient } = useSmartAccount();
const signer = useEthersSigner({ client: smartAccountClient });

return useMutation({
mutationFn: async (applicationIds: string[]) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import { Hex } from "viem";

import { config, eas } from "~/config";
import { type TransactionError } from "~/features/voters/hooks/useApproveVoters";
import { useAttest, useCreateAttestation } from "~/hooks/useEAS";
import { useUploadMetadata } from "~/hooks/useMetadata";

import type { Application } from "../types";
import type { Transaction } from "@ethereum-attestation-service/eas-sdk";

export type TUseCreateApplicationReturn = Omit<
UseMutationResult<Transaction<string[]>, Error | TransactionError, Application>,
UseMutationResult<Hex, Error | TransactionError, Application>,
"error"
> & {
error: Error | TransactionError | null;
Expand All @@ -18,7 +18,7 @@ export type TUseCreateApplicationReturn = Omit<
};

export function useCreateApplication(options: {
onSuccess: (data: Transaction<string[]>) => void;
onSuccess: (hash: Hex) => void;
onError: (err: TransactionError) => void;
}): TUseCreateApplicationReturn {
const attestation = useCreateAttestation();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { type Transaction } from "@ethereum-attestation-service/eas-sdk";
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import { Hex } from "viem";

import { config, eas } from "~/config";
import { useAttest } from "~/hooks/useEAS";
import { useEthersSigner } from "~/hooks/useEthersSigner";
import useSmartAccount from "~/hooks/useSmartAccount";
import { createAttestation } from "~/lib/eas/createAttestation";

// TODO: Move this to a shared folders
Expand All @@ -15,9 +16,10 @@ export interface TransactionError {
export function useApproveVoters(options: {
onSuccess: () => void;
onError: (err: TransactionError) => void;
}): UseMutationResult<Transaction<string[]>, unknown, string[]> {
}): UseMutationResult<Hex, unknown, string[]> {
const attest = useAttest();
const signer = useEthersSigner();
const { smartAccountClient } = useSmartAccount();
const signer = useEthersSigner({ client: smartAccountClient });

return useMutation({
mutationFn: async (voters: string[]) => {
Expand Down
60 changes: 49 additions & 11 deletions packages/interface/src/hooks/useEAS.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { AttestationRequest, Transaction, type MultiAttestationRequest } from "@ethereum-attestation-service/eas-sdk";
import { AttestationRequest, NO_EXPIRATION, ZERO_ADDRESS, ZERO_BYTES32, type MultiAttestationRequest } from "@ethereum-attestation-service/eas-sdk";
import { EAS__factory as EASFactory } from '@ethereum-attestation-service/eas-contracts';
import { type DefaultError, type UseMutationResult, useMutation } from "@tanstack/react-query";

import { useEthersSigner } from "~/hooks/useEthersSigner";
import { createAttestation } from "~/lib/eas/createAttestation";
import { createEAS } from "~/lib/eas/createEAS";
import useSmartAccount from "./useSmartAccount";
import { Address, Hex } from "viem";
import { publicClient } from "~/utils/permissionless";
import { eas } from "~/config";

export function useCreateAttestation(): UseMutationResult<
AttestationRequest,
DefaultError,
{ values: Record<string, unknown>; schemaUID: string }
> {
const signer = useEthersSigner();
const { smartAccountClient } = useSmartAccount();
const signer = useEthersSigner({ client: smartAccountClient });

return useMutation({
mutationFn: async (data: { values: Record<string, unknown>; schemaUID: string }) => {
Expand All @@ -23,17 +28,50 @@ export function useCreateAttestation(): UseMutationResult<
});
}

export function useAttest(): UseMutationResult<Transaction<string[]>, DefaultError, MultiAttestationRequest[]> {
const signer = useEthersSigner();
export function useAttest(): UseMutationResult<Hex, DefaultError, MultiAttestationRequest[]> {
const { smartAccount, smartAccountClient } = useSmartAccount();
const signer = useEthersSigner({ client: smartAccountClient });

return useMutation({
mutationFn: (attestations: MultiAttestationRequest[]) => {
if (!signer) {
throw new Error("Connect wallet first");
mutationFn: async (attestations: MultiAttestationRequest[]) => {
if (!smartAccount || !smartAccountClient) {
throw new Error("Smart account not connected");
}

const multiAttestationRequests = attestations.map((r) => ({
schema: r.schema as Hex,
data: r.data.map((d) => ({
recipient: (d.recipient ?? ZERO_ADDRESS) as Address,
expirationTime: d.expirationTime ?? NO_EXPIRATION,
revocable: d.revocable ?? true,
refUID: (d.refUID ?? ZERO_BYTES32) as Hex,
data: (d.data ?? ZERO_BYTES32) as Hex,
value: d.value ?? 0n
}))
}));

const requestedValue = multiAttestationRequests.reduce((res, { data }) => {
const total = data.reduce((res, r) => res + r.value, 0n);
return res + total;
}, 0n);

if (requestedValue > 0n) {
throw new Error("Cannot sponsor a user operation that sends value")
}

try {
const { request } = await publicClient.simulateContract({
account: smartAccount,
address: eas.contracts.eas as Address,
abi: EASFactory.abi,
functionName: "multiAttest",
args: [multiAttestationRequests],
});
return await smartAccountClient.writeContract(request);
} catch (error: unknown) {
console.error(error);
throw new Error("Error attesting");
}
const eas = createEAS(signer);

return eas.multiAttest(attestations);
},
});
}
2 changes: 1 addition & 1 deletion packages/interface/src/hooks/useEthersSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useConnectorClient } from "wagmi";
import { getRPCURL } from "~/config";

function clientToSigner(client: SmartAccountClient<EntryPoint, HttpTransport, Chain>): JsonRpcSigner | undefined {
const { account, chain, transport } = client;
const { account, chain } = client;

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!chain || !account) {
Expand Down
6 changes: 3 additions & 3 deletions packages/interface/src/hooks/useIsAdmin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useAccount } from "wagmi";

import { config } from "~/config";
import useSmartAccount from "./useSmartAccount";

export function useIsAdmin(): boolean {
const { address } = useAccount();
// TODO: (merge-ok) figure out how we set embedded smart account to admin
const { address } = useSmartAccount();

return config.admin === address!;
}
Loading