Skip to content

Commit bfe0d2c

Browse files
committed
update
1 parent f25d1d8 commit bfe0d2c

File tree

13 files changed

+455
-85
lines changed

13 files changed

+455
-85
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.test.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
22
import { render, screen, waitFor } from "~test/react-render.js";
33
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
44
import { defineChain } from "../../../../../chains/utils.js";
5-
import { ChainName } from "./name.js";
5+
import { ChainName, fetchChainName } from "./name.js";
66
import { ChainProvider } from "./provider.js";
77

88
describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
@@ -70,4 +70,31 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
7070
).toBeInTheDocument(),
7171
);
7272
});
73+
74+
it("fetchChainName should respect nameResolver as a string", async () => {
75+
const res = await fetchChainName({
76+
chain: ethereum,
77+
nameResolver: "eth_mainnet",
78+
});
79+
expect(res).toBe("eth_mainnet");
80+
});
81+
82+
it("fetchChainName should respect nameResolver as a non-async function", async () => {
83+
const res = await fetchChainName({
84+
chain: ethereum,
85+
nameResolver: () => "eth_mainnet",
86+
});
87+
expect(res).toBe("eth_mainnet");
88+
});
89+
90+
it("fetchChainName should respect nameResolver as an async function", async () => {
91+
const res = await fetchChainName({
92+
chain: ethereum,
93+
nameResolver: async () => {
94+
await new Promise((resolve) => setTimeout(resolve, 2000));
95+
return "eth_mainnet";
96+
},
97+
});
98+
expect(res).toBe("eth_mainnet");
99+
});
73100
});

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

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
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 { getFunctionId } from "../../../../../utils/function-id.js";
79
import { useChainContext } from "./provider.js";
810

911
/**
@@ -155,19 +157,19 @@ export function ChainName({
155157
}: ChainNameProps) {
156158
const { chain } = useChainContext();
157159
const nameQuery = useQuery({
158-
queryKey: ["_internal_chain_name_", chain.id] as const,
159-
queryFn: async () => {
160-
if (typeof nameResolver === "string") {
161-
return nameResolver;
162-
}
163-
if (typeof nameResolver === "function") {
164-
return nameResolver();
165-
}
166-
if (chain.name) {
167-
return chain.name;
168-
}
169-
return getChainMetadata(chain).then((data) => data.name);
170-
},
160+
queryKey: [
161+
"_internal_chain_name_",
162+
chain.id,
163+
{
164+
resolver:
165+
typeof nameResolver === "string"
166+
? nameResolver
167+
: typeof nameResolver === "function"
168+
? getFunctionId(nameResolver)
169+
: undefined,
170+
},
171+
] as const,
172+
queryFn: async () => fetchChainName({ chain, nameResolver }),
171173
...queryOptions,
172174
});
173175

@@ -183,3 +185,23 @@ export function ChainName({
183185

184186
return <span {...restProps}>{displayValue}</span>;
185187
}
188+
189+
/**
190+
* @internal Exported for tests only
191+
*/
192+
export async function fetchChainName(props: {
193+
chain: Chain;
194+
nameResolver?: string | (() => string) | (() => Promise<string>);
195+
}) {
196+
const { nameResolver, chain } = props;
197+
if (typeof nameResolver === "string") {
198+
return nameResolver;
199+
}
200+
if (typeof nameResolver === "function") {
201+
return nameResolver();
202+
}
203+
if (chain.name) {
204+
return chain.name;
205+
}
206+
return getChainMetadata(chain).then((data) => data.name);
207+
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { describe, expect, it } from "vitest";
2+
import { ANVIL_CHAIN } from "~test/chains.js";
3+
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
4+
import { TEST_CLIENT } from "~test/test-clients.js";
25
import {
36
DOODLES_CONTRACT,
47
DROP1155_CONTRACT,
58
UNISWAPV3_FACTORY_CONTRACT,
69
} from "~test/test-contracts.js";
10+
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
11+
import { getContract } from "../../../../../contract/contract.js";
12+
import { mintTo } from "../../../../../extensions/erc721/write/mintTo.js";
13+
import { deployERC721Contract } from "../../../../../extensions/prebuilts/deploy-erc721.js";
14+
import { sendAndConfirmTransaction } from "../../../../../transaction/actions/send-and-confirm-transaction.js";
715
import { fetchNftDescription } from "./description.js";
816

917
describe.runIf(process.env.TW_SECRET_KEY)("NFTDescription", () => {
@@ -60,4 +68,37 @@ describe.runIf(process.env.TW_SECRET_KEY)("NFTDescription", () => {
6068
}),
6169
).rejects.toThrowError("Failed to resolve NFT info");
6270
});
71+
72+
it("fetchNftDescription should throw error if description is not of type string", async () => {
73+
const address = await deployERC721Contract({
74+
client: TEST_CLIENT,
75+
chain: ANVIL_CHAIN,
76+
account: TEST_ACCOUNT_A,
77+
type: "TokenERC721",
78+
params: {
79+
name: "NFTCollection",
80+
contractURI: TEST_CONTRACT_URI,
81+
},
82+
});
83+
const contract = getContract({
84+
client: TEST_CLIENT,
85+
chain: ANVIL_CHAIN,
86+
address,
87+
});
88+
const transaction = mintTo({
89+
contract,
90+
nft: { name: "token 0" },
91+
to: TEST_ACCOUNT_A.address,
92+
});
93+
await sendAndConfirmTransaction({
94+
transaction,
95+
account: TEST_ACCOUNT_A,
96+
});
97+
await expect(() =>
98+
fetchNftDescription({
99+
contract: contract,
100+
tokenId: 0n,
101+
}),
102+
).rejects.toThrowError("Failed to resolve NFT description");
103+
});
63104
});

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+
});

0 commit comments

Comments
 (0)