From eb6410b10e77234804755d302459aa20a8da4a0e Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Sat, 28 Dec 2024 16:32:55 +0700 Subject: [PATCH 01/16] feat: add conversion from schema string to zod --- .../src/hooks/eas/use-form-field.test.ts | 82 +++++++++++++++++++ .../ui-react/src/hooks/eas/use-form-field.ts | 31 +++++++ 2 files changed, 113 insertions(+) create mode 100644 packages/ui-react/src/hooks/eas/use-form-field.test.ts create mode 100644 packages/ui-react/src/hooks/eas/use-form-field.ts diff --git a/packages/ui-react/src/hooks/eas/use-form-field.test.ts b/packages/ui-react/src/hooks/eas/use-form-field.test.ts new file mode 100644 index 00000000..553ea12c --- /dev/null +++ b/packages/ui-react/src/hooks/eas/use-form-field.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import { getZodSchemaFromSchemaString } from "./use-form-field"; +import { getRandomAddress } from "@geist/domain/user.fixture"; +import { Z } from "vitest/dist/chunks/reporters.D7Jzd9GS.js"; +import { z } from "zod"; + +describe("use-form-field", () => { + describe("#getZodSchemaFromSchemaString", () => { + it("should correctly produce zod schema", () => { + const sampleEasSchema = getZodSchemaFromSchemaString( + "address walletAddress,string requestId,bool hasClaimedNFT,string message", + ); + + const randomAddress = getRandomAddress(); + + expect( + sampleEasSchema.parse({ + walletAddress: randomAddress, + requestId: "123", + hasClaimedNFT: true, + message: "hello world", + }), + ).toEqual({ + walletAddress: randomAddress, + requestId: "123", + hasClaimedNFT: true, + message: "hello world", + }); + + expect(() => + sampleEasSchema.parse({ + walletAddress: randomAddress, + requestId: "123", + hasClaimedNFT: true, + }), + ).toThrow(); + + expect(() => + sampleEasSchema.parse({ + walletAddress: randomAddress, + requestId: "123", + hasClaimedNFT: "true", + message: "hello world", + }), + ).toThrow(); + }); + + it("should correctly produce zod schema with array", () => { + const sampleEasSchema = getZodSchemaFromSchemaString( + "address walletAddress,string requestId,bool hasClaimedNFT,string message,string[] characters", + ); + + const randomAddress = getRandomAddress(); + + expect(() => + sampleEasSchema.parse({ + walletAddress: randomAddress, + requestId: "123", + hasClaimedNFT: true, + message: "hello world", + characters: "vitalik", + }), + ).toThrow(); + + expect( + sampleEasSchema.parse({ + walletAddress: randomAddress, + requestId: "123", + hasClaimedNFT: true, + message: "hello world", + characters: ["tony", "alice", "vitalik"], + }), + ).toEqual({ + walletAddress: randomAddress, + requestId: "123", + hasClaimedNFT: true, + message: "hello world", + characters: ["tony", "alice", "vitalik"], + }); + }); + }); +}); diff --git a/packages/ui-react/src/hooks/eas/use-form-field.ts b/packages/ui-react/src/hooks/eas/use-form-field.ts new file mode 100644 index 00000000..f1957ba2 --- /dev/null +++ b/packages/ui-react/src/hooks/eas/use-form-field.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +// function to get JSON from attestation (we cannot dynamically create zod schemas) +export const getZodSchemaFromSchemaString = (schemaString: string) => { + const stringPairs = schemaString.split(","); + const entries = stringPairs.map((pair) => pair.split(" ")); + const zodSchemaObject: any = {}; + + for (const entry of entries) { + const [type = "", name = ""] = entry; + + // process the type + let zodSchemaType: any = z.string(); + if (type === "bool") { + zodSchemaType = z.boolean(); + } else if (type.includes("uint")) { + zodSchemaType = z.number(); + } + + // if we see array, we define as array schema + if (type.includes("[]")) { + zodSchemaType = zodSchemaType.array(); + console.log("array attached"); + } + + // attach to object + zodSchemaObject[name] = zodSchemaType; + } + + return z.object(zodSchemaObject); +}; From 48e9a388ad8048fc1d2611ae2d278b17cefd31ed Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 30 Dec 2024 11:16:13 +0700 Subject: [PATCH 02/16] feat: add form field hook --- .../ui-react/src/hooks/eas/use-form-field.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/ui-react/src/hooks/eas/use-form-field.ts b/packages/ui-react/src/hooks/eas/use-form-field.ts index f1957ba2..78b2a7e7 100644 --- a/packages/ui-react/src/hooks/eas/use-form-field.ts +++ b/packages/ui-react/src/hooks/eas/use-form-field.ts @@ -1,3 +1,6 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; import { z } from "zod"; // function to get JSON from attestation (we cannot dynamically create zod schemas) @@ -29,3 +32,35 @@ export const getZodSchemaFromSchemaString = (schemaString: string) => { return z.object(zodSchemaObject); }; + +// do we want to make it safe? +export function useEASSchemaForm({ + schemaString, + schemaId, + isEnabled = true, // whether to use this hook or not +}: { + schemaString?: string; + schemaId?: string; + isEnabled?: boolean; +}) { + if (!schemaString && !schemaId) + throw new Error( + "[useEASSchemaForm] at least one of schemaString and schemaId must be present", + ); + + const [schemaStringState, setSchemaStringState] = useState(schemaString); + + const formSchema = useMemo(() => { + if (!!schemaStringState) + return getZodSchemaFromSchemaString(schemaStringState); + return z.object({}); + }, [schemaString]); + + const form = useForm>({ + resolver: zodResolver(formSchema), + disabled: !isEnabled, + }); + + // we return a react hook form instance + return form; +} From 4262907471941740a81780e4c8ca54e7b3d630b0 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 30 Dec 2024 11:17:42 +0700 Subject: [PATCH 03/16] chore: rename files by convention --- .../{use-form-field.test.ts => use-eas-schema-form.test.ts} | 6 ++---- .../hooks/eas/{use-form-field.ts => use-eas-schema-form.ts} | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) rename packages/ui-react/src/hooks/eas/{use-form-field.test.ts => use-eas-schema-form.test.ts} (91%) rename packages/ui-react/src/hooks/eas/{use-form-field.ts => use-eas-schema-form.ts} (94%) diff --git a/packages/ui-react/src/hooks/eas/use-form-field.test.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts similarity index 91% rename from packages/ui-react/src/hooks/eas/use-form-field.test.ts rename to packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts index 553ea12c..488359c0 100644 --- a/packages/ui-react/src/hooks/eas/use-form-field.test.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts @@ -1,10 +1,8 @@ import { describe, expect, it } from "vitest"; -import { getZodSchemaFromSchemaString } from "./use-form-field"; +import { getZodSchemaFromSchemaString } from "./use-eas-schema-form"; import { getRandomAddress } from "@geist/domain/user.fixture"; -import { Z } from "vitest/dist/chunks/reporters.D7Jzd9GS.js"; -import { z } from "zod"; -describe("use-form-field", () => { +describe("use-schema-eas-form", () => { describe("#getZodSchemaFromSchemaString", () => { it("should correctly produce zod schema", () => { const sampleEasSchema = getZodSchemaFromSchemaString( diff --git a/packages/ui-react/src/hooks/eas/use-form-field.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts similarity index 94% rename from packages/ui-react/src/hooks/eas/use-form-field.ts rename to packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index 78b2a7e7..010ce0a2 100644 --- a/packages/ui-react/src/hooks/eas/use-form-field.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -34,7 +34,7 @@ export const getZodSchemaFromSchemaString = (schemaString: string) => { }; // do we want to make it safe? -export function useEASSchemaForm({ +export function useEasSchemaForm({ schemaString, schemaId, isEnabled = true, // whether to use this hook or not @@ -45,7 +45,7 @@ export function useEASSchemaForm({ }) { if (!schemaString && !schemaId) throw new Error( - "[useEASSchemaForm] at least one of schemaString and schemaId must be present", + "[useEasSchemaForm] at least one of schemaString and schemaId must be present", ); const [schemaStringState, setSchemaStringState] = useState(schemaString); From 1641773c39a7b8e1b0eb314d8a22a9317dea6a05 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 30 Dec 2024 21:44:53 +0700 Subject: [PATCH 04/16] feat: first hook implementation --- .../src/hooks/eas/use-eas-schema-form.ts | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index 010ce0a2..82111271 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -1,8 +1,15 @@ +import { getEasscanEndpoint } from "#lib/eas/easscan.js"; +import { gql } from "@geist/graphql"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { is } from "date-fns/locale"; +import { rawRequest } from "graphql-request"; import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +type SchemaByQuery = any; + // function to get JSON from attestation (we cannot dynamically create zod schemas) export const getZodSchemaFromSchemaString = (schemaString: string) => { const stringPairs = schemaString.split(","); @@ -33,15 +40,28 @@ export const getZodSchemaFromSchemaString = (schemaString: string) => { return z.object(zodSchemaObject); }; +const schemaByQuery = gql(` + query schemaBy($where: SchemaWhereUniqueInput!) { + schema(where: $where) { + schemaString: schema + index + revocable + creator + } + } +`); + // do we want to make it safe? export function useEasSchemaForm({ schemaString, schemaId, + chainId = 1, isEnabled = true, // whether to use this hook or not }: { schemaString?: string; schemaId?: string; isEnabled?: boolean; + chainId?: number; }) { if (!schemaString && !schemaId) throw new Error( @@ -50,6 +70,28 @@ export function useEasSchemaForm({ const [schemaStringState, setSchemaStringState] = useState(schemaString); + const schemaQueryResults = useQuery({ + queryKey: ["schemaBy", schemaId], + queryFn: async () => { + try { + const { data } = await rawRequest( + `${getEasscanEndpoint(chainId)}/graphql`, + schemaByQuery.toString(), + { + where: { + id: schemaId, + }, + }, + ); + setSchemaStringState(data.data.schema.schemaString ?? undefined); + } catch (e) { + console.error(`[useEasSchemaForm] ` + e); + setSchemaStringState(undefined); + } + }, + enabled: !!isEnabled && !!schemaId, + }); + const formSchema = useMemo(() => { if (!!schemaStringState) return getZodSchemaFromSchemaString(schemaStringState); @@ -58,9 +100,11 @@ export function useEasSchemaForm({ const form = useForm>({ resolver: zodResolver(formSchema), - disabled: !isEnabled, + disabled: !isEnabled || schemaQueryResults.isLoading, }); + const isLoading = schemaQueryResults.isLoading; + // we return a react hook form instance - return form; + return { form, isLoading }; } From b12093f18478da4e9b9fd6640202619b774475be Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 30 Dec 2024 21:45:05 +0700 Subject: [PATCH 05/16] fix: graphql error --- packages/gql/src/graphql/eas/gql.ts | 6 +++--- packages/gql/src/graphql/gql.ts | 6 ++++++ packages/gql/src/graphql/graphql.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/gql/src/graphql/eas/gql.ts b/packages/gql/src/graphql/eas/gql.ts index dd8ddc33..e48f006b 100644 --- a/packages/gql/src/graphql/eas/gql.ts +++ b/packages/gql/src/graphql/eas/gql.ts @@ -19,9 +19,9 @@ const documents = { /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql( - source: "\n query allAttestationsBy(\n $where: AttestationWhereInput\n ) {\n attestations(where: $where) {\n id\n txid\n recipient\n schema {\n index\n schemaNames {\n name\n }\n }\n time\n isOffchain\n schemaId\n attester\n }\n }\n", -): typeof import("./graphql").AllAttestationsByDocument; +// export function gql( +// source: "\n query allAttestationsBy(\n $where: AttestationWhereInput\n ) {\n attestations(where: $where) {\n id\n txid\n recipient\n schema {\n index\n schemaNames {\n name\n }\n }\n time\n isOffchain\n schemaId\n attester\n }\n }\n", +// ): typeof import("./graphql").AllAttestationsByDocument; export function gql(source: string) { return (documents as any)[source] ?? {}; diff --git a/packages/gql/src/graphql/gql.ts b/packages/gql/src/graphql/gql.ts index dd8ddc33..ec572d6e 100644 --- a/packages/gql/src/graphql/gql.ts +++ b/packages/gql/src/graphql/gql.ts @@ -14,6 +14,8 @@ import * as types from "./graphql"; const documents = { "\n query allAttestationsBy(\n $where: AttestationWhereInput\n ) {\n attestations(where: $where) {\n id\n txid\n recipient\n schema {\n index\n schemaNames {\n name\n }\n }\n time\n isOffchain\n schemaId\n attester\n }\n }\n": types.AllAttestationsByDocument, + "\n\tquery schemaBy($where: SchemaWhereUniqueInput!) {\n\t\tschema(where: $where) {\n\t\t\tschemaString: schema\n\t\t\tindex\n\t\t\trevocable\n\t\t\tcreator\n\t\t}\n\t}\n": + types.SchemaByDocument, }; /** @@ -23,6 +25,10 @@ export function gql( source: "\n query allAttestationsBy(\n $where: AttestationWhereInput\n ) {\n attestations(where: $where) {\n id\n txid\n recipient\n schema {\n index\n schemaNames {\n name\n }\n }\n time\n isOffchain\n schemaId\n attester\n }\n }\n", ): typeof import("./graphql").AllAttestationsByDocument; +export function gql( + source: "\n\tquery schemaBy($where: SchemaWhereUniqueInput!) {\n\t\tschema(where: $where) {\n\t\t\tschemaString: schema\n\t\t\tindex\n\t\t\trevocable\n\t\t\tcreator\n\t\t}\n\t}\n", +): typeof import("./graphql").SchemaByDocument; + export function gql(source: string) { return (documents as any)[source] ?? {}; } diff --git a/packages/gql/src/graphql/graphql.ts b/packages/gql/src/graphql/graphql.ts index a3eb7161..fe294d69 100644 --- a/packages/gql/src/graphql/graphql.ts +++ b/packages/gql/src/graphql/graphql.ts @@ -2842,3 +2842,29 @@ export const AllAttestationsByDocument = new TypedDocumentString(` AllAttestationsByQuery, AllAttestationsByQueryVariables >; + +export type SchemaByQueryVariables = Exact<{ + where?: InputMaybe; +}>; + +export type SchemaByQuery = { + __typename?: "Query"; + schema: { + __typename?: "Schema"; + index: string; + schemaString: string; + revocable: boolean; + creator: string; + }; +}; + +export const SchemaByDocument = new TypedDocumentString(` + query schemaBy($where: SchemaWhereUniqueInput!) { + schema(where: $where) { + schemaString: schema + index + revocable + creator + } + } +`) as unknown as TypedDocumentString; From 63bf4f9a6c75c91947a2aa6fa5d292131dcb9eec Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 6 Jan 2025 13:12:37 +0700 Subject: [PATCH 06/16] chore: update fixture and stories --- .../AttestationFormEasSdk.stories.tsx | 11 ++-- .../AttestationFormWagmi.stories.tsx | 18 ++++--- packages/domain/src/user.fixture.ts | 51 ++++++++++--------- .../attestations/attestation-form.tsx | 34 ++++++++----- .../src/hooks/eas/use-eas-schema-form.ts | 2 +- 5 files changed, 66 insertions(+), 50 deletions(-) diff --git a/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx b/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx index 826f6055..1048191c 100644 --- a/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx +++ b/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx @@ -12,6 +12,7 @@ import { Chain, Hex, zeroHash } from "viem"; import { mainnet, optimism, optimismSepolia, sepolia } from "viem/chains"; import { withToaster } from "../decorators/toaster"; import { withWalletControl } from "../decorators/wallet-control"; +import { withQueryClientProvider } from "#stories/decorators/wagmi.tsx"; export interface AttestationFormEasSdkProps { privateKey: string; @@ -83,7 +84,7 @@ const meta = { parameters: { layout: "centered", }, - decorators: [withToaster(), withWalletControl()], + decorators: [withToaster(), withWalletControl(), withQueryClientProvider()], args: {}, } satisfies Meta; @@ -109,9 +110,9 @@ export const OnchainSepolia: Story = { args: { isOffchain: false, ...createArgs( - SCHEMA_BY_NAME.IS_A_FRIEND, + SCHEMA_BY_NAME.VOTE, sepolia, - SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, + SCHEMA_BY_NAME.VOTE.byFixture.vote, ), }, decorators: [], @@ -133,9 +134,9 @@ export const OffchainSepolia: Story = { args: { isOffchain: true, ...createArgs( - SCHEMA_BY_NAME.IS_A_FRIEND, + SCHEMA_BY_NAME.VOTE, sepolia, - SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, + SCHEMA_BY_NAME.VOTE.byFixture.vote, ), }, decorators: [], diff --git a/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx b/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx index 8553ec95..afd98681 100644 --- a/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx +++ b/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx @@ -9,13 +9,16 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Account, Address, Chain, Hex, stringToHex, zeroHash } from "viem"; import { sepolia } from "viem/chains"; import { withToaster } from "../decorators/toaster"; -import { withMockAccount, withWagmiProvider } from "../decorators/wagmi"; +import { + withMockAccount, + withQueryClientProvider, + withWagmiProvider, +} from "../decorators/wagmi"; import { withWalletControlWagmi } from "../decorators/wallet-control"; const AttestationFormWagmi = ({ schemaId, schemaIndex, - account, isOffchain, schemaString, @@ -77,6 +80,7 @@ const meta = { withWalletControlWagmi(), withMockAccount(), withWagmiProvider(), + withQueryClientProvider(), ], args: {}, } satisfies Meta; @@ -110,23 +114,25 @@ const createArgs = (schema: any, chain: Chain, fixture: any) => { // TODO chain control at withWalletControlWagmi export const AttestationWagmiOffchain: Story = { + // @ts-expect-error withMockAccount() decorator should inject an account. args: { isOffchain: true, ...createArgs( - SCHEMA_BY_NAME.IS_A_FRIEND, + SCHEMA_BY_NAME.VOTE, sepolia, - SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, + SCHEMA_BY_NAME.VOTE.byFixture.vote, ), }, }; export const AttestationWagmiOnchain: Story = { + // @ts-expect-error withMockAccount() decorator should inject an account. args: { isOffchain: false, ...createArgs( - SCHEMA_BY_NAME.IS_A_FRIEND, + SCHEMA_BY_NAME.VOTE, sepolia, - SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, + SCHEMA_BY_NAME.VOTE.byFixture.vote, ), }, }; diff --git a/packages/domain/src/user.fixture.ts b/packages/domain/src/user.fixture.ts index 943bb670..c2f631f8 100644 --- a/packages/domain/src/user.fixture.ts +++ b/packages/domain/src/user.fixture.ts @@ -4,26 +4,27 @@ import { Address, Hex } from "viem"; import config from "./config"; const vitalik = { - ens: "vitalik.eth", - address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as Address, + ens: "vitalik.eth", + address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as Address, }; // stable private key // 0x4E123166e7DfDE7AbA29162Fb3a5c6Af562443D4 const user = { - privateKey: config.test.user.privateKey as Hex, - address: "", + privateKey: + "0x9e0fbda2334ed9a312bfb8c59bbc55f6c059a69fae1a1e818ddcd6c60843375b" as Hex, + address: "0x8178c9834DDaE72D4fcAB4655bF16bCe9C7bE557", }; const eas = { - mockReceipient: { - address: "0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165" as Address, - }, + mockReceipient: { + address: "0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165" as Address, + }, }; const filecoinTopHolder = { - address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - filAddress: "f1m2swr32yrlouzs7ijui3jttwgc6lxa5n5sookhi", + address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + filAddress: "f1m2swr32yrlouzs7ijui3jttwgc6lxa5n5sookhi", }; user.address = addr.fromPrivateKey(user.privateKey) as Address; @@ -31,32 +32,32 @@ user.address = addr.fromPrivateKey(user.privateKey) as Address; // mainnet, base https://easscan.org/address/0x1e3de6aE412cA218FD2ae3379750388D414532dc // https://github.com/ethereum-attestation-service/eas-sdk/blob/master/README.md?plain=1#L123 const easSampleAttester = { - address: "0x1e3de6aE412cA218FD2ae3379750388D414532dc" as Address, + address: "0x1e3de6aE412cA218FD2ae3379750388D414532dc" as Address, }; export const BY_USER = { - vitalik, - easSampleAttester, - filecoinTopHolder, - user, - eas, + vitalik, + easSampleAttester, + filecoinTopHolder, + user, + eas, }; export const getRandomAccount = () => { - return addr.random(); + return addr.random(); }; export const getRandomAddress = () => { - return addr.random().address as Address; + return addr.random().address as Address; }; export const TRANSACTION = { - VITALIK_DEPOSIT: { - txnHash: - "0x31634ddc4975cc9f2c3c6426034fe0ed30163000cd067e80857136ab86dc0a3b", - }, - VITALIK_TRANSFER: { - txnHash: - "0xed46c306a58821dd2dcbc78d7b7af9715c809c71d7a53b0dfc90c42dfdf59b67", - }, + VITALIK_DEPOSIT: { + txnHash: + "0x31634ddc4975cc9f2c3c6426034fe0ed30163000cd067e80857136ab86dc0a3b", + }, + VITALIK_TRANSFER: { + txnHash: + "0xed46c306a58821dd2dcbc78d7b7af9715c809c71d7a53b0dfc90c42dfdf59b67", + }, }; diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx index 1f1c58b2..89ecf9c7 100644 --- a/packages/ui-react/src/components/attestations/attestation-form.tsx +++ b/packages/ui-react/src/components/attestations/attestation-form.tsx @@ -19,6 +19,7 @@ import { toast } from "#hooks/shadcn/use-toast"; import { getEasscanAttestationUrl } from "#lib/eas/easscan"; import { getShortHex } from "#lib/utils/hex"; import { AttestationSchemaBadge } from "./attestation-schema-badge"; +import { useEasSchemaForm } from "#hooks/eas/use-eas-schema-form.js"; // TODO dynamic enough to generate fields // now focus on sdk part @@ -40,20 +41,27 @@ export const AttestationForm = ({ isOffchain, signAttestation, }: AttestationFormParams) => { - const formSchema = z.object({ - recipient: z - .string() - .length(42, { - message: "address must be 42 characters.", - }) - .brand
(), - }); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - recipient: "0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165", - }, + // const formSchema = z.object({ + // recipient: z + // .string() + // .length(42, { + // message: "address must be 42 characters.", + // }) + // .brand
(), + // }); + + // const form = useForm>({ + // resolver: zodResolver(formSchema), + // defaultValues: { + // recipient: "0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165", + // }, + // }); + + const { formSchema, form, isLoading } = useEasSchemaForm({ + schemaId, + chainId, }); + function onSubmit(values: z.infer) { signAttestation().then(({ uids, txnReceipt }: any) => { const [uid] = uids; diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index 82111271..33ec2c6e 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -106,5 +106,5 @@ export function useEasSchemaForm({ const isLoading = schemaQueryResults.isLoading; // we return a react hook form instance - return { form, isLoading }; + return { form, formSchema, isLoading }; } From 7f69024dabb455bae9210f9b3627c148a8eb53d6 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 6 Jan 2025 14:17:11 +0700 Subject: [PATCH 07/16] feat: UI generation according to schema --- .../attestations/attestation-form.tsx | 117 ++++++++++-------- .../src/hooks/eas/use-eas-schema-form.ts | 46 +++---- 2 files changed, 85 insertions(+), 78 deletions(-) diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx index 89ecf9c7..9e9e3cec 100644 --- a/packages/ui-react/src/components/attestations/attestation-form.tsx +++ b/packages/ui-react/src/components/attestations/attestation-form.tsx @@ -1,13 +1,9 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { Address } from "viem"; -import { z } from "zod"; +import { z, ZodNumber } from "zod"; import { Button } from "#components/shadcn/button"; import { Card, CardContent } from "#components/shadcn/card"; import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, @@ -19,7 +15,9 @@ import { toast } from "#hooks/shadcn/use-toast"; import { getEasscanAttestationUrl } from "#lib/eas/easscan"; import { getShortHex } from "#lib/utils/hex"; import { AttestationSchemaBadge } from "./attestation-schema-badge"; -import { useEasSchemaForm } from "#hooks/eas/use-eas-schema-form.js"; +import { useEasSchemaForm } from "#hooks/eas/use-eas-schema-form"; +import { Badge } from "#components/shadcn/badge"; +import { Spinner } from "@radix-ui/themes"; // TODO dynamic enough to generate fields // now focus on sdk part @@ -41,23 +39,7 @@ export const AttestationForm = ({ isOffchain, signAttestation, }: AttestationFormParams) => { - // const formSchema = z.object({ - // recipient: z - // .string() - // .length(42, { - // message: "address must be 42 characters.", - // }) - // .brand
(), - // }); - - // const form = useForm>({ - // resolver: zodResolver(formSchema), - // defaultValues: { - // recipient: "0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165", - // }, - // }); - - const { formSchema, form, isLoading } = useEasSchemaForm({ + const { formSchema, form, isLoading, schemaDetails } = useEasSchemaForm({ schemaId, chainId, }); @@ -88,38 +70,63 @@ export const AttestationForm = ({ return ( -
- - ( - -
- - IS A FRIEND -
- Recipient - - - - - Attest You met this person in real life - - -
- )} - /> - - - + {isLoading ? ( + + ) : ( +
+ +
+
+ + {getShortHex(schemaId as unknown as `0x${string}`)} +
+ +
+ {!!schemaDetails?.revocable && ( + REVOCABLE + )} + {!!isOffchain && OFFCHAIN} +
+
+ {Object.keys(formSchema.shape).map((schemaKey) => { + return ( + ( + + {schemaKey} + + + + + + )} + /> + ); + })} + + + + + )}
); diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index 33ec2c6e..b1efce2a 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -2,9 +2,8 @@ import { getEasscanEndpoint } from "#lib/eas/easscan.js"; import { gql } from "@geist/graphql"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; -import { is } from "date-fns/locale"; import { rawRequest } from "graphql-request"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -30,7 +29,6 @@ export const getZodSchemaFromSchemaString = (schemaString: string) => { // if we see array, we define as array schema if (type.includes("[]")) { zodSchemaType = zodSchemaType.array(); - console.log("array attached"); } // attach to object @@ -68,35 +66,32 @@ export function useEasSchemaForm({ "[useEasSchemaForm] at least one of schemaString and schemaId must be present", ); - const [schemaStringState, setSchemaStringState] = useState(schemaString); - const schemaQueryResults = useQuery({ queryKey: ["schemaBy", schemaId], queryFn: async () => { - try { - const { data } = await rawRequest( - `${getEasscanEndpoint(chainId)}/graphql`, - schemaByQuery.toString(), - { - where: { - id: schemaId, - }, + const { data } = await rawRequest( + `${getEasscanEndpoint(chainId)}/graphql`, + schemaByQuery.toString(), + { + where: { + id: schemaId, }, - ); - setSchemaStringState(data.data.schema.schemaString ?? undefined); - } catch (e) { - console.error(`[useEasSchemaForm] ` + e); - setSchemaStringState(undefined); - } + }, + ); + + return data.schema; }, enabled: !!isEnabled && !!schemaId, }); const formSchema = useMemo(() => { - if (!!schemaStringState) - return getZodSchemaFromSchemaString(schemaStringState); + if (!!schemaQueryResults.data?.schemaString) + return getZodSchemaFromSchemaString(schemaQueryResults.data.schemaString); + + if (!!schemaString) return getZodSchemaFromSchemaString(schemaString); + return z.object({}); - }, [schemaString]); + }, [schemaQueryResults?.data?.schemaString, schemaString]); const form = useForm>({ resolver: zodResolver(formSchema), @@ -106,5 +101,10 @@ export function useEasSchemaForm({ const isLoading = schemaQueryResults.isLoading; // we return a react hook form instance - return { form, formSchema, isLoading }; + return { + form, + formSchema, + isLoading, + schemaDetails: schemaQueryResults.data, + }; } From 93da53711bcef6ae5e833cedc2cec40ada1e293b Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 6 Jan 2025 14:39:17 +0700 Subject: [PATCH 08/16] fix: some style enchancements for the form --- .../src/components/attestations/attestation-form.tsx | 10 ++++++++++ packages/ui-react/src/hooks/eas/use-eas-schema-form.ts | 3 +++ 2 files changed, 13 insertions(+) diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx index 9e9e3cec..5addfe40 100644 --- a/packages/ui-react/src/components/attestations/attestation-form.tsx +++ b/packages/ui-react/src/components/attestations/attestation-form.tsx @@ -107,6 +107,16 @@ export const AttestationForm = ({ + formSchema.shape[schemaKey] instanceof ZodNumber + ? field.onChange(value.target.valueAsNumber) + : field.onChange(value.target.value) + } placeholder={ formSchema.shape[schemaKey] instanceof ZodNumber ? "number" diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index b1efce2a..262f9ce7 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -96,6 +96,9 @@ export function useEasSchemaForm({ const form = useForm>({ resolver: zodResolver(formSchema), disabled: !isEnabled || schemaQueryResults.isLoading, + defaultValues: Object.fromEntries( + Object.keys(formSchema.shape).map((key) => [key, ""]), + ), }); const isLoading = schemaQueryResults.isLoading; From e504749749285f0146a09ed301d39f1cd153334c Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 6 Jan 2025 14:39:54 +0700 Subject: [PATCH 09/16] chore: run biome --- .../AttestationFormEasSdk.stories.tsx | 2 +- packages/domain/src/user.fixture.ts | 52 +++++++++---------- .../attestations/attestation-form.tsx | 8 +-- .../src/hooks/eas/use-eas-schema-form.test.ts | 2 +- .../src/hooks/eas/use-eas-schema-form.ts | 2 +- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx b/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx index 1048191c..6190df4e 100644 --- a/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx +++ b/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx @@ -10,9 +10,9 @@ import type { Meta, StoryObj } from "@storybook/react"; import { encodeBytes32String } from "ethers"; import { Chain, Hex, zeroHash } from "viem"; import { mainnet, optimism, optimismSepolia, sepolia } from "viem/chains"; +import { withQueryClientProvider } from "#stories/decorators/wagmi.tsx"; import { withToaster } from "../decorators/toaster"; import { withWalletControl } from "../decorators/wallet-control"; -import { withQueryClientProvider } from "#stories/decorators/wagmi.tsx"; export interface AttestationFormEasSdkProps { privateKey: string; diff --git a/packages/domain/src/user.fixture.ts b/packages/domain/src/user.fixture.ts index c2f631f8..c20ed56d 100644 --- a/packages/domain/src/user.fixture.ts +++ b/packages/domain/src/user.fixture.ts @@ -4,27 +4,27 @@ import { Address, Hex } from "viem"; import config from "./config"; const vitalik = { - ens: "vitalik.eth", - address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as Address, + ens: "vitalik.eth", + address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as Address, }; // stable private key // 0x4E123166e7DfDE7AbA29162Fb3a5c6Af562443D4 const user = { - privateKey: - "0x9e0fbda2334ed9a312bfb8c59bbc55f6c059a69fae1a1e818ddcd6c60843375b" as Hex, - address: "0x8178c9834DDaE72D4fcAB4655bF16bCe9C7bE557", + privateKey: + "0x9e0fbda2334ed9a312bfb8c59bbc55f6c059a69fae1a1e818ddcd6c60843375b" as Hex, + address: "0x8178c9834DDaE72D4fcAB4655bF16bCe9C7bE557", }; const eas = { - mockReceipient: { - address: "0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165" as Address, - }, + mockReceipient: { + address: "0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165" as Address, + }, }; const filecoinTopHolder = { - address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - filAddress: "f1m2swr32yrlouzs7ijui3jttwgc6lxa5n5sookhi", + address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + filAddress: "f1m2swr32yrlouzs7ijui3jttwgc6lxa5n5sookhi", }; user.address = addr.fromPrivateKey(user.privateKey) as Address; @@ -32,32 +32,32 @@ user.address = addr.fromPrivateKey(user.privateKey) as Address; // mainnet, base https://easscan.org/address/0x1e3de6aE412cA218FD2ae3379750388D414532dc // https://github.com/ethereum-attestation-service/eas-sdk/blob/master/README.md?plain=1#L123 const easSampleAttester = { - address: "0x1e3de6aE412cA218FD2ae3379750388D414532dc" as Address, + address: "0x1e3de6aE412cA218FD2ae3379750388D414532dc" as Address, }; export const BY_USER = { - vitalik, - easSampleAttester, - filecoinTopHolder, - user, - eas, + vitalik, + easSampleAttester, + filecoinTopHolder, + user, + eas, }; export const getRandomAccount = () => { - return addr.random(); + return addr.random(); }; export const getRandomAddress = () => { - return addr.random().address as Address; + return addr.random().address as Address; }; export const TRANSACTION = { - VITALIK_DEPOSIT: { - txnHash: - "0x31634ddc4975cc9f2c3c6426034fe0ed30163000cd067e80857136ab86dc0a3b", - }, - VITALIK_TRANSFER: { - txnHash: - "0xed46c306a58821dd2dcbc78d7b7af9715c809c71d7a53b0dfc90c42dfdf59b67", - }, + VITALIK_DEPOSIT: { + txnHash: + "0x31634ddc4975cc9f2c3c6426034fe0ed30163000cd067e80857136ab86dc0a3b", + }, + VITALIK_TRANSFER: { + txnHash: + "0xed46c306a58821dd2dcbc78d7b7af9715c809c71d7a53b0dfc90c42dfdf59b67", + }, }; diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx index 5addfe40..ee3ef199 100644 --- a/packages/ui-react/src/components/attestations/attestation-form.tsx +++ b/packages/ui-react/src/components/attestations/attestation-form.tsx @@ -1,4 +1,6 @@ -import { z, ZodNumber } from "zod"; +import { Spinner } from "@radix-ui/themes"; +import { ZodNumber, z } from "zod"; +import { Badge } from "#components/shadcn/badge"; import { Button } from "#components/shadcn/button"; import { Card, CardContent } from "#components/shadcn/card"; import { @@ -11,13 +13,11 @@ import { } from "#components/shadcn/form"; import { Input } from "#components/shadcn/input"; import { ToastAction } from "#components/shadcn/toast"; +import { useEasSchemaForm } from "#hooks/eas/use-eas-schema-form"; import { toast } from "#hooks/shadcn/use-toast"; import { getEasscanAttestationUrl } from "#lib/eas/easscan"; import { getShortHex } from "#lib/utils/hex"; import { AttestationSchemaBadge } from "./attestation-schema-badge"; -import { useEasSchemaForm } from "#hooks/eas/use-eas-schema-form"; -import { Badge } from "#components/shadcn/badge"; -import { Spinner } from "@radix-ui/themes"; // TODO dynamic enough to generate fields // now focus on sdk part diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts index 488359c0..7ff8820d 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts @@ -1,6 +1,6 @@ +import { getRandomAddress } from "@geist/domain/user.fixture"; import { describe, expect, it } from "vitest"; import { getZodSchemaFromSchemaString } from "./use-eas-schema-form"; -import { getRandomAddress } from "@geist/domain/user.fixture"; describe("use-schema-eas-form", () => { describe("#getZodSchemaFromSchemaString", () => { diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index 262f9ce7..ac2cc878 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -1,4 +1,3 @@ -import { getEasscanEndpoint } from "#lib/eas/easscan.js"; import { gql } from "@geist/graphql"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; @@ -6,6 +5,7 @@ import { rawRequest } from "graphql-request"; import { useMemo } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { getEasscanEndpoint } from "#lib/eas/easscan.js"; type SchemaByQuery = any; From 40193fca92a42bed86bad0a0217cd43d2cc904b8 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 6 Jan 2025 15:31:34 +0700 Subject: [PATCH 10/16] fix: input validation not working --- .../attestations/attestation-form.tsx | 19 +++++++++---------- .../src/hooks/eas/use-eas-schema-form.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx index ee3ef199..db6446fa 100644 --- a/packages/ui-react/src/components/attestations/attestation-form.tsx +++ b/packages/ui-react/src/components/attestations/attestation-form.tsx @@ -19,9 +19,6 @@ import { getEasscanAttestationUrl } from "#lib/eas/easscan"; import { getShortHex } from "#lib/utils/hex"; import { AttestationSchemaBadge } from "./attestation-schema-badge"; -// TODO dynamic enough to generate fields -// now focus on sdk part - export interface AttestationFormParams { chainId: number; schemaId: string; @@ -30,8 +27,6 @@ export interface AttestationFormParams { signAttestation: () => Promise; } -// TODO dynamic schema. For now, hardcode the MetIRL -// https://github.com/fractaldotbox/geist-dapp-kit/issues/56 export const AttestationForm = ({ chainId, schemaId, @@ -67,6 +62,7 @@ export const AttestationForm = ({ }); } + // TODO: array schema handling return ( @@ -112,11 +108,14 @@ export const AttestationForm = ({ ? "number" : "text" } - onChange={(value) => - formSchema.shape[schemaKey] instanceof ZodNumber - ? field.onChange(value.target.valueAsNumber) - : field.onChange(value.target.value) - } + value={field.value ?? ""} + onChange={(e) => { + const value = + formSchema.shape[schemaKey] instanceof ZodNumber + ? e.target.valueAsNumber || 0 + : e.target.value; + field.onChange(value); + }} placeholder={ formSchema.shape[schemaKey] instanceof ZodNumber ? "number" diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index ac2cc878..78dcffcb 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query"; import { rawRequest } from "graphql-request"; import { useMemo } from "react"; import { useForm } from "react-hook-form"; -import { z } from "zod"; +import { z, ZodNumber } from "zod"; import { getEasscanEndpoint } from "#lib/eas/easscan.js"; type SchemaByQuery = any; @@ -93,11 +93,15 @@ export function useEasSchemaForm({ return z.object({}); }, [schemaQueryResults?.data?.schemaString, schemaString]); + // TODO: array schema handling const form = useForm>({ resolver: zodResolver(formSchema), - disabled: !isEnabled || schemaQueryResults.isLoading, + disabled: !isEnabled, defaultValues: Object.fromEntries( - Object.keys(formSchema.shape).map((key) => [key, ""]), + Object.keys(formSchema.shape).map((key) => [ + key, + formSchema.shape[key] instanceof ZodNumber ? 0 : "", + ]), ), }); From 27e6c85669c9fa89e7ee98a0b158139487715ae1 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Sun, 19 Jan 2025 20:10:36 +0700 Subject: [PATCH 11/16] feat: add submission parser --- .../AttestationFormEasSdk.stories.tsx | 27 ++++++----- .../AttestationFormWagmi.stories.tsx | 6 +-- .../attestations/attestation-form.tsx | 45 +++++++++++-------- .../src/hooks/eas/use-eas-schema-form.test.ts | 26 ++++++++++- .../src/hooks/eas/use-eas-schema-form.ts | 34 +++++++++++++- 5 files changed, 102 insertions(+), 36 deletions(-) diff --git a/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx b/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx index 6190df4e..12e68f3c 100644 --- a/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx +++ b/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx @@ -60,17 +60,17 @@ const AttestationFormEasSdk = ({ schemaId={schemaId} schemaIndex={schemaIndex} isOffchain={isOffchain} - signAttestation={async (): Promise => { + signAttestation={async (values: any): Promise => { // TODO fix encode data structure const now = BigInt(Date.now()); - const { recipient, revocable, expirationTime, refUID, data, salt } = + const { recipient, revocable, expirationTime, refUID, salt } = requestTemplate; return signAttestation({ ...requestTemplate, recipient, - data, + data: values, time: now, }); }} @@ -95,6 +95,7 @@ const createArgs = (schema: any, chain: Chain, fixture: any) => { const { schemaString, byChain } = schema; const { uid, index } = byChain[chain.id]; const { data, attestData } = fixture; + return { chainId: chain.id, privateKey: BY_USER.user.privateKey, @@ -143,11 +144,15 @@ export const OffchainSepolia: Story = { }; // Until dynamic form -// export const OffchainVote: Story = { -// args: { -// isOffchain: true, -// ...createArgs(SCHEMA_BY_NAME.VOTE, sepolia.id), -// ...SCHEMA_BY_NAME.VOTE.byFixture.vote, -// }, -// decorators: [], -// }; +export const OffchainVote: Story = { + args: { + isOffchain: true, + ...createArgs( + SCHEMA_BY_NAME.VOTE, + sepolia, + SCHEMA_BY_NAME.VOTE.byFixture.vote, + ), + ...SCHEMA_BY_NAME.VOTE.byFixture.vote, + }, + decorators: [], +}; diff --git a/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx b/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx index afd98681..b991aaa6 100644 --- a/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx +++ b/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx @@ -23,7 +23,6 @@ const AttestationFormWagmi = ({ isOffchain, schemaString, chain, - data, attestData, }: { schemaId: string; @@ -32,7 +31,6 @@ const AttestationFormWagmi = ({ isOffchain: boolean; schemaString: string; chain: Chain; - data: any; attestData: Omit; }) => { if (!account) { @@ -57,10 +55,10 @@ const AttestationFormWagmi = ({ schemaId={schemaId} schemaIndex={schemaIndex} isOffchain={isOffchain} - signAttestation={async () => + signAttestation={async (values: any) => signAttestation({ ...attestData, - data, + data: values, recipient, // attester: account.address, }) diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx index db6446fa..e34a33e9 100644 --- a/packages/ui-react/src/components/attestations/attestation-form.tsx +++ b/packages/ui-react/src/components/attestations/attestation-form.tsx @@ -13,7 +13,10 @@ import { } from "#components/shadcn/form"; import { Input } from "#components/shadcn/input"; import { ToastAction } from "#components/shadcn/toast"; -import { useEasSchemaForm } from "#hooks/eas/use-eas-schema-form"; +import { + getSubmissionData, + useEasSchemaForm, +} from "#hooks/eas/use-eas-schema-form"; import { toast } from "#hooks/shadcn/use-toast"; import { getEasscanAttestationUrl } from "#lib/eas/easscan"; import { getShortHex } from "#lib/utils/hex"; @@ -24,7 +27,7 @@ export interface AttestationFormParams { schemaId: string; schemaIndex?: string; isOffchain: boolean; - signAttestation: () => Promise; + signAttestation: (values: any) => Promise; } export const AttestationForm = ({ @@ -40,26 +43,30 @@ export const AttestationForm = ({ }); function onSubmit(values: z.infer) { - signAttestation().then(({ uids, txnReceipt }: any) => { - const [uid] = uids; - const url = getEasscanAttestationUrl(chainId, uid, isOffchain); + if (!!schemaDetails) + signAttestation( + getSubmissionData(schemaDetails.schemaString, values), + ).then(({ uids, txnReceipt }: any) => { + console.log({ uids, txnReceipt }); + const [uid] = uids; + const url = getEasscanAttestationUrl(chainId, uid, isOffchain); - const description = isOffchain - ? getShortHex(uid) - : `attested ${txnReceipt?.transactionHash}`; + const description = isOffchain + ? getShortHex(uid) + : `attested ${txnReceipt?.transactionHash}`; - toast({ - title: "Attestation success", - description, - action: ( - - - View on EASSCAN - - - ), + toast({ + title: "Attestation success", + description, + action: ( + + + View on EASSCAN + + + ), + }); }); - }); } // TODO: array schema handling diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts index 7ff8820d..869ceeba 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.test.ts @@ -1,6 +1,9 @@ import { getRandomAddress } from "@geist/domain/user.fixture"; import { describe, expect, it } from "vitest"; -import { getZodSchemaFromSchemaString } from "./use-eas-schema-form"; +import { + getSubmissionData, + getZodSchemaFromSchemaString, +} from "./use-eas-schema-form"; describe("use-schema-eas-form", () => { describe("#getZodSchemaFromSchemaString", () => { @@ -77,4 +80,25 @@ describe("use-schema-eas-form", () => { }); }); }); + + describe("#getSubmissionData", () => { + it("happy flow", () => { + const data = getSubmissionData( + "address walletAddress,string requestId,bool hasClaimedNFT,string message", + { + walletAddress: "0x123", + requestId: "abcdef", + hasClaimedNFT: false, + message: "test", + }, + ); + + expect(data).toEqual([ + { name: "walletAddress", value: "0x123", type: "address" }, + { name: "requestId", value: "abcdef", type: "string" }, + { name: "hasClaimedNFT", value: false, type: "bool" }, + { name: "message", value: "test", type: "string" }, + ]); + }); + }); }); diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index 78dcffcb..7685259b 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -6,6 +6,7 @@ import { useMemo } from "react"; import { useForm } from "react-hook-form"; import { z, ZodNumber } from "zod"; import { getEasscanEndpoint } from "#lib/eas/easscan.js"; +import { Address } from "viem"; type SchemaByQuery = any; @@ -38,6 +39,32 @@ export const getZodSchemaFromSchemaString = (schemaString: string) => { return z.object(zodSchemaObject); }; +// function to get submission data that is attestation-ready +export const getSubmissionData = (schemaString: string, values: any) => { + const stringPairs = schemaString.split(","); + const entries = stringPairs.map((pair) => pair.split(" ")); + + const result: { + name: string; + value: string | boolean | number | bigint; + type: string; + }[] = []; + + for (const entry of entries) { + const [type = "", name = ""] = entry; + + const resultEntry = { + name, + value: values[name], + type, + }; + + result.push(resultEntry); + } + + return result; +}; + const schemaByQuery = gql(` query schemaBy($where: SchemaWhereUniqueInput!) { schema(where: $where) { @@ -79,7 +106,12 @@ export function useEasSchemaForm({ }, ); - return data.schema; + return data.schema as { + schemaString: string; + index: string; + revocable: true; + creator: Address; + }; }, enabled: !!isEnabled && !!schemaId, }); From 0d6c930a4188e1909cac9965f10967ce59657133 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Sun, 19 Jan 2025 20:13:18 +0700 Subject: [PATCH 12/16] chore: run biome --- packages/ui-react/src/hooks/eas/use-eas-schema-form.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index 7685259b..635db01e 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -4,9 +4,9 @@ import { useQuery } from "@tanstack/react-query"; import { rawRequest } from "graphql-request"; import { useMemo } from "react"; import { useForm } from "react-hook-form"; -import { z, ZodNumber } from "zod"; -import { getEasscanEndpoint } from "#lib/eas/easscan.js"; import { Address } from "viem"; +import { ZodNumber, z } from "zod"; +import { getEasscanEndpoint } from "#lib/eas/easscan.js"; type SchemaByQuery = any; From 3d3dac12232a25c8031cb0b6e93aa53f72f9ed89 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 20 Jan 2025 06:49:42 +0700 Subject: [PATCH 13/16] chore: revert user fixture --- packages/domain/src/user.fixture.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/domain/src/user.fixture.ts b/packages/domain/src/user.fixture.ts index c20ed56d..399acc66 100644 --- a/packages/domain/src/user.fixture.ts +++ b/packages/domain/src/user.fixture.ts @@ -11,9 +11,8 @@ const vitalik = { // stable private key // 0x4E123166e7DfDE7AbA29162Fb3a5c6Af562443D4 const user = { - privateKey: - "0x9e0fbda2334ed9a312bfb8c59bbc55f6c059a69fae1a1e818ddcd6c60843375b" as Hex, - address: "0x8178c9834DDaE72D4fcAB4655bF16bCe9C7bE557", + privateKey: config.test.user.privateKey as Hex, + address: "0x4E123166e7DfDE7AbA29162Fb3a5c6Af562443D4", }; const eas = { From a8be80cbfd73f175e4d1e9eb4baa275434c6b6ff Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 20 Jan 2025 07:22:41 +0700 Subject: [PATCH 14/16] chore: revert story entries --- .../AttestationFormEasSdk.stories.tsx | 16 ++++++---------- .../AttestationFormWagmi.stories.tsx | 8 ++++---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx b/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx index 12e68f3c..40283830 100644 --- a/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx +++ b/apps/storybook/src/stories/attestations/AttestationFormEasSdk.stories.tsx @@ -111,12 +111,11 @@ export const OnchainSepolia: Story = { args: { isOffchain: false, ...createArgs( - SCHEMA_BY_NAME.VOTE, + SCHEMA_BY_NAME.IS_A_FRIEND, sepolia, - SCHEMA_BY_NAME.VOTE.byFixture.vote, + SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, ), }, - decorators: [], }; export const OnchainOptimismSepolia: Story = { @@ -128,16 +127,15 @@ export const OnchainOptimismSepolia: Story = { SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, ), }, - decorators: [], }; export const OffchainSepolia: Story = { args: { isOffchain: true, ...createArgs( - SCHEMA_BY_NAME.VOTE, + SCHEMA_BY_NAME.IS_A_FRIEND, sepolia, - SCHEMA_BY_NAME.VOTE.byFixture.vote, + SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, ), }, decorators: [], @@ -148,11 +146,9 @@ export const OffchainVote: Story = { args: { isOffchain: true, ...createArgs( - SCHEMA_BY_NAME.VOTE, + SCHEMA_BY_NAME.IS_A_FRIEND, sepolia, - SCHEMA_BY_NAME.VOTE.byFixture.vote, + SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, ), - ...SCHEMA_BY_NAME.VOTE.byFixture.vote, }, - decorators: [], }; diff --git a/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx b/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx index b991aaa6..8769e6f4 100644 --- a/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx +++ b/apps/storybook/src/stories/attestations/AttestationFormWagmi.stories.tsx @@ -116,9 +116,9 @@ export const AttestationWagmiOffchain: Story = { args: { isOffchain: true, ...createArgs( - SCHEMA_BY_NAME.VOTE, + SCHEMA_BY_NAME.IS_A_FRIEND, sepolia, - SCHEMA_BY_NAME.VOTE.byFixture.vote, + SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, ), }, }; @@ -128,9 +128,9 @@ export const AttestationWagmiOnchain: Story = { args: { isOffchain: false, ...createArgs( - SCHEMA_BY_NAME.VOTE, + SCHEMA_BY_NAME.IS_A_FRIEND, sepolia, - SCHEMA_BY_NAME.VOTE.byFixture.vote, + SCHEMA_BY_NAME.IS_A_FRIEND.byFixture.isFriend, ), }, }; From 273f91df53f0c7263d763e508e39b89dca023b20 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Mon, 20 Jan 2025 07:51:08 +0700 Subject: [PATCH 15/16] feat: add bool for attestation form --- .../attestations/attestation-form.tsx | 59 +++++++++++-------- .../ui-react/src/components/shadcn/switch.tsx | 29 +++++++++ .../src/hooks/eas/use-eas-schema-form.ts | 15 +++-- 3 files changed, 73 insertions(+), 30 deletions(-) create mode 100644 packages/ui-react/src/components/shadcn/switch.tsx diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx index e34a33e9..6dded0aa 100644 --- a/packages/ui-react/src/components/attestations/attestation-form.tsx +++ b/packages/ui-react/src/components/attestations/attestation-form.tsx @@ -1,8 +1,9 @@ import { Spinner } from "@radix-ui/themes"; -import { ZodNumber, z } from "zod"; +import { ZodBoolean, ZodNumber, z } from "zod"; import { Badge } from "#components/shadcn/badge"; import { Button } from "#components/shadcn/button"; import { Card, CardContent } from "#components/shadcn/card"; +import { Switch } from "#components/shadcn/switch"; import { Form, FormControl, @@ -105,31 +106,41 @@ export const AttestationForm = ({ control={form.control} name={schemaKey} render={({ field }) => ( - + {schemaKey} - - { - const value = + {formSchema.shape[schemaKey] instanceof ZodBoolean ? ( + + + + ) : ( + + - + ? "number" + : "text" + } + value={field.value ?? ""} + onChange={(e) => { + const value = + formSchema.shape[schemaKey] instanceof + ZodNumber + ? e.target.valueAsNumber || 0 + : e.target.value; + field.onChange(value); + }} + placeholder={ + formSchema.shape[schemaKey] instanceof ZodNumber + ? "number" + : "string" + } + /> + + )} )} diff --git a/packages/ui-react/src/components/shadcn/switch.tsx b/packages/ui-react/src/components/shadcn/switch.tsx new file mode 100644 index 00000000..89d2e154 --- /dev/null +++ b/packages/ui-react/src/components/shadcn/switch.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; + +import { cn } from "#lib/shadcn/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts index 635db01e..fc24f50b 100644 --- a/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts +++ b/packages/ui-react/src/hooks/eas/use-eas-schema-form.ts @@ -5,8 +5,8 @@ import { rawRequest } from "graphql-request"; import { useMemo } from "react"; import { useForm } from "react-hook-form"; import { Address } from "viem"; -import { ZodNumber, z } from "zod"; -import { getEasscanEndpoint } from "#lib/eas/easscan.js"; +import { ZodArray, ZodBoolean, ZodNumber, z } from "zod"; +import { getEasscanEndpoint } from "#lib/eas/easscan"; type SchemaByQuery = any; @@ -130,10 +130,13 @@ export function useEasSchemaForm({ resolver: zodResolver(formSchema), disabled: !isEnabled, defaultValues: Object.fromEntries( - Object.keys(formSchema.shape).map((key) => [ - key, - formSchema.shape[key] instanceof ZodNumber ? 0 : "", - ]), + Object.keys(formSchema.shape).map((key) => { + let defaultValue: string | number | boolean | any[] = ""; + if (formSchema.shape[key] instanceof ZodNumber) defaultValue = 0; + if (formSchema.shape[key] instanceof ZodBoolean) defaultValue = false; + if (formSchema.shape[key] instanceof ZodArray) defaultValue = []; + return [key, defaultValue]; + }), ), }); From 73907829ed727d6e95a921bf321343320f7200df Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Tue, 18 Feb 2025 20:26:58 +0700 Subject: [PATCH 16/16] wip: use bool switch --- .../ui-react/src/components/attestations/attestation-form.tsx | 2 +- packages/ui-react/src/components/shadcn/switch.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx index 6dded0aa..10352fae 100644 --- a/packages/ui-react/src/components/attestations/attestation-form.tsx +++ b/packages/ui-react/src/components/attestations/attestation-form.tsx @@ -3,7 +3,6 @@ import { ZodBoolean, ZodNumber, z } from "zod"; import { Badge } from "#components/shadcn/badge"; import { Button } from "#components/shadcn/button"; import { Card, CardContent } from "#components/shadcn/card"; -import { Switch } from "#components/shadcn/switch"; import { Form, FormControl, @@ -13,6 +12,7 @@ import { FormMessage, } from "#components/shadcn/form"; import { Input } from "#components/shadcn/input"; +import { Switch } from "#components/shadcn/switch"; import { ToastAction } from "#components/shadcn/toast"; import { getSubmissionData, diff --git a/packages/ui-react/src/components/shadcn/switch.tsx b/packages/ui-react/src/components/shadcn/switch.tsx index 89d2e154..ffa4c45a 100644 --- a/packages/ui-react/src/components/shadcn/switch.tsx +++ b/packages/ui-react/src/components/shadcn/switch.tsx @@ -1,7 +1,7 @@ "use client"; -import * as React from "react"; import * as SwitchPrimitives from "@radix-ui/react-switch"; +import * as React from "react"; import { cn } from "#lib/shadcn/utils";