diff --git a/.changeset/fluffy-pets-tie.md b/.changeset/fluffy-pets-tie.md
new file mode 100644
index 00000000000..453fbf82b60
--- /dev/null
+++ b/.changeset/fluffy-pets-tie.md
@@ -0,0 +1,5 @@
+---
+"thirdweb": patch
+---
+
+Fix caching issues for headless component; improve code coverage
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx
index 356fe69b58b..8b09fe73032 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx
@@ -2,6 +2,7 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import { getChainMetadata } from "../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
+import { getFunctionId } from "../../../../../utils/function-id.js";
import { resolveScheme } from "../../../../../utils/ipfs.js";
import { useChainContext } from "./provider.js";
@@ -119,7 +120,18 @@ export function ChainIcon({
}: ChainIconProps) {
const { chain } = useChainContext();
const iconQuery = useQuery({
- queryKey: ["_internal_chain_icon_", chain.id] as const,
+ queryKey: [
+ "_internal_chain_icon_",
+ chain.id,
+ {
+ resolver:
+ typeof iconResolver === "string"
+ ? iconResolver
+ : typeof iconResolver === "function"
+ ? getFunctionId(iconResolver)
+ : undefined,
+ },
+ ] as const,
queryFn: async () => {
if (typeof iconResolver === "string") {
return iconResolver;
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx
index b8278e4e635..1279dbceb3f 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx
@@ -2,17 +2,17 @@ import { describe, expect, it } from "vitest";
import { render, screen, waitFor } from "~test/react-render.js";
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
import { defineChain } from "../../../../../chains/utils.js";
-import { ChainName } from "./name.js";
+import { ChainName, fetchChainName } from "./name.js";
import { ChainProvider } from "./provider.js";
describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
- it("should return the correct chain name, if the name exists in the chain object", () => {
+ it("should return the correct chain name, if the name exists in the chain object", async () => {
render(
,
);
- waitFor(() =>
+ await waitFor(() =>
expect(
screen.getByText("Ethereum", {
exact: true,
@@ -22,13 +22,13 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
);
});
- it("should return the correct chain name, if the name is loaded from the server", () => {
+ it("should return the correct chain name, if the name is loaded from the server", async () => {
render(
,
);
- waitFor(() =>
+ await waitFor(() =>
expect(
screen.getByText("Ethereum Mainnet", {
exact: true,
@@ -38,13 +38,13 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
);
});
- it("should return the correct FORMATTED chain name", () => {
+ it("should return the correct FORMATTED chain name", async () => {
render(
`${str}-formatted`} />
,
);
- waitFor(() =>
+ await waitFor(() =>
expect(
screen.getByText("Ethereum-formatted", {
exact: true,
@@ -54,14 +54,14 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
);
});
- it("should fallback properly when fail to resolve chain name", () => {
+ it("should fallback properly when fail to resolve chain name", async () => {
render(
oops} />
,
);
- waitFor(() =>
+ await waitFor(() =>
expect(
screen.getByText("oops", {
exact: true,
@@ -70,4 +70,31 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
).toBeInTheDocument(),
);
});
+
+ it("fetchChainName should respect nameResolver as a string", async () => {
+ const res = await fetchChainName({
+ chain: ethereum,
+ nameResolver: "eth_mainnet",
+ });
+ expect(res).toBe("eth_mainnet");
+ });
+
+ it("fetchChainName should respect nameResolver as a non-async function", async () => {
+ const res = await fetchChainName({
+ chain: ethereum,
+ nameResolver: () => "eth_mainnet",
+ });
+ expect(res).toBe("eth_mainnet");
+ });
+
+ it("fetchChainName should respect nameResolver as an async function", async () => {
+ const res = await fetchChainName({
+ chain: ethereum,
+ nameResolver: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ return "eth_mainnet";
+ },
+ });
+ expect(res).toBe("eth_mainnet");
+ });
});
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx
index 0bd4518128b..7cbfa0e6a0f 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx
@@ -3,7 +3,9 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type React from "react";
import type { JSX } from "react";
+import type { Chain } from "../../../../../chains/types.js";
import { getChainMetadata } from "../../../../../chains/utils.js";
+import { getFunctionId } from "../../../../../utils/function-id.js";
import { useChainContext } from "./provider.js";
/**
@@ -155,19 +157,19 @@ export function ChainName({
}: ChainNameProps) {
const { chain } = useChainContext();
const nameQuery = useQuery({
- queryKey: ["_internal_chain_name_", chain.id] as const,
- queryFn: async () => {
- if (typeof nameResolver === "string") {
- return nameResolver;
- }
- if (typeof nameResolver === "function") {
- return nameResolver();
- }
- if (chain.name) {
- return chain.name;
- }
- return getChainMetadata(chain).then((data) => data.name);
- },
+ queryKey: [
+ "_internal_chain_name_",
+ chain.id,
+ {
+ resolver:
+ typeof nameResolver === "string"
+ ? nameResolver
+ : typeof nameResolver === "function"
+ ? getFunctionId(nameResolver)
+ : undefined,
+ },
+ ] as const,
+ queryFn: async () => fetchChainName({ chain, nameResolver }),
...queryOptions,
});
@@ -183,3 +185,23 @@ export function ChainName({
return {displayValue};
}
+
+/**
+ * @internal Exported for tests only
+ */
+export async function fetchChainName(props: {
+ chain: Chain;
+ nameResolver?: string | (() => string) | (() => Promise);
+}) {
+ const { nameResolver, chain } = props;
+ if (typeof nameResolver === "string") {
+ return nameResolver;
+ }
+ if (typeof nameResolver === "function") {
+ return nameResolver();
+ }
+ if (chain.name) {
+ return chain.name;
+ }
+ return getChainMetadata(chain).then((data) => data.name);
+}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx
index 7f2b245cfe0..7a9a23e41e4 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx
@@ -3,6 +3,7 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
+import { getFunctionId } from "../../../../../utils/function-id.js";
import { useNFTContext } from "./provider.js";
import { getNFTInfo } from "./utils.js";
@@ -100,7 +101,7 @@ export function NFTDescription({
typeof descriptionResolver === "string"
? descriptionResolver
: typeof descriptionResolver === "function"
- ? descriptionResolver.toString()
+ ? getFunctionId(descriptionResolver)
: undefined,
},
],
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx
index 814de3b124c..d1b894dabcc 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx
@@ -1,6 +1,7 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
+import { getFunctionId } from "../../../../../utils/function-id.js";
import { MediaRenderer } from "../../MediaRenderer/MediaRenderer.js";
import type { MediaRendererProps } from "../../MediaRenderer/types.js";
import { useNFTContext } from "./provider.js";
@@ -138,7 +139,7 @@ export function NFTMedia({
typeof mediaResolver === "object"
? mediaResolver
: typeof mediaResolver === "function"
- ? mediaResolver.toString()
+ ? getFunctionId(mediaResolver)
: undefined,
},
],
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx
index 8d5da597a20..65c06e8dc0d 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx
@@ -1,6 +1,7 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
+import { getFunctionId } from "../../../../../utils/function-id.js";
import { useNFTContext } from "./provider.js";
import { getNFTInfo } from "./utils.js";
@@ -100,7 +101,7 @@ export function NFTName({
typeof nameResolver === "string"
? nameResolver
: typeof nameResolver === "function"
- ? nameResolver.toString()
+ ? getFunctionId(nameResolver)
: undefined,
},
],
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
index 4c9bb4cff6a..d272bbaa0a3 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
@@ -4,6 +4,7 @@ import { getChainMetadata } from "../../../../../chains/utils.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
import { getContract } from "../../../../../contract/contract.js";
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
+import { getFunctionId } from "../../../../../utils/function-id.js";
import { resolveScheme } from "../../../../../utils/ipfs.js";
import { useTokenContext } from "./provider.js";
@@ -115,7 +116,19 @@ export function TokenIcon({
}: TokenIconProps) {
const { address, client, chain } = useTokenContext();
const iconQuery = useQuery({
- queryKey: ["_internal_token_icon_", chain.id, address] as const,
+ queryKey: [
+ "_internal_token_icon_",
+ chain.id,
+ address,
+ {
+ resolver:
+ typeof iconResolver === "string"
+ ? iconResolver
+ : typeof iconResolver === "function"
+ ? getFunctionId(iconResolver)
+ : undefined,
+ },
+ ] as const,
queryFn: async () => {
if (typeof iconResolver === "string") {
return iconResolver;
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.test.tsx
new file mode 100644
index 00000000000..38c45c0e83c
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.test.tsx
@@ -0,0 +1,86 @@
+import { describe, expect, it } from "vitest";
+import { ANVIL_CHAIN } from "~test/chains.js";
+import {} from "~test/react-render.js";
+import { TEST_CLIENT } from "~test/test-clients.js";
+import {
+ UNISWAPV3_FACTORY_CONTRACT,
+ USDT_CONTRACT,
+} from "~test/test-contracts.js";
+import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
+import { fetchTokenName } from "./name.js";
+
+const client = TEST_CLIENT;
+
+describe.runIf(process.env.TW_SECRET_KEY)("TokenName component", () => {
+ it("fetchTokenName should respect the nameResolver being a string", async () => {
+ const res = await fetchTokenName({
+ address: "thing",
+ client,
+ chain: ANVIL_CHAIN,
+ nameResolver: "tw",
+ });
+ expect(res).toBe("tw");
+ });
+
+ it("fetchTokenName should respect the nameResolver being a non-async function", async () => {
+ const res = await fetchTokenName({
+ address: "thing",
+ client,
+ chain: ANVIL_CHAIN,
+ nameResolver: () => "tw",
+ });
+
+ expect(res).toBe("tw");
+ });
+
+ it("fetchTokenName should respect the nameResolver being an async function", async () => {
+ const res = await fetchTokenName({
+ address: "thing",
+ client,
+ chain: ANVIL_CHAIN,
+ nameResolver: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ return "tw";
+ },
+ });
+
+ expect(res).toBe("tw");
+ });
+
+ it("fetchTokenName should work for contract with `name` function", async () => {
+ const res = await fetchTokenName({
+ address: USDT_CONTRACT.address,
+ client,
+ chain: USDT_CONTRACT.chain,
+ });
+
+ expect(res).toBe("Tether USD");
+ });
+
+ it("fetchTokenName should work for native token", async () => {
+ const res = await fetchTokenName({
+ address: NATIVE_TOKEN_ADDRESS,
+ client,
+ chain: ethereum,
+ });
+
+ expect(res).toBe("Ether");
+ });
+
+ it("fetchTokenName should try to fallback to the contract metadata if fails to resolves from `name()`", async () => {
+ // todo: find a contract with name in contractMetadata, but does not have a name function
+ });
+
+ it("fetchTokenName should throw in the end where all fallback solutions failed to resolve to any name", async () => {
+ await expect(() =>
+ fetchTokenName({
+ address: UNISWAPV3_FACTORY_CONTRACT.address,
+ client,
+ chain: UNISWAPV3_FACTORY_CONTRACT.chain,
+ }),
+ ).rejects.toThrowError(
+ "Failed to resolve name from both name() and contract metadata",
+ );
+ });
+});
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.tsx
index 806fcba3537..87fb5b91ab5 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.tsx
@@ -3,11 +3,14 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type React from "react";
import type { JSX } from "react";
+import type { Chain } from "../../../../../chains/types.js";
import { getChainMetadata } from "../../../../../chains/utils.js";
+import type { ThirdwebClient } from "../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
import { getContract } from "../../../../../contract/contract.js";
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
import { name } from "../../../../../extensions/common/read/name.js";
+import { getFunctionId } from "../../../../../utils/function-id.js";
import { useTokenContext } from "./provider.js";
/**
@@ -157,33 +160,21 @@ export function TokenName({
}: TokenNameProps) {
const { address, client, chain } = useTokenContext();
const nameQuery = useQuery({
- queryKey: ["_internal_token_name_", chain.id, address] as const,
- queryFn: async () => {
- if (typeof nameResolver === "string") {
- return nameResolver;
- }
- if (typeof nameResolver === "function") {
- return nameResolver();
- }
- if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
- // Don't wanna use `getChainNativeCurrencyName` because it has some side effect (it catches error and defaults to "ETH")
- return getChainMetadata(chain).then((data) => data.nativeCurrency.name);
- }
- // Try to fetch the name from both the `name()` function and the contract metadata
- // then prioritize the `name()`
- const contract = getContract({ address, client, chain });
- const [_name, contractMetadata] = await Promise.all([
- name({ contract }),
- getContractMetadata({ contract }),
- ]);
- if (!_name && !contractMetadata.name) {
- throw new Error(
- "Failed to resolve name from both name() and contract metadata",
- );
- }
-
- return _name || contractMetadata.name;
- },
+ queryKey: [
+ "_internal_token_name_",
+ chain.id,
+ address,
+ {
+ resolver:
+ typeof nameResolver === "string"
+ ? nameResolver
+ : typeof nameResolver === "function"
+ ? getFunctionId(nameResolver)
+ : undefined,
+ },
+ ] as const,
+ queryFn: async () =>
+ fetchTokenName({ address, chain, client, nameResolver }),
...queryOptions,
});
@@ -195,7 +186,48 @@ export function TokenName({
return fallbackComponent || null;
}
- const displayValue = formatFn ? formatFn(nameQuery.data) : nameQuery.data;
+ if (formatFn && typeof formatFn === "function") {
+ return {formatFn(nameQuery.data)};
+ }
+
+ return {nameQuery.data};
+}
- return {displayValue};
+/**
+ * @internal Exported for tests only
+ */
+export async function fetchTokenName(props: {
+ address: string;
+ client: ThirdwebClient;
+ chain: Chain;
+ nameResolver?: string | (() => string) | (() => Promise);
+}) {
+ const { nameResolver, address, client, chain } = props;
+ if (typeof nameResolver === "string") {
+ return nameResolver;
+ }
+ if (typeof nameResolver === "function") {
+ return nameResolver();
+ }
+ if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
+ // Don't wanna use `getChainName` because it has some side effect (it catches error and defaults to "ETH")
+ return getChainMetadata(chain).then((data) => data.nativeCurrency.name);
+ }
+
+ // Try to fetch the name from both the `name` function and the contract metadata
+ // then prioritize its result
+ const contract = getContract({ address, client, chain });
+ const [_name, contractMetadata] = await Promise.all([
+ name({ contract }).catch(() => undefined),
+ getContractMetadata({ contract }).catch(() => undefined),
+ ]);
+ if (typeof _name === "string") {
+ return _name;
+ }
+ if (typeof contractMetadata?.name === "string") {
+ return contractMetadata.name;
+ }
+ throw new Error(
+ "Failed to resolve name from both name() and contract metadata",
+ );
}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.test.tsx
index 0a09e414c18..44b95ba71e2 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.test.tsx
@@ -1,30 +1,86 @@
import { describe, expect, it } from "vitest";
-import { render, screen, waitFor } from "~test/react-render.js";
+import { ANVIL_CHAIN } from "~test/chains.js";
+import {} from "~test/react-render.js";
import { TEST_CLIENT } from "~test/test-clients.js";
+import {
+ UNISWAPV3_FACTORY_CONTRACT,
+ USDT_CONTRACT,
+} from "~test/test-contracts.js";
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
-import { TokenProvider } from "./provider.js";
-import { TokenSymbol } from "./symbol.js";
+import { fetchTokenSymbol } from "./symbol.js";
+
+const client = TEST_CLIENT;
describe.runIf(process.env.TW_SECRET_KEY)("TokenSymbol component", () => {
- it("should pass the address correctly to the children props", () => {
- render(
-
-
- ,
- );
+ it("fetchTokenSymbol should respect the symbolResolver being a string", async () => {
+ const res = await fetchTokenSymbol({
+ address: "thing",
+ client,
+ chain: ANVIL_CHAIN,
+ symbolResolver: "tw",
+ });
+ expect(res).toBe("tw");
+ });
+
+ it("fetchTokenSymbol should respect the symbolResolver being a non-async function", async () => {
+ const res = await fetchTokenSymbol({
+ address: "thing",
+ client,
+ chain: ANVIL_CHAIN,
+ symbolResolver: () => "tw",
+ });
+
+ expect(res).toBe("tw");
+ });
+
+ it("fetchTokenSymbol should respect the symbolResolver being an async function", async () => {
+ const res = await fetchTokenSymbol({
+ address: "thing",
+ client,
+ chain: ANVIL_CHAIN,
+ symbolResolver: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ return "tw";
+ },
+ });
+
+ expect(res).toBe("tw");
+ });
+
+ it("fetchTokenSymbol should work for contract with `symbol` function", async () => {
+ const res = await fetchTokenSymbol({
+ address: USDT_CONTRACT.address,
+ client,
+ chain: USDT_CONTRACT.chain,
+ });
+
+ expect(res).toBe("USDT");
+ });
+
+ it("fetchTokenSymbol should work for native token", async () => {
+ const res = await fetchTokenSymbol({
+ address: NATIVE_TOKEN_ADDRESS,
+ client,
+ chain: ethereum,
+ });
+
+ expect(res).toBe("ETH");
+ });
+
+ it("fetchTokenSymbol should try to fallback to the contract metadata if fails to resolves from `symbol()`", async () => {
+ // todo: find a contract with symbol in contractMetadata, but does not have a symbol function
+ });
- waitFor(() =>
- expect(
- screen.getByText("ETH", {
- exact: true,
- selector: "span",
- }),
- ).toBeInTheDocument(),
+ it("fetchTokenSymbol should throw in the end where all fallback solutions failed to resolve to any symbol", async () => {
+ await expect(() =>
+ fetchTokenSymbol({
+ address: UNISWAPV3_FACTORY_CONTRACT.address,
+ client,
+ chain: UNISWAPV3_FACTORY_CONTRACT.chain,
+ }),
+ ).rejects.toThrowError(
+ "Failed to resolve symbol from both symbol() and contract metadata",
);
});
});
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.tsx
index f770a600401..2975ad60570 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.tsx
@@ -3,11 +3,14 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type React from "react";
import type { JSX } from "react";
+import type { Chain } from "../../../../../chains/types.js";
import { getChainMetadata } from "../../../../../chains/utils.js";
+import type { ThirdwebClient } from "../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
import { getContract } from "../../../../../contract/contract.js";
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
import { symbol } from "../../../../../extensions/common/read/symbol.js";
+import { getFunctionId } from "../../../../../utils/function-id.js";
import { useTokenContext } from "./provider.js";
/**
@@ -154,36 +157,21 @@ export function TokenSymbol({
}: TokenSymbolProps) {
const { address, client, chain } = useTokenContext();
const symbolQuery = useQuery({
- queryKey: ["_internal_token_symbol_", chain.id, address] as const,
- queryFn: async () => {
- if (typeof symbolResolver === "string") {
- return symbolResolver;
- }
- if (typeof symbolResolver === "function") {
- return symbolResolver();
- }
- if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
- // Don't wanna use `getChainSymbol` because it has some side effect (it catches error and defaults to "ETH")
- return getChainMetadata(chain).then(
- (data) => data.nativeCurrency.symbol,
- );
- }
-
- // Try to fetch the symbol from both the `symbol` function and the contract metadata
- // then prioritize the `symbol()`
- const contract = getContract({ address, client, chain });
- const [_symbol, contractMetadata] = await Promise.all([
- symbol({ contract }),
- getContractMetadata({ contract }),
- ]);
- if (!_symbol && !contractMetadata.symbol) {
- throw new Error(
- "Failed to resolve symbol from both symbol() and contract metadata",
- );
- }
-
- return _symbol || contractMetadata.symbol;
- },
+ queryKey: [
+ "_internal_token_symbol_",
+ chain.id,
+ address,
+ {
+ resolver:
+ typeof symbolResolver === "string"
+ ? symbolResolver
+ : typeof symbolResolver === "function"
+ ? getFunctionId(symbolResolver)
+ : undefined,
+ },
+ ] as const,
+ queryFn: async () =>
+ fetchTokenSymbol({ symbolResolver, address, chain, client }),
...queryOptions,
});
@@ -195,7 +183,48 @@ export function TokenSymbol({
return fallbackComponent || null;
}
- const displayValue = formatFn ? formatFn(symbolQuery.data) : symbolQuery.data;
+ if (formatFn && typeof formatFn === "function") {
+ return {formatFn(symbolQuery.data)};
+ }
+
+ return {symbolQuery.data};
+}
+
+/**
+ * @internal Exported for tests only
+ */
+export async function fetchTokenSymbol(props: {
+ address: string;
+ client: ThirdwebClient;
+ chain: Chain;
+ symbolResolver?: string | (() => string) | (() => Promise);
+}): Promise {
+ const { symbolResolver, address, client, chain } = props;
+ if (typeof symbolResolver === "string") {
+ return symbolResolver;
+ }
+ if (typeof symbolResolver === "function") {
+ return symbolResolver();
+ }
+ if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
+ // Don't wanna use `getChainSymbol` because it has some side effect (it catches error and defaults to "ETH")
+ return getChainMetadata(chain).then((data) => data.nativeCurrency.symbol);
+ }
- return {displayValue};
+ // Try to fetch the symbol from both the `symbol` function and the contract metadata
+ // then prioritize its result
+ const contract = getContract({ address, client, chain });
+ const [_symbol, contractMetadata] = await Promise.all([
+ symbol({ contract }).catch(() => undefined),
+ getContractMetadata({ contract }).catch(() => undefined),
+ ]);
+ if (typeof _symbol === "string") {
+ return _symbol;
+ }
+ if (typeof contractMetadata?.symbol === "string") {
+ return contractMetadata.symbol;
+ }
+ throw new Error(
+ "Failed to resolve symbol from both symbol() and contract metadata",
+ );
}