From e405133bbed2ebd894ab708f14fac878fe7d7abb Mon Sep 17 00:00:00 2001 From: eduramme Date: Wed, 27 Nov 2024 12:53:48 -0300 Subject: [PATCH 1/9] refactor: create ens sdk --- {lib => ens-sdk}/create-subdomain/service.ts | 10 +++++----- .../ensManager.ts | 20 +++++++++++-------- ens-sdk/index.ts | 2 ++ ens-sdk/package.json | 11 ++++++++++ 4 files changed, 30 insertions(+), 13 deletions(-) rename {lib => ens-sdk}/create-subdomain/service.ts (94%) rename lib/utils/blockchain-txs.ts => ens-sdk/ensManager.ts (97%) create mode 100644 ens-sdk/index.ts create mode 100644 ens-sdk/package.json diff --git a/lib/create-subdomain/service.ts b/ens-sdk/create-subdomain/service.ts similarity index 94% rename from lib/create-subdomain/service.ts rename to ens-sdk/create-subdomain/service.ts index 6532c8d..1d1c326 100644 --- a/lib/create-subdomain/service.ts +++ b/ens-sdk/create-subdomain/service.ts @@ -15,17 +15,17 @@ import { stringToHex, toHex, } from "viem"; -import { getRevertErrorData, handleDBStorage } from "../utils/blockchain-txs"; -import { DomainData, MessageData } from "../utils/types"; -import L1ResolverABI from "../abi/arbitrum-resolver.json"; +import { getRevertErrorData, handleDBStorage } from "@/ens-sdk"; +import { DomainData, MessageData } from "@/lib/utils/types"; +import L1ResolverABI from "../../lib/abi/arbitrum-resolver.json"; import toast from "react-hot-toast"; import { getCoderByCoinName } from "@ensdomains/address-encoder"; import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; import * as chains from "viem/chains"; import { packetToBytes } from "viem/ens"; import { SECONDS_PER_YEAR } from "@namehash/ens-utils"; -import { getNameRegistrationSecret } from "../name-registration/localStorage"; -import { DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES } from "../name-registration/constants"; +import { getNameRegistrationSecret } from "../../lib/name-registration/localStorage"; +import { DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES } from "../../lib/name-registration/constants"; interface CreateSubdomainArgs { resolverAddress: Address; diff --git a/lib/utils/blockchain-txs.ts b/ens-sdk/ensManager.ts similarity index 97% rename from lib/utils/blockchain-txs.ts rename to ens-sdk/ensManager.ts index 56ee8fd..19677b1 100644 --- a/lib/utils/blockchain-txs.ts +++ b/ens-sdk/ensManager.ts @@ -2,11 +2,11 @@ /* eslint-disable import/named */ import ENSReverseRegistrarABI from "@/lib/abi/ens-reverse-registrar.json"; import ETHRegistrarABI from "@/lib/abi/eth-registrar.json"; -import L1ResolverABI from "../abi/arbitrum-resolver.json"; +import L1ResolverABI from "../lib/abi/arbitrum-resolver.json"; import { DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES, nameRegistrationSmartContracts, -} from "../name-registration/constants"; +} from "../lib/name-registration/constants"; import { namehash, @@ -24,25 +24,29 @@ import { Chain, stringToHex, } from "viem"; -import { SupportedNetwork } from "../wallet/chains"; +import { SupportedNetwork } from "../lib/wallet/chains"; import { SECONDS_PER_YEAR, ENSName } from "@namehash/ens-utils"; import { TransactionErrorType, getBlockchainTransactionError, -} from "../wallet/txError"; +} from "../lib/wallet/txError"; import { getNameRegistrationSecret } from "@/lib/name-registration/localStorage"; import { parseAccount } from "viem/utils"; -import DomainResolverABI from "../abi/offchain-resolver.json"; +import DomainResolverABI from "../lib/abi/offchain-resolver.json"; import { normalize } from "viem/ens"; -import { supportedCoinTypes } from "../domain-page"; +import { supportedCoinTypes } from "../lib/domain-page"; import { coinNameToTypeMap, getCoderByCoinName, } from "@ensdomains/address-encoder"; -import { CcipRequestParameters, DomainData, MessageData } from "./types"; +import { + CcipRequestParameters, + DomainData, + MessageData, +} from "../lib/utils/types"; import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; import { getAvailable } from "@ensdomains/ensjs/public"; -import { getChain } from "../create-subdomain/service"; +import { getChain } from "./create-subdomain/service"; import toast from "react-hot-toast"; import { sepolia } from "viem/chains"; diff --git a/ens-sdk/index.ts b/ens-sdk/index.ts new file mode 100644 index 0000000..a33e930 --- /dev/null +++ b/ens-sdk/index.ts @@ -0,0 +1,2 @@ +export * from "./ensManager"; +export * from "./create-subdomain/service"; diff --git a/ens-sdk/package.json b/ens-sdk/package.json new file mode 100644 index 0000000..cc2edd3 --- /dev/null +++ b/ens-sdk/package.json @@ -0,0 +1,11 @@ +{ + "name": "@nameful/sdk", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "" +} From 8e48910c14dceef14fae0fde5ef2c705130730c6 Mon Sep 17 00:00:00 2001 From: eduramme Date: Wed, 27 Nov 2024 12:54:09 -0300 Subject: [PATCH 2/9] refactor: update imports --- .../NameRegisteredAwaitingRecordsSettingComponent.tsx | 2 +- .../NameSecuredToBeRegisteredComponent.tsx | 2 +- .../RecordsSetAwaitingPrimaryNameSetting.tsx | 2 +- .../molecules/registration/RegistrationSummary.tsx | 11 ++++++----- .../registration/RequestToRegisterComponent.tsx | 4 ++-- components/organisms/CreateSubdomainModalContent.tsx | 2 +- components/organisms/EditModalContent.tsx | 2 +- lib/name-registration/localStorage.tsx | 2 +- pages/index.tsx | 2 +- pages/register/[name]/index.tsx | 2 +- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/components/molecules/registration/NameRegisteredAwaitingRecordsSettingComponent.tsx b/components/molecules/registration/NameRegisteredAwaitingRecordsSettingComponent.tsx index 22cb53d..9ac97e9 100644 --- a/components/molecules/registration/NameRegisteredAwaitingRecordsSettingComponent.tsx +++ b/components/molecules/registration/NameRegisteredAwaitingRecordsSettingComponent.tsx @@ -1,5 +1,5 @@ import { BackButton, BlockchainCTA } from "@/components/atoms"; -import { setDomainRecords } from "@/lib/utils/blockchain-txs"; +import { setDomainRecords } from "@/ens-sdk"; import { useNameRegistration } from "@/lib/name-registration/useNameRegistration"; import { TransactionErrorType, diff --git a/components/molecules/registration/NameSecuredToBeRegisteredComponent.tsx b/components/molecules/registration/NameSecuredToBeRegisteredComponent.tsx index fed691a..c2bda0f 100644 --- a/components/molecules/registration/NameSecuredToBeRegisteredComponent.tsx +++ b/components/molecules/registration/NameSecuredToBeRegisteredComponent.tsx @@ -1,5 +1,5 @@ import { BackButton, BlockchainCTA } from "@/components/atoms"; -import { register } from "@/lib/utils/blockchain-txs"; +import { register } from "@/ens-sdk"; import { useNameRegistration } from "@/lib/name-registration/useNameRegistration"; import { TransactionErrorType } from "@/lib/wallet/txError"; import { PublicClient, TransactionReceipt } from "viem"; diff --git a/components/molecules/registration/RecordsSetAwaitingPrimaryNameSetting.tsx b/components/molecules/registration/RecordsSetAwaitingPrimaryNameSetting.tsx index b41efdc..082282a 100644 --- a/components/molecules/registration/RecordsSetAwaitingPrimaryNameSetting.tsx +++ b/components/molecules/registration/RecordsSetAwaitingPrimaryNameSetting.tsx @@ -1,5 +1,5 @@ import { BackButton, BlockchainCTA } from "@/components/atoms"; -import { setDomainAsPrimaryName } from "@/lib/utils/blockchain-txs"; +import { setDomainAsPrimaryName } from "@/ens-sdk"; import { useNameRegistration } from "@/lib/name-registration/useNameRegistration"; import { TransactionErrorType, diff --git a/components/molecules/registration/RegistrationSummary.tsx b/components/molecules/registration/RegistrationSummary.tsx index 5eb6fb7..68e78a0 100644 --- a/components/molecules/registration/RegistrationSummary.tsx +++ b/components/molecules/registration/RegistrationSummary.tsx @@ -7,15 +7,16 @@ import { EthIcon, InfoCircleIcon, } from "@/components/atoms"; -import { - getGasPrice, - getNamePrice, - getNameRegistrationGasEstimate, -} from "@/lib/utils/blockchain-txs"; + import { useEffect, useState } from "react"; import { formatEther, PublicClient } from "viem"; import { usePublicClient } from "wagmi"; import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; +import { + getGasPrice, + getNamePrice, + getNameRegistrationGasEstimate, +} from "@/ens-sdk"; export const RegistrationSummary = () => { const { diff --git a/components/molecules/registration/RequestToRegisterComponent.tsx b/components/molecules/registration/RequestToRegisterComponent.tsx index 361ec56..ad333d3 100644 --- a/components/molecules/registration/RequestToRegisterComponent.tsx +++ b/components/molecules/registration/RequestToRegisterComponent.tsx @@ -1,16 +1,16 @@ -import { Button, WalletSVG } from "@ensdomains/thorin"; +import { Button } from "@ensdomains/thorin"; import { BackButton, BlockchainCTA, TransactionConfirmedInBlockchainCTA, } from "@/components/atoms"; -import { commit } from "@/lib/utils/blockchain-txs"; import { useNameRegistration } from "@/lib/name-registration/useNameRegistration"; import { useAccount, useBalance, usePublicClient } from "wagmi"; import { TransactionErrorType } from "@/lib/wallet/txError"; import { setNameRegistrationInLocalStorage } from "@/lib/name-registration/localStorage"; import { PublicClient } from "viem"; import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; +import { commit } from "@/ens-sdk"; interface RequestToRegisterComponentProps { handlePreviousStep: () => void; diff --git a/components/organisms/CreateSubdomainModalContent.tsx b/components/organisms/CreateSubdomainModalContent.tsx index d4a1421..3afcce1 100644 --- a/components/organisms/CreateSubdomainModalContent.tsx +++ b/components/organisms/CreateSubdomainModalContent.tsx @@ -1,4 +1,4 @@ -import { createSubdomain } from "@/lib/create-subdomain/service"; +import { createSubdomain } from "@/ens-sdk"; import { Button, Input, Spinner } from "@ensdomains/thorin"; import { buildENSName } from "@namehash/ens-utils"; import { normalize } from "viem/ens"; diff --git a/components/organisms/EditModalContent.tsx b/components/organisms/EditModalContent.tsx index 003b4ea..cf2ba65 100644 --- a/components/organisms/EditModalContent.tsx +++ b/components/organisms/EditModalContent.tsx @@ -26,7 +26,7 @@ import { TransactionErrorType, getBlockchainTransactionError, } from "@/lib/wallet/txError"; -import { setDomainRecords } from "@/lib/utils/blockchain-txs"; +import { setDomainRecords } from "@/ens-sdk"; import { buildENSName } from "@namehash/ens-utils"; import { getResolver } from "@ensdomains/ensjs/public"; import { useAccount, usePublicClient } from "wagmi"; diff --git a/lib/name-registration/localStorage.tsx b/lib/name-registration/localStorage.tsx index 66641d5..59f86de 100644 --- a/lib/name-registration/localStorage.tsx +++ b/lib/name-registration/localStorage.tsx @@ -3,7 +3,7 @@ import { ENS_REGISTRATIONS_SECRET_KEY, OPEN_REGISTRATIONS_LOCAL_STORAGE_KEY, } from "@/lib/name-registration/constants"; -import { createNameRegistrationSecret } from "@/lib/utils/blockchain-txs"; +import { createNameRegistrationSecret } from "@/ens-sdk"; import { LocalNameRegistrationData } from "./types"; // GET diff --git a/pages/index.tsx b/pages/index.tsx index 4ece762..3a3061a 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, useState } from "react"; import { CrossCircleSVG } from "@ensdomains/thorin"; import { HomepageBg } from "@/components/atoms"; -import { isNameAvailable } from "@/lib/utils/blockchain-txs"; +import { isNameAvailable } from "@/ens-sdk"; import { ENSName, buildENSName } from "@namehash/ens-utils"; import { DebounceInput } from "react-debounce-input"; import { useRouter } from "next/router"; diff --git a/pages/register/[name]/index.tsx b/pages/register/[name]/index.tsx index 90ace66..8d029e4 100644 --- a/pages/register/[name]/index.tsx +++ b/pages/register/[name]/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { isNameAvailable } from "@/lib/utils/blockchain-txs"; +import { isNameAvailable } from "@/ens-sdk"; import { ProgressBar } from "@/components/atoms"; import { ProgressBlock, From c022fa630eac26303201b37403c99924237ef25a Mon Sep 17 00:00:00 2001 From: eduramme Date: Thu, 28 Nov 2024 15:40:04 -0300 Subject: [PATCH 3/9] refactor: organize sdk folder structure --- .../service.ts => createSubdomain.ts} | 58 ++++---- ens-sdk/{ensManager.ts => ensFunctions.ts} | 115 +++------------ ens-sdk/errorHandling.ts | 16 +++ ens-sdk/index.ts | 7 +- ens-sdk/types.ts | 4 + ens-sdk/utils.ts | 134 ++++++++++++++++++ 6 files changed, 199 insertions(+), 135 deletions(-) rename ens-sdk/{create-subdomain/service.ts => createSubdomain.ts} (78%) rename ens-sdk/{ensManager.ts => ensFunctions.ts} (85%) create mode 100644 ens-sdk/errorHandling.ts create mode 100644 ens-sdk/types.ts create mode 100644 ens-sdk/utils.ts diff --git a/ens-sdk/create-subdomain/service.ts b/ens-sdk/createSubdomain.ts similarity index 78% rename from ens-sdk/create-subdomain/service.ts rename to ens-sdk/createSubdomain.ts index 1d1c326..c2b5d0e 100644 --- a/ens-sdk/create-subdomain/service.ts +++ b/ens-sdk/createSubdomain.ts @@ -15,17 +15,19 @@ import { stringToHex, toHex, } from "viem"; -import { getRevertErrorData, handleDBStorage } from "@/ens-sdk"; +import { handleDBStorage } from "@/ens-sdk"; import { DomainData, MessageData } from "@/lib/utils/types"; -import L1ResolverABI from "../../lib/abi/arbitrum-resolver.json"; +import L1ResolverABI from "../lib/abi/arbitrum-resolver.json"; import toast from "react-hot-toast"; import { getCoderByCoinName } from "@ensdomains/address-encoder"; import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; import * as chains from "viem/chains"; import { packetToBytes } from "viem/ens"; import { SECONDS_PER_YEAR } from "@namehash/ens-utils"; -import { getNameRegistrationSecret } from "../../lib/name-registration/localStorage"; -import { DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES } from "../../lib/name-registration/constants"; +import { getNameRegistrationSecret } from "../lib/name-registration/localStorage"; +import { DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES } from "../lib/name-registration/constants"; +import { getChain } from "./utils"; +import { getRevertErrorData } from "./errorHandling"; interface CreateSubdomainArgs { resolverAddress: Address; @@ -38,7 +40,23 @@ interface CreateSubdomainArgs { chain: Chain; } -// TO-DO: Fix function later to accept more text / address params +/** + * Creates a subdomain with associated records (website, description, and address) for an ENS name. + * This function is specifically designed to handle offchain and L2 domains. + * + * The function performs the following steps: + * 1. Prepares call data for setting text records (website and description) if provided. + * 2. Prepares call data for setting the address record if provided. + * 3. Calculates the registration fee and retrieves necessary parameters from the resolver contract. + * 4. Attempts to simulate the contract call for subdomain creation. + * 5. Handles two specific scenarios based on the simulation result: + * - If storage is handled by an off-chain database, it processes the request accordingly. + * - If storage is handled by an L2 network, it switches to the appropriate network, + * performs the transaction, and switches back to the original network. + * + * Note: This function does not support standard L1 ENS registrations. It is specifically + * tailored for offchain storage solutions and L2 network integrations. + */ export const createSubdomain = async ({ resolverAddress, signerAddress, @@ -192,36 +210,8 @@ export const createSubdomain = async ({ return { ok: true }; } else { - toast.error("error"); + toast.error("Unsupported domain type"); console.error("writing failed: ", { error }); } } }; - -export function getChain(chainId: number) { - return [ - ...Object.values(chains), - defineChain({ - id: Number(chainId), - name: "Arbitrum Local", - nativeCurrency: { - name: "Arbitrum Sepolia Ether", - symbol: "ETH", - decimals: 18, - }, - rpcUrls: { - default: { - http: [ - `https://arb-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_TESTNET_KEY}`, - ], - }, - }, - }), - ].find((chain) => chain.id === chainId); -} - -// gather the first part of the domain (e.g. floripa.blockful.eth -> floripa, floripa.normal.blockful.eth -> floripa.normal) -const extractLabelFromName = (name: string): string => { - const [, label] = /^(.+?)\.\w+\.\w+$/.exec(name) || []; - return label; -}; diff --git a/ens-sdk/ensManager.ts b/ens-sdk/ensFunctions.ts similarity index 85% rename from ens-sdk/ensManager.ts rename to ens-sdk/ensFunctions.ts index 19677b1..f38bf22 100644 --- a/ens-sdk/ensManager.ts +++ b/ens-sdk/ensFunctions.ts @@ -46,9 +46,10 @@ import { } from "../lib/utils/types"; import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; import { getAvailable } from "@ensdomains/ensjs/public"; -import { getChain } from "./create-subdomain/service"; import toast from "react-hot-toast"; import { sepolia } from "viem/chains"; +import { getChain, getNamePrice } from "./utils"; +import { getRevertErrorData } from "./errorHandling"; const walletConnectProjectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; @@ -57,6 +58,15 @@ if (!walletConnectProjectId) { throw new Error("No wallet connect project ID informed"); } +/* + create subdmains + create domains with records - with a different resolver + set domain as primary name + set text records + set addresses + set abi / contenthash +*/ + /* commitment value is used in both 'commit' and 'register' functions in the registrar contract. It works as a secret @@ -65,6 +75,8 @@ if (!walletConnectProjectId) { it in the local storage. */ +type EnsPublicClient = PublicClient & ClientWithEns; + interface MakeCommitmentParams { name: string; data: string[]; @@ -74,7 +86,7 @@ interface MakeCommitmentParams { durationInYears: bigint; ownerControlledFuses: number; authenticatedAddress: Address; - publicClient: PublicClient & ClientWithEns; + publicClient: EnsPublicClient; } export async function makeCommitment({ @@ -192,7 +204,7 @@ interface CommitParams { resolverAddress: Address; authenticatedAddress: Address; registerAndSetAsPrimaryName: boolean; - publicClient: PublicClient & ClientWithEns; + publicClient: EnsPublicClient; chain: Chain; } @@ -266,7 +278,7 @@ interface RegisterParams { durationInYears: bigint; authenticatedAddress: Address; registerAndSetAsPrimaryName: boolean; - publicClient: PublicClient & ClientWithEns; + publicClient: EnsPublicClient; chain: Chain; } @@ -581,98 +593,3 @@ export const setDomainAsPrimaryName = async ({ return errorType; } }; - -// Error handling ⬇️ - -export function getRevertErrorData(err: unknown) { - if (!(err instanceof BaseError)) return undefined; - const error = err.walk() as RawContractError; - return error?.data as { errorName: string; args: unknown[] }; -} - -// Utils ⬇️ - -export const createNameRegistrationSecret = (): string => { - const platformHex = namehash("blockful-ens-external-resolver").slice(2, 10); - const platformBytes = platformHex.length; - const randomHex = [...Array(64 - platformBytes)] - .map(() => Math.floor(Math.random() * 16).toString(16)) - .join(""); - - return "0x" + platformHex + randomHex; -}; - -interface NamePrice { - base: bigint; - premium: bigint; -} - -interface GetNamePriceParams { - ensName: ENSName; - durationInYears: bigint; - publicClient: PublicClient & ClientWithEns; -} - -export const getNamePrice = async ({ - ensName, - durationInYears, - publicClient, -}: GetNamePriceParams): Promise => { - const ensNameDirectSubname = ensName.name.split(".eth")[0]; - - const chain = publicClient.chain; - - if (!Object.values(SupportedNetwork).includes(chain.id)) { - throw new Error(`Unsupported network: ${chain.id}`); - } - - const nameRegistrationContracts = - nameRegistrationSmartContracts[chain.id as SupportedNetwork]; - - const price = await publicClient.readContract({ - args: [ensNameDirectSubname, durationInYears * SECONDS_PER_YEAR.seconds], - address: nameRegistrationContracts.ETH_REGISTRAR, - functionName: "rentPrice", - abi: ETHRegistrarABI, - }); - if (price) { - return (price as NamePrice).base + (price as NamePrice).premium; - } else { - throw new Error("Error getting name price"); - } -}; - -interface GetGasPriceParams { - publicClient: PublicClient & ClientWithEns; -} - -export const getGasPrice = async ({ - publicClient, -}: GetGasPriceParams): Promise => { - return publicClient - .getGasPrice() - .then((gasPrice) => { - return gasPrice; - }) - .catch((error) => { - return error; - }); -}; - -export const getNameRegistrationGasEstimate = (): bigint => { - return 47606n + 324230n; -}; - -interface IsNameAvailableParams { - ensName: string; - publicClient: PublicClient & ClientWithEns; -} - -export const isNameAvailable = async ({ - ensName, - publicClient, -}: IsNameAvailableParams): Promise => { - const result = await getAvailable(publicClient, { name: ensName }); - - return result; -}; diff --git a/ens-sdk/errorHandling.ts b/ens-sdk/errorHandling.ts new file mode 100644 index 0000000..ce15a39 --- /dev/null +++ b/ens-sdk/errorHandling.ts @@ -0,0 +1,16 @@ +import { BaseError, RawContractError } from "viem"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +// Error handling ⬇️ + +export function getRevertErrorData(err: unknown) { + if (!(err instanceof BaseError)) return undefined; + const error = err.walk() as RawContractError; + return error?.data as { errorName: string; args: unknown[] }; +} diff --git a/ens-sdk/index.ts b/ens-sdk/index.ts index a33e930..cdb2344 100644 --- a/ens-sdk/index.ts +++ b/ens-sdk/index.ts @@ -1,2 +1,5 @@ -export * from "./ensManager"; -export * from "./create-subdomain/service"; +export * from "./createSubdomain"; +export * from "./ensFunctions"; +export * from "./errorHandling"; +export * from "./types"; +export * from "./utils"; diff --git a/ens-sdk/types.ts b/ens-sdk/types.ts new file mode 100644 index 0000000..c10b3d1 --- /dev/null +++ b/ens-sdk/types.ts @@ -0,0 +1,4 @@ +import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; +import { PublicClient } from "viem"; + +export type EnsPublicClient = PublicClient & ClientWithEns; diff --git a/ens-sdk/utils.ts b/ens-sdk/utils.ts new file mode 100644 index 0000000..6d7a9b0 --- /dev/null +++ b/ens-sdk/utils.ts @@ -0,0 +1,134 @@ +/* eslint-disable import/no-named-as-default */ +/* eslint-disable import/named */ +import ETHRegistrarABI from "@/lib/abi/eth-registrar.json"; +import { nameRegistrationSmartContracts } from "../lib/name-registration/constants"; +import * as chains from "viem/chains"; + +import { defineChain, namehash } from "viem"; +import { SupportedNetwork } from "../lib/wallet/chains"; +import { SECONDS_PER_YEAR, ENSName } from "@namehash/ens-utils"; + +import { getAvailable } from "@ensdomains/ensjs/public"; +import { EnsPublicClient } from "./types"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +// Utils ⬇️ + +export const createNameRegistrationSecret = (): string => { + const platformHex = namehash("blockful-ens-external-resolver").slice(2, 10); + const platformBytes = platformHex.length; + const randomHex = [...Array(64 - platformBytes)] + .map(() => Math.floor(Math.random() * 16).toString(16)) + .join(""); + + return "0x" + platformHex + randomHex; +}; + +interface NamePrice { + base: bigint; + premium: bigint; +} + +interface GetNamePriceParams { + ensName: ENSName; + durationInYears: bigint; + publicClient: EnsPublicClient; +} + +export const getNamePrice = async ({ + ensName, + durationInYears, + publicClient, +}: GetNamePriceParams): Promise => { + const ensNameDirectSubname = ensName.name.split(".eth")[0]; + + const chain = publicClient.chain; + + if (!Object.values(SupportedNetwork).includes(chain.id)) { + throw new Error(`Unsupported network: ${chain.id}`); + } + + const nameRegistrationContracts = + nameRegistrationSmartContracts[chain.id as SupportedNetwork]; + + const price = await publicClient.readContract({ + args: [ensNameDirectSubname, durationInYears * SECONDS_PER_YEAR.seconds], + address: nameRegistrationContracts.ETH_REGISTRAR, + functionName: "rentPrice", + abi: ETHRegistrarABI, + }); + if (price) { + return (price as NamePrice).base + (price as NamePrice).premium; + } else { + throw new Error("Error getting name price"); + } +}; + +interface GetGasPriceParams { + publicClient: EnsPublicClient; +} + +export const getGasPrice = async ({ + publicClient, +}: GetGasPriceParams): Promise => { + return publicClient + .getGasPrice() + .then((gasPrice) => { + return gasPrice; + }) + .catch((error) => { + return error; + }); +}; + +export const getNameRegistrationGasEstimate = (): bigint => { + return 47606n + 324230n; +}; + +interface IsNameAvailableParams { + ensName: string; + publicClient: EnsPublicClient; +} + +export const isNameAvailable = async ({ + ensName, + publicClient, +}: IsNameAvailableParams): Promise => { + const result = await getAvailable(publicClient, { name: ensName }); + + return result; +}; + +export function getChain(chainId: number) { + return [ + ...Object.values(chains), + defineChain({ + id: Number(chainId), + name: "Arbitrum Local", + nativeCurrency: { + name: "Arbitrum Sepolia Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: { + default: { + http: [ + `https://arb-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_TESTNET_KEY}`, + ], + }, + }, + }), + ].find((chain) => chain.id === chainId); +} + +// gather the first part of the domain (e.g. floripa.blockful.eth -> floripa, floripa.normal.blockful.eth -> floripa.normal) +const extractLabelFromName = (name: string): string => { + const [, label] = /^(.+?)\.\w+\.\w+$/.exec(name) || []; + return label; +}; From 39779a1f421beba0a5c5d2d26e455d17932f9662 Mon Sep 17 00:00:00 2001 From: eduramme Date: Thu, 28 Nov 2024 16:15:17 -0300 Subject: [PATCH 4/9] refactor: improve naming conventions --- ens-sdk/createSubdomain.ts | 4 ++-- ens-sdk/ensFunctions.ts | 46 ++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/ens-sdk/createSubdomain.ts b/ens-sdk/createSubdomain.ts index c2b5d0e..63ee88c 100644 --- a/ens-sdk/createSubdomain.ts +++ b/ens-sdk/createSubdomain.ts @@ -15,7 +15,7 @@ import { stringToHex, toHex, } from "viem"; -import { handleDBStorage } from "@/ens-sdk"; +import { storeDataInDb } from "@/ens-sdk"; import { DomainData, MessageData } from "@/lib/utils/types"; import L1ResolverABI from "../lib/abi/arbitrum-resolver.json"; import toast from "react-hot-toast"; @@ -160,7 +160,7 @@ export const createSubdomain = async ({ MessageData, ]; - const response = await handleDBStorage({ + const response = await storeDataInDb({ domain, url, message, diff --git a/ens-sdk/ensFunctions.ts b/ens-sdk/ensFunctions.ts index f38bf22..6eae8f8 100644 --- a/ens-sdk/ensFunctions.ts +++ b/ens-sdk/ensFunctions.ts @@ -15,8 +15,6 @@ import { encodeFunctionData, createWalletClient, custom, - BaseError, - RawContractError, Hash, fromBytes, Address, @@ -135,7 +133,7 @@ export async function makeCommitment({ }); } -export async function ccipRequest({ +export async function sendCcipRequest({ body, url, }: CcipRequestParameters): Promise { @@ -150,7 +148,7 @@ export async function ccipRequest({ }); } -interface HandleDBStorageParams { +interface HandleDbStorageParams { domain: DomainData; url: string; message: MessageData; @@ -158,13 +156,13 @@ interface HandleDBStorageParams { chain: Chain; } -export async function handleDBStorage({ +export async function storeDataInDb({ domain, url, message, authenticatedAddress, chain, -}: HandleDBStorageParams): Promise { +}: HandleDbStorageParams): Promise { const client = createWalletClient({ account: authenticatedAddress, chain: chain, @@ -184,7 +182,7 @@ export async function handleDBStorage({ primaryType: "Message", }); - return await ccipRequest({ + return await sendCcipRequest({ body: { data: message.callData, signature: { message, domain, signature }, @@ -348,7 +346,7 @@ export const register = async ({ MessageData, ]; - const signedData = await handleDBStorage({ + const signedData = await storeDataInDb({ domain, url, message, @@ -465,8 +463,8 @@ export const setDomainRecords = async ({ account: authenticatedAddress, address: localResolverAddress, }); - } catch (err) { - const data = getRevertErrorData(err); + } catch (error) { + const data = getRevertErrorData(error); if (data?.errorName === "StorageHandledByOffChainDatabase") { const [domain, url, message] = data.args as [ DomainData, @@ -475,7 +473,7 @@ export const setDomainRecords = async ({ ]; try { - await handleDBStorage({ + await storeDataInDb({ domain, url, message, @@ -485,8 +483,8 @@ export const setDomainRecords = async ({ return 200; } catch (error) { - console.error("writing failed: ", { err }); - const errorType = getBlockchainTransactionError(err); + console.error("writing failed: ", { error }); + const errorType = getBlockchainTransactionError(error); return errorType; } } else if (data?.errorName === "StorageHandledByL2") { @@ -523,8 +521,8 @@ export const setDomainRecords = async ({ return 200; } else { - console.error("writing failed: ", { err }); - const errorType = getBlockchainTransactionError(err); + console.error("writing failed: ", { error }); + const errorType = getBlockchainTransactionError(error); return errorType; } } @@ -551,6 +549,10 @@ export const setDomainAsPrimaryName = async ({ chain, }: SetDomainAsPrimaryNameParams) => { try { + if (!Object.values(SupportedNetwork).includes(chain.id)) { + throw new Error(`Unsupported network: ${chain.id}`); + } + // Create a wallet client for sending transactions to the blockchain const walletClient = createWalletClient({ chain: chain, @@ -566,10 +568,6 @@ export const setDomainAsPrimaryName = async ({ ? ensName.name : `${ensName.name}.eth`; - if (!Object.values(SupportedNetwork).includes(chain.id)) { - throw new Error(`Unsupported network: ${chain.id}`); - } - const network = chain.id as SupportedNetwork; const publicAddress = normalize(nameWithTLD); @@ -582,14 +580,14 @@ export const setDomainAsPrimaryName = async ({ args: [publicAddress], }); - const setAsPrimaryNameRes = await client.writeContract(request); + const setAsPrimaryNameResult = await client.writeContract(request); - if (!!setAsPrimaryNameRes) { + if (!!setAsPrimaryNameResult) { return 200; } - } catch (err) { - console.error("writing failed: ", { err }); - const errorType = getBlockchainTransactionError(err); + } catch (error) { + console.error("writing failed: ", { error }); + const errorType = getBlockchainTransactionError(error); return errorType; } }; From 76304572e9feb13bad962b3aa2093286c86a669a Mon Sep 17 00:00:00 2001 From: eduramme Date: Thu, 28 Nov 2024 16:24:43 -0300 Subject: [PATCH 5/9] refactor code --- ens-sdk/createSubdomain.ts | 7 +-- ens-sdk/ensFunctions.ts | 102 ++++++++++++++++++++++++++++++++----- ens-sdk/errorHandling.ts | 13 ++++- ens-sdk/types.ts | 6 +++ ens-sdk/utils.ts | 12 +---- 5 files changed, 109 insertions(+), 31 deletions(-) diff --git a/ens-sdk/createSubdomain.ts b/ens-sdk/createSubdomain.ts index 63ee88c..0204c52 100644 --- a/ens-sdk/createSubdomain.ts +++ b/ens-sdk/createSubdomain.ts @@ -3,7 +3,6 @@ import { Chain, createWalletClient, custom, - defineChain, encodeFunctionData, fromBytes, Hash, @@ -11,16 +10,14 @@ import { keccak256, namehash, publicActions, - PublicClient, stringToHex, toHex, } from "viem"; -import { storeDataInDb } from "@/ens-sdk"; +import { EnsPublicClient, storeDataInDb } from "@/ens-sdk"; import { DomainData, MessageData } from "@/lib/utils/types"; import L1ResolverABI from "../lib/abi/arbitrum-resolver.json"; import toast from "react-hot-toast"; import { getCoderByCoinName } from "@ensdomains/address-encoder"; -import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; import * as chains from "viem/chains"; import { packetToBytes } from "viem/ens"; import { SECONDS_PER_YEAR } from "@namehash/ens-utils"; @@ -36,7 +33,7 @@ interface CreateSubdomainArgs { address: string; website: string; description: string; - client: PublicClient & ClientWithEns; + client: EnsPublicClient; chain: Chain; } diff --git a/ens-sdk/ensFunctions.ts b/ens-sdk/ensFunctions.ts index 6eae8f8..d7ef760 100644 --- a/ens-sdk/ensFunctions.ts +++ b/ens-sdk/ensFunctions.ts @@ -48,6 +48,7 @@ import toast from "react-hot-toast"; import { sepolia } from "viem/chains"; import { getChain, getNamePrice } from "./utils"; import { getRevertErrorData } from "./errorHandling"; +import { EnsPublicClient } from "./types"; const walletConnectProjectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; @@ -57,24 +58,23 @@ if (!walletConnectProjectId) { } /* - create subdmains - create domains with records - with a different resolver - set domain as primary name - set text records - set addresses - set abi / contenthash + This file contains functions for ENS operations: + - Creating subdomains + - Creating domains with records (using a different resolver) + - Setting a domain as the primary name + - Setting text records + - Setting addresses + - Setting ABI / contenthash */ /* - commitment value is used in both 'commit' and 'register' - functions in the registrar contract. It works as a secret + The commitment value is used in both 'commit' and 'register' + functions in the registrar contract. It acts as a secret to prevent frontrunning attacks. The commitment value must - be the same in both functions calls, this is why we store - it in the local storage. + be the same in both function calls, which is why we store + it in local storage. */ -type EnsPublicClient = PublicClient & ClientWithEns; - interface MakeCommitmentParams { name: string; data: string[]; @@ -87,6 +87,16 @@ interface MakeCommitmentParams { publicClient: EnsPublicClient; } +/** + * Generates a commitment for ENS name registration + * + * This function creates a commitment hash that is used in the two-step ENS registration process. + * It helps prevent front-running attacks by keeping the details of the registration secret until + * the actual registration takes place. + * + * @param {MakeCommitmentParams} params - The parameters required for making the commitment + * @returns {Promise} - The generated commitment hash or an error + */ export async function makeCommitment({ name, data, @@ -133,6 +143,15 @@ export async function makeCommitment({ }); } +/** + * Sends a CCIP (Cross-Chain Interoperability Protocol) request + * + * This function is used to send off-chain requests, typically for resolving ENS names + * or storing data related to ENS operations. + * + * @param {CcipRequestParameters} params - The parameters for the CCIP request + * @returns {Promise} - The response from the CCIP request + */ export async function sendCcipRequest({ body, url, @@ -156,6 +175,15 @@ interface HandleDbStorageParams { chain: Chain; } +/** + * Stores ENS-related data in an off-chain database + * + * This function is used when certain ENS operations require off-chain storage. + * It signs the data with the user's wallet and sends it to a specified URL for storage. + * + * @param {HandleDbStorageParams} params - The parameters for storing data + * @returns {Promise} - The response from the storage request + */ export async function storeDataInDb({ domain, url, @@ -206,6 +234,16 @@ interface CommitParams { chain: Chain; } +/** + * Commits to registering an ENS name + * + * This is the first step in the two-step ENS registration process. It creates and submits + * a commitment to the blockchain, which must be followed by the actual registration after + * a waiting period. + * + * @param {CommitParams} params - The parameters for the commit operation + * @returns {Promise<`0x${string}` | TransactionErrorType>} - The transaction hash or an error + */ export const commit = async ({ ensName, durationInYears, @@ -280,6 +318,15 @@ interface RegisterParams { chain: Chain; } +/** + * Registers an ENS name + * + * This is the second step in the two-step ENS registration process. It finalizes the registration + * of the ENS name after the commitment has been made and the waiting period has passed. + * + * @param {RegisterParams} params - The parameters for the registration + * @returns {Promise<`0x${string}` | TransactionErrorType>} - The transaction hash or an error + */ export const register = async ({ ensName, resolverAddress, @@ -383,6 +430,15 @@ interface SetDomainRecordsParams { chain: Chain; } +/** + * Sets various records for an ENS domain + * + * This function allows setting text records, addresses, and other records for an ENS domain. + * It can handle both on-chain and off-chain storage depending on the resolver configuration. + * + * @param {SetDomainRecordsParams} params - The parameters for setting domain records + * @returns {Promise} - A status code or an error + */ export const setDomainRecords = async ({ ensName, resolverAddress, @@ -397,9 +453,10 @@ export const setDomainRecords = async ({ try { const publicAddress = normalize(ensName.name); - // duplicated function logic on service.ts - createSubdomain + // Prepare calls for setting various records const calls: Hash[] = []; + // Add calls for text records for (let i = 0; i < Object.keys(textRecords).length; i++) { const key = Object.keys(textRecords)[i]; const value = textRecords[key]; @@ -415,6 +472,7 @@ export const setDomainRecords = async ({ } } + // Add calls for address records for (let i = 0; i < Object.keys(addresses).length; i++) { const [cryptocurrencyName, address] = Object.entries(addresses)[i]; if (supportedCoinTypes.includes(cryptocurrencyName.toUpperCase())) { @@ -435,12 +493,13 @@ export const setDomainRecords = async ({ calls.push(callData); } + // Add calls for other records (e.g., contenthash) for (let i = 0; i < Object.keys(others).length; i++) { const [key, value] = Object.entries(others)[i]; const callData = encodeFunctionData({ functionName: "setContenthash", abi: L1ResolverABI, - args: [namehash(publicAddress), stringToHex(value)], // vallue = url + args: [namehash(publicAddress), stringToHex(value)], // value = url }); calls.push(callData); } @@ -456,6 +515,7 @@ export const setDomainRecords = async ({ throw new Error("No domain resolver informed"); } + // Simulate the multicall contract interaction await client.simulateContract({ functionName: "multicall", abi: L1ResolverABI, @@ -466,6 +526,7 @@ export const setDomainRecords = async ({ } catch (error) { const data = getRevertErrorData(error); if (data?.errorName === "StorageHandledByOffChainDatabase") { + // Handle off-chain storage const [domain, url, message] = data.args as [ DomainData, string, @@ -488,6 +549,7 @@ export const setDomainRecords = async ({ return errorType; } } else if (data?.errorName === "StorageHandledByL2") { + // Handle L2 storage const [chainId, contractAddress] = data.args as [bigint, `0x${string}`]; const selectedChain = getChain(Number(chainId)); @@ -543,6 +605,16 @@ interface SetDomainAsPrimaryNameParams { chain: Chain; } +/** + * Sets an ENS domain as the primary name for an Ethereum address + * + * This function allows a user to set their newly registered or existing ENS domain + * as the primary name associated with their Ethereum address. This is typically done + * after registering a new ENS name or when changing the primary name. + * + * @param {SetDomainAsPrimaryNameParams} params - The parameters for setting the primary name + * @returns {Promise} - A status code or an error + */ export const setDomainAsPrimaryName = async ({ authenticatedAddress, ensName, @@ -572,6 +644,7 @@ export const setDomainAsPrimaryName = async ({ const publicAddress = normalize(nameWithTLD); + // Simulate the setName contract interaction const { request } = await client.simulateContract({ address: nameRegistrationSmartContracts[network].ENS_REVERSE_REGISTRAR, account: authenticatedAddress, @@ -580,6 +653,7 @@ export const setDomainAsPrimaryName = async ({ args: [publicAddress], }); + // Execute the setName transaction const setAsPrimaryNameResult = await client.writeContract(request); if (!!setAsPrimaryNameResult) { diff --git a/ens-sdk/errorHandling.ts b/ens-sdk/errorHandling.ts index ce15a39..fa6d884 100644 --- a/ens-sdk/errorHandling.ts +++ b/ens-sdk/errorHandling.ts @@ -1,5 +1,6 @@ import { BaseError, RawContractError } from "viem"; +// Validate required environment variable is present const walletConnectProjectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; @@ -7,8 +8,16 @@ if (!walletConnectProjectId) { throw new Error("No wallet connect project ID informed"); } -// Error handling ⬇️ - +/** + * Extracts revert error data from a contract error + * + * This function takes an unknown error and attempts to extract structured revert data + * if it originated from a contract call. It walks the error chain to find the root + * cause and returns the error name and arguments if available. + * + * @param err - The error to extract data from + * @returns The error data containing errorName and args if available, undefined otherwise + */ export function getRevertErrorData(err: unknown) { if (!(err instanceof BaseError)) return undefined; const error = err.walk() as RawContractError; diff --git a/ens-sdk/types.ts b/ens-sdk/types.ts index c10b3d1..49ba5f1 100644 --- a/ens-sdk/types.ts +++ b/ens-sdk/types.ts @@ -1,4 +1,10 @@ import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; +import { ENSName } from "@namehash/ens-utils"; import { PublicClient } from "viem"; export type EnsPublicClient = PublicClient & ClientWithEns; + +export interface NamePrice { + base: bigint; + premium: bigint; +} diff --git a/ens-sdk/utils.ts b/ens-sdk/utils.ts index 6d7a9b0..9765da6 100644 --- a/ens-sdk/utils.ts +++ b/ens-sdk/utils.ts @@ -6,10 +6,10 @@ import * as chains from "viem/chains"; import { defineChain, namehash } from "viem"; import { SupportedNetwork } from "../lib/wallet/chains"; -import { SECONDS_PER_YEAR, ENSName } from "@namehash/ens-utils"; +import { ENSName, SECONDS_PER_YEAR } from "@namehash/ens-utils"; import { getAvailable } from "@ensdomains/ensjs/public"; -import { EnsPublicClient } from "./types"; +import { EnsPublicClient, NamePrice } from "./types"; const walletConnectProjectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; @@ -18,8 +18,6 @@ if (!walletConnectProjectId) { throw new Error("No wallet connect project ID informed"); } -// Utils ⬇️ - export const createNameRegistrationSecret = (): string => { const platformHex = namehash("blockful-ens-external-resolver").slice(2, 10); const platformBytes = platformHex.length; @@ -29,12 +27,6 @@ export const createNameRegistrationSecret = (): string => { return "0x" + platformHex + randomHex; }; - -interface NamePrice { - base: bigint; - premium: bigint; -} - interface GetNamePriceParams { ensName: ENSName; durationInYears: bigint; From 1574567de95474109fe8ca06c0153b5b18e88c86 Mon Sep 17 00:00:00 2001 From: eduramme Date: Thu, 28 Nov 2024 16:25:49 -0300 Subject: [PATCH 6/9] remove unused import --- ens-sdk/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ens-sdk/types.ts b/ens-sdk/types.ts index 49ba5f1..04f6eb2 100644 --- a/ens-sdk/types.ts +++ b/ens-sdk/types.ts @@ -1,5 +1,4 @@ import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; -import { ENSName } from "@namehash/ens-utils"; import { PublicClient } from "viem"; export type EnsPublicClient = PublicClient & ClientWithEns; From b8ee9899ac68b01e0d1c76eb0f87738d93992ab4 Mon Sep 17 00:00:00 2001 From: eduramme Date: Thu, 28 Nov 2024 16:26:00 -0300 Subject: [PATCH 7/9] export function --- ens-sdk/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ens-sdk/utils.ts b/ens-sdk/utils.ts index 9765da6..a6a2613 100644 --- a/ens-sdk/utils.ts +++ b/ens-sdk/utils.ts @@ -120,7 +120,7 @@ export function getChain(chainId: number) { } // gather the first part of the domain (e.g. floripa.blockful.eth -> floripa, floripa.normal.blockful.eth -> floripa.normal) -const extractLabelFromName = (name: string): string => { +export const extractLabelFromName = (name: string): string => { const [, label] = /^(.+?)\.\w+\.\w+$/.exec(name) || []; return label; }; From a5360cf7ba63a72bf363d73167faf97d026a7ec7 Mon Sep 17 00:00:00 2001 From: eduramme Date: Thu, 28 Nov 2024 16:41:14 -0300 Subject: [PATCH 8/9] create utils folder --- ens-sdk/ensFunctions.ts | 2 +- ens-sdk/utils.ts | 126 ------------------ ens-sdk/utils/createNameRegistrationSecret.ts | 18 +++ ens-sdk/utils/extractLabelFromName.ts | 5 + ens-sdk/utils/getChain.ts | 32 +++++ ens-sdk/utils/getGasPrice.ts | 25 ++++ ens-sdk/utils/getNamePrice.ts | 48 +++++++ .../utils/getNameRegistrationGasEstimate.ts | 3 + ens-sdk/utils/index.ts | 7 + ens-sdk/utils/isNameAvailable.ts | 23 ++++ 10 files changed, 162 insertions(+), 127 deletions(-) delete mode 100644 ens-sdk/utils.ts create mode 100644 ens-sdk/utils/createNameRegistrationSecret.ts create mode 100644 ens-sdk/utils/extractLabelFromName.ts create mode 100644 ens-sdk/utils/getChain.ts create mode 100644 ens-sdk/utils/getGasPrice.ts create mode 100644 ens-sdk/utils/getNamePrice.ts create mode 100644 ens-sdk/utils/getNameRegistrationGasEstimate.ts create mode 100644 ens-sdk/utils/index.ts create mode 100644 ens-sdk/utils/isNameAvailable.ts diff --git a/ens-sdk/ensFunctions.ts b/ens-sdk/ensFunctions.ts index d7ef760..58fa08c 100644 --- a/ens-sdk/ensFunctions.ts +++ b/ens-sdk/ensFunctions.ts @@ -46,7 +46,7 @@ import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; import { getAvailable } from "@ensdomains/ensjs/public"; import toast from "react-hot-toast"; import { sepolia } from "viem/chains"; -import { getChain, getNamePrice } from "./utils"; +import { getNamePrice, getChain } from "./utils"; import { getRevertErrorData } from "./errorHandling"; import { EnsPublicClient } from "./types"; diff --git a/ens-sdk/utils.ts b/ens-sdk/utils.ts deleted file mode 100644 index a6a2613..0000000 --- a/ens-sdk/utils.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* eslint-disable import/no-named-as-default */ -/* eslint-disable import/named */ -import ETHRegistrarABI from "@/lib/abi/eth-registrar.json"; -import { nameRegistrationSmartContracts } from "../lib/name-registration/constants"; -import * as chains from "viem/chains"; - -import { defineChain, namehash } from "viem"; -import { SupportedNetwork } from "../lib/wallet/chains"; -import { ENSName, SECONDS_PER_YEAR } from "@namehash/ens-utils"; - -import { getAvailable } from "@ensdomains/ensjs/public"; -import { EnsPublicClient, NamePrice } from "./types"; - -const walletConnectProjectId = - process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; - -if (!walletConnectProjectId) { - throw new Error("No wallet connect project ID informed"); -} - -export const createNameRegistrationSecret = (): string => { - const platformHex = namehash("blockful-ens-external-resolver").slice(2, 10); - const platformBytes = platformHex.length; - const randomHex = [...Array(64 - platformBytes)] - .map(() => Math.floor(Math.random() * 16).toString(16)) - .join(""); - - return "0x" + platformHex + randomHex; -}; -interface GetNamePriceParams { - ensName: ENSName; - durationInYears: bigint; - publicClient: EnsPublicClient; -} - -export const getNamePrice = async ({ - ensName, - durationInYears, - publicClient, -}: GetNamePriceParams): Promise => { - const ensNameDirectSubname = ensName.name.split(".eth")[0]; - - const chain = publicClient.chain; - - if (!Object.values(SupportedNetwork).includes(chain.id)) { - throw new Error(`Unsupported network: ${chain.id}`); - } - - const nameRegistrationContracts = - nameRegistrationSmartContracts[chain.id as SupportedNetwork]; - - const price = await publicClient.readContract({ - args: [ensNameDirectSubname, durationInYears * SECONDS_PER_YEAR.seconds], - address: nameRegistrationContracts.ETH_REGISTRAR, - functionName: "rentPrice", - abi: ETHRegistrarABI, - }); - if (price) { - return (price as NamePrice).base + (price as NamePrice).premium; - } else { - throw new Error("Error getting name price"); - } -}; - -interface GetGasPriceParams { - publicClient: EnsPublicClient; -} - -export const getGasPrice = async ({ - publicClient, -}: GetGasPriceParams): Promise => { - return publicClient - .getGasPrice() - .then((gasPrice) => { - return gasPrice; - }) - .catch((error) => { - return error; - }); -}; - -export const getNameRegistrationGasEstimate = (): bigint => { - return 47606n + 324230n; -}; - -interface IsNameAvailableParams { - ensName: string; - publicClient: EnsPublicClient; -} - -export const isNameAvailable = async ({ - ensName, - publicClient, -}: IsNameAvailableParams): Promise => { - const result = await getAvailable(publicClient, { name: ensName }); - - return result; -}; - -export function getChain(chainId: number) { - return [ - ...Object.values(chains), - defineChain({ - id: Number(chainId), - name: "Arbitrum Local", - nativeCurrency: { - name: "Arbitrum Sepolia Ether", - symbol: "ETH", - decimals: 18, - }, - rpcUrls: { - default: { - http: [ - `https://arb-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_TESTNET_KEY}`, - ], - }, - }, - }), - ].find((chain) => chain.id === chainId); -} - -// gather the first part of the domain (e.g. floripa.blockful.eth -> floripa, floripa.normal.blockful.eth -> floripa.normal) -export const extractLabelFromName = (name: string): string => { - const [, label] = /^(.+?)\.\w+\.\w+$/.exec(name) || []; - return label; -}; diff --git a/ens-sdk/utils/createNameRegistrationSecret.ts b/ens-sdk/utils/createNameRegistrationSecret.ts new file mode 100644 index 0000000..0dffcad --- /dev/null +++ b/ens-sdk/utils/createNameRegistrationSecret.ts @@ -0,0 +1,18 @@ +import { namehash } from "viem"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +export const createNameRegistrationSecret = (): string => { + const platformHex = namehash("blockful-ens-external-resolver").slice(2, 10); + const platformBytes = platformHex.length; + const randomHex = [...Array(64 - platformBytes)] + .map(() => Math.floor(Math.random() * 16).toString(16)) + .join(""); + + return "0x" + platformHex + randomHex; +}; diff --git a/ens-sdk/utils/extractLabelFromName.ts b/ens-sdk/utils/extractLabelFromName.ts new file mode 100644 index 0000000..bfbbd12 --- /dev/null +++ b/ens-sdk/utils/extractLabelFromName.ts @@ -0,0 +1,5 @@ +// gather the first part of the domain (e.g. floripa.blockful.eth -> floripa, floripa.normal.blockful.eth -> floripa.normal) +export const extractLabelFromName = (name: string): string => { + const [, label] = /^(.+?)\.\w+\.\w+$/.exec(name) || []; + return label; +}; diff --git a/ens-sdk/utils/getChain.ts b/ens-sdk/utils/getChain.ts new file mode 100644 index 0000000..a087c85 --- /dev/null +++ b/ens-sdk/utils/getChain.ts @@ -0,0 +1,32 @@ +import * as chains from "viem/chains"; + +import { defineChain } from "viem"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +export function getChain(chainId: number) { + return [ + ...Object.values(chains), + defineChain({ + id: Number(chainId), + name: "Arbitrum Local", + nativeCurrency: { + name: "Arbitrum Sepolia Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: { + default: { + http: [ + `https://arb-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_TESTNET_KEY}`, + ], + }, + }, + }), + ].find((chain) => chain.id === chainId); +} diff --git a/ens-sdk/utils/getGasPrice.ts b/ens-sdk/utils/getGasPrice.ts new file mode 100644 index 0000000..6fc923e --- /dev/null +++ b/ens-sdk/utils/getGasPrice.ts @@ -0,0 +1,25 @@ +import { EnsPublicClient } from "../types"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +interface GetGasPriceParams { + publicClient: EnsPublicClient; +} + +export const getGasPrice = async ({ + publicClient, +}: GetGasPriceParams): Promise => { + return publicClient + .getGasPrice() + .then((gasPrice) => { + return gasPrice; + }) + .catch((error) => { + return error; + }); +}; diff --git a/ens-sdk/utils/getNamePrice.ts b/ens-sdk/utils/getNamePrice.ts new file mode 100644 index 0000000..c2ccac6 --- /dev/null +++ b/ens-sdk/utils/getNamePrice.ts @@ -0,0 +1,48 @@ +import ETHRegistrarABI from "@/lib/abi/eth-registrar.json"; +import { nameRegistrationSmartContracts } from "../../lib/name-registration/constants"; +import { SupportedNetwork } from "../../lib/wallet/chains"; +import { ENSName, SECONDS_PER_YEAR } from "@namehash/ens-utils"; + +import { EnsPublicClient, NamePrice } from "../types"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +interface GetNamePriceParams { + ensName: ENSName; + durationInYears: bigint; + publicClient: EnsPublicClient; +} + +export const getNamePrice = async ({ + ensName, + durationInYears, + publicClient, +}: GetNamePriceParams): Promise => { + const ensNameDirectSubname = ensName.name.split(".eth")[0]; + + const chain = publicClient.chain; + + if (!Object.values(SupportedNetwork).includes(chain.id)) { + throw new Error(`Unsupported network: ${chain.id}`); + } + + const nameRegistrationContracts = + nameRegistrationSmartContracts[chain.id as SupportedNetwork]; + + const price = await publicClient.readContract({ + args: [ensNameDirectSubname, durationInYears * SECONDS_PER_YEAR.seconds], + address: nameRegistrationContracts.ETH_REGISTRAR, + functionName: "rentPrice", + abi: ETHRegistrarABI, + }); + if (price) { + return (price as NamePrice).base + (price as NamePrice).premium; + } else { + throw new Error("Error getting name price"); + } +}; diff --git a/ens-sdk/utils/getNameRegistrationGasEstimate.ts b/ens-sdk/utils/getNameRegistrationGasEstimate.ts new file mode 100644 index 0000000..36190b8 --- /dev/null +++ b/ens-sdk/utils/getNameRegistrationGasEstimate.ts @@ -0,0 +1,3 @@ +export const getNameRegistrationGasEstimate = (): bigint => { + return 47606n + 324230n; +}; diff --git a/ens-sdk/utils/index.ts b/ens-sdk/utils/index.ts new file mode 100644 index 0000000..f9568ae --- /dev/null +++ b/ens-sdk/utils/index.ts @@ -0,0 +1,7 @@ +export * from "./getChain"; +export * from "./extractLabelFromName"; +export * from "./getNamePrice"; +export * from "./getGasPrice"; +export * from "./createNameRegistrationSecret"; +export * from "./getNameRegistrationGasEstimate"; +export * from "./isNameAvailable"; diff --git a/ens-sdk/utils/isNameAvailable.ts b/ens-sdk/utils/isNameAvailable.ts new file mode 100644 index 0000000..2f9e51e --- /dev/null +++ b/ens-sdk/utils/isNameAvailable.ts @@ -0,0 +1,23 @@ +import { getAvailable } from "@ensdomains/ensjs/public"; +import { EnsPublicClient } from "../types"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +interface IsNameAvailableParams { + ensName: string; + publicClient: EnsPublicClient; +} + +export const isNameAvailable = async ({ + ensName, + publicClient, +}: IsNameAvailableParams): Promise => { + const result = await getAvailable(publicClient, { name: ensName }); + + return result; +}; From 4ec03b0f1db44853c33b11d8eaa4b94e17217f27 Mon Sep 17 00:00:00 2001 From: eduramme Date: Thu, 28 Nov 2024 17:08:05 -0300 Subject: [PATCH 9/9] crete ensOperations folder --- ens-sdk/ensFunctions.ts | 667 ------------------ ens-sdk/ensOperations/commit.ts | 115 +++ .../{ => ensOperations}/createSubdomain.ts | 10 +- ens-sdk/ensOperations/index.ts | 8 + ens-sdk/ensOperations/makeCommitment.ts | 95 +++ ens-sdk/ensOperations/register.ts | 144 ++++ ens-sdk/ensOperations/sendCcipRequest.ts | 38 + .../ensOperations/setDomainAsPrimaryName.ts | 86 +++ ens-sdk/ensOperations/setDomainRecords.ts | 220 ++++++ ens-sdk/ensOperations/storeDataInDb.ts | 56 ++ ens-sdk/index.ts | 4 +- .../getRevertErrorData.ts} | 0 ens-sdk/utils/index.ts | 1 + 13 files changed, 769 insertions(+), 675 deletions(-) delete mode 100644 ens-sdk/ensFunctions.ts create mode 100644 ens-sdk/ensOperations/commit.ts rename ens-sdk/{ => ensOperations}/createSubdomain.ts (95%) create mode 100644 ens-sdk/ensOperations/index.ts create mode 100644 ens-sdk/ensOperations/makeCommitment.ts create mode 100644 ens-sdk/ensOperations/register.ts create mode 100644 ens-sdk/ensOperations/sendCcipRequest.ts create mode 100644 ens-sdk/ensOperations/setDomainAsPrimaryName.ts create mode 100644 ens-sdk/ensOperations/setDomainRecords.ts create mode 100644 ens-sdk/ensOperations/storeDataInDb.ts rename ens-sdk/{errorHandling.ts => utils/getRevertErrorData.ts} (100%) diff --git a/ens-sdk/ensFunctions.ts b/ens-sdk/ensFunctions.ts deleted file mode 100644 index 58fa08c..0000000 --- a/ens-sdk/ensFunctions.ts +++ /dev/null @@ -1,667 +0,0 @@ -/* eslint-disable import/no-named-as-default */ -/* eslint-disable import/named */ -import ENSReverseRegistrarABI from "@/lib/abi/ens-reverse-registrar.json"; -import ETHRegistrarABI from "@/lib/abi/eth-registrar.json"; -import L1ResolverABI from "../lib/abi/arbitrum-resolver.json"; -import { - DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES, - nameRegistrationSmartContracts, -} from "../lib/name-registration/constants"; - -import { - namehash, - publicActions, - type WalletClient, - encodeFunctionData, - createWalletClient, - custom, - Hash, - fromBytes, - Address, - PublicClient, - Chain, - stringToHex, -} from "viem"; -import { SupportedNetwork } from "../lib/wallet/chains"; -import { SECONDS_PER_YEAR, ENSName } from "@namehash/ens-utils"; -import { - TransactionErrorType, - getBlockchainTransactionError, -} from "../lib/wallet/txError"; -import { getNameRegistrationSecret } from "@/lib/name-registration/localStorage"; -import { parseAccount } from "viem/utils"; -import DomainResolverABI from "../lib/abi/offchain-resolver.json"; -import { normalize } from "viem/ens"; -import { supportedCoinTypes } from "../lib/domain-page"; -import { - coinNameToTypeMap, - getCoderByCoinName, -} from "@ensdomains/address-encoder"; -import { - CcipRequestParameters, - DomainData, - MessageData, -} from "../lib/utils/types"; -import { ClientWithEns } from "@ensdomains/ensjs/dist/types/contracts/consts"; -import { getAvailable } from "@ensdomains/ensjs/public"; -import toast from "react-hot-toast"; -import { sepolia } from "viem/chains"; -import { getNamePrice, getChain } from "./utils"; -import { getRevertErrorData } from "./errorHandling"; -import { EnsPublicClient } from "./types"; - -const walletConnectProjectId = - process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; - -if (!walletConnectProjectId) { - throw new Error("No wallet connect project ID informed"); -} - -/* - This file contains functions for ENS operations: - - Creating subdomains - - Creating domains with records (using a different resolver) - - Setting a domain as the primary name - - Setting text records - - Setting addresses - - Setting ABI / contenthash -*/ - -/* - The commitment value is used in both 'commit' and 'register' - functions in the registrar contract. It acts as a secret - to prevent frontrunning attacks. The commitment value must - be the same in both function calls, which is why we store - it in local storage. -*/ - -interface MakeCommitmentParams { - name: string; - data: string[]; - secret: string; - reverseRecord: boolean; - resolverAddress: Address; - durationInYears: bigint; - ownerControlledFuses: number; - authenticatedAddress: Address; - publicClient: EnsPublicClient; -} - -/** - * Generates a commitment for ENS name registration - * - * This function creates a commitment hash that is used in the two-step ENS registration process. - * It helps prevent front-running attacks by keeping the details of the registration secret until - * the actual registration takes place. - * - * @param {MakeCommitmentParams} params - The parameters required for making the commitment - * @returns {Promise} - The generated commitment hash or an error - */ -export async function makeCommitment({ - name, - data, - secret, - reverseRecord, - resolverAddress, - durationInYears, - ownerControlledFuses, - authenticatedAddress, - publicClient, -}: MakeCommitmentParams) { - const chain = publicClient.chain; - - if (!Object.values(SupportedNetwork).includes(chain.id)) { - throw new Error(`Unsupported network: ${chain.id}`); - } - - const nameRegistrationContracts = - nameRegistrationSmartContracts[chain.id as SupportedNetwork]; - - return publicClient - .readContract({ - account: parseAccount(authenticatedAddress), - address: nameRegistrationContracts.ETH_REGISTRAR, - abi: ETHRegistrarABI, - args: [ - name, - authenticatedAddress, - durationInYears * SECONDS_PER_YEAR.seconds, - secret, - resolverAddress, - data, - reverseRecord, - ownerControlledFuses, - ], - functionName: "makeCommitment", - }) - .then((generatedReservationNumber) => { - return generatedReservationNumber; - }) - .catch((error) => { - const errorType = getBlockchainTransactionError(error); - return errorType || error; - }); -} - -/** - * Sends a CCIP (Cross-Chain Interoperability Protocol) request - * - * This function is used to send off-chain requests, typically for resolving ENS names - * or storing data related to ENS operations. - * - * @param {CcipRequestParameters} params - The parameters for the CCIP request - * @returns {Promise} - The response from the CCIP request - */ -export async function sendCcipRequest({ - body, - url, -}: CcipRequestParameters): Promise { - return fetch(url.replace("/{sender}/{data}.json", ""), { - body: JSON.stringify(body, (_, value) => - typeof value === "bigint" ? value.toString() : value, - ), - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); -} - -interface HandleDbStorageParams { - domain: DomainData; - url: string; - message: MessageData; - authenticatedAddress: `0x${string}`; - chain: Chain; -} - -/** - * Stores ENS-related data in an off-chain database - * - * This function is used when certain ENS operations require off-chain storage. - * It signs the data with the user's wallet and sends it to a specified URL for storage. - * - * @param {HandleDbStorageParams} params - The parameters for storing data - * @returns {Promise} - The response from the storage request - */ -export async function storeDataInDb({ - domain, - url, - message, - authenticatedAddress, - chain, -}: HandleDbStorageParams): Promise { - const client = createWalletClient({ - account: authenticatedAddress, - chain: chain, - transport: custom(window.ethereum), - }); - - const signature = await client.signTypedData({ - domain, - message, - types: { - Message: [ - { name: "callData", type: "bytes" }, - { name: "sender", type: "address" }, - { name: "expirationTimestamp", type: "uint256" }, - ], - }, - primaryType: "Message", - }); - - return await sendCcipRequest({ - body: { - data: message.callData, - signature: { message, domain, signature }, - sender: message.sender, - }, - url, - }); -} - -/* - 1st step of a name registration -*/ - -interface CommitParams { - ensName: ENSName; - durationInYears: bigint; - resolverAddress: Address; - authenticatedAddress: Address; - registerAndSetAsPrimaryName: boolean; - publicClient: EnsPublicClient; - chain: Chain; -} - -/** - * Commits to registering an ENS name - * - * This is the first step in the two-step ENS registration process. It creates and submits - * a commitment to the blockchain, which must be followed by the actual registration after - * a waiting period. - * - * @param {CommitParams} params - The parameters for the commit operation - * @returns {Promise<`0x${string}` | TransactionErrorType>} - The transaction hash or an error - */ -export const commit = async ({ - ensName, - durationInYears, - resolverAddress, - authenticatedAddress, - registerAndSetAsPrimaryName, - publicClient, - chain, -}: CommitParams): Promise<`0x${string}` | TransactionErrorType> => { - try { - const walletClient = createWalletClient({ - account: authenticatedAddress, - chain: chain, - transport: custom(window.ethereum), - }); - - const client = walletClient.extend(publicActions); - - if (!client) throw new Error("WalletClient not found"); - - const nameWithoutTLD = ensName.name.replace(".eth", ""); - - const commitmentWithConfigHash = await makeCommitment({ - name: nameWithoutTLD, - data: [], - authenticatedAddress, - durationInYears: durationInYears, - secret: getNameRegistrationSecret(), - reverseRecord: registerAndSetAsPrimaryName, - resolverAddress: resolverAddress, - ownerControlledFuses: DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES, - publicClient: publicClient, - }); - - if (!Object.values(SupportedNetwork).includes(chain.id)) { - throw new Error(`Unsupported network: ${chain.id}`); - } - - const nameRegistrationContracts = - nameRegistrationSmartContracts[chain.id as SupportedNetwork]; - - const { request } = await client.simulateContract({ - account: parseAccount(authenticatedAddress), - address: nameRegistrationContracts.ETH_REGISTRAR, - args: [commitmentWithConfigHash], - functionName: "commit", - abi: ETHRegistrarABI, - gas: 70000n, - }); - - const txHash = await client.writeContract(request); - - return txHash; - } catch (error) { - console.error(error); - const errorType = getBlockchainTransactionError(error); - return errorType; - } -}; - -/* - 2nd step of a name registration -*/ - -interface RegisterParams { - ensName: ENSName; - resolverAddress: Address; - durationInYears: bigint; - authenticatedAddress: Address; - registerAndSetAsPrimaryName: boolean; - publicClient: EnsPublicClient; - chain: Chain; -} - -/** - * Registers an ENS name - * - * This is the second step in the two-step ENS registration process. It finalizes the registration - * of the ENS name after the commitment has been made and the waiting period has passed. - * - * @param {RegisterParams} params - The parameters for the registration - * @returns {Promise<`0x${string}` | TransactionErrorType>} - The transaction hash or an error - */ -export const register = async ({ - ensName, - resolverAddress, - durationInYears, - authenticatedAddress, - registerAndSetAsPrimaryName, - publicClient, - chain, -}: RegisterParams): Promise<`0x${string}` | TransactionErrorType> => { - try { - const walletClient = createWalletClient({ - account: authenticatedAddress, - chain: chain, - transport: custom(window.ethereum), - }); - - const client = walletClient.extend(publicActions); - - if (!client) throw new Error("WalletClient not found"); - - const nameWithoutTLD = ensName.name.replace(".eth", ""); - - const namePrice = await getNamePrice({ - ensName, - durationInYears, - publicClient, - }); - - if (!Object.values(SupportedNetwork).includes(chain.id)) { - throw new Error(`Unsupported network: ${chain.id}`); - } - - const nameRegistrationContracts = - nameRegistrationSmartContracts[chain.id as SupportedNetwork]; - - const txHash = await client.writeContract({ - address: nameRegistrationContracts.ETH_REGISTRAR, - chain: chain, - account: authenticatedAddress, - args: [ - nameWithoutTLD, - authenticatedAddress, - durationInYears * SECONDS_PER_YEAR.seconds, - getNameRegistrationSecret(), - resolverAddress, - [], - registerAndSetAsPrimaryName, - DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES, - ], - value: namePrice, - abi: ETHRegistrarABI, - functionName: "register", - gas: 500000n, - }); - - return txHash; - } catch (error: unknown) { - const data = getRevertErrorData(error); - - if (data?.errorName === "StorageHandledByOffChainDatabase") { - const [domain, url, message] = data.args as [ - DomainData, - string, - MessageData, - ]; - - const signedData = await storeDataInDb({ - domain, - url, - message, - authenticatedAddress, - chain: chain, - }); - - if (typeof signedData === "string") { - return signedData as `0x${string}`; - } else { - throw new Error("Error handling off-chain storage"); - } - } else { - console.error(error); - const errorType = getBlockchainTransactionError(error); - return errorType; - } - } -}; - -/* - 3rd step of a name registration - set text records -*/ - -interface SetDomainRecordsParams { - ensName: ENSName; - resolverAddress?: Address; - domainResolverAddress?: `0x${string}`; - authenticatedAddress: Address; - textRecords: Record; - addresses: Record; - others: Record; - client: PublicClient & WalletClient; - chain: Chain; -} - -/** - * Sets various records for an ENS domain - * - * This function allows setting text records, addresses, and other records for an ENS domain. - * It can handle both on-chain and off-chain storage depending on the resolver configuration. - * - * @param {SetDomainRecordsParams} params - The parameters for setting domain records - * @returns {Promise} - A status code or an error - */ -export const setDomainRecords = async ({ - ensName, - resolverAddress, - domainResolverAddress, - authenticatedAddress, - textRecords, - addresses, - others, - client, - chain, -}: SetDomainRecordsParams) => { - try { - const publicAddress = normalize(ensName.name); - - // Prepare calls for setting various records - const calls: Hash[] = []; - - // Add calls for text records - for (let i = 0; i < Object.keys(textRecords).length; i++) { - const key = Object.keys(textRecords)[i]; - const value = textRecords[key]; - - if (value !== null && value !== undefined) { - const callData = encodeFunctionData({ - functionName: "setText", - abi: DomainResolverABI, - args: [namehash(publicAddress), key, value], - }); - - calls.push(callData); - } - } - - // Add calls for address records - for (let i = 0; i < Object.keys(addresses).length; i++) { - const [cryptocurrencyName, address] = Object.entries(addresses)[i]; - if (supportedCoinTypes.includes(cryptocurrencyName.toUpperCase())) { - console.error(`cryptocurrency ${cryptocurrencyName} not supported`); - continue; - } - - const coinType = - coinNameToTypeMap[cryptocurrencyName as keyof typeof coinNameToTypeMap]; - - const coder = getCoderByCoinName(cryptocurrencyName.toLocaleLowerCase()); - const addressEncoded = fromBytes(coder.decode(address), "hex"); - const callData = encodeFunctionData({ - functionName: "setAddr", - abi: DomainResolverABI, - args: [namehash(publicAddress), BigInt(coinType), addressEncoded], - }); - calls.push(callData); - } - - // Add calls for other records (e.g., contenthash) - for (let i = 0; i < Object.keys(others).length; i++) { - const [key, value] = Object.entries(others)[i]; - const callData = encodeFunctionData({ - functionName: "setContenthash", - abi: L1ResolverABI, - args: [namehash(publicAddress), stringToHex(value)], // value = url - }); - calls.push(callData); - } - - try { - let localResolverAddress; - - if (resolverAddress) { - localResolverAddress = resolverAddress; - } else if (domainResolverAddress) { - localResolverAddress = domainResolverAddress; - } else { - throw new Error("No domain resolver informed"); - } - - // Simulate the multicall contract interaction - await client.simulateContract({ - functionName: "multicall", - abi: L1ResolverABI, - args: [calls], - account: authenticatedAddress, - address: localResolverAddress, - }); - } catch (error) { - const data = getRevertErrorData(error); - if (data?.errorName === "StorageHandledByOffChainDatabase") { - // Handle off-chain storage - const [domain, url, message] = data.args as [ - DomainData, - string, - MessageData, - ]; - - try { - await storeDataInDb({ - domain, - url, - message, - authenticatedAddress, - chain: chain, - }); - - return 200; - } catch (error) { - console.error("writing failed: ", { error }); - const errorType = getBlockchainTransactionError(error); - return errorType; - } - } else if (data?.errorName === "StorageHandledByL2") { - // Handle L2 storage - const [chainId, contractAddress] = data.args as [bigint, `0x${string}`]; - - const selectedChain = getChain(Number(chainId)); - - if (!selectedChain) { - toast.error("error"); - return; - } - - const clientWithWallet = createWalletClient({ - chain: selectedChain, - transport: custom(window.ethereum), - }).extend(publicActions); - - await clientWithWallet.addChain({ chain: selectedChain }); - - try { - const { request } = await clientWithWallet.simulateContract({ - functionName: "multicall", - abi: L1ResolverABI, - args: [calls], - account: authenticatedAddress, - address: contractAddress, - }); - await clientWithWallet.writeContract(request); - } catch { - await clientWithWallet.switchChain({ id: sepolia.id }); - } - - await clientWithWallet.switchChain({ id: sepolia.id }); - - return 200; - } else { - console.error("writing failed: ", { error }); - const errorType = getBlockchainTransactionError(error); - return errorType; - } - } - } catch (error: unknown) { - console.error(error); - const errorType = getBlockchainTransactionError(error); - return errorType; - } -}; - -/* - 4th step of a name registration - set domain as primary name if user wants -*/ - -interface SetDomainAsPrimaryNameParams { - authenticatedAddress: `0x${string}`; - ensName: ENSName; - chain: Chain; -} - -/** - * Sets an ENS domain as the primary name for an Ethereum address - * - * This function allows a user to set their newly registered or existing ENS domain - * as the primary name associated with their Ethereum address. This is typically done - * after registering a new ENS name or when changing the primary name. - * - * @param {SetDomainAsPrimaryNameParams} params - The parameters for setting the primary name - * @returns {Promise} - A status code or an error - */ -export const setDomainAsPrimaryName = async ({ - authenticatedAddress, - ensName, - chain, -}: SetDomainAsPrimaryNameParams) => { - try { - if (!Object.values(SupportedNetwork).includes(chain.id)) { - throw new Error(`Unsupported network: ${chain.id}`); - } - - // Create a wallet client for sending transactions to the blockchain - const walletClient = createWalletClient({ - chain: chain, - transport: custom(window.ethereum), - account: authenticatedAddress, - }); - - const client = walletClient.extend(publicActions); - - if (!client) throw new Error("WalletClient not found"); - - const nameWithTLD = ensName.name.includes(".eth") - ? ensName.name - : `${ensName.name}.eth`; - - const network = chain.id as SupportedNetwork; - - const publicAddress = normalize(nameWithTLD); - - // Simulate the setName contract interaction - const { request } = await client.simulateContract({ - address: nameRegistrationSmartContracts[network].ENS_REVERSE_REGISTRAR, - account: authenticatedAddress, - abi: ENSReverseRegistrarABI, - functionName: "setName", - args: [publicAddress], - }); - - // Execute the setName transaction - const setAsPrimaryNameResult = await client.writeContract(request); - - if (!!setAsPrimaryNameResult) { - return 200; - } - } catch (error) { - console.error("writing failed: ", { error }); - const errorType = getBlockchainTransactionError(error); - return errorType; - } -}; diff --git a/ens-sdk/ensOperations/commit.ts b/ens-sdk/ensOperations/commit.ts new file mode 100644 index 0000000..fa4156a --- /dev/null +++ b/ens-sdk/ensOperations/commit.ts @@ -0,0 +1,115 @@ +import ETHRegistrarABI from "@/lib/abi/eth-registrar.json"; +import { + DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES, + nameRegistrationSmartContracts, +} from "../../lib/name-registration/constants"; + +import { + publicActions, + createWalletClient, + custom, + Address, + Chain, +} from "viem"; +import { SupportedNetwork } from "../../lib/wallet/chains"; +import { ENSName } from "@namehash/ens-utils"; +import { + TransactionErrorType, + getBlockchainTransactionError, +} from "../../lib/wallet/txError"; +import { getNameRegistrationSecret } from "@/lib/name-registration/localStorage"; +import { parseAccount } from "viem/utils"; + +import { EnsPublicClient } from "../types"; +import { makeCommitment } from "./makeCommitment"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +/* + 1st step of a name registration +*/ + +interface CommitParams { + ensName: ENSName; + durationInYears: bigint; + resolverAddress: Address; + authenticatedAddress: Address; + registerAndSetAsPrimaryName: boolean; + publicClient: EnsPublicClient; + chain: Chain; +} + +/** + * Commits to registering an ENS name + * + * This is the first step in the two-step ENS registration process. It creates and submits + * a commitment to the blockchain, which must be followed by the actual registration after + * a waiting period. + * + * @param {CommitParams} params - The parameters for the commit operation + * @returns {Promise<`0x${string}` | TransactionErrorType>} - The transaction hash or an error + */ +export const commit = async ({ + ensName, + durationInYears, + resolverAddress, + authenticatedAddress, + registerAndSetAsPrimaryName, + publicClient, + chain, +}: CommitParams): Promise<`0x${string}` | TransactionErrorType> => { + try { + const walletClient = createWalletClient({ + account: authenticatedAddress, + chain: chain, + transport: custom(window.ethereum), + }); + + const client = walletClient.extend(publicActions); + + if (!client) throw new Error("WalletClient not found"); + + const nameWithoutTLD = ensName.name.replace(".eth", ""); + + const commitmentWithConfigHash = await makeCommitment({ + name: nameWithoutTLD, + data: [], + authenticatedAddress, + durationInYears: durationInYears, + secret: getNameRegistrationSecret(), + reverseRecord: registerAndSetAsPrimaryName, + resolverAddress: resolverAddress, + ownerControlledFuses: DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES, + publicClient: publicClient, + }); + + if (!Object.values(SupportedNetwork).includes(chain.id)) { + throw new Error(`Unsupported network: ${chain.id}`); + } + + const nameRegistrationContracts = + nameRegistrationSmartContracts[chain.id as SupportedNetwork]; + + const { request } = await client.simulateContract({ + account: parseAccount(authenticatedAddress), + address: nameRegistrationContracts.ETH_REGISTRAR, + args: [commitmentWithConfigHash], + functionName: "commit", + abi: ETHRegistrarABI, + gas: 70000n, + }); + + const txHash = await client.writeContract(request); + + return txHash; + } catch (error) { + console.error(error); + const errorType = getBlockchainTransactionError(error); + return errorType; + } +}; diff --git a/ens-sdk/createSubdomain.ts b/ens-sdk/ensOperations/createSubdomain.ts similarity index 95% rename from ens-sdk/createSubdomain.ts rename to ens-sdk/ensOperations/createSubdomain.ts index 0204c52..9ea15ec 100644 --- a/ens-sdk/createSubdomain.ts +++ b/ens-sdk/ensOperations/createSubdomain.ts @@ -15,16 +15,16 @@ import { } from "viem"; import { EnsPublicClient, storeDataInDb } from "@/ens-sdk"; import { DomainData, MessageData } from "@/lib/utils/types"; -import L1ResolverABI from "../lib/abi/arbitrum-resolver.json"; +import L1ResolverABI from "../../lib/abi/arbitrum-resolver.json"; import toast from "react-hot-toast"; import { getCoderByCoinName } from "@ensdomains/address-encoder"; import * as chains from "viem/chains"; import { packetToBytes } from "viem/ens"; import { SECONDS_PER_YEAR } from "@namehash/ens-utils"; -import { getNameRegistrationSecret } from "../lib/name-registration/localStorage"; -import { DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES } from "../lib/name-registration/constants"; -import { getChain } from "./utils"; -import { getRevertErrorData } from "./errorHandling"; +import { getNameRegistrationSecret } from "../../lib/name-registration/localStorage"; +import { DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES } from "../../lib/name-registration/constants"; +import { getChain } from "../utils"; +import { getRevertErrorData } from "../utils/getRevertErrorData"; interface CreateSubdomainArgs { resolverAddress: Address; diff --git a/ens-sdk/ensOperations/index.ts b/ens-sdk/ensOperations/index.ts new file mode 100644 index 0000000..01b7bd8 --- /dev/null +++ b/ens-sdk/ensOperations/index.ts @@ -0,0 +1,8 @@ +export * from "./setDomainAsPrimaryName"; +export * from "./setDomainRecords"; +export * from "./storeDataInDb"; +export * from "./commit"; +export * from "./makeCommitment"; +export * from "./register"; +export * from "./sendCcipRequest"; +export * from "./createSubdomain"; diff --git a/ens-sdk/ensOperations/makeCommitment.ts b/ens-sdk/ensOperations/makeCommitment.ts new file mode 100644 index 0000000..4772bf3 --- /dev/null +++ b/ens-sdk/ensOperations/makeCommitment.ts @@ -0,0 +1,95 @@ +/* eslint-disable import/no-named-as-default */ +/* eslint-disable import/named */ +import ETHRegistrarABI from "@/lib/abi/eth-registrar.json"; +import { nameRegistrationSmartContracts } from "../../lib/name-registration/constants"; + +import { Address } from "viem"; +import { SupportedNetwork } from "../../lib/wallet/chains"; +import { SECONDS_PER_YEAR } from "@namehash/ens-utils"; +import { getBlockchainTransactionError } from "../../lib/wallet/txError"; +import { parseAccount } from "viem/utils"; + +import { EnsPublicClient } from "../types"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +/* + The commitment value is used in both 'commit' and 'register' + functions in the registrar contract. It acts as a secret + to prevent frontrunning attacks. The commitment value must + be the same in both function calls, which is why we store + it in local storage. +*/ + +interface MakeCommitmentParams { + name: string; + data: string[]; + secret: string; + reverseRecord: boolean; + resolverAddress: Address; + durationInYears: bigint; + ownerControlledFuses: number; + authenticatedAddress: Address; + publicClient: EnsPublicClient; +} + +/** + * Generates a commitment for ENS name registration + * + * This function creates a commitment hash that is used in the two-step ENS registration process. + * It helps prevent front-running attacks by keeping the details of the registration secret until + * the actual registration takes place. + * + * @param {MakeCommitmentParams} params - The parameters required for making the commitment + * @returns {Promise} - The generated commitment hash or an error + */ +export async function makeCommitment({ + name, + data, + secret, + reverseRecord, + resolverAddress, + durationInYears, + ownerControlledFuses, + authenticatedAddress, + publicClient, +}: MakeCommitmentParams) { + const chain = publicClient.chain; + + if (!Object.values(SupportedNetwork).includes(chain.id)) { + throw new Error(`Unsupported network: ${chain.id}`); + } + + const nameRegistrationContracts = + nameRegistrationSmartContracts[chain.id as SupportedNetwork]; + + return publicClient + .readContract({ + account: parseAccount(authenticatedAddress), + address: nameRegistrationContracts.ETH_REGISTRAR, + abi: ETHRegistrarABI, + args: [ + name, + authenticatedAddress, + durationInYears * SECONDS_PER_YEAR.seconds, + secret, + resolverAddress, + data, + reverseRecord, + ownerControlledFuses, + ], + functionName: "makeCommitment", + }) + .then((generatedReservationNumber) => { + return generatedReservationNumber; + }) + .catch((error) => { + const errorType = getBlockchainTransactionError(error); + return errorType || error; + }); +} diff --git a/ens-sdk/ensOperations/register.ts b/ens-sdk/ensOperations/register.ts new file mode 100644 index 0000000..2f2fe35 --- /dev/null +++ b/ens-sdk/ensOperations/register.ts @@ -0,0 +1,144 @@ +import ETHRegistrarABI from "@/lib/abi/eth-registrar.json"; +import { + DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES, + nameRegistrationSmartContracts, +} from "../../lib/name-registration/constants"; + +import { + publicActions, + createWalletClient, + custom, + Address, + Chain, +} from "viem"; +import { SupportedNetwork } from "../../lib/wallet/chains"; +import { SECONDS_PER_YEAR, ENSName } from "@namehash/ens-utils"; +import { + TransactionErrorType, + getBlockchainTransactionError, +} from "../../lib/wallet/txError"; +import { getNameRegistrationSecret } from "@/lib/name-registration/localStorage"; + +import { DomainData, MessageData } from "../../lib/utils/types"; + +import { getNamePrice, getChain } from "../utils"; +import { getRevertErrorData } from "../utils/getRevertErrorData"; +import { EnsPublicClient } from "../types"; +import { storeDataInDb } from "./storeDataInDb"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +/* + 2nd step of a name registration +*/ + +interface RegisterParams { + ensName: ENSName; + resolverAddress: Address; + durationInYears: bigint; + authenticatedAddress: Address; + registerAndSetAsPrimaryName: boolean; + publicClient: EnsPublicClient; + chain: Chain; +} + +/** + * Registers an ENS name + * + * This is the second step in the two-step ENS registration process. It finalizes the registration + * of the ENS name after the commitment has been made and the waiting period has passed. + * + * @param {RegisterParams} params - The parameters for the registration + * @returns {Promise<`0x${string}` | TransactionErrorType>} - The transaction hash or an error + */ +export const register = async ({ + ensName, + resolverAddress, + durationInYears, + authenticatedAddress, + registerAndSetAsPrimaryName, + publicClient, + chain, +}: RegisterParams): Promise<`0x${string}` | TransactionErrorType> => { + try { + const walletClient = createWalletClient({ + account: authenticatedAddress, + chain: chain, + transport: custom(window.ethereum), + }); + + const client = walletClient.extend(publicActions); + + if (!client) throw new Error("WalletClient not found"); + + const nameWithoutTLD = ensName.name.replace(".eth", ""); + + const namePrice = await getNamePrice({ + ensName, + durationInYears, + publicClient, + }); + + if (!Object.values(SupportedNetwork).includes(chain.id)) { + throw new Error(`Unsupported network: ${chain.id}`); + } + + const nameRegistrationContracts = + nameRegistrationSmartContracts[chain.id as SupportedNetwork]; + + const txHash = await client.writeContract({ + address: nameRegistrationContracts.ETH_REGISTRAR, + chain: chain, + account: authenticatedAddress, + args: [ + nameWithoutTLD, + authenticatedAddress, + durationInYears * SECONDS_PER_YEAR.seconds, + getNameRegistrationSecret(), + resolverAddress, + [], + registerAndSetAsPrimaryName, + DEFAULT_REGISTRATION_DOMAIN_CONTROLLED_FUSES, + ], + value: namePrice, + abi: ETHRegistrarABI, + functionName: "register", + gas: 500000n, + }); + + return txHash; + } catch (error: unknown) { + const data = getRevertErrorData(error); + + if (data?.errorName === "StorageHandledByOffChainDatabase") { + const [domain, url, message] = data.args as [ + DomainData, + string, + MessageData, + ]; + + const signedData = await storeDataInDb({ + domain, + url, + message, + authenticatedAddress, + chain: chain, + }); + + if (typeof signedData === "string") { + return signedData as `0x${string}`; + } else { + throw new Error("Error handling off-chain storage"); + } + } else { + console.error(error); + const errorType = getBlockchainTransactionError(error); + return errorType; + } + } +}; diff --git a/ens-sdk/ensOperations/sendCcipRequest.ts b/ens-sdk/ensOperations/sendCcipRequest.ts new file mode 100644 index 0000000..9ad9a96 --- /dev/null +++ b/ens-sdk/ensOperations/sendCcipRequest.ts @@ -0,0 +1,38 @@ +import { Address, Hex } from "viem"; +import { TypedSignature } from "../../lib/utils/types"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +type CcipRequestParameters = { + body: { data: Hex; signature: TypedSignature; sender: Address }; + url: string; +}; + +/** + * Sends a CCIP (Cross-Chain Interoperability Protocol) request + * + * This function is used to send off-chain requests, typically for resolving ENS names + * or storing data related to ENS operations. + * + * @param {CcipRequestParameters} params - The parameters for the CCIP request + * @returns {Promise} - The response from the CCIP request + */ +export async function sendCcipRequest({ + body, + url, +}: CcipRequestParameters): Promise { + return fetch(url.replace("/{sender}/{data}.json", ""), { + body: JSON.stringify(body, (_, value) => + typeof value === "bigint" ? value.toString() : value, + ), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); +} diff --git a/ens-sdk/ensOperations/setDomainAsPrimaryName.ts b/ens-sdk/ensOperations/setDomainAsPrimaryName.ts new file mode 100644 index 0000000..6cd518f --- /dev/null +++ b/ens-sdk/ensOperations/setDomainAsPrimaryName.ts @@ -0,0 +1,86 @@ +import ENSReverseRegistrarABI from "@/lib/abi/ens-reverse-registrar.json"; +import { nameRegistrationSmartContracts } from "../../lib/name-registration/constants"; +import { publicActions, createWalletClient, custom, Chain } from "viem"; +import { SupportedNetwork } from "../../lib/wallet/chains"; +import { ENSName } from "@namehash/ens-utils"; +import { getBlockchainTransactionError } from "../../lib/wallet/txError"; + +import { normalize } from "viem/ens"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +/* + 4th step of a name registration - set domain as primary name if user wants +*/ + +interface SetDomainAsPrimaryNameParams { + authenticatedAddress: `0x${string}`; + ensName: ENSName; + chain: Chain; +} + +/** + * Sets an ENS domain as the primary name for an Ethereum address + * + * This function allows a user to set their newly registered or existing ENS domain + * as the primary name associated with their Ethereum address. This is typically done + * after registering a new ENS name or when changing the primary name. + * + * @param {SetDomainAsPrimaryNameParams} params - The parameters for setting the primary name + * @returns {Promise} - A status code or an error + */ +export const setDomainAsPrimaryName = async ({ + authenticatedAddress, + ensName, + chain, +}: SetDomainAsPrimaryNameParams) => { + try { + if (!Object.values(SupportedNetwork).includes(chain.id)) { + throw new Error(`Unsupported network: ${chain.id}`); + } + + // Create a wallet client for sending transactions to the blockchain + const walletClient = createWalletClient({ + chain: chain, + transport: custom(window.ethereum), + account: authenticatedAddress, + }); + + const client = walletClient.extend(publicActions); + + if (!client) throw new Error("WalletClient not found"); + + const nameWithTLD = ensName.name.includes(".eth") + ? ensName.name + : `${ensName.name}.eth`; + + const network = chain.id as SupportedNetwork; + + const publicAddress = normalize(nameWithTLD); + + // Simulate the setName contract interaction + const { request } = await client.simulateContract({ + address: nameRegistrationSmartContracts[network].ENS_REVERSE_REGISTRAR, + account: authenticatedAddress, + abi: ENSReverseRegistrarABI, + functionName: "setName", + args: [publicAddress], + }); + + // Execute the setName transaction + const setAsPrimaryNameResult = await client.writeContract(request); + + if (!!setAsPrimaryNameResult) { + return 200; + } + } catch (error) { + console.error("writing failed: ", { error }); + const errorType = getBlockchainTransactionError(error); + return errorType; + } +}; diff --git a/ens-sdk/ensOperations/setDomainRecords.ts b/ens-sdk/ensOperations/setDomainRecords.ts new file mode 100644 index 0000000..66684a4 --- /dev/null +++ b/ens-sdk/ensOperations/setDomainRecords.ts @@ -0,0 +1,220 @@ +import L1ResolverABI from "../../lib/abi/arbitrum-resolver.json"; + +import { + namehash, + publicActions, + type WalletClient, + encodeFunctionData, + createWalletClient, + custom, + Hash, + fromBytes, + Address, + PublicClient, + Chain, + stringToHex, +} from "viem"; +import { ENSName } from "@namehash/ens-utils"; +import { getBlockchainTransactionError } from "../../lib/wallet/txError"; + +import DomainResolverABI from "../../lib/abi/offchain-resolver.json"; +import { normalize } from "viem/ens"; +import { supportedCoinTypes } from "../../lib/domain-page"; +import { + coinNameToTypeMap, + getCoderByCoinName, +} from "@ensdomains/address-encoder"; +import { DomainData, MessageData } from "../../lib/utils/types"; +import toast from "react-hot-toast"; +import { sepolia } from "viem/chains"; +import { getChain } from "../utils"; +import { getRevertErrorData } from "../utils/getRevertErrorData"; +import { storeDataInDb } from "./storeDataInDb"; + +const walletConnectProjectId = + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + +if (!walletConnectProjectId) { + throw new Error("No wallet connect project ID informed"); +} + +/* + 3rd step of a name registration - set text records +*/ + +interface SetDomainRecordsParams { + ensName: ENSName; + resolverAddress?: Address; + domainResolverAddress?: `0x${string}`; + authenticatedAddress: Address; + textRecords: Record; + addresses: Record; + others: Record; + client: PublicClient & WalletClient; + chain: Chain; +} + +/** + * Sets various records for an ENS domain + * + * This function allows setting text records, addresses, and other records for an ENS domain. + * It can handle both on-chain and off-chain storage depending on the resolver configuration. + * + * @param {SetDomainRecordsParams} params - The parameters for setting domain records + * @returns {Promise} - A status code or an error + */ +export const setDomainRecords = async ({ + ensName, + resolverAddress, + domainResolverAddress, + authenticatedAddress, + textRecords, + addresses, + others, + client, + chain, +}: SetDomainRecordsParams) => { + try { + const publicAddress = normalize(ensName.name); + + // Prepare calls for setting various records + const calls: Hash[] = []; + + // Add calls for text records + for (let i = 0; i < Object.keys(textRecords).length; i++) { + const key = Object.keys(textRecords)[i]; + const value = textRecords[key]; + + if (value !== null && value !== undefined) { + const callData = encodeFunctionData({ + functionName: "setText", + abi: DomainResolverABI, + args: [namehash(publicAddress), key, value], + }); + + calls.push(callData); + } + } + + // Add calls for address records + for (let i = 0; i < Object.keys(addresses).length; i++) { + const [cryptocurrencyName, address] = Object.entries(addresses)[i]; + if (supportedCoinTypes.includes(cryptocurrencyName.toUpperCase())) { + console.error(`cryptocurrency ${cryptocurrencyName} not supported`); + continue; + } + + const coinType = + coinNameToTypeMap[cryptocurrencyName as keyof typeof coinNameToTypeMap]; + + const coder = getCoderByCoinName(cryptocurrencyName.toLocaleLowerCase()); + const addressEncoded = fromBytes(coder.decode(address), "hex"); + const callData = encodeFunctionData({ + functionName: "setAddr", + abi: DomainResolverABI, + args: [namehash(publicAddress), BigInt(coinType), addressEncoded], + }); + calls.push(callData); + } + + // Add calls for other records (e.g., contenthash) + for (let i = 0; i < Object.keys(others).length; i++) { + const [key, value] = Object.entries(others)[i]; + const callData = encodeFunctionData({ + functionName: "setContenthash", + abi: L1ResolverABI, + args: [namehash(publicAddress), stringToHex(value)], // value = url + }); + calls.push(callData); + } + + try { + let localResolverAddress; + + if (resolverAddress) { + localResolverAddress = resolverAddress; + } else if (domainResolverAddress) { + localResolverAddress = domainResolverAddress; + } else { + throw new Error("No domain resolver informed"); + } + + // Simulate the multicall contract interaction + await client.simulateContract({ + functionName: "multicall", + abi: L1ResolverABI, + args: [calls], + account: authenticatedAddress, + address: localResolverAddress, + }); + } catch (error) { + const data = getRevertErrorData(error); + if (data?.errorName === "StorageHandledByOffChainDatabase") { + // Handle off-chain storage + const [domain, url, message] = data.args as [ + DomainData, + string, + MessageData, + ]; + + try { + await storeDataInDb({ + domain, + url, + message, + authenticatedAddress, + chain: chain, + }); + + return 200; + } catch (error) { + console.error("writing failed: ", { error }); + const errorType = getBlockchainTransactionError(error); + return errorType; + } + } else if (data?.errorName === "StorageHandledByL2") { + // Handle L2 storage + const [chainId, contractAddress] = data.args as [bigint, `0x${string}`]; + + const selectedChain = getChain(Number(chainId)); + + if (!selectedChain) { + toast.error("error"); + return; + } + + const clientWithWallet = createWalletClient({ + chain: selectedChain, + transport: custom(window.ethereum), + }).extend(publicActions); + + await clientWithWallet.addChain({ chain: selectedChain }); + + try { + const { request } = await clientWithWallet.simulateContract({ + functionName: "multicall", + abi: L1ResolverABI, + args: [calls], + account: authenticatedAddress, + address: contractAddress, + }); + await clientWithWallet.writeContract(request); + } catch { + await clientWithWallet.switchChain({ id: sepolia.id }); + } + + await clientWithWallet.switchChain({ id: sepolia.id }); + + return 200; + } else { + console.error("writing failed: ", { error }); + const errorType = getBlockchainTransactionError(error); + return errorType; + } + } + } catch (error: unknown) { + console.error(error); + const errorType = getBlockchainTransactionError(error); + return errorType; + } +}; diff --git a/ens-sdk/ensOperations/storeDataInDb.ts b/ens-sdk/ensOperations/storeDataInDb.ts new file mode 100644 index 0000000..c04452b --- /dev/null +++ b/ens-sdk/ensOperations/storeDataInDb.ts @@ -0,0 +1,56 @@ +import { DomainData, MessageData } from "@/lib/utils/types"; +import { Chain, createWalletClient, custom } from "viem"; +import { sendCcipRequest } from "./sendCcipRequest"; + +interface StoreDataInDbParams { + domain: DomainData; + url: string; + message: MessageData; + authenticatedAddress: `0x${string}`; + chain: Chain; +} + +/** + * Stores ENS-related data in an off-chain database + * + * This function is used when certain ENS operations require off-chain storage. + * It signs the data with the user's wallet and sends it to a specified URL for storage. + * + * @param {StoreDataInDbParams} params - The parameters for storing data + * @returns {Promise} - The response from the storage request + */ +export async function storeDataInDb({ + domain, + url, + message, + authenticatedAddress, + chain, +}: StoreDataInDbParams): Promise { + const client = createWalletClient({ + account: authenticatedAddress, + chain: chain, + transport: custom(window.ethereum), + }); + + const signature = await client.signTypedData({ + domain, + message, + types: { + Message: [ + { name: "callData", type: "bytes" }, + { name: "sender", type: "address" }, + { name: "expirationTimestamp", type: "uint256" }, + ], + }, + primaryType: "Message", + }); + + return await sendCcipRequest({ + body: { + data: message.callData, + signature: { message, domain, signature }, + sender: message.sender, + }, + url, + }); +} diff --git a/ens-sdk/index.ts b/ens-sdk/index.ts index cdb2344..41ed17c 100644 --- a/ens-sdk/index.ts +++ b/ens-sdk/index.ts @@ -1,5 +1,3 @@ -export * from "./createSubdomain"; -export * from "./ensFunctions"; -export * from "./errorHandling"; +export * from "./ensOperations"; export * from "./types"; export * from "./utils"; diff --git a/ens-sdk/errorHandling.ts b/ens-sdk/utils/getRevertErrorData.ts similarity index 100% rename from ens-sdk/errorHandling.ts rename to ens-sdk/utils/getRevertErrorData.ts diff --git a/ens-sdk/utils/index.ts b/ens-sdk/utils/index.ts index f9568ae..172e85b 100644 --- a/ens-sdk/utils/index.ts +++ b/ens-sdk/utils/index.ts @@ -5,3 +5,4 @@ export * from "./getGasPrice"; export * from "./createNameRegistrationSecret"; export * from "./getNameRegistrationGasEstimate"; export * from "./isNameAvailable"; +export * from "./getRevertErrorData";