thirdweb 5.75.0 → 5.76.0-nightly-015293eb826b346871696183e96d49da44f31393-20241209000527

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/cjs/exports/react.js.map +1 -1
  2. package/dist/cjs/exports/utils.js +5 -1
  3. package/dist/cjs/exports/utils.js.map +1 -1
  4. package/dist/cjs/pay/convert/cryptoToFiat.js.map +1 -1
  5. package/dist/cjs/pay/convert/fiatToCrypto.js +1 -1
  6. package/dist/cjs/pay/convert/fiatToCrypto.js.map +1 -1
  7. package/dist/cjs/pay/convert/type.js +4 -0
  8. package/dist/cjs/pay/convert/type.js.map +1 -0
  9. package/dist/cjs/react/web/ui/ConnectWallet/Details.js +56 -16
  10. package/dist/cjs/react/web/ui/ConnectWallet/Details.js.map +1 -1
  11. package/dist/cjs/react/web/ui/ConnectWallet/screens/ReceiveFunds.js +1 -1
  12. package/dist/cjs/react/web/ui/ConnectWallet/screens/ReceiveFunds.js.map +1 -1
  13. package/dist/cjs/react/web/ui/ConnectWallet/screens/SendFunds.js +3 -2
  14. package/dist/cjs/react/web/ui/ConnectWallet/screens/SendFunds.js.map +1 -1
  15. package/dist/cjs/react/web/ui/components/CopyIcon.js +1 -1
  16. package/dist/cjs/react/web/ui/components/CopyIcon.js.map +1 -1
  17. package/dist/cjs/react/web/ui/components/Skeleton.js +1 -1
  18. package/dist/cjs/react/web/ui/components/Skeleton.js.map +1 -1
  19. package/dist/cjs/react/web/ui/prebuilt/Account/balance.js +114 -26
  20. package/dist/cjs/react/web/ui/prebuilt/Account/balance.js.map +1 -1
  21. package/dist/cjs/react/web/ui/prebuilt/Account/provider.js +3 -0
  22. package/dist/cjs/react/web/ui/prebuilt/Account/provider.js.map +1 -1
  23. package/dist/cjs/utils/formatNumber.js +7 -1
  24. package/dist/cjs/utils/formatNumber.js.map +1 -1
  25. package/dist/cjs/utils/shortenLargeNumber.js +47 -0
  26. package/dist/cjs/utils/shortenLargeNumber.js.map +1 -0
  27. package/dist/cjs/version.js +1 -1
  28. package/dist/cjs/version.js.map +1 -1
  29. package/dist/cjs/wallets/manager/index.js +35 -24
  30. package/dist/cjs/wallets/manager/index.js.map +1 -1
  31. package/dist/esm/exports/react.js.map +1 -1
  32. package/dist/esm/exports/utils.js +2 -0
  33. package/dist/esm/exports/utils.js.map +1 -1
  34. package/dist/esm/pay/convert/cryptoToFiat.js.map +1 -1
  35. package/dist/esm/pay/convert/fiatToCrypto.js +1 -1
  36. package/dist/esm/pay/convert/fiatToCrypto.js.map +1 -1
  37. package/dist/esm/pay/convert/type.js +3 -0
  38. package/dist/esm/pay/convert/type.js.map +1 -0
  39. package/dist/esm/react/web/ui/ConnectWallet/Details.js +57 -23
  40. package/dist/esm/react/web/ui/ConnectWallet/Details.js.map +1 -1
  41. package/dist/esm/react/web/ui/ConnectWallet/screens/ReceiveFunds.js +1 -1
  42. package/dist/esm/react/web/ui/ConnectWallet/screens/ReceiveFunds.js.map +1 -1
  43. package/dist/esm/react/web/ui/ConnectWallet/screens/SendFunds.js +3 -3
  44. package/dist/esm/react/web/ui/ConnectWallet/screens/SendFunds.js.map +1 -1
  45. package/dist/esm/react/web/ui/components/CopyIcon.js +1 -1
  46. package/dist/esm/react/web/ui/components/CopyIcon.js.map +1 -1
  47. package/dist/esm/react/web/ui/components/Skeleton.js +1 -1
  48. package/dist/esm/react/web/ui/components/Skeleton.js.map +1 -1
  49. package/dist/esm/react/web/ui/prebuilt/Account/balance.js +113 -28
  50. package/dist/esm/react/web/ui/prebuilt/Account/balance.js.map +1 -1
  51. package/dist/esm/react/web/ui/prebuilt/Account/provider.js +3 -0
  52. package/dist/esm/react/web/ui/prebuilt/Account/provider.js.map +1 -1
  53. package/dist/esm/utils/formatNumber.js +7 -1
  54. package/dist/esm/utils/formatNumber.js.map +1 -1
  55. package/dist/esm/utils/shortenLargeNumber.js +44 -0
  56. package/dist/esm/utils/shortenLargeNumber.js.map +1 -0
  57. package/dist/esm/version.js +1 -1
  58. package/dist/esm/version.js.map +1 -1
  59. package/dist/esm/wallets/manager/index.js +33 -24
  60. package/dist/esm/wallets/manager/index.js.map +1 -1
  61. package/dist/types/exports/react.d.ts +1 -1
  62. package/dist/types/exports/react.d.ts.map +1 -1
  63. package/dist/types/exports/utils.d.ts +2 -0
  64. package/dist/types/exports/utils.d.ts.map +1 -1
  65. package/dist/types/pay/convert/cryptoToFiat.d.ts +2 -1
  66. package/dist/types/pay/convert/cryptoToFiat.d.ts.map +1 -1
  67. package/dist/types/pay/convert/fiatToCrypto.d.ts +2 -1
  68. package/dist/types/pay/convert/fiatToCrypto.d.ts.map +1 -1
  69. package/dist/types/pay/convert/type.d.ts +7 -0
  70. package/dist/types/pay/convert/type.d.ts.map +1 -0
  71. package/dist/types/react/core/hooks/connection/ConnectButtonProps.d.ts +11 -0
  72. package/dist/types/react/core/hooks/connection/ConnectButtonProps.d.ts.map +1 -1
  73. package/dist/types/react/web/ui/ConnectWallet/Details.d.ts +60 -0
  74. package/dist/types/react/web/ui/ConnectWallet/Details.d.ts.map +1 -1
  75. package/dist/types/react/web/ui/ConnectWallet/screens/ReceiveFunds.d.ts.map +1 -1
  76. package/dist/types/react/web/ui/ConnectWallet/screens/SendFunds.d.ts +15 -0
  77. package/dist/types/react/web/ui/ConnectWallet/screens/SendFunds.d.ts.map +1 -1
  78. package/dist/types/react/web/ui/components/CopyIcon.d.ts.map +1 -1
  79. package/dist/types/react/web/ui/components/Skeleton.d.ts +1 -0
  80. package/dist/types/react/web/ui/components/Skeleton.d.ts.map +1 -1
  81. package/dist/types/react/web/ui/prebuilt/Account/balance.d.ts +58 -9
  82. package/dist/types/react/web/ui/prebuilt/Account/balance.d.ts.map +1 -1
  83. package/dist/types/react/web/ui/prebuilt/Account/provider.d.ts.map +1 -1
  84. package/dist/types/utils/formatNumber.d.ts +7 -1
  85. package/dist/types/utils/formatNumber.d.ts.map +1 -1
  86. package/dist/types/utils/shortenLargeNumber.d.ts +16 -0
  87. package/dist/types/utils/shortenLargeNumber.d.ts.map +1 -0
  88. package/dist/types/version.d.ts +1 -1
  89. package/dist/types/version.d.ts.map +1 -1
  90. package/dist/types/wallets/manager/index.d.ts +4 -0
  91. package/dist/types/wallets/manager/index.d.ts.map +1 -1
  92. package/dist/types/wallets/wallet-connect/types.d.ts +1 -1
  93. package/dist/types/wallets/wallet-connect/types.d.ts.map +1 -1
  94. package/dist/types/wallets/wallet-types.d.ts +1 -1
  95. package/package.json +7 -6
  96. package/src/exports/react.ts +1 -0
  97. package/src/exports/utils.ts +3 -0
  98. package/src/pay/convert/cryptoToFiat.test.ts +21 -1
  99. package/src/pay/convert/cryptoToFiat.ts +2 -1
  100. package/src/pay/convert/fiatToCrypto.test.ts +21 -1
  101. package/src/pay/convert/fiatToCrypto.ts +3 -2
  102. package/src/pay/convert/type.ts +5 -0
  103. package/src/react/core/hooks/connection/ConnectButtonProps.ts +13 -0
  104. package/src/react/web/ui/ConnectWallet/ConnectButton.test.tsx +15 -0
  105. package/src/react/web/ui/ConnectWallet/Details.test.tsx +587 -0
  106. package/src/react/web/ui/ConnectWallet/Details.tsx +174 -67
  107. package/src/react/web/ui/ConnectWallet/screens/PrivateKey.test.tsx +54 -0
  108. package/src/react/web/ui/ConnectWallet/screens/ReceiveFunds.test.tsx +37 -0
  109. package/src/react/web/ui/ConnectWallet/screens/ReceiveFunds.tsx +6 -1
  110. package/src/react/web/ui/ConnectWallet/screens/SendFunds.test.tsx +67 -0
  111. package/src/react/web/ui/ConnectWallet/screens/SendFunds.tsx +3 -2
  112. package/src/react/web/ui/ConnectWallet/screens/StartScreen.test.tsx +71 -0
  113. package/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.test.ts +60 -0
  114. package/src/react/web/ui/ConnectWallet/screens/nativeToken.test.ts +42 -0
  115. package/src/react/web/ui/components/CopyIcon.tsx +5 -1
  116. package/src/react/web/ui/components/Skeleton.tsx +2 -0
  117. package/src/react/web/ui/prebuilt/Account/balance.test.tsx +167 -36
  118. package/src/react/web/ui/prebuilt/Account/balance.tsx +168 -28
  119. package/src/react/web/ui/prebuilt/Account/provider.test.tsx +16 -0
  120. package/src/react/web/ui/prebuilt/Account/provider.tsx +5 -0
  121. package/src/utils/formatNumber.ts +7 -1
  122. package/src/utils/shortenLargeNumber.test.ts +30 -0
  123. package/src/utils/shortenLargeNumber.ts +48 -0
  124. package/src/version.ts +1 -1
  125. package/src/wallets/manager/connection-manager.test.ts +290 -3
  126. package/src/wallets/manager/index.ts +45 -32
  127. package/src/wallets/wallet-connect/receiver/receiver.test.ts +1 -1
  128. package/src/wallets/wallet-connect/types.ts +1 -1
  129. package/src/wallets/wallet-types.ts +1 -1
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatTokenBalance } from "./formatTokenBalance.js";
3
+
4
+ describe("formatTokenBalance", () => {
5
+ const mockBalanceData = {
6
+ symbol: "ETH",
7
+ name: "Ethereum",
8
+ decimals: 18,
9
+ displayValue: "1.23456789",
10
+ };
11
+
12
+ it("formats balance with symbol by default", () => {
13
+ const result = formatTokenBalance(mockBalanceData);
14
+ expect(result).toBe("1.23457 ETH");
15
+ });
16
+
17
+ it("formats balance without symbol when showSymbol is false", () => {
18
+ const result = formatTokenBalance(mockBalanceData, false);
19
+ expect(result).toBe("1.23457");
20
+ });
21
+
22
+ it("respects custom decimal places", () => {
23
+ const result = formatTokenBalance(mockBalanceData, true, 3);
24
+ expect(result).toBe("1.235 ETH");
25
+ });
26
+
27
+ it("handles zero balance", () => {
28
+ const zeroBalance = { ...mockBalanceData, displayValue: "0" };
29
+ const result = formatTokenBalance(zeroBalance);
30
+ expect(result).toBe("0 ETH");
31
+ });
32
+
33
+ it("handles very small numbers", () => {
34
+ const smallBalance = { ...mockBalanceData, displayValue: "0.0000001" };
35
+ const result = formatTokenBalance(smallBalance);
36
+ expect(result).toBe("0.00001 ETH");
37
+ });
38
+
39
+ it("handles large numbers", () => {
40
+ const largeBalance = { ...mockBalanceData, displayValue: "1234567.89" };
41
+ const result = formatTokenBalance(largeBalance);
42
+ expect(result).toBe("1234567.89 ETH");
43
+ });
44
+
45
+ it("rounds up for very small non-zero values", () => {
46
+ const tinyBalance = { ...mockBalanceData, displayValue: "0.000000001" };
47
+ const result = formatTokenBalance(tinyBalance, true, 8);
48
+ expect(result).toBe("1e-8 ETH");
49
+ });
50
+
51
+ it("handles different token symbols", () => {
52
+ const usdcBalance = {
53
+ ...mockBalanceData,
54
+ symbol: "USDC",
55
+ displayValue: "100.5",
56
+ };
57
+ const result = formatTokenBalance(usdcBalance);
58
+ expect(result).toBe("100.5 USDC");
59
+ });
60
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
3
+ import {
4
+ NATIVE_TOKEN,
5
+ type NativeToken,
6
+ isNativeToken,
7
+ } from "./nativeToken.js"; // Replace with the actual file name// Assuming this is defined in a constants file
8
+
9
+ describe("isNativeToken", () => {
10
+ it("should return true for NATIVE_TOKEN", () => {
11
+ expect(isNativeToken(NATIVE_TOKEN)).toBe(true);
12
+ });
13
+
14
+ it("should return true for an object with nativeToken property", () => {
15
+ const token: NativeToken = { nativeToken: true };
16
+ expect(isNativeToken(token)).toBe(true);
17
+ });
18
+
19
+ it("should return true for a token with the native token address", () => {
20
+ const token = { address: NATIVE_TOKEN_ADDRESS };
21
+ expect(isNativeToken(token)).toBe(true);
22
+ });
23
+
24
+ it("should return true for a token with the native token address in uppercase", () => {
25
+ const token = { address: NATIVE_TOKEN_ADDRESS.toUpperCase() };
26
+ expect(isNativeToken(token)).toBe(true);
27
+ });
28
+
29
+ it("should return false for a non-native token", () => {
30
+ const token = { address: "0x1234567890123456789012345678901234567890" };
31
+ expect(isNativeToken(token)).toBe(false);
32
+ });
33
+
34
+ it("should return false for an empty object", () => {
35
+ expect(isNativeToken({})).toBe(false);
36
+ });
37
+
38
+ it("should return false for a token with a similar but incorrect address", () => {
39
+ const token = { address: `${NATIVE_TOKEN_ADDRESS.slice(0, -1)}0` };
40
+ expect(isNativeToken(token)).toBe(false);
41
+ });
42
+ });
@@ -38,7 +38,11 @@ export const CopyIcon: React.FC<{
38
38
  flex="row"
39
39
  center="both"
40
40
  >
41
- {showCheckIcon ? <CheckIcon /> : <CopyIconSVG />}
41
+ {showCheckIcon ? (
42
+ <CheckIcon className="tw-check-icon" />
43
+ ) : (
44
+ <CopyIconSVG className="tw-copy-icon" />
45
+ )}
42
46
  </Container>
43
47
  </div>
44
48
  </ToolTip>
@@ -11,6 +11,7 @@ export const Skeleton: React.FC<{
11
11
  height: string;
12
12
  width?: string;
13
13
  color?: keyof Theme["colors"];
14
+ className?: string;
14
15
  }> = (props) => {
15
16
  return (
16
17
  <SkeletonDiv
@@ -19,6 +20,7 @@ export const Skeleton: React.FC<{
19
20
  height: props.height,
20
21
  width: props.width || "auto",
21
22
  }}
23
+ className={props.className || ""}
22
24
  />
23
25
  );
24
26
  };
@@ -1,54 +1,185 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1
2
  import { describe, expect, it } from "vitest";
2
- import { ANVIL_CHAIN } from "~test/chains.js";
3
- import { render, screen, waitFor } from "~test/react-render.js";
3
+ import { VITALIK_WALLET } from "~test/addresses.js";
4
+ import { render } from "~test/react-render.js";
4
5
  import { TEST_CLIENT } from "~test/test-clients.js";
5
- import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
6
- import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
7
- import { AccountBalance } from "./balance.js";
6
+ import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
7
+ import { sepolia } from "../../../../../chains/chain-definitions/sepolia.js";
8
+ import { defineChain } from "../../../../../chains/utils.js";
9
+ import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
10
+ import {
11
+ AccountBalance,
12
+ formatAccountFiatBalance,
13
+ formatAccountTokenBalance,
14
+ loadAccountBalance,
15
+ } from "./balance.js";
8
16
  import { AccountProvider } from "./provider.js";
9
17
 
18
+ const queryClient = new QueryClient();
19
+
10
20
  describe.runIf(process.env.TW_SECRET_KEY)("AccountBalance component", () => {
11
- it("format the balance properly", async () => {
12
- const roundTo1Decimal = (num: number): number => Math.round(num * 10) / 10;
13
- const balance = await getWalletBalance({
14
- chain: ANVIL_CHAIN,
21
+ it("should render", async () => {
22
+ const { container } = render(
23
+ <QueryClientProvider client={queryClient}>
24
+ <AccountProvider address={VITALIK_WALLET} client={TEST_CLIENT}>
25
+ <AccountBalance
26
+ chain={ethereum}
27
+ loadingComponent={<span />}
28
+ fallbackComponent={<span />}
29
+ formatFn={() => "nope"}
30
+ />
31
+ </AccountProvider>
32
+ </QueryClientProvider>,
33
+ );
34
+
35
+ const spans = container.getElementsByTagName("span");
36
+ expect(spans).toHaveLength(1);
37
+ });
38
+
39
+ it("`loadAccountBalance` should fetch the native balance properly", async () => {
40
+ const result = await loadAccountBalance({
15
41
  client: TEST_CLIENT,
16
- address: TEST_ACCOUNT_A.address,
42
+ chain: ethereum,
43
+ address: VITALIK_WALLET,
17
44
  });
18
45
 
19
- render(
20
- <AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
21
- <AccountBalance chain={ANVIL_CHAIN} formatFn={roundTo1Decimal} />
22
- </AccountProvider>,
46
+ expect(Number.isNaN(result.balance)).toBe(false);
47
+ expect(result.symbol).toBe("ETH");
48
+ });
49
+
50
+ it("`loadAccountBalance` should fetch the token balance properly", async () => {
51
+ const result = await loadAccountBalance({
52
+ client: TEST_CLIENT,
53
+ chain: ethereum,
54
+ address: VITALIK_WALLET,
55
+ // USDC
56
+ tokenAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
57
+ });
58
+
59
+ expect(Number.isNaN(result.balance)).toBe(false);
60
+ expect(result.symbol).toBe("USDC");
61
+ });
62
+
63
+ it("`loadAccountBalance` should fetch the fiat balance properly", async () => {
64
+ const result = await loadAccountBalance({
65
+ client: TEST_CLIENT,
66
+ chain: ethereum,
67
+ address: VITALIK_WALLET,
68
+ showBalanceInFiat: "USD",
69
+ });
70
+
71
+ expect(Number.isNaN(result.balance)).toBe(false);
72
+ expect(result.symbol).toBe("$");
73
+ });
74
+
75
+ it("`loadAccountBalance` should throw if `chain` is not passed", async () => {
76
+ await expect(() =>
77
+ loadAccountBalance({ client: TEST_CLIENT, address: VITALIK_WALLET }),
78
+ ).rejects.toThrowError("chain is required");
79
+ });
80
+
81
+ it("`loadAccountBalance` should throw if `tokenAddress` is mistakenly passed as native token", async () => {
82
+ await expect(() =>
83
+ loadAccountBalance({
84
+ client: TEST_CLIENT,
85
+ address: VITALIK_WALLET,
86
+ tokenAddress: NATIVE_TOKEN_ADDRESS,
87
+ chain: ethereum,
88
+ }),
89
+ ).rejects.toThrowError(
90
+ `Invalid tokenAddress - cannot be ${NATIVE_TOKEN_ADDRESS}`,
91
+ );
92
+ });
93
+
94
+ it("`loadAccountBalance` should throw if `address` is not a valid evm address", async () => {
95
+ await expect(() =>
96
+ loadAccountBalance({
97
+ client: TEST_CLIENT,
98
+ address: "haha",
99
+ chain: ethereum,
100
+ }),
101
+ ).rejects.toThrowError("Invalid wallet address. Expected an EVM address");
102
+ });
103
+
104
+ it("`loadAccountBalance` should throw if `tokenAddress` is passed but is not a valid evm address", async () => {
105
+ await expect(() =>
106
+ loadAccountBalance({
107
+ client: TEST_CLIENT,
108
+ address: VITALIK_WALLET,
109
+ tokenAddress: "haha",
110
+ chain: ethereum,
111
+ }),
112
+ ).rejects.toThrowError(
113
+ "Invalid tokenAddress. Expected an EVM contract address",
114
+ );
115
+ });
116
+
117
+ it("`formatAccountTokenBalance` should display a rounded-up value + symbol", () => {
118
+ expect(
119
+ formatAccountTokenBalance({
120
+ balance: 1.1999,
121
+ symbol: "ETH",
122
+ decimals: 1,
123
+ }),
124
+ ).toBe("1.2 ETH");
125
+ });
126
+
127
+ it("`formatAccountFiatBalance` should display fiat symbol followed by a rounded-up fiat value", () => {
128
+ expect(
129
+ formatAccountFiatBalance({ balance: 55.001, symbol: "$", decimals: 0 }),
130
+ ).toBe("$55");
131
+ });
132
+
133
+ it("`loadAccountBalance` should throw if failed to load tokenBalance (native token)", async () => {
134
+ await expect(() =>
135
+ loadAccountBalance({
136
+ client: TEST_CLIENT,
137
+ address: VITALIK_WALLET,
138
+ chain: defineChain(-1),
139
+ }),
140
+ ).rejects.toThrowError(
141
+ `Failed to retrieve native token balance for address: ${VITALIK_WALLET} on chainId:-1`,
23
142
  );
143
+ });
24
144
 
25
- waitFor(() =>
26
- expect(
27
- screen.getByText(roundTo1Decimal(Number(balance.displayValue)), {
28
- exact: true,
29
- selector: "span",
30
- }),
31
- ).toBeInTheDocument(),
145
+ it("`loadAccountBalance` should throw if failed to load tokenBalance (erc20 token)", async () => {
146
+ await expect(() =>
147
+ loadAccountBalance({
148
+ client: TEST_CLIENT,
149
+ address: VITALIK_WALLET,
150
+ chain: defineChain(-1),
151
+ tokenAddress: "0xFfEBd97b29AD3b2BecF8E554e4a638A56C6Bbd59",
152
+ }),
153
+ ).rejects.toThrowError(
154
+ `Failed to retrieve token: 0xFfEBd97b29AD3b2BecF8E554e4a638A56C6Bbd59 balance for address: ${VITALIK_WALLET} on chainId:-1`,
32
155
  );
33
156
  });
34
157
 
35
- it("should fallback properly if failed to load", () => {
36
- render(
37
- <AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
38
- <AccountBalance
39
- chain={undefined}
40
- fallbackComponent={<span>oops</span>}
41
- />
42
- </AccountProvider>,
158
+ it("if fetching fiat value then it should throw if failed to resolve (native token)", async () => {
159
+ await expect(() =>
160
+ loadAccountBalance({
161
+ client: TEST_CLIENT,
162
+ address: VITALIK_WALLET,
163
+ chain: sepolia,
164
+ showBalanceInFiat: "USD",
165
+ }),
166
+ ).rejects.toThrowError(
167
+ `Failed to resolve fiat value for native token on chainId: ${sepolia.id}`,
43
168
  );
169
+ });
44
170
 
45
- waitFor(() =>
46
- expect(
47
- screen.getByText("oops", {
48
- exact: true,
49
- selector: "span",
50
- }),
51
- ).toBeInTheDocument(),
171
+ it("if fetching fiat value then it should throw if failed to resolve (erc20 token)", async () => {
172
+ await expect(() =>
173
+ loadAccountBalance({
174
+ client: TEST_CLIENT,
175
+ address: VITALIK_WALLET,
176
+ chain: sepolia,
177
+ showBalanceInFiat: "USD",
178
+ // this is a random erc20 token on sepolia that vitalik's wallet owns
179
+ tokenAddress: "0xFfEBd97b29AD3b2BecF8E554e4a638A56C6Bbd59",
180
+ }),
181
+ ).rejects.toThrowError(
182
+ `Failed to resolve fiat value for token: 0xFfEBd97b29AD3b2BecF8E554e4a638A56C6Bbd59 on chainId: ${sepolia.id}`,
52
183
  );
53
184
  });
54
185
  });
@@ -1,16 +1,36 @@
1
1
  "use client";
2
2
 
3
3
  import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
4
+ import type { Address } from "abitype";
4
5
  import type React from "react";
5
6
  import type { JSX } from "react";
6
7
  import type { Chain } from "../../../../../chains/types.js";
8
+ import type { ThirdwebClient } from "../../../../../client/client.js";
9
+ import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
10
+ import { convertCryptoToFiat } from "../../../../../exports/pay.js";
11
+ import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
7
12
  import { useActiveWalletChain } from "../../../../../react/core/hooks/wallets/useActiveWalletChain.js";
8
- import {
9
- type GetWalletBalanceResult,
10
- getWalletBalance,
11
- } from "../../../../../wallets/utils/getWalletBalance.js";
13
+ import { isAddress } from "../../../../../utils/address.js";
14
+ import { formatNumber } from "../../../../../utils/formatNumber.js";
15
+ import { shortenLargeNumber } from "../../../../../utils/shortenLargeNumber.js";
16
+ import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
12
17
  import { useAccountContext } from "./provider.js";
13
18
 
19
+ /**
20
+ * @component
21
+ * @wallet
22
+ */
23
+ export type AccountBalanceInfo = {
24
+ /**
25
+ * Represents either token balance or fiat balance.
26
+ */
27
+ balance: number;
28
+ /**
29
+ * Represents either token symbol or fiat symbol
30
+ */
31
+ symbol: string;
32
+ };
33
+
14
34
  /**
15
35
  * Props for the AccountBalance component
16
36
  * @component
@@ -33,7 +53,7 @@ export interface AccountBalanceProps
33
53
  * use this function to transform the balance display value like round up the number
34
54
  * Particularly useful to avoid overflowing-UI issues
35
55
  */
36
- formatFn?: (num: number) => number;
56
+ formatFn?: (props: AccountBalanceInfo) => string;
37
57
  /**
38
58
  * This component will be shown while the balance of the account is being fetched
39
59
  * If not passed, the component will return `null`.
@@ -67,9 +87,14 @@ export interface AccountBalanceProps
67
87
  * Optional `useQuery` params
68
88
  */
69
89
  queryOptions?: Omit<
70
- UseQueryOptions<GetWalletBalanceResult>,
90
+ UseQueryOptions<AccountBalanceInfo>,
71
91
  "queryFn" | "queryKey"
72
92
  >;
93
+
94
+ /**
95
+ * Show the token balance in a supported fiat currency (e.g "USD")
96
+ */
97
+ showBalanceInFiat?: SupportedFiatCurrency;
73
98
  }
74
99
 
75
100
  /**
@@ -94,18 +119,21 @@ export interface AccountBalanceProps
94
119
  *
95
120
  *
96
121
  * ### Format the balance (round up, shorten etc.)
97
- * The AccountBalance component accepts a `formatFn` which takes in a number and outputs a number
98
- * The function is used to modify the display value of the wallet balance
122
+ * The AccountBalance component accepts a `formatFn` which takes in an object of type `AccountBalanceInfo` and outputs a string
123
+ * The function is used to modify the display value of the wallet balance (either in crypto or fiat)
99
124
  *
100
125
  * ```tsx
101
- * const roundTo1Decimal = (num: number):number => Math.round(num * 10) / 10;
126
+ * import type { AccountBalanceInfo } from "thirdweb/react";
127
+ * import { formatNumber } from "thirdweb/utils";
102
128
  *
103
- * <AccountBalance formatFn={roundTo1Decimal} />
129
+ * const format = (props: AccountInfoBalance):string => `${formatNumber(props.balance, 1)} ${props.symbol.toLowerCase()}`
130
+ *
131
+ * <AccountBalance formatFn={format} />
104
132
  * ```
105
133
  *
106
134
  * Result:
107
135
  * ```html
108
- * <span>1.1 ETH</span>
136
+ * <span>1.1 eth</span> // the balance is rounded up to 1 decimal and the symbol is lowercased
109
137
  * ```
110
138
  *
111
139
  * ### Show a loading sign when the balance is being fetched
@@ -149,10 +177,11 @@ export interface AccountBalanceProps
149
177
  export function AccountBalance({
150
178
  chain,
151
179
  tokenAddress,
152
- formatFn,
153
180
  loadingComponent,
154
181
  fallbackComponent,
155
182
  queryOptions,
183
+ formatFn,
184
+ showBalanceInFiat,
156
185
  ...restProps
157
186
  }: AccountBalanceProps) {
158
187
  const { address, client } = useAccountContext();
@@ -160,25 +189,20 @@ export function AccountBalance({
160
189
  const chainToLoad = chain || walletChain;
161
190
  const balanceQuery = useQuery({
162
191
  queryKey: [
163
- "walletBalance",
192
+ "internal_account_balance",
164
193
  chainToLoad?.id || -1,
165
- address || "0x0",
194
+ address,
166
195
  { tokenAddress },
196
+ showBalanceInFiat,
167
197
  ] as const,
168
- queryFn: async () => {
169
- if (!chainToLoad) {
170
- throw new Error("chain is required");
171
- }
172
- if (!client) {
173
- throw new Error("client is required");
174
- }
175
- return getWalletBalance({
198
+ queryFn: async (): Promise<AccountBalanceInfo> =>
199
+ loadAccountBalance({
176
200
  chain: chainToLoad,
177
201
  client,
178
202
  address,
179
203
  tokenAddress,
180
- });
181
- },
204
+ showBalanceInFiat,
205
+ }),
182
206
  retry: false,
183
207
  ...queryOptions,
184
208
  });
@@ -191,13 +215,129 @@ export function AccountBalance({
191
215
  return fallbackComponent || null;
192
216
  }
193
217
 
194
- const displayValue = formatFn
195
- ? formatFn(Number(balanceQuery.data.displayValue))
196
- : balanceQuery.data.displayValue;
218
+ // Prioritize using the formatFn from users
219
+ if (formatFn) {
220
+ return <span {...restProps}>{formatFn(balanceQuery.data)}</span>;
221
+ }
222
+
223
+ if (showBalanceInFiat) {
224
+ return (
225
+ <span {...restProps}>
226
+ {formatAccountFiatBalance({ ...balanceQuery.data, decimals: 0 })}
227
+ </span>
228
+ );
229
+ }
197
230
 
198
231
  return (
199
232
  <span {...restProps}>
200
- {displayValue} {balanceQuery.data.symbol}
233
+ {formatAccountTokenBalance({
234
+ ...balanceQuery.data,
235
+ decimals: balanceQuery.data.balance < 1 ? 3 : 2,
236
+ })}
201
237
  </span>
202
238
  );
203
239
  }
240
+
241
+ /**
242
+ * @internal Exported for tests
243
+ */
244
+ export async function loadAccountBalance(props: {
245
+ chain?: Chain;
246
+ client: ThirdwebClient;
247
+ address: Address;
248
+ tokenAddress?: Address;
249
+ showBalanceInFiat?: SupportedFiatCurrency;
250
+ }): Promise<AccountBalanceInfo> {
251
+ const { chain, client, address, tokenAddress, showBalanceInFiat } = props;
252
+ if (!chain) {
253
+ throw new Error("chain is required");
254
+ }
255
+
256
+ if (
257
+ tokenAddress &&
258
+ tokenAddress?.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()
259
+ ) {
260
+ throw new Error(`Invalid tokenAddress - cannot be ${NATIVE_TOKEN_ADDRESS}`);
261
+ }
262
+
263
+ if (!isAddress(address)) {
264
+ throw new Error("Invalid wallet address. Expected an EVM address");
265
+ }
266
+
267
+ if (tokenAddress && !isAddress(tokenAddress)) {
268
+ throw new Error("Invalid tokenAddress. Expected an EVM contract address");
269
+ }
270
+
271
+ const tokenBalanceData = await getWalletBalance({
272
+ chain,
273
+ client,
274
+ address,
275
+ tokenAddress,
276
+ }).catch(() => undefined);
277
+
278
+ if (!tokenBalanceData) {
279
+ throw new Error(
280
+ `Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chain.id}`,
281
+ );
282
+ }
283
+
284
+ if (showBalanceInFiat) {
285
+ const fiatData = await convertCryptoToFiat({
286
+ fromAmount: Number(tokenBalanceData.displayValue),
287
+ fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
288
+ to: showBalanceInFiat,
289
+ chain,
290
+ client,
291
+ }).catch(() => undefined);
292
+
293
+ if (fiatData === undefined) {
294
+ throw new Error(
295
+ `Failed to resolve fiat value for ${tokenAddress ? `token: ${tokenAddress}` : "native token"} on chainId: ${chain.id}`,
296
+ );
297
+ }
298
+ return {
299
+ balance: fiatData?.result,
300
+ symbol:
301
+ new Intl.NumberFormat("en", {
302
+ style: "currency",
303
+ currency: showBalanceInFiat,
304
+ minimumFractionDigits: 0,
305
+ maximumFractionDigits: 0,
306
+ })
307
+ .formatToParts(0)
308
+ .find((p) => p.type === "currency")?.value ||
309
+ showBalanceInFiat.toUpperCase(),
310
+ };
311
+ }
312
+
313
+ return {
314
+ balance: Number(tokenBalanceData.displayValue),
315
+ symbol: tokenBalanceData.symbol,
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Format the display balance for both crypto and fiat, in the Details button and Modal
321
+ * If both crypto balance and fiat balance exist, we have to keep the string very short to avoid UI issues.
322
+ * @internal
323
+ * Used internally for the Details button and the Details Modal
324
+ */
325
+ export function formatAccountTokenBalance(
326
+ props: AccountBalanceInfo & { decimals: number },
327
+ ): string {
328
+ const formattedTokenBalance = formatNumber(props.balance, props.decimals);
329
+ return `${formattedTokenBalance} ${props.symbol}`;
330
+ }
331
+
332
+ /**
333
+ * Used internally for the Details button and Details Modal
334
+ * @internal
335
+ */
336
+ export function formatAccountFiatBalance(
337
+ props: AccountBalanceInfo & { decimals: number },
338
+ ) {
339
+ const num = formatNumber(props.balance, props.decimals);
340
+ // Need to keep them short to avoid UI overflow issues
341
+ const formattedFiatBalance = shortenLargeNumber(num);
342
+ return `${props.symbol}${formattedFiatBalance}`;
343
+ }
@@ -35,4 +35,20 @@ describe.runIf(process.env.TW_SECRET_KEY)("AccountProvider component", () => {
35
35
  }),
36
36
  ).toBeInTheDocument();
37
37
  });
38
+
39
+ it("should throw an error if no address is provided", () => {
40
+ expect(() => {
41
+ render(
42
+ <AccountProvider
43
+ // biome-ignore lint/suspicious/noExplicitAny: testing invalid input
44
+ address={undefined as any}
45
+ client={TEST_CLIENT}
46
+ >
47
+ <AccountAddress />
48
+ </AccountProvider>,
49
+ );
50
+ }).toThrowError(
51
+ "AccountProvider: No address passed. Ensure an address is always provided to the AccountProvider",
52
+ );
53
+ });
38
54
  });
@@ -48,6 +48,11 @@ const AccountProviderContext = /* @__PURE__ */ createContext<
48
48
  export function AccountProvider(
49
49
  props: React.PropsWithChildren<AccountProviderProps>,
50
50
  ) {
51
+ if (!props.address) {
52
+ throw new Error(
53
+ "AccountProvider: No address passed. Ensure an address is always provided to the AccountProvider",
54
+ );
55
+ }
51
56
  return (
52
57
  <AccountProviderContext.Provider value={props}>
53
58
  {props.children}
@@ -1,5 +1,11 @@
1
1
  /**
2
- * @internal
2
+ * Round up a number to a certain decimal place
3
+ * @example
4
+ * ```ts
5
+ * import { formatNumber } from "thirdweb/utils";
6
+ * const value = formatNumber(12.1214141, 1); // 12.1
7
+ * ```
8
+ * @utils
3
9
  */
4
10
  export function formatNumber(value: number, decimalPlaces: number) {
5
11
  if (value === 0) return 0;