Skip to content

Commit 000438f

Browse files
committed
update
1 parent f69d1aa commit 000438f

File tree

6 files changed

+185
-56
lines changed

6 files changed

+185
-56
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;

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,19 @@ export function TokenName({
157157
}: TokenNameProps) {
158158
const { address, client, chain } = useTokenContext();
159159
const nameQuery = useQuery({
160-
queryKey: ["_internal_token_name_", chain.id, address] as const,
160+
queryKey: [
161+
"_internal_token_name_",
162+
chain.id,
163+
address,
164+
{
165+
resolver:
166+
typeof nameResolver === "string"
167+
? nameResolver
168+
: typeof nameResolver === "function"
169+
? nameResolver.toString()
170+
: undefined,
171+
},
172+
] as const,
161173
queryFn: async () => {
162174
if (typeof nameResolver === "string") {
163175
return nameResolver;
Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,85 @@
11
import { describe, expect, it } from "vitest";
2-
import { render, screen, waitFor } from "~test/react-render.js";
2+
import { ANVIL_CHAIN } from "~test/chains.js";
33
import { TEST_CLIENT } from "~test/test-clients.js";
4+
import {
5+
UNISWAPV3_FACTORY_CONTRACT,
6+
USDT_CONTRACT,
7+
} from "~test/test-contracts.js";
48
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
59
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
6-
import { TokenProvider } from "./provider.js";
7-
import { TokenSymbol } from "./symbol.js";
10+
import { fetchTokenSymbol } from "./symbol.js";
11+
12+
const client = TEST_CLIENT;
813

914
describe.runIf(process.env.TW_SECRET_KEY)("TokenSymbol component", () => {
10-
it("should pass the address correctly to the children props", () => {
11-
render(
12-
<TokenProvider
13-
address={NATIVE_TOKEN_ADDRESS}
14-
client={TEST_CLIENT}
15-
chain={ethereum}
16-
>
17-
<TokenSymbol />
18-
</TokenProvider>,
19-
);
15+
it("fetchTokenSymbol should respect the symbolResolver being a string", async () => {
16+
const res = await fetchTokenSymbol({
17+
address: "thing",
18+
client,
19+
chain: ANVIL_CHAIN,
20+
symbolResolver: "tw",
21+
});
22+
expect(res).toBe("tw");
23+
});
24+
25+
it("fetchTokenSymbol should respect the symbolResolver being a non-async function", async () => {
26+
const res = await fetchTokenSymbol({
27+
address: "thing",
28+
client,
29+
chain: ANVIL_CHAIN,
30+
symbolResolver: () => "tw",
31+
});
32+
33+
expect(res).toBe("tw");
34+
});
35+
36+
it("fetchTokenSymbol should respect the symbolResolver being an async function", async () => {
37+
const res = await fetchTokenSymbol({
38+
address: "thing",
39+
client,
40+
chain: ANVIL_CHAIN,
41+
symbolResolver: async () => {
42+
await new Promise((resolve) => setTimeout(resolve, 2000));
43+
return "tw";
44+
},
45+
});
46+
47+
expect(res).toBe("tw");
48+
});
49+
50+
it("fetchTokenSymbol should work for contract with `symbol` function", async () => {
51+
const res = await fetchTokenSymbol({
52+
address: USDT_CONTRACT.address,
53+
client,
54+
chain: USDT_CONTRACT.chain,
55+
});
56+
57+
expect(res).toBe("USDT");
58+
});
59+
60+
it("fetchTokenSymbol should work for native token", async () => {
61+
const res = await fetchTokenSymbol({
62+
address: NATIVE_TOKEN_ADDRESS,
63+
client,
64+
chain: ethereum,
65+
});
66+
67+
expect(res).toBe("ETH");
68+
});
69+
70+
it("fetchTokenSymbol should try to fallback to the contract metadata if fails to resolves from `symbol()`", async () => {
71+
// todo: find a contract with symbol in contractMetadata, but does not have a symbol function
72+
});
2073

21-
waitFor(() =>
22-
expect(
23-
screen.getByText("ETH", {
24-
exact: true,
25-
selector: "span",
26-
}),
27-
).toBeInTheDocument(),
74+
it("fetchTokenSymbol should throw in the end where all fallback solutions failed to resolve to any symbol", async () => {
75+
await expect(() =>
76+
fetchTokenSymbol({
77+
address: UNISWAPV3_FACTORY_CONTRACT.address,
78+
client,
79+
chain: UNISWAPV3_FACTORY_CONTRACT.chain,
80+
}),
81+
).rejects.toThrowError(
82+
"Failed to resolve symbol from both symbol() and contract metadata",
2883
);
2984
});
3085
});

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

Lines changed: 60 additions & 32 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";
@@ -154,36 +156,21 @@ export function TokenSymbol({
154156
}: TokenSymbolProps) {
155157
const { address, client, chain } = useTokenContext();
156158
const symbolQuery = useQuery({
157-
queryKey: ["_internal_token_symbol_", chain.id, address] as const,
158-
queryFn: async () => {
159-
if (typeof symbolResolver === "string") {
160-
return symbolResolver;
161-
}
162-
if (typeof symbolResolver === "function") {
163-
return symbolResolver();
164-
}
165-
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
166-
// Don't wanna use `getChainSymbol` because it has some side effect (it catches error and defaults to "ETH")
167-
return getChainMetadata(chain).then(
168-
(data) => data.nativeCurrency.symbol,
169-
);
170-
}
171-
172-
// Try to fetch the symbol from both the `symbol` function and the contract metadata
173-
// then prioritize the `symbol()`
174-
const contract = getContract({ address, client, chain });
175-
const [_symbol, contractMetadata] = await Promise.all([
176-
symbol({ contract }),
177-
getContractMetadata({ contract }),
178-
]);
179-
if (!_symbol && !contractMetadata.symbol) {
180-
throw new Error(
181-
"Failed to resolve symbol from both symbol() and contract metadata",
182-
);
183-
}
184-
185-
return _symbol || contractMetadata.symbol;
186-
},
159+
queryKey: [
160+
"_internal_token_symbol_",
161+
chain.id,
162+
address,
163+
{
164+
resolver:
165+
typeof symbolResolver === "string"
166+
? symbolResolver
167+
: typeof symbolResolver === "function"
168+
? symbolResolver.toString()
169+
: undefined,
170+
},
171+
] as const,
172+
queryFn: async () =>
173+
fetchTokenSymbol({ symbolResolver, address, chain, client }),
187174
...queryOptions,
188175
});
189176

@@ -195,7 +182,48 @@ export function TokenSymbol({
195182
return fallbackComponent || null;
196183
}
197184

198-
const displayValue = formatFn ? formatFn(symbolQuery.data) : symbolQuery.data;
185+
if (formatFn && typeof formatFn === "function") {
186+
return <span {...restProps}>{formatFn(symbolQuery.data)}</span>;
187+
}
188+
189+
return <span {...restProps}>{symbolQuery.data}</span>;
190+
}
191+
192+
/**
193+
* @internal Exported for tests only
194+
*/
195+
export async function fetchTokenSymbol(props: {
196+
address: string;
197+
client: ThirdwebClient;
198+
chain: Chain;
199+
symbolResolver?: string | (() => string) | (() => Promise<string>);
200+
}): Promise<string> {
201+
const { symbolResolver, address, client, chain } = props;
202+
if (typeof symbolResolver === "string") {
203+
return symbolResolver;
204+
}
205+
if (typeof symbolResolver === "function") {
206+
return symbolResolver();
207+
}
208+
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
209+
// Don't wanna use `getChainSymbol` because it has some side effect (it catches error and defaults to "ETH")
210+
return getChainMetadata(chain).then((data) => data.nativeCurrency.symbol);
211+
}
199212

200-
return <span {...restProps}>{displayValue}</span>;
213+
// Try to fetch the symbol from both the `symbol` function and the contract metadata
214+
// then prioritize the `symbol()`
215+
const contract = getContract({ address, client, chain });
216+
const [_symbol, contractMetadata] = await Promise.all([
217+
symbol({ contract }).catch(() => undefined),
218+
getContractMetadata({ contract }).catch(() => undefined),
219+
]);
220+
if (typeof _symbol === "string") {
221+
return _symbol;
222+
}
223+
if (typeof contractMetadata?.symbol === "string") {
224+
return contractMetadata.symbol;
225+
}
226+
throw new Error(
227+
"Failed to resolve symbol from both symbol() and contract metadata",
228+
);
201229
}

0 commit comments

Comments
 (0)