thirdweb 5.75.0 → 5.76.0-nightly-8234dbae8fcf73ca83bbda31d929aa57ca521a53-20241208000407
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.
- package/dist/cjs/exports/react.js.map +1 -1
- package/dist/cjs/exports/utils.js +5 -1
- package/dist/cjs/exports/utils.js.map +1 -1
- package/dist/cjs/pay/convert/cryptoToFiat.js.map +1 -1
- package/dist/cjs/pay/convert/fiatToCrypto.js +1 -1
- package/dist/cjs/pay/convert/fiatToCrypto.js.map +1 -1
- package/dist/cjs/pay/convert/type.js +4 -0
- package/dist/cjs/pay/convert/type.js.map +1 -0
- package/dist/cjs/react/web/ui/ConnectWallet/Details.js +56 -16
- package/dist/cjs/react/web/ui/ConnectWallet/Details.js.map +1 -1
- package/dist/cjs/react/web/ui/ConnectWallet/screens/ReceiveFunds.js +1 -1
- package/dist/cjs/react/web/ui/ConnectWallet/screens/ReceiveFunds.js.map +1 -1
- package/dist/cjs/react/web/ui/ConnectWallet/screens/SendFunds.js +3 -2
- package/dist/cjs/react/web/ui/ConnectWallet/screens/SendFunds.js.map +1 -1
- package/dist/cjs/react/web/ui/components/CopyIcon.js +1 -1
- package/dist/cjs/react/web/ui/components/CopyIcon.js.map +1 -1
- package/dist/cjs/react/web/ui/components/Skeleton.js +1 -1
- package/dist/cjs/react/web/ui/components/Skeleton.js.map +1 -1
- package/dist/cjs/react/web/ui/prebuilt/Account/balance.js +114 -26
- package/dist/cjs/react/web/ui/prebuilt/Account/balance.js.map +1 -1
- package/dist/cjs/react/web/ui/prebuilt/Account/provider.js +3 -0
- package/dist/cjs/react/web/ui/prebuilt/Account/provider.js.map +1 -1
- package/dist/cjs/utils/formatNumber.js +7 -1
- package/dist/cjs/utils/formatNumber.js.map +1 -1
- package/dist/cjs/utils/shortenLargeNumber.js +47 -0
- package/dist/cjs/utils/shortenLargeNumber.js.map +1 -0
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/version.js.map +1 -1
- package/dist/cjs/wallets/manager/index.js +35 -24
- package/dist/cjs/wallets/manager/index.js.map +1 -1
- package/dist/esm/exports/react.js.map +1 -1
- package/dist/esm/exports/utils.js +2 -0
- package/dist/esm/exports/utils.js.map +1 -1
- package/dist/esm/pay/convert/cryptoToFiat.js.map +1 -1
- package/dist/esm/pay/convert/fiatToCrypto.js +1 -1
- package/dist/esm/pay/convert/fiatToCrypto.js.map +1 -1
- package/dist/esm/pay/convert/type.js +3 -0
- package/dist/esm/pay/convert/type.js.map +1 -0
- package/dist/esm/react/web/ui/ConnectWallet/Details.js +57 -23
- package/dist/esm/react/web/ui/ConnectWallet/Details.js.map +1 -1
- package/dist/esm/react/web/ui/ConnectWallet/screens/ReceiveFunds.js +1 -1
- package/dist/esm/react/web/ui/ConnectWallet/screens/ReceiveFunds.js.map +1 -1
- package/dist/esm/react/web/ui/ConnectWallet/screens/SendFunds.js +3 -3
- package/dist/esm/react/web/ui/ConnectWallet/screens/SendFunds.js.map +1 -1
- package/dist/esm/react/web/ui/components/CopyIcon.js +1 -1
- package/dist/esm/react/web/ui/components/CopyIcon.js.map +1 -1
- package/dist/esm/react/web/ui/components/Skeleton.js +1 -1
- package/dist/esm/react/web/ui/components/Skeleton.js.map +1 -1
- package/dist/esm/react/web/ui/prebuilt/Account/balance.js +113 -28
- package/dist/esm/react/web/ui/prebuilt/Account/balance.js.map +1 -1
- package/dist/esm/react/web/ui/prebuilt/Account/provider.js +3 -0
- package/dist/esm/react/web/ui/prebuilt/Account/provider.js.map +1 -1
- package/dist/esm/utils/formatNumber.js +7 -1
- package/dist/esm/utils/formatNumber.js.map +1 -1
- package/dist/esm/utils/shortenLargeNumber.js +44 -0
- package/dist/esm/utils/shortenLargeNumber.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/esm/wallets/manager/index.js +33 -24
- package/dist/esm/wallets/manager/index.js.map +1 -1
- package/dist/types/exports/react.d.ts +1 -1
- package/dist/types/exports/react.d.ts.map +1 -1
- package/dist/types/exports/utils.d.ts +2 -0
- package/dist/types/exports/utils.d.ts.map +1 -1
- package/dist/types/pay/convert/cryptoToFiat.d.ts +2 -1
- package/dist/types/pay/convert/cryptoToFiat.d.ts.map +1 -1
- package/dist/types/pay/convert/fiatToCrypto.d.ts +2 -1
- package/dist/types/pay/convert/fiatToCrypto.d.ts.map +1 -1
- package/dist/types/pay/convert/type.d.ts +7 -0
- package/dist/types/pay/convert/type.d.ts.map +1 -0
- package/dist/types/react/core/hooks/connection/ConnectButtonProps.d.ts +11 -0
- package/dist/types/react/core/hooks/connection/ConnectButtonProps.d.ts.map +1 -1
- package/dist/types/react/web/ui/ConnectWallet/Details.d.ts +60 -0
- package/dist/types/react/web/ui/ConnectWallet/Details.d.ts.map +1 -1
- package/dist/types/react/web/ui/ConnectWallet/screens/ReceiveFunds.d.ts.map +1 -1
- package/dist/types/react/web/ui/ConnectWallet/screens/SendFunds.d.ts +15 -0
- package/dist/types/react/web/ui/ConnectWallet/screens/SendFunds.d.ts.map +1 -1
- package/dist/types/react/web/ui/components/CopyIcon.d.ts.map +1 -1
- package/dist/types/react/web/ui/components/Skeleton.d.ts +1 -0
- package/dist/types/react/web/ui/components/Skeleton.d.ts.map +1 -1
- package/dist/types/react/web/ui/prebuilt/Account/balance.d.ts +58 -9
- package/dist/types/react/web/ui/prebuilt/Account/balance.d.ts.map +1 -1
- package/dist/types/react/web/ui/prebuilt/Account/provider.d.ts.map +1 -1
- package/dist/types/utils/formatNumber.d.ts +7 -1
- package/dist/types/utils/formatNumber.d.ts.map +1 -1
- package/dist/types/utils/shortenLargeNumber.d.ts +16 -0
- package/dist/types/utils/shortenLargeNumber.d.ts.map +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types/version.d.ts.map +1 -1
- package/dist/types/wallets/manager/index.d.ts +4 -0
- package/dist/types/wallets/manager/index.d.ts.map +1 -1
- package/dist/types/wallets/wallet-connect/types.d.ts +1 -1
- package/dist/types/wallets/wallet-connect/types.d.ts.map +1 -1
- package/dist/types/wallets/wallet-types.d.ts +1 -1
- package/package.json +7 -6
- package/src/exports/react.ts +1 -0
- package/src/exports/utils.ts +3 -0
- package/src/pay/convert/cryptoToFiat.test.ts +21 -1
- package/src/pay/convert/cryptoToFiat.ts +2 -1
- package/src/pay/convert/fiatToCrypto.test.ts +21 -1
- package/src/pay/convert/fiatToCrypto.ts +3 -2
- package/src/pay/convert/type.ts +5 -0
- package/src/react/core/hooks/connection/ConnectButtonProps.ts +13 -0
- package/src/react/web/ui/ConnectWallet/ConnectButton.test.tsx +15 -0
- package/src/react/web/ui/ConnectWallet/Details.test.tsx +587 -0
- package/src/react/web/ui/ConnectWallet/Details.tsx +174 -67
- package/src/react/web/ui/ConnectWallet/screens/PrivateKey.test.tsx +54 -0
- package/src/react/web/ui/ConnectWallet/screens/ReceiveFunds.test.tsx +37 -0
- package/src/react/web/ui/ConnectWallet/screens/ReceiveFunds.tsx +6 -1
- package/src/react/web/ui/ConnectWallet/screens/SendFunds.test.tsx +67 -0
- package/src/react/web/ui/ConnectWallet/screens/SendFunds.tsx +3 -2
- package/src/react/web/ui/ConnectWallet/screens/StartScreen.test.tsx +71 -0
- package/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.test.ts +60 -0
- package/src/react/web/ui/ConnectWallet/screens/nativeToken.test.ts +42 -0
- package/src/react/web/ui/components/CopyIcon.tsx +5 -1
- package/src/react/web/ui/components/Skeleton.tsx +2 -0
- package/src/react/web/ui/prebuilt/Account/balance.test.tsx +167 -36
- package/src/react/web/ui/prebuilt/Account/balance.tsx +168 -28
- package/src/react/web/ui/prebuilt/Account/provider.test.tsx +16 -0
- package/src/react/web/ui/prebuilt/Account/provider.tsx +5 -0
- package/src/utils/formatNumber.ts +7 -1
- package/src/utils/shortenLargeNumber.test.ts +30 -0
- package/src/utils/shortenLargeNumber.ts +48 -0
- package/src/version.ts +1 -1
- package/src/wallets/manager/connection-manager.test.ts +290 -3
- package/src/wallets/manager/index.ts +45 -32
- package/src/wallets/wallet-connect/receiver/receiver.test.ts +1 -1
- package/src/wallets/wallet-connect/types.ts +1 -1
- 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 ?
|
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 {
|
3
|
-
import { render
|
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 {
|
6
|
-
import {
|
7
|
-
import {
|
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("
|
12
|
-
const
|
13
|
-
|
14
|
-
|
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
|
-
|
42
|
+
chain: ethereum,
|
43
|
+
address: VITALIK_WALLET,
|
17
44
|
});
|
18
45
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
10
|
-
|
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?: (
|
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<
|
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
|
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
|
-
*
|
126
|
+
* import type { AccountBalanceInfo } from "thirdweb/react";
|
127
|
+
* import { formatNumber } from "thirdweb/utils";
|
102
128
|
*
|
103
|
-
*
|
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
|
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
|
-
"
|
192
|
+
"internal_account_balance",
|
164
193
|
chainToLoad?.id || -1,
|
165
|
-
address
|
194
|
+
address,
|
166
195
|
{ tokenAddress },
|
196
|
+
showBalanceInFiat,
|
167
197
|
] as const,
|
168
|
-
queryFn: async () =>
|
169
|
-
|
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
|
-
|
195
|
-
|
196
|
-
|
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
|
-
{
|
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
|
-
*
|
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;
|