diff --git a/public/.well-known/walletconnect.txt b/public/.well-known/walletconnect.txt new file mode 100644 index 00000000..d9974ca5 --- /dev/null +++ b/public/.well-known/walletconnect.txt @@ -0,0 +1 @@ +8e64b458-f676-4d5d-915b-9a15170b65b7=bbf017f88c8435a30e24b0575fc87e18a21a1ed561ec5e5cab4fb5349699e32b \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index fa5e55c1..50cb38e9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -22,21 +22,6 @@ export const theme = { colorMode: "light", }; -export const eas = { - contracts: { - eas: - process.env.NEXT_PUBLIC_EAS_CONTRACT_ADDRESS ?? - "0x4200000000000000000000000000000000000021", - schemaRegistry: - process.env.NEXT_PUBLIC_EAS_SCHEMA_REGISTRY_ADDRESS ?? - "0x4200000000000000000000000000000000000020", - }, - schemas: { - metadata: process.env.NEXT_PUBLIC_METADATA_SCHEMA!, - approval: process.env.NEXT_PUBLIC_APPROVAL_SCHEMA!, - }, -}; - export const networks = { mainnet: "mainnet", optimism: "optimism", @@ -46,7 +31,50 @@ export const networks = { sepolia: "sepolia", base: "base", baseGoerli: "baseGoerli", + celo: "celo", } as const; + +export const eas = { + contracts: { + [networks.mainnet]: { + eas: "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587", + registry: "0xA7b39296258348C78294F95B872b282326A97BDF", + }, + [networks.arbitrum]: { + eas: "0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458", + registry: "0xA310da9c5B885E7fb3fbA9D66E9Ba6Df512b78eB", + }, + [networks.celo]: { + eas: "0x72E1d8ccf5299fb36fEfD8CC4394B8ef7e98Af92", + registry: "0x5ece93bE4BDCF293Ed61FA78698B594F2135AF34", + schemas: { + metadata: + "0xf01bd22db2b104f6a7096f3625307b1c03b863b73f08e71557ebf1adc20cf1bf", + approval: + "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", + }, + }, + [networks.linea]: { + eas: "0xaEF4103A04090071165F78D45D83A0C0782c2B2a", + registry: "0x55D26f9ae0203EF95494AE4C170eD35f4Cf77797", + }, + [networks.sepolia]: { + eas: "0xC2679fBD37d54388Ce493F1DB75320D236e1815e", + registry: "0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0", + }, + default: { + eas: "0x4200000000000000000000000000000000000021", + registry: "0x4200000000000000000000000000000000000020", + schemas: { + approval: + "0x858e0bc94997c072d762d90440966759b57c8bca892d4c9447d2eeb205f14c69", + metadata: + "0xd00c966351896bd3dc37d22017bf1ef23165f859d7546a2aba12a01623dec912", + }, + }, + }, +}; + export const supportedNetworks = Object.values(networks).map((chain) => ({ ...allChains[chain], chain, @@ -64,6 +92,7 @@ export const easApiEndpoints = { [networks.sepolia]: "https://sepolia.easscan.org/graphql", [networks.base]: "https://base.easscan.org/graphql", [networks.baseGoerli]: "https://base-goerli.easscan.org/graphql", + [networks.celo]: "https://celo.easscan.org/graphql", } as const; export const impactCategories = { diff --git a/src/env.js b/src/env.js index 31565abb..247119fd 100644 --- a/src/env.js +++ b/src/env.js @@ -40,8 +40,6 @@ export const env = createEnv({ NEXT_PUBLIC_SIGN_STATEMENT: z.string().optional(), NEXT_PUBLIC_FEEDBACK_URL: z.string().default("#"), - NEXT_PUBLIC_EAS_CONTRACT_ADDRESS: z.string(), - NEXT_PUBLIC_APPROVAL_SCHEMA: z.string().startsWith("0x"), NEXT_PUBLIC_METADATA_SCHEMA: z.string().startsWith("0x"), @@ -72,8 +70,6 @@ export const env = createEnv({ NEXT_PUBLIC_FEEDBACK_URL: process.env.NEXT_PUBLIC_FEEDBACK_URL, - NEXT_PUBLIC_EAS_CONTRACT_ADDRESS: - process.env.NEXT_PUBLIC_EAS_CONTRACT_ADDRESS, NEXT_PUBLIC_WALLETCONNECT_ID: process.env.NEXT_PUBLIC_WALLETCONNECT_ID, NEXT_PUBLIC_ALCHEMY_ID: process.env.NEXT_PUBLIC_ALCHEMY_ID, diff --git a/src/features/applications/hooks/useApproveApplication.ts b/src/features/applications/hooks/useApproveApplication.ts index bc64ad63..2cc30ed4 100644 --- a/src/features/applications/hooks/useApproveApplication.ts +++ b/src/features/applications/hooks/useApproveApplication.ts @@ -5,6 +5,7 @@ import { eas } from "~/config"; import { useEthersSigner } from "~/hooks/useEthersSigner"; import { toast } from "sonner"; import { useCurrentRound } from "~/features/rounds/hooks/useRound"; +import { getContracts } from "~/lib/eas/createEAS"; export function useApproveApplication(opts?: { onSuccess?: () => void }) { const attest = useAttest(); @@ -12,8 +13,6 @@ export function useApproveApplication(opts?: { onSuccess?: () => void }) { const { data: round } = useCurrentRound(); - const roundId = String(round?.id); - return useMutation({ onSuccess: () => { toast.success("Application approved successfully!"); @@ -25,16 +24,20 @@ export function useApproveApplication(opts?: { onSuccess?: () => void }) { }), mutationFn: async (applicationIds: string[]) => { if (!signer) throw new Error("Connect wallet first"); + if (!round?.network) throw new Error("Round network not configured"); + + const contracts = getContracts(round.network); const attestations = await Promise.all( applicationIds.map((refUID) => createAttestation( { - values: { type: "application", round: roundId }, - schemaUID: eas.schemas.approval, + values: { type: "application", round: round.id }, + schemaUID: contracts.schemas.approval, refUID, }, signer, + contracts, ), ), ); diff --git a/src/features/applications/hooks/useApprovedApplications.ts b/src/features/applications/hooks/useApprovedApplications.ts index 763a935e..44512b99 100644 --- a/src/features/applications/hooks/useApprovedApplications.ts +++ b/src/features/applications/hooks/useApprovedApplications.ts @@ -1,4 +1,3 @@ -import { useCurrentRound } from "~/features/rounds/hooks/useRound"; import { api } from "~/utils/api"; export function useApprovedApplications(ids?: string[]) { diff --git a/src/features/applications/hooks/useCreateApplication.ts b/src/features/applications/hooks/useCreateApplication.ts index 4b9e20cb..26e1189a 100644 --- a/src/features/applications/hooks/useCreateApplication.ts +++ b/src/features/applications/hooks/useCreateApplication.ts @@ -5,6 +5,7 @@ import { useAttest, useCreateAttestation } from "~/hooks/useEAS"; import type { Application, Profile } from "../types"; import { type TransactionError } from "~/features/voters/hooks/useApproveVoters"; import { useCurrentRound } from "~/features/rounds/hooks/useRound"; +import { getContracts } from "~/lib/eas/createEAS"; export function useCreateApplication({ onSuccess, @@ -29,11 +30,14 @@ export function useCreateApplication({ }) => { if (!roundId) throw new Error("Round ID must be defined"); console.log("Uploading profile and application metadata"); + if (!round?.network) throw new Error("Round network must be configured"); + + const contracts = getContracts(round.network); return Promise.all([ upload.mutateAsync(values.application).then(({ url: metadataPtr }) => { console.log("Creating application attestation data"); return attestation.mutateAsync({ - schemaUID: eas.schemas.metadata, + schemaUID: contracts.schemas.metadata, values: { name: values.application.name, metadataType: 0, // "http" @@ -46,7 +50,7 @@ export function useCreateApplication({ upload.mutateAsync(values.profile).then(({ url: metadataPtr }) => { console.log("Creating profile attestation data"); return attestation.mutateAsync({ - schemaUID: eas.schemas.metadata, + schemaUID: contracts.schemas.metadata, values: { name: values.profile.name, metadataType: 0, // "http" diff --git a/src/features/lists/hooks/useCreateList.ts b/src/features/lists/hooks/useCreateList.ts index dc45fb69..860cdc01 100644 --- a/src/features/lists/hooks/useCreateList.ts +++ b/src/features/lists/hooks/useCreateList.ts @@ -5,6 +5,7 @@ import { useAttest, useCreateAttestation } from "~/hooks/useEAS"; import { type TransactionError } from "~/features/voters/hooks/useApproveVoters"; import { type List } from "../types"; import { useCurrentRound } from "~/features/rounds/hooks/useRound"; +import { getContracts } from "~/lib/eas/createEAS"; export function useCreateList({ onSuccess, @@ -25,12 +26,14 @@ export function useCreateList({ onError, mutationFn: async (values: List) => { console.log("Uploading list metadata"); + if (!round?.network) throw new Error("Round network must be configured"); + const contracts = getContracts(round.network); return upload .mutateAsync(values) .then(({ url: metadataPtr }) => { console.log("Creating application attestation data"); return attestation.mutateAsync({ - schemaUID: eas.schemas.metadata, + schemaUID: contracts.schemas.metadata, values: { name: values.name, metadataType: 0, // "http" diff --git a/src/features/rounds/types/index.ts b/src/features/rounds/types/index.ts index 70968484..76e3a640 100644 --- a/src/features/rounds/types/index.ts +++ b/src/features/rounds/types/index.ts @@ -79,7 +79,7 @@ export const RoundSchema = z admins: z.array(EthAddressSchema), description: z.string().nullable(), network: z.string().nullable(), - tokenAddress: EthAddressSchema.nullable(), + tokenAddress: EthAddressSchema.or(z.string().nullish()), poolId: z.number().nullable(), calculationType: CalculationTypeSchema, calculationConfig: z diff --git a/src/features/voters/hooks/useApproveVoters.ts b/src/features/voters/hooks/useApproveVoters.ts index b65b3bef..0af73ea0 100644 --- a/src/features/voters/hooks/useApproveVoters.ts +++ b/src/features/voters/hooks/useApproveVoters.ts @@ -5,6 +5,7 @@ import { useMutation } from "@tanstack/react-query"; import { createAttestation } from "~/lib/eas/createAttestation"; import { useCurrentRound } from "~/features/rounds/hooks/useRound"; import { api } from "~/utils/api"; +import { getContracts } from "~/lib/eas/createEAS"; // TODO: Move this to a shared folders export type TransactionError = { reason?: string; data?: { message: string } }; @@ -23,22 +24,24 @@ export function useApproveVoters({ const attest = useAttest(); const signer = useEthersSigner(); const { data: round } = useCurrentRound(); - const roundId = String(round?.id); return useMutation({ mutationFn: async (voters: string[]) => { if (!signer) throw new Error("Connect wallet first"); - if (!roundId) throw new Error("Round ID must be defined"); + if (!round) throw new Error("Round must be defined"); + if (!round?.network) throw new Error("Round network must be configured"); + const contracts = getContracts(round.network); const attestations = await Promise.all( voters.map((recipient) => createAttestation( { - values: { type: "voter", round: roundId }, - schemaUID: eas.schemas.approval, + values: { type: "voter", round: round.id }, + schemaUID: contracts.schemas.approval, recipient, }, signer, + contracts, ), ), ); diff --git a/src/hooks/useEAS.ts b/src/hooks/useEAS.ts index 13b8a270..4bf76e60 100644 --- a/src/hooks/useEAS.ts +++ b/src/hooks/useEAS.ts @@ -3,28 +3,32 @@ import { type MultiAttestationRequest } from "@ethereum-attestation-service/eas- import { useEthersSigner } from "~/hooks/useEthersSigner"; import { createAttestation } from "~/lib/eas/createAttestation"; -import { createEAS } from "~/lib/eas/createEAS"; +import { createEAS, getContracts } from "~/lib/eas/createEAS"; +import { useCurrentRound } from "~/features/rounds/hooks/useRound"; export function useCreateAttestation() { const signer = useEthersSigner(); + const { data: round } = useCurrentRound(); return useMutation({ mutationFn: async (data: { values: Record; schemaUID: string; }) => { if (!signer) throw new Error("Connect wallet first"); - return createAttestation(data, signer); + if (!round?.network) throw new Error("Round network not configured"); + return createAttestation(data, signer, getContracts(round.network)); }, }); } export function useAttest() { const signer = useEthersSigner(); + const { data: round } = useCurrentRound(); return useMutation({ mutationFn: async (attestations: MultiAttestationRequest[]) => { if (!signer) throw new Error("Connect wallet first"); - const eas = createEAS(signer); - + if (!round?.network) throw new Error("Round network not configured"); + const eas = createEAS(signer, round?.network); return eas.multiAttest(attestations); }, }); diff --git a/src/layouts/DefaultLayout.tsx b/src/layouts/DefaultLayout.tsx index 92da517c..e92bf87b 100644 --- a/src/layouts/DefaultLayout.tsx +++ b/src/layouts/DefaultLayout.tsx @@ -10,6 +10,8 @@ import { } from "~/features/rounds/hooks/useRound"; import { useRoundState } from "~/features/rounds/hooks/useRoundState"; import { useSession } from "next-auth/react"; +import { Button } from "~/components/ui/Button"; +import Link from "next/link"; type Props = PropsWithChildren< { @@ -21,7 +23,8 @@ export const Layout = ({ children, ...props }: Props) => { const { address } = useAccount(); const domain = useCurrentDomain(); - const { data: round } = useCurrentRound(); + const { data: round, isPending } = useCurrentRound(); + const navLinks = [ { href: `/${domain}/projects`, @@ -51,6 +54,19 @@ export const Layout = ({ children, ...props }: Props) => { ); } + if (!isPending && !round) { + return ( + +
+ Round not found + +
+
+ ); + } + return ( }> {children} diff --git a/src/lib/eas/createAttestation.ts b/src/lib/eas/createAttestation.ts index 13b1338e..d89a9b27 100644 --- a/src/lib/eas/createAttestation.ts +++ b/src/lib/eas/createAttestation.ts @@ -3,9 +3,11 @@ import { SchemaRegistry, type SchemaValue, type AttestationRequest, + type SchemaRecord, } from "@ethereum-attestation-service/eas-sdk"; import { type Signer } from "ethers"; -import * as config from "~/config"; + +import { eas } from "~/config"; type Params = { values: Record; @@ -17,12 +19,21 @@ type Params = { export async function createAttestation( params: Params, signer: Signer, + contracts: typeof eas.contracts.default, ): Promise { console.log("Getting recipient address"); const recipient = params.recipient ?? (await signer.getAddress()); + const schemaRegistry = new SchemaRegistry(contracts.registry); + console.log("Connecting signer to SchemaRegistry..."); + schemaRegistry.connect(signer); + console.log("Getting schema record...", params.schemaUID); + const schemaRecord = await schemaRegistry.getSchema({ + uid: params.schemaUID, + }); + console.log("Encoding attestation data"); - const data = await encodeData(params, signer); + const data = await encodeData(params, schemaRecord); return { schema: params.schemaUID, @@ -36,15 +47,8 @@ export async function createAttestation( }; } -async function encodeData({ values, schemaUID }: Params, signer: Signer) { - const schemaRegistry = new SchemaRegistry( - config.eas.contracts.schemaRegistry, - ); - console.log("Connecting signer to SchemaRegistry..."); - schemaRegistry.connect(signer); - +async function encodeData({ values }: Params, schemaRecord: SchemaRecord) { console.log("Getting schema record..."); - const schemaRecord = await schemaRegistry.getSchema({ uid: schemaUID }); const schemaEncoder = new SchemaEncoder(schemaRecord.schema); diff --git a/src/lib/eas/createEAS.ts b/src/lib/eas/createEAS.ts index 35e76398..0531547b 100644 --- a/src/lib/eas/createEAS.ts +++ b/src/lib/eas/createEAS.ts @@ -1,12 +1,25 @@ -import { type providers } from "ethers"; -import { EAS } from "@ethereum-attestation-service/eas-sdk"; -import { type SignerOrProvider } from "@ethereum-attestation-service/eas-sdk/dist/transaction"; +import { type JsonRpcSigner } from "ethers"; +import { + EAS, + type TransactionSigner, +} from "@ethereum-attestation-service/eas-sdk"; import * as config from "~/config"; -export function createEAS(signer: providers.JsonRpcSigner): EAS { +export function createEAS(signer: JsonRpcSigner, network: string): EAS { console.log("Creating EAS instance"); - const eas = new EAS(config.eas.contracts.eas); + const contracts = getContracts(network); + + const eas = new EAS(contracts.eas); + console.log("Connecting signer to EAS"); - return eas.connect(signer as unknown as SignerOrProvider); + return eas.connect(signer as unknown as TransactionSigner); +} + +export function getContracts(network: string) { + return { + ...config.eas.contracts.default, + ...(config.eas.contracts[network as keyof typeof config.eas.contracts] ?? + {}), + }; } diff --git a/src/lib/eas/registerSchemas.ts b/src/lib/eas/registerSchemas.ts index 5bea145c..25013f14 100644 --- a/src/lib/eas/registerSchemas.ts +++ b/src/lib/eas/registerSchemas.ts @@ -1,14 +1,25 @@ import "dotenv/config"; -import { ethers } from "ethers"; -import { formatEther } from "ethers/lib/utils"; -import { - SchemaRegistry, - ZERO_ADDRESS, - getSchemaUID, -} from "@ethereum-attestation-service/eas-sdk"; -import type { SignerOrProvider } from "@ethereum-attestation-service/eas-sdk/dist/transaction"; +import { SchemaRegistry } from "@ethereum-attestation-service/eas-sdk"; +import { type Address, formatEther, publicActions } from "viem"; +import { createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import * as allChains from "viem/chains"; +import { getContracts } from "./createEAS"; +import { clientToSigner } from "~/hooks/useEthersSigner"; -import { eas, config } from "~/config"; +const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Address); +const contracts = getContracts(process.env.NETWORK!); +const chain = allChains[process.env.NETWORK as keyof typeof allChains]; +if (!chain) + throw new Error( + "Environment variable NETWORK must be set to a valid network", + ); + +const client = createWalletClient({ + account, + chain, + transport: http(), +}).extend(publicActions); /* This file defines and registers the EAS schemas. @@ -25,37 +36,22 @@ const metadataSchema = "string name, string metadataPtr, uint256 metadataType, bytes32 type, bytes32 round"; const schemas = [ - { name: "Voter Approval", schema: approvalSchema }, - { name: "Application Approval", schema: approvalSchema }, - { name: "Application Metadata", schema: metadataSchema }, - { name: "Profile Metadata", schema: metadataSchema }, - { name: "List Metadata", schema: metadataSchema }, + { name: "Approval", schema: approvalSchema }, + { name: "Metadata", schema: metadataSchema }, ]; -const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY!).connect( - new ethers.providers.AlchemyProvider( - config.network.network, - process.env.NEXT_PUBLIC_ALCHEMY_ID, - ), -); - -const schemaRegistry = new SchemaRegistry(eas.contracts.schemaRegistry); -schemaRegistry.connect(wallet as unknown as SignerOrProvider); +const schemaRegistry = new SchemaRegistry(contracts.registry); +schemaRegistry.connect(clientToSigner(client)); export async function registerSchemas() { - console.log("Balance: ", await wallet.getBalance().then(formatEther)); + console.log( + "Balance: ", + await client.getBalance({ address: account.address }).then(formatEther), + ); return Promise.all( schemas.map(async ({ name, schema }) => { console.log(`Registering schema: ${name}`); - const exists = await schemaRegistry - .getSchema({ - uid: getSchemaUID(schema, ZERO_ADDRESS, true), - }) - .catch(); - console.log("exists", exists); - if (exists) return { name, ...exists }; - return schemaRegistry .register({ schema, revocable: true }) .then(async (tx) => ({ name, uid: await tx.wait() })); diff --git a/src/pages/[domain]/admin/token/index.tsx b/src/pages/[domain]/admin/token/index.tsx index 60d77e22..b9c94183 100644 --- a/src/pages/[domain]/admin/token/index.tsx +++ b/src/pages/[domain]/admin/token/index.tsx @@ -79,7 +79,7 @@ export default function AdminTokenPage() { label="Token address" hint={} > - +