Skip to content

Commit e2424f5

Browse files
committed
update
1 parent f25d1d8 commit e2424f5

File tree

11 files changed

+364
-72
lines changed

11 files changed

+364
-72
lines changed

.changeset/fluffy-pets-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Fix caching issues for headless component; improve code coverage

packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
22
import type { JSX } from "react";
33
import { getChainMetadata } from "../../../../../chains/utils.js";
44
import type { ThirdwebClient } from "../../../../../client/client.js";
5+
import { getFunctionId } from "../../../../../utils/function-id.js";
56
import { resolveScheme } from "../../../../../utils/ipfs.js";
67
import { useChainContext } from "./provider.js";
78

@@ -119,7 +120,18 @@ export function ChainIcon({
119120
}: ChainIconProps) {
120121
const { chain } = useChainContext();
121122
const iconQuery = useQuery({
122-
queryKey: ["_internal_chain_icon_", chain.id] as const,
123+
queryKey: [
124+
"_internal_chain_icon_",
125+
chain.id,
126+
{
127+
resolver:
128+
typeof iconResolver === "string"
129+
? iconResolver
130+
: typeof iconResolver === "function"
131+
? getFunctionId(iconResolver)
132+
: undefined,
133+
},
134+
] as const,
123135
queryFn: async () => {
124136
if (typeof iconResolver === "string") {
125137
return iconResolver;

packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type React from "react";
55
import type { JSX } from "react";
66
import { getChainMetadata } from "../../../../../chains/utils.js";
7+
import { getFunctionId } from "../../../../../utils/function-id.js";
78
import { useChainContext } from "./provider.js";
89

910
/**
@@ -155,7 +156,18 @@ export function ChainName({
155156
}: ChainNameProps) {
156157
const { chain } = useChainContext();
157158
const nameQuery = useQuery({
158-
queryKey: ["_internal_chain_name_", chain.id] as const,
159+
queryKey: [
160+
"_internal_chain_name_",
161+
chain.id,
162+
{
163+
resolver:
164+
typeof nameResolver === "string"
165+
? nameResolver
166+
: typeof nameResolver === "function"
167+
? getFunctionId(nameResolver)
168+
: undefined,
169+
},
170+
] as const,
159171
queryFn: async () => {
160172
if (typeof nameResolver === "string") {
161173
return nameResolver;

packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type { JSX } from "react";
55
import type { ThirdwebContract } from "../../../../../contract/contract.js";
6+
import { getFunctionId } from "../../../../../utils/function-id.js";
67
import { useNFTContext } from "./provider.js";
78
import { getNFTInfo } from "./utils.js";
89

@@ -100,7 +101,7 @@ export function NFTDescription({
100101
typeof descriptionResolver === "string"
101102
? descriptionResolver
102103
: typeof descriptionResolver === "function"
103-
? descriptionResolver.toString()
104+
? getFunctionId(descriptionResolver)
104105
: undefined,
105106
},
106107
],

packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
22
import type { JSX } from "react";
33
import type { ThirdwebContract } from "../../../../../contract/contract.js";
4+
import { getFunctionId } from "../../../../../utils/function-id.js";
45
import { MediaRenderer } from "../../MediaRenderer/MediaRenderer.js";
56
import type { MediaRendererProps } from "../../MediaRenderer/types.js";
67
import { useNFTContext } from "./provider.js";
@@ -138,7 +139,7 @@ export function NFTMedia({
138139
typeof mediaResolver === "object"
139140
? mediaResolver
140141
: typeof mediaResolver === "function"
141-
? mediaResolver.toString()
142+
? getFunctionId(mediaResolver)
142143
: undefined,
143144
},
144145
],

packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
22
import type { JSX } from "react";
33
import type { ThirdwebContract } from "../../../../../contract/contract.js";
4+
import { getFunctionId } from "../../../../../utils/function-id.js";
45
import { useNFTContext } from "./provider.js";
56
import { getNFTInfo } from "./utils.js";
67

@@ -100,7 +101,7 @@ export function NFTName({
100101
typeof nameResolver === "string"
101102
? nameResolver
102103
: typeof nameResolver === "function"
103-
? nameResolver.toString()
104+
? getFunctionId(nameResolver)
104105
: undefined,
105106
},
106107
],

packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getChainMetadata } from "../../../../../chains/utils.js";
44
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
55
import { getContract } from "../../../../../contract/contract.js";
66
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
7+
import { getFunctionId } from "../../../../../utils/function-id.js";
78
import { resolveScheme } from "../../../../../utils/ipfs.js";
89
import { useTokenContext } from "./provider.js";
910

@@ -115,7 +116,19 @@ export function TokenIcon({
115116
}: TokenIconProps) {
116117
const { address, client, chain } = useTokenContext();
117118
const iconQuery = useQuery({
118-
queryKey: ["_internal_token_icon_", chain.id, address] as const,
119+
queryKey: [
120+
"_internal_token_icon_",
121+
chain.id,
122+
address,
123+
{
124+
resolver:
125+
typeof iconResolver === "string"
126+
? iconResolver
127+
: typeof iconResolver === "function"
128+
? getFunctionId(iconResolver)
129+
: undefined,
130+
},
131+
] as const,
119132
queryFn: async () => {
120133
if (typeof iconResolver === "string") {
121134
return iconResolver;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, expect, it } from "vitest";
2+
import { ANVIL_CHAIN } from "~test/chains.js";
3+
import { render, screen, waitFor } from "~test/react-render.js";
4+
import { TEST_CLIENT } from "~test/test-clients.js";
5+
import {
6+
UNISWAPV3_FACTORY_CONTRACT,
7+
USDT_CONTRACT,
8+
} from "~test/test-contracts.js";
9+
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
10+
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
11+
import { TokenName, fetchTokenName } from "./name.js";
12+
import { TokenProvider } from "./provider.js";
13+
14+
const client = TEST_CLIENT;
15+
16+
describe.runIf(process.env.TW_SECRET_KEY)("TokenName component", () => {
17+
it("should render", async () => {
18+
render(
19+
<TokenProvider
20+
address={NATIVE_TOKEN_ADDRESS}
21+
client={client}
22+
chain={ethereum}
23+
>
24+
<TokenName className="tw-name" />
25+
</TokenProvider>,
26+
);
27+
28+
await waitFor(() =>
29+
expect(
30+
screen.getByText("Ether", {
31+
exact: true,
32+
selector: "span",
33+
}),
34+
).toBeInTheDocument(),
35+
);
36+
});
37+
38+
it("fetchTokenName should respect the nameResolver being a string", async () => {
39+
const res = await fetchTokenName({
40+
address: "thing",
41+
client,
42+
chain: ANVIL_CHAIN,
43+
nameResolver: "tw",
44+
});
45+
expect(res).toBe("tw");
46+
});
47+
48+
it("fetchTokenName should respect the nameResolver being a non-async function", async () => {
49+
const res = await fetchTokenName({
50+
address: "thing",
51+
client,
52+
chain: ANVIL_CHAIN,
53+
nameResolver: () => "tw",
54+
});
55+
56+
expect(res).toBe("tw");
57+
});
58+
59+
it("fetchTokenName should respect the nameResolver being an async function", async () => {
60+
const res = await fetchTokenName({
61+
address: "thing",
62+
client,
63+
chain: ANVIL_CHAIN,
64+
nameResolver: async () => {
65+
await new Promise((resolve) => setTimeout(resolve, 2000));
66+
return "tw";
67+
},
68+
});
69+
70+
expect(res).toBe("tw");
71+
});
72+
73+
it("fetchTokenName should work for contract with `name` function", async () => {
74+
const res = await fetchTokenName({
75+
address: USDT_CONTRACT.address,
76+
client,
77+
chain: USDT_CONTRACT.chain,
78+
});
79+
80+
expect(res).toBe("Tether USD");
81+
});
82+
83+
it("fetchTokenName should work for native token", async () => {
84+
const res = await fetchTokenName({
85+
address: NATIVE_TOKEN_ADDRESS,
86+
client,
87+
chain: ethereum,
88+
});
89+
90+
expect(res).toBe("Ether");
91+
});
92+
93+
it("fetchTokenName should try to fallback to the contract metadata if fails to resolves from `name()`", async () => {
94+
// todo: find a contract with name in contractMetadata, but does not have a name function
95+
});
96+
97+
it("fetchTokenName should throw in the end where all fallback solutions failed to resolve to any name", async () => {
98+
await expect(() =>
99+
fetchTokenName({
100+
address: UNISWAPV3_FACTORY_CONTRACT.address,
101+
client,
102+
chain: UNISWAPV3_FACTORY_CONTRACT.chain,
103+
}),
104+
).rejects.toThrowError(
105+
"Failed to resolve name from both name() and contract metadata",
106+
);
107+
});
108+
});

packages/thirdweb/src/react/web/ui/prebuilt/Token/name.tsx

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type React from "react";
55
import type { JSX } from "react";
6+
import type { Chain } from "../../../../../chains/types.js";
67
import { getChainMetadata } from "../../../../../chains/utils.js";
8+
import type { ThirdwebClient } from "../../../../../client/client.js";
79
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
810
import { getContract } from "../../../../../contract/contract.js";
911
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
1012
import { name } from "../../../../../extensions/common/read/name.js";
13+
import { getFunctionId } from "../../../../../utils/function-id.js";
1114
import { useTokenContext } from "./provider.js";
1215

1316
/**
@@ -157,33 +160,21 @@ export function TokenName({
157160
}: TokenNameProps) {
158161
const { address, client, chain } = useTokenContext();
159162
const nameQuery = useQuery({
160-
queryKey: ["_internal_token_name_", chain.id, address] as const,
161-
queryFn: async () => {
162-
if (typeof nameResolver === "string") {
163-
return nameResolver;
164-
}
165-
if (typeof nameResolver === "function") {
166-
return nameResolver();
167-
}
168-
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
169-
// Don't wanna use `getChainNativeCurrencyName` because it has some side effect (it catches error and defaults to "ETH")
170-
return getChainMetadata(chain).then((data) => data.nativeCurrency.name);
171-
}
172-
// Try to fetch the name from both the `name()` function and the contract metadata
173-
// then prioritize the `name()`
174-
const contract = getContract({ address, client, chain });
175-
const [_name, contractMetadata] = await Promise.all([
176-
name({ contract }),
177-
getContractMetadata({ contract }),
178-
]);
179-
if (!_name && !contractMetadata.name) {
180-
throw new Error(
181-
"Failed to resolve name from both name() and contract metadata",
182-
);
183-
}
184-
185-
return _name || contractMetadata.name;
186-
},
163+
queryKey: [
164+
"_internal_token_name_",
165+
chain.id,
166+
address,
167+
{
168+
resolver:
169+
typeof nameResolver === "string"
170+
? nameResolver
171+
: typeof nameResolver === "function"
172+
? getFunctionId(nameResolver)
173+
: undefined,
174+
},
175+
] as const,
176+
queryFn: async () =>
177+
fetchTokenName({ address, chain, client, nameResolver }),
187178
...queryOptions,
188179
});
189180

@@ -195,7 +186,48 @@ export function TokenName({
195186
return fallbackComponent || null;
196187
}
197188

198-
const displayValue = formatFn ? formatFn(nameQuery.data) : nameQuery.data;
189+
if (formatFn && typeof formatFn === "function") {
190+
return <span {...restProps}>{formatFn(nameQuery.data)}</span>;
191+
}
192+
193+
return <span {...restProps}>{nameQuery.data}</span>;
194+
}
199195

200-
return <span {...restProps}>{displayValue}</span>;
196+
/**
197+
* @internal Exported for tests only
198+
*/
199+
export async function fetchTokenName(props: {
200+
address: string;
201+
client: ThirdwebClient;
202+
chain: Chain;
203+
nameResolver?: string | (() => string) | (() => Promise<string>);
204+
}) {
205+
const { nameResolver, address, client, chain } = props;
206+
if (typeof nameResolver === "string") {
207+
return nameResolver;
208+
}
209+
if (typeof nameResolver === "function") {
210+
return nameResolver();
211+
}
212+
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
213+
// Don't wanna use `getChainName` because it has some side effect (it catches error and defaults to "ETH")
214+
return getChainMetadata(chain).then((data) => data.nativeCurrency.name);
215+
}
216+
217+
// Try to fetch the name from both the `name` function and the contract metadata
218+
// then prioritize its result
219+
const contract = getContract({ address, client, chain });
220+
const [_name, contractMetadata] = await Promise.all([
221+
name({ contract }).catch(() => undefined),
222+
getContractMetadata({ contract }).catch(() => undefined),
223+
]);
224+
if (typeof _name === "string") {
225+
return _name;
226+
}
227+
if (typeof contractMetadata?.name === "string") {
228+
return contractMetadata.name;
229+
}
230+
throw new Error(
231+
"Failed to resolve name from both name() and contract metadata",
232+
);
201233
}

0 commit comments

Comments
 (0)