Skip to content

Commit f1d1277

Browse files
committed
update
1 parent f69d1aa commit f1d1277

File tree

7 files changed

+348
-69
lines changed

7 files changed

+348
-69
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,18 @@ export function ChainIcon({
119119
}: ChainIconProps) {
120120
const { chain } = useChainContext();
121121
const iconQuery = useQuery({
122-
queryKey: ["_internal_chain_icon_", chain.id] as const,
122+
queryKey: [
123+
"_internal_chain_icon_",
124+
chain.id,
125+
{
126+
resolver:
127+
typeof iconResolver === "string"
128+
? iconResolver
129+
: typeof iconResolver === "function"
130+
? iconResolver.toString()
131+
: undefined,
132+
},
133+
] as const,
123134
queryFn: async () => {
124135
if (typeof iconResolver === "string") {
125136
return iconResolver;

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,18 @@ export function ChainName({
155155
}: ChainNameProps) {
156156
const { chain } = useChainContext();
157157
const nameQuery = useQuery({
158-
queryKey: ["_internal_chain_name_", chain.id] as const,
158+
queryKey: [
159+
"_internal_chain_name_",
160+
chain.id,
161+
{
162+
resolver:
163+
typeof nameResolver === "string"
164+
? nameResolver
165+
: typeof nameResolver === "function"
166+
? nameResolver.toString()
167+
: undefined,
168+
},
169+
] as const,
159170
queryFn: async () => {
160171
if (typeof nameResolver === "string") {
161172
return nameResolver;

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,19 @@ export function TokenIcon({
115115
}: TokenIconProps) {
116116
const { address, client, chain } = useTokenContext();
117117
const iconQuery = useQuery({
118-
queryKey: ["_internal_token_icon_", chain.id, address] as const,
118+
queryKey: [
119+
"_internal_token_icon_",
120+
chain.id,
121+
address,
122+
{
123+
resolver:
124+
typeof iconResolver === "string"
125+
? iconResolver
126+
: typeof iconResolver === "function"
127+
? iconResolver.toString()
128+
: undefined,
129+
},
130+
] as const,
119131
queryFn: async () => {
120132
if (typeof iconResolver === "string") {
121133
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: 60 additions & 29 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 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";
@@ -157,33 +159,21 @@ export function TokenName({
157159
}: TokenNameProps) {
158160
const { address, client, chain } = useTokenContext();
159161
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-
},
162+
queryKey: [
163+
"_internal_token_name_",
164+
chain.id,
165+
address,
166+
{
167+
resolver:
168+
typeof nameResolver === "string"
169+
? nameResolver
170+
: typeof nameResolver === "function"
171+
? nameResolver.toString()
172+
: undefined,
173+
},
174+
] as const,
175+
queryFn: async () =>
176+
fetchTokenName({ address, chain, client, nameResolver }),
187177
...queryOptions,
188178
});
189179

@@ -195,7 +185,48 @@ export function TokenName({
195185
return fallbackComponent || null;
196186
}
197187

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

200-
return <span {...restProps}>{displayValue}</span>;
195+
/**
196+
* @internal Exported for tests only
197+
*/
198+
export async function fetchTokenName(props: {
199+
address: string;
200+
client: ThirdwebClient;
201+
chain: Chain;
202+
nameResolver?: string | (() => string) | (() => Promise<string>);
203+
}) {
204+
const { nameResolver, address, client, chain } = props;
205+
if (typeof nameResolver === "string") {
206+
return nameResolver;
207+
}
208+
if (typeof nameResolver === "function") {
209+
return nameResolver();
210+
}
211+
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
212+
// Don't wanna use `getChainName` because it has some side effect (it catches error and defaults to "ETH")
213+
return getChainMetadata(chain).then((data) => data.nativeCurrency.name);
214+
}
215+
216+
// Try to fetch the name from both the `name` function and the contract metadata
217+
// then prioritize its result
218+
const contract = getContract({ address, client, chain });
219+
const [_name, contractMetadata] = await Promise.all([
220+
name({ contract }).catch(() => undefined),
221+
getContractMetadata({ contract }).catch(() => undefined),
222+
]);
223+
if (typeof _name === "string") {
224+
return _name;
225+
}
226+
if (typeof contractMetadata?.name === "string") {
227+
return contractMetadata.name;
228+
}
229+
throw new Error(
230+
"Failed to resolve name from both name() and contract metadata",
231+
);
201232
}
Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
import { describe, expect, it } from "vitest";
2+
import { ANVIL_CHAIN } from "~test/chains.js";
23
import { render, screen, waitFor } from "~test/react-render.js";
34
import { TEST_CLIENT } from "~test/test-clients.js";
5+
import {
6+
UNISWAPV3_FACTORY_CONTRACT,
7+
USDT_CONTRACT,
8+
} from "~test/test-contracts.js";
49
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
510
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
611
import { TokenProvider } from "./provider.js";
7-
import { TokenSymbol } from "./symbol.js";
12+
import { TokenSymbol, fetchTokenSymbol } from "./symbol.js";
13+
14+
const client = TEST_CLIENT;
815

916
describe.runIf(process.env.TW_SECRET_KEY)("TokenSymbol component", () => {
10-
it("should pass the address correctly to the children props", () => {
17+
it("should render", async () => {
1118
render(
1219
<TokenProvider
1320
address={NATIVE_TOKEN_ADDRESS}
14-
client={TEST_CLIENT}
21+
client={client}
1522
chain={ethereum}
1623
>
17-
<TokenSymbol />
24+
<TokenSymbol className="tw-token-symbol" />
1825
</TokenProvider>,
1926
);
2027

21-
waitFor(() =>
28+
await waitFor(() =>
2229
expect(
2330
screen.getByText("ETH", {
2431
exact: true,
@@ -27,4 +34,75 @@ describe.runIf(process.env.TW_SECRET_KEY)("TokenSymbol component", () => {
2734
).toBeInTheDocument(),
2835
);
2936
});
37+
38+
it("fetchTokenSymbol should respect the symbolResolver being a string", async () => {
39+
const res = await fetchTokenSymbol({
40+
address: "thing",
41+
client,
42+
chain: ANVIL_CHAIN,
43+
symbolResolver: "tw",
44+
});
45+
expect(res).toBe("tw");
46+
});
47+
48+
it("fetchTokenSymbol should respect the symbolResolver being a non-async function", async () => {
49+
const res = await fetchTokenSymbol({
50+
address: "thing",
51+
client,
52+
chain: ANVIL_CHAIN,
53+
symbolResolver: () => "tw",
54+
});
55+
56+
expect(res).toBe("tw");
57+
});
58+
59+
it("fetchTokenSymbol should respect the symbolResolver being an async function", async () => {
60+
const res = await fetchTokenSymbol({
61+
address: "thing",
62+
client,
63+
chain: ANVIL_CHAIN,
64+
symbolResolver: async () => {
65+
await new Promise((resolve) => setTimeout(resolve, 2000));
66+
return "tw";
67+
},
68+
});
69+
70+
expect(res).toBe("tw");
71+
});
72+
73+
it("fetchTokenSymbol should work for contract with `symbol` function", async () => {
74+
const res = await fetchTokenSymbol({
75+
address: USDT_CONTRACT.address,
76+
client,
77+
chain: USDT_CONTRACT.chain,
78+
});
79+
80+
expect(res).toBe("USDT");
81+
});
82+
83+
it("fetchTokenSymbol should work for native token", async () => {
84+
const res = await fetchTokenSymbol({
85+
address: NATIVE_TOKEN_ADDRESS,
86+
client,
87+
chain: ethereum,
88+
});
89+
90+
expect(res).toBe("ETH");
91+
});
92+
93+
it("fetchTokenSymbol should try to fallback to the contract metadata if fails to resolves from `symbol()`", async () => {
94+
// todo: find a contract with symbol in contractMetadata, but does not have a symbol function
95+
});
96+
97+
it("fetchTokenSymbol should throw in the end where all fallback solutions failed to resolve to any symbol", async () => {
98+
await expect(() =>
99+
fetchTokenSymbol({
100+
address: UNISWAPV3_FACTORY_CONTRACT.address,
101+
client,
102+
chain: UNISWAPV3_FACTORY_CONTRACT.chain,
103+
}),
104+
).rejects.toThrowError(
105+
"Failed to resolve symbol from both symbol() and contract metadata",
106+
);
107+
});
30108
});

0 commit comments

Comments
 (0)