Skip to content

Commit

Permalink
submit application & use useSmartAccount hook (#17)
Browse files Browse the repository at this point in the history
* submit application & use useSmartAccount hook

* Use Hex & Address types
  • Loading branch information
JohnGuilding authored Aug 28, 2024
1 parent 1c06824 commit a6787b5
Show file tree
Hide file tree
Showing 25 changed files with 190 additions and 100 deletions.
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

0 comments on commit a6787b5

Please sign in to comment.