diff --git a/README.md b/README.md index 3833db6..fac5803 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,31 @@ # Intent-Based ENS Solver Network For ENS Auto-Renewals +A decentralized public good enabling gasless, automated ENS domain renewals through intent-based transactions and off-chain solvers. + +## Intent Management: + +Users generate signed intents specifying: target ENS domain names, maximum price parameters (renewal fee + gas fee + solver reward), renewal conditions (expiration threshold, gas price threshold). + +## Off-Chain Infrastructure: + +- REST API endpoints for intent submission and validation +- Local mempool implementation for intent storage and management +- Continuous monitoring of: ENS domain expiration timestamps, Ethereum network gas prices, user token balances. + +## Value Proposition: + +- Eliminates manual renewal tracking +- Reduces gas costs thanks to execution timing +- Improves UX through gasless intent submission +- Ensures ENS domains for individuals and important projects in web3 don't expire + ## Frontend -ToDo +User friendly UI with intent creation and management. ## Contracts -ToDo +Deployed to local fork of Ethereum Sepolia. ## API Overview diff --git a/web/package.json b/web/package.json index b645f56..1b9647c 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "@tanstack/react-form": "^0.35.0", "@tanstack/react-query": "^5.60.2", "@tanstack/react-router": "^1.81.9", + "axios": "^1.7.7", "connectkit": "^1.8.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index bf4ab7a..eb61413 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@tanstack/react-router': specifier: ^1.81.9 version: 1.81.9(@tanstack/router-generator@1.81.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + axios: + specifier: ^1.7.7 + version: 1.7.7 connectkit: specifier: ^1.8.2 version: 1.8.2(@babel/core@7.26.0)(@tanstack/react-query@5.60.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.32(@tanstack/query-core@5.59.20)(@tanstack/react-query@5.60.2(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) @@ -2171,6 +2174,9 @@ packages: async-mutex@0.2.6: resolution: {integrity: sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -2186,6 +2192,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + babel-core@7.0.0-bridge.0: resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: @@ -2392,6 +2401,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -2555,6 +2568,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + denodeify@1.2.1: resolution: {integrity: sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==} @@ -2851,6 +2868,15 @@ packages: resolution: {integrity: sha512-EbxtzRIzp8dDSzTloPhsc6uOvrEFIyu08cqQzXBWLAgxK+i2d/5qOos9ryQHRmk+RyDDXfnz/7qteh3jnAlc4w==} engines: {node: '>=0.4.0'} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -2858,6 +2884,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -3878,6 +3908,9 @@ packages: proxy-compare@2.5.1: resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -7478,6 +7511,8 @@ snapshots: dependencies: tslib: 2.8.1 + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} autoprefixer@10.4.20(postcss@8.4.49): @@ -7494,6 +7529,14 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-core@7.0.0-bridge.0(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -7770,6 +7813,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@12.1.0: {} commander@2.20.3: {} @@ -7911,6 +7958,8 @@ snapshots: defu@6.1.4: {} + delayed-stream@1.0.0: {} + denodeify@1.2.1: {} depd@2.0.0: {} @@ -8295,6 +8344,8 @@ snapshots: flow-parser@0.253.0: {} + follow-redirects@1.15.9: {} + for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -8304,6 +8355,12 @@ snapshots: cross-spawn: 7.0.5 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fraction.js@4.3.7: {} framer-motion@6.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -9425,6 +9482,8 @@ snapshots: proxy-compare@2.5.1: {} + proxy-from-env@1.1.0: {} + pump@3.0.2: dependencies: end-of-stream: 1.4.4 diff --git a/web/src/routes/action/new.lazy.tsx b/web/src/routes/action/new.lazy.tsx index aae2270..c2f2b92 100644 --- a/web/src/routes/action/new.lazy.tsx +++ b/web/src/routes/action/new.lazy.tsx @@ -1,11 +1,11 @@ import { createLazyFileRoute, useNavigate } from "@tanstack/react-router"; import { - Button, - Card, - Helper, - Input, - PlusSVG, - TrashSVG, + Button, + Card, + Helper, + Input, + PlusSVG, + TrashSVG, } from "@ensdomains/thorin"; import { useForm } from "@tanstack/react-form"; import { useMutation, useQueryClient } from "@tanstack/react-query"; @@ -14,259 +14,234 @@ import { signMessage } from "wagmi/actions"; import { useAccount, useConfig } from "wagmi"; import { Header } from "../../components/Header"; import { Action } from "../../types"; +import { + submitRenewalIntent, + checkNameExpiry, + getNamePrice, +} from "../../submitFunctions"; export const Route = createLazyFileRoute("/action/new")({ - component: RouteComponent, + component: RouteComponent, }); function RouteComponent() { - const { address } = useAccount(); - const navigate = useNavigate(); - const config = useConfig(); - const queryClient = useQueryClient(); + const { address } = useAccount(); + const navigate = useNavigate(); + const config = useConfig(); + const queryClient = useQueryClient(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { mutate } = useMutation({ - mutationKey: ["create-action"], - mutationFn: async (data: { names: string[]; reward: number }) => { - // TODO: Implement correct message here - const message = JSON.stringify(data); - const signature = await signMessage(config, { - message, - }); + const checkExpiry = async (name: string) => { + const isExpiring = await checkNameExpiry(name); + console.log(`${name} is expiring:`, isExpiring); + }; - const response = await fetch("https://API/verify", { - method: "POST", - body: JSON.stringify({ - msg: data, - sig: signature, - signer: address, - }), - }); + const getPrice = async (name: string) => { + const price = await getNamePrice(name); + console.log(`${name} price:`, price, "ETH"); + }; - if (!response.ok) { - throw new Error("Failed to verify signature"); - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { mutate } = useMutation({ + mutationKey: ["create-action"], + mutationFn: async (data: { names: string[]; reward: number }) => { + // TODO: Implement correct message here + // const message = JSON.stringify(data); + // const signature = await signMessage(config, { + // message, + // }); - return response.json(); - }, - }); + const result = await submitRenewalIntent(data.names, data.value); + console.log("Success:", result); - const form = useForm({ - defaultValues: { - names: [] as Array, - reward: 0, - }, - onSubmit({ value }) { - if (!address) return; - // TODO: run create action mutation and sign message - // Temporary adding to local storage - const actions = localStorage.getItem("actions") ?? "[]"; - localStorage.setItem( - "actions", - JSON.stringify([ - ...JSON.parse(actions), - { - ...value, - owner: address, - type: "RENEW_NAME", - } satisfies Action, - ]) - ); + // const response = await fetch("https://API/verify", { + // method: "POST", + // body: JSON.stringify({ + // msg: data, + // sig: signature, + // signer: address, + // }), + // }); - navigate({ to: "/" }); - }, - }); + // if (!response.ok) { + // throw new Error("Failed to verify signature"); + // } - return ( -
-
-
- -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - className="space-y-4" - > - { - if (value.length === 0) - return "At least one name is required"; - return undefined; - }, - }} - > - {(field) => { - return ( -
- {field.state.value.map((_, i) => { - return ( - { - if (!value) - return "Name is required"; - // check if valid domain name - if ( - !value.includes(".") - ) - return "Invalid domain name"; - return undefined; - }, - onChangeAsync: async ({ - value, - }) => { - const result = - await queryClient - .fetchQuery( - lookupEnsNameQueryOptions( - value - ) - ) - .catch( - () => {} - ); + // return response.json(); + }, + }); - if (!result) { - return "ENS name couldn't be resolved"; - } - }, - onChangeAsyncDebounceMs: 500, - }} - > - {(subField) => { - return ( -
- - subField.handleChange( - e.target - .value - ) - } - width={ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - "100%" as any - } - actionIcon={ -
- -
- } - alwaysShowAction={ - true - } - onClickAction={() => - field.removeValue( - i - ) - } - error={ - subField - .state - .meta - .errors - .length > - 0 - ? subField.state.meta.errors.join( - ", " - ) - : null - } - /> -
- ); - }} -
- ); - })} - {field.state.value.length === 0 && ( - <> - - No names added - - - )} - -
- ); - }} -
-
- , + reward: 0, + }, + onSubmit({ value }) { + if (!address) return; + // TODO: run create action mutation and sign message + // Temporary adding to local storage + const actions = localStorage.getItem("actions") ?? "[]"; + localStorage.setItem( + "actions", + JSON.stringify([ + ...JSON.parse(actions), + { + ...value, + owner: address, + type: "RENEW_NAME", + } satisfies Action, + ]) + ); + + mutate(value.names, value.reward); + + // navigate({ to: "/" }); + }, + }); + + return ( +
+
+
+ + { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > + { + if (value.length === 0) return "At least one name is required"; + return undefined; + }, + }} + > + {(field) => { + return ( +
+ {field.state.value.map((_, i) => { + return ( + { - console.log(value); - if (!value) return "Reward is required"; - if (isNaN(Number(value))) - return "Must be a number"; - if (Number(value) <= 0) - return "Must be greater than 0"; - return undefined; - }, + onChange: ({ value }) => { + if (!value) return "Name is required"; + // check if valid domain name + if (!value.includes(".")) + return "Invalid domain name"; + return undefined; + }, + onChangeAsync: async ({ value }) => { + const result = await queryClient + .fetchQuery(lookupEnsNameQueryOptions(value)) + .catch(() => {}); + + if (!result) { + return "ENS name couldn't be resolved"; + } + }, + onChangeAsyncDebounceMs: 500, }} - > - {(field) => ( -
- - field.handleChange( - Number(e.target.value) - ) - } - error={ - field.state.meta.errors.length > 0 - ? field.state.meta.errors[0] - : undefined - } - /> - - The reward paid to executors for renewing - your names (in GWEI) - + > + {(subField) => { + return ( +
+ + subField.handleChange(e.target.value) + } + width={ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + "100%" as any + } + actionIcon={ +
+ +
+ } + alwaysShowAction={true} + onClickAction={() => field.removeValue(i)} + error={ + subField.state.meta.errors.length > 0 + ? subField.state.meta.errors.join(", ") + : null + } + />
- )} - - [ - state.canSubmit, - state.isSubmitting, - ]} - children={([canSubmit, isSubmitting]) => ( - - )} - /> - - -
- ); + ); + }} +
+ ); + })} + {field.state.value.length === 0 && ( + <> + No names added + + )} + +
+ ); + }} +
+
+ { + console.log(value); + if (!value) return "Reward is required"; + if (isNaN(Number(value))) return "Must be a number"; + if (Number(value) <= 0) return "Must be greater than 0"; + return undefined; + }, + }} + > + {(field) => ( +
+ field.handleChange(Number(e.target.value))} + error={ + field.state.meta.errors.length > 0 + ? field.state.meta.errors[0] + : undefined + } + /> + + The reward paid to executors for renewing your names (in GWEI) + +
+ )} +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> + +
+
+ ); } diff --git a/web/src/routes/list/index.lazy.tsx b/web/src/routes/list/index.lazy.tsx index 0a43a25..9f59582 100644 --- a/web/src/routes/list/index.lazy.tsx +++ b/web/src/routes/list/index.lazy.tsx @@ -7,7 +7,7 @@ import { Header } from "../../components/Header"; import { Action } from "../../types"; export const Route = createLazyFileRoute("/list/")({ - component: Index, + component: Index, }); /** @@ -20,72 +20,73 @@ export const Route = createLazyFileRoute("/list/")({ // eslint-disable-next-line @typescript-eslint/no-unused-vars const getActions = async (owner: string): Promise => { - // Temporary storage of actions in local storage - const stored = localStorage.getItem("actions"); - if (!stored) { - return []; - } + // Temporary storage of actions in local storage + const stored = localStorage.getItem("actions"); + if (!stored) { + return []; + } - try { - const actions = JSON.parse(stored) as Action[]; - return actions.filter((action) => action.owner === owner); - } catch { - return []; - } + try { + const actions = JSON.parse(stored) as Action[]; + return actions.filter((action) => action.owner === owner); + } catch { + return []; + } }; const ActionRow = ({ action }: { action: Action }) => { - return ( -
  • - - {match(action.type) - .with("RENEW_NAME", () => "Renew Name") - .otherwise(() => "Unknown")} - + return ( +
  • + + {match(action.type) + .with("RENEW_NAME", () => "Renew Name") + .otherwise(() => "Unknown")} + -
    - - {action.names.join(", ")} - - - {action.reward} GWEI - -
    -
  • - ); +
    + {action.names.join(", ")} + + {action.reward} GWEI + + + + +
    + + ); }; function Index() { - const { address } = useAccount(); - const { data: actions } = useQuery({ - queryKey: ["actions", address], - queryFn: () => getActions(address ?? "0x0"), - }); + const { address } = useAccount(); + const { data: actions } = useQuery({ + queryKey: ["actions", address], + queryFn: () => getActions(address ?? "0x0"), + }); - return ( -
    -
    -
    - - Add your ENS names to the list below to automatically renew - them when needed. - - View all actions -
    -
    - -
      - {actions?.map((action) => ( - - ))} -
    - {actions?.length === 0 && ( - You have not created any actions yet - )} - -
    -
    - ); + return ( +
    +
    +
    + + Add your ENS names to the list below to automatically renew them when + needed. + + View all actions +
    +
    + +
      + {actions?.map((action) => ( + + ))} +
    + {actions?.length === 0 && ( + You have not created any actions yet + )} + {/* */} +
    +
    + ); } diff --git a/web/src/submitFunctions.ts b/web/src/submitFunctions.ts new file mode 100644 index 0000000..d25660f --- /dev/null +++ b/web/src/submitFunctions.ts @@ -0,0 +1,137 @@ +import { + createPublicClient, + createWalletClient, + custom, + formatEther, + parseEther, + http, +} from "viem"; +import { mainnet } from "viem/chains"; +import axios from "axios"; + +const API_URL = "http://localhost:3000/api"; +const CONTRACT_ADDRESS = "0xfa34D7D7aE7C0A2F43A04F4Abf03e8a25c7C8023"; + +const CONTRACT_ABI = [ + { + name: "calculateIntentHash", + type: "function", + stateMutability: "pure", + inputs: [ + { name: "names", type: "string[]" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + { name: "oneTime", type: "bool" }, + ], + outputs: [{ type: "bytes32" }], + }, + { + name: "isNameExpiringSoon", + type: "function", + stateMutability: "view", + inputs: [{ name: "name", type: "string" }], + outputs: [{ type: "bool" }], + }, + { + name: "getNamePrice", + type: "function", + stateMutability: "view", + inputs: [{ name: "name", type: "string" }], + outputs: [{ type: "uint256" }], + }, +] as const; + +interface SubmitResponse { + status: string; + data: { + signer: string; + messageHash: string; + }; +} + +export async function submitRenewalIntent( + names: string[], + value: string, + oneTime: boolean = true +): Promise { + if (!window.ethereum) { + throw new Error("MetaMask not found"); + } + + const publicClient = createPublicClient({ + chain: mainnet, + transport: http(), + }); + + const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum), + }); + + const [address] = await walletClient.requestAddresses(); + const nonce = BigInt(Date.now()); + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); + const valueWei = parseEther(value); + + const messageHash = await publicClient.readContract({ + address: CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "calculateIntentHash", + args: [names, valueWei, nonce, deadline, oneTime], + }); + + const signature = await walletClient.signMessage({ + account: address, + message: { raw: messageHash as `0x${string}` }, + }); + + const response = await axios.post(`${API_URL}/messages`, { + names, + value: valueWei.toString(), + nonce: nonce.toString(), + deadline: deadline.toString(), + oneTime, + signature, + }); + + return response.data; +} + +export async function checkNameExpiry(name: string): Promise { + if (!window.ethereum) { + throw new Error("MetaMask not found"); + } + + const publicClient = createPublicClient({ + chain: mainnet, + transport: http(), + }); + + return await publicClient.readContract({ + address: CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "isNameExpiringSoon", + args: [name], + }); +} + +export async function getNamePrice(name: string): Promise { + if (!window.ethereum) { + throw new Error("MetaMask not found"); + } + + const publicClient = createPublicClient({ + chain: mainnet, + transport: http(), + }); + + const price = await publicClient.readContract({ + address: CONTRACT_ADDRESS, + abi: CONTRACT_ABI, + functionName: "getNamePrice", + args: [name], + }); + + return formatEther(price); +}