Skip to content

Commit 65bb12e

Browse files
committed
update
1 parent f69d1aa commit 65bb12e

File tree

6 files changed

+193
-41
lines changed

6 files changed

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

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)