Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/happy-carrots-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"thirdweb": patch
---

Add `isValidENSName` utility function for checking if a string is a valid ENS name. It does not check if the name is actually registered, it only checks if the string is in a valid format.

```ts
import { isValidENSName } from "thirdweb/utils";

isValidENSName("thirdweb.eth"); // true
isValidENSName("foo.bar.com"); // true
isValidENSName("foo"); // false
```
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { getBytecode, getContract } from "thirdweb/contract";
import { getPublishedUriFromCompilerUri } from "thirdweb/extensions/thirdweb";
import { getInstalledModules } from "thirdweb/modules";
import { download } from "thirdweb/storage";
import { extractIPFSUri } from "thirdweb/utils";
import { extractIPFSUri, isValidENSName } from "thirdweb/utils";
import { fetchPublishedContractsFromDeploy } from "../../../../../../../components/contract-components/fetchPublishedContractsFromDeploy";
import { isEnsName, resolveEns } from "../../../../../../../lib/ens";
import { resolveEns } from "../../../../../../../lib/ens";

type ModuleMetadataPickedKeys = {
publisher: string;
Expand Down Expand Up @@ -52,7 +52,7 @@ export async function getPublishedByCardProps(params: {

// get publisher address/ens
let publisherAddressOrEns = publishedContractToShow.publisher;
if (!isEnsName(publishedContractToShow.publisher)) {
if (!isValidENSName(publishedContractToShow.publisher)) {
try {
const res = await resolveEns(publishedContractToShow.publisher);
if (res.ensName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getAddress, isAddress } from "thirdweb";
import { isValidENSName } from "thirdweb/utils";
import { mapThirdwebPublisher } from "../../../../components/contract-components/fetch-contracts-with-versions";
import { isEnsName, resolveEns } from "../../../../lib/ens";
import { resolveEns } from "../../../../lib/ens";

type ResolvedAddressInfo = {
address: string;
Expand All @@ -17,7 +18,7 @@ export async function resolveAddressAndEns(
};
}

if (isEnsName(addressOrEns)) {
if (isValidENSName(addressOrEns)) {
const mappedEns = mapThirdwebPublisher(addressOrEns);
const res = await resolveEns(mappedEns).catch(() => null);
if (res?.address) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export default async function PublishedContractPage(
<SimpleGrid columns={12} gap={{ base: 6, md: 10 }} w="full">
<PublishedContract
publishedContract={publishedContract}
walletOrEns={params.publisher}
twAccount={account}
/>
</SimpleGrid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export default async function PublishedContractPage(
<div className="grid w-full grid-cols-12 gap-6 md:gap-10">
<PublishedContract
publishedContract={publishedContract}
walletOrEns={params.publisher}
twAccount={account}
/>
</div>
Expand Down
10 changes: 5 additions & 5 deletions apps/dashboard/src/components/contract-components/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { queryOptions, useQuery } from "@tanstack/react-query";
import type { Abi } from "abitype";
import { isEnsName, resolveEns } from "lib/ens";
import { resolveEns } from "lib/ens";
import { useV5DashboardChain } from "lib/v5-adapter";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { getContract, resolveContractAbi } from "thirdweb/contract";
import { isAddress } from "thirdweb/utils";
import { isAddress, isValidENSName } from "thirdweb/utils";
import {
type PublishedContractWithVersion,
fetchPublishedContractVersions,
Expand Down Expand Up @@ -130,7 +130,7 @@ function ensQuery(addressOrEnsName?: string) {
return placeholderData;
}
// if it is neither an address or an ens name then return the placeholder data only
if (!isAddress(addressOrEnsName) && !isEnsName(addressOrEnsName)) {
if (!isAddress(addressOrEnsName) && !isValidENSName(addressOrEnsName)) {
throw new Error("Invalid address or ENS name.");
}

Expand All @@ -143,7 +143,7 @@ function ensQuery(addressOrEnsName?: string) {
}),
);

if (isEnsName(addressOrEnsName) && !address) {
if (isValidENSName(addressOrEnsName) && !address) {
throw new Error("Failed to resolve ENS name.");
}

Expand All @@ -154,7 +154,7 @@ function ensQuery(addressOrEnsName?: string) {
},
enabled:
!!addressOrEnsName &&
(isAddress(addressOrEnsName) || isEnsName(addressOrEnsName)),
(isAddress(addressOrEnsName) || isValidENSName(addressOrEnsName)),
// 24h
gcTime: 60 * 60 * 24 * 1000,
// 1h
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,11 @@ interface ExtendedPublishedContract extends PublishedContractWithVersion {

interface PublishedContractProps {
publishedContract: ExtendedPublishedContract;
walletOrEns: string;
twAccount: Account | undefined;
}

export const PublishedContract: React.FC<PublishedContractProps> = ({
publishedContract,
walletOrEns,
twAccount,
}) => {
const address = useActiveAccount()?.address;
Expand Down Expand Up @@ -154,7 +152,9 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
</GridItem>
<GridItem colSpan={{ base: 12, md: 3 }}>
<Flex flexDir="column" gap={6}>
{walletOrEns && <PublisherHeader wallet={walletOrEns} />}
{publishedContract.publisher && (
<PublisherHeader wallet={publishedContract.publisher} />
)}
<Divider />
<Flex flexDir="column" gap={4}>
<Heading as="h4" size="title.sm">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,16 @@ export const PublisherHeader: React.FC<PublisherHeaderProps> = ({
>
<AccountName
fallbackComponent={
<AccountAddress
formatFn={(addr) =>
shortenIfAddress(replaceDeployerAddress(addr))
}
/>
// When social profile API support other TLDs as well - we can remove this condition
ensQuery.data?.ensName ? (
<span> {ensQuery.data?.ensName} </span>
) : (
<AccountAddress
formatFn={(addr) =>
shortenIfAddress(replaceDeployerAddress(addr))
}
/>
)
}
loadingComponent={<Skeleton className="h-8 w-40" />}
formatFn={(name) => replaceDeployerAddress(name)}
Expand Down
17 changes: 5 additions & 12 deletions apps/dashboard/src/constants/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { resolveEns } from "lib/ens";
import { isAddress } from "thirdweb";
import { isValidENSName } from "thirdweb/utils";
import z from "zod";

/**
Expand All @@ -15,19 +16,11 @@ export const BasisPointsSchema = z
.min(0, "Cannot be below 0%");

// @internal
type EnsName = `${string}.eth` | `${string}.cb.id`;
type EnsName = string;

// Only pass through to provider call if value ends with .eth or .cb.id
const EnsSchema: z.ZodType<
`0x${string}`,
z.ZodTypeDef,
`${string}.eth` | `${string}.cb.id`
> = z
.custom<EnsName>(
(ens) =>
typeof ens === "string" &&
(ens.endsWith(".eth") || ens.endsWith(".cb.id")),
)
// Only pass through to provider call if value is a valid ENS name
const EnsSchema: z.ZodType<`0x${string}`, z.ZodTypeDef, string> = z
.custom<EnsName>((ens) => typeof ens === "string" && isValidENSName(ens))
.transform(async (ens) => (await resolveEns(ens)).address)
.refine(
(address): address is `0x${string}` => !!address && isAddress(address),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useEns } from "components/contract-components/hooks";
import { CheckIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import { isAddress } from "thirdweb/utils";
import { isAddress, isValidENSName } from "thirdweb/utils";
import { FormHelperText } from "tw-components";
import type { SolidityInputProps } from ".";
import { validateAddress } from "./helpers";
Expand Down Expand Up @@ -77,14 +77,19 @@ export const SolidityAddressInput: React.FC<SolidityInputProps> = ({

const resolvingEns = useMemo(
() =>
localInput?.endsWith(".eth") &&
localInput &&
isValidENSName(localInput) &&
!ensQuery.isError &&
!ensQuery.data?.address,
[ensQuery.data?.address, ensQuery.isError, localInput],
);

const resolvedAddress = useMemo(
() => localInput?.endsWith(".eth") && !hasError && ensQuery.data?.address,
() =>
localInput &&
isValidENSName(localInput) &&
!hasError &&
ensQuery.data?.address,
[ensQuery.data?.address, hasError, localInput],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAddress, isBytes, isHex } from "thirdweb/utils";
import { isAddress, isBytes, isHex, isValidENSName } from "thirdweb/utils";

// int and uint
function calculateIntMinValues(solidityType: string) {
Expand Down Expand Up @@ -147,7 +147,7 @@ export const validateBytes = (value: string, solidityType: string) => {

// address
export const validateAddress = (value: string) => {
if (!isAddress(value) && !value.endsWith(".eth")) {
if (!isAddress(value) && !isValidENSName(value)) {
return {
type: "pattern",
message: "Input is not a valid address or ENS name.",
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/lib/address-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { isAddress } from "thirdweb";
import { isEnsName } from "./ens";
import { isValidENSName } from "thirdweb/utils";

// if a string is a valid address or ens name
export function isPossibleEVMAddress(address?: string, ignoreEns?: boolean) {
if (!address) {
return false;
}
if (isEnsName(address) && !ignoreEns) {
if (isValidENSName(address) && !ignoreEns) {
return true;
}
return isAddress(address);
Expand Down
7 changes: 2 additions & 5 deletions apps/dashboard/src/lib/ens.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { isAddress } from "thirdweb";
import { resolveAddress, resolveName } from "thirdweb/extensions/ens";
import { isValidENSName } from "thirdweb/utils";

interface ENSResolveResult {
ensName: string | null;
address: string | null;
}

export function isEnsName(name: string): boolean {
return name?.endsWith(".eth");
}

export async function resolveEns(
ensNameOrAddress: string,
): Promise<ENSResolveResult> {
Expand All @@ -24,7 +21,7 @@ export async function resolveEns(
};
}

if (!isEnsName(ensNameOrAddress)) {
if (!isValidENSName(ensNameOrAddress)) {
throw new Error("Invalid ENS name");
}

Expand Down
7 changes: 4 additions & 3 deletions apps/dashboard/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { type NextRequest, NextResponse } from "next/server";
import { getAddress } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { isValidENSName } from "thirdweb/utils";
import { defineDashboardChain } from "./lib/defineDashboardChain";

// ignore assets, api - only intercept page routes
Expand Down Expand Up @@ -136,7 +137,7 @@ export async function middleware(request: NextRequest) {
// DIFFERENT DYNAMIC ROUTING CASES

// /<address>/... case
if (paths[0] && isPossibleEVMAddress(paths[0])) {
if (paths[0] && isPossibleAddressOrENSName(paths[0])) {
// special case for "deployer.thirdweb.eth"
// we want to always redirect this to "thirdweb.eth/..."
if (paths[0] === "deployer.thirdweb.eth") {
Expand Down Expand Up @@ -181,8 +182,8 @@ export async function middleware(request: NextRequest) {
}
}

function isPossibleEVMAddress(address: string) {
return address?.startsWith("0x") || address?.endsWith(".eth");
function isPossibleAddressOrENSName(address: string) {
return address.startsWith("0x") || isValidENSName(address);
}

// utils for rewriting and redirecting with relative paths
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { isAddress } from "thirdweb";
import { resolveAddress } from "thirdweb/extensions/ens";
import { type SocialProfile, getSocialProfiles } from "thirdweb/social";
import { resolveScheme } from "thirdweb/storage";
import { isValidENSName } from "thirdweb/utils";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
Expand All @@ -19,7 +20,7 @@ export function SocialProfiles() {
const { mutate: searchProfiles, isPending } = useMutation({
mutationFn: async (address: string) => {
const resolvedAddress = await (async () => {
if (address.endsWith(".eth")) {
if (isValidENSName(address)) {
return resolveAddress({
client: THIRDWEB_CLIENT,
name: address,
Expand Down
3 changes: 3 additions & 0 deletions packages/thirdweb/src/exports/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,6 @@ export type {

export { shortenLargeNumber } from "../utils/shortenLargeNumber.js";
export { formatNumber } from "../utils/formatNumber.js";

// ENS
export { isValidENSName } from "../utils/ens/isValidENSName.js";
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
import { prepareTransaction } from "../../../../transaction/prepare-transaction.js";
import { isAddress } from "../../../../utils/address.js";
import { isValidENSName } from "../../../../utils/ens/isValidENSName.js";
import { toWei } from "../../../../utils/units.js";
import { useActiveWallet } from "./useActiveWallet.js";

Expand Down Expand Up @@ -53,7 +54,7 @@
// input validation
if (
!receiverAddress ||
(!receiverAddress.endsWith(".eth") && !isAddress(receiverAddress))
(!isValidENSName(receiverAddress) && !isAddress(receiverAddress))

Check warning on line 57 in packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts#L57

Added line #L57 was not covered by tests
) {
throw new Error("Invalid receiver address");
}
Expand Down
39 changes: 39 additions & 0 deletions packages/thirdweb/src/utils/ens/isValidENSName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { isValidENSName } from "./isValidENSName.js";

describe("isValidENSName", () => {
it("should return true for a valid ENS name", () => {
expect(isValidENSName("thirdweb.eth")).toBe(true);
expect(isValidENSName("deployer.thirdweb.eth")).toBe(true);
expect(isValidENSName("x.eth")).toBe(true);
expect(isValidENSName("foo.bar.com")).toBe(true);
expect(isValidENSName("foo.com")).toBe(true);
expect(isValidENSName("somename.xyz")).toBe(true);
expect(isValidENSName("_foo.bar")).toBe(true);
expect(isValidENSName("-foo.bar.com")).toBe(true);
});

it("should return false for an invalid ENS name", () => {
// No TLD
expect(isValidENSName("")).toBe(false);
expect(isValidENSName("foo")).toBe(false);

// parts with length < 2
expect(isValidENSName(".eth")).toBe(false);
expect(isValidENSName("foo..com")).toBe(false);
expect(isValidENSName("thirdweb.eth.")).toBe(false);

// numeric TLD
expect(isValidENSName("foo.123")).toBe(false);

// whitespace in parts
expect(isValidENSName("foo .com")).toBe(false);
expect(isValidENSName("foo. com")).toBe(false);

// full-width characters
expect(isValidENSName("foo.bar.com")).toBe(false);

// wildcard characters
expect(isValidENSName("foo*bar.com")).toBe(false);
});
});
Loading
Loading