@b3dotfun/sdk 0.1.70-alpha.18 → 0.1.70-alpha.19

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 (155) hide show
  1. package/README.md +3 -3
  2. package/dist/cjs/anyspend/constants/rpc.d.ts +1 -0
  3. package/dist/cjs/anyspend/constants/rpc.js +3 -1
  4. package/dist/cjs/anyspend/react/components/AnySpendDeposit.d.ts +1 -3
  5. package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +14 -12
  6. package/dist/cjs/anyspend/react/components/AnySpendStakeB3.js +32 -13
  7. package/dist/cjs/anyspend/react/components/AnySpendStakeB3ExactIn.js +32 -10
  8. package/dist/cjs/anyspend/react/components/checkout/CryptoPayPanel.js +16 -8
  9. package/dist/cjs/anyspend/react/components/common/TokenBalance.js +4 -5
  10. package/dist/cjs/anyspend/react/hooks/useSigMint.d.ts +1 -1
  11. package/dist/cjs/anyspend/utils/chain.d.ts +45 -0
  12. package/dist/cjs/anyspend/utils/chain.js +51 -2
  13. package/dist/cjs/global-account/react/components/B3Provider/B3Provider.d.ts +1 -2
  14. package/dist/cjs/global-account/react/components/B3Provider/B3Provider.js +2 -2
  15. package/dist/cjs/global-account/react/components/B3Provider/RelayKitProviderWrapper.d.ts +1 -2
  16. package/dist/cjs/global-account/react/components/B3Provider/RelayKitProviderWrapper.js +4 -5
  17. package/dist/cjs/global-account/react/components/ManageAccount/ContentTokens.js +23 -15
  18. package/dist/cjs/global-account/react/components/ManageAccount/NFTContent.js +2 -2
  19. package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +8 -6
  20. package/dist/cjs/global-account/react/components/ManageAccount/TokenContent.js +17 -8
  21. package/dist/cjs/global-account/react/components/Send/Send.js +15 -11
  22. package/dist/cjs/global-account/react/hooks/index.d.ts +2 -2
  23. package/dist/cjs/global-account/react/hooks/index.js +7 -8
  24. package/dist/cjs/global-account/react/hooks/useAuth.d.ts +2 -1
  25. package/dist/cjs/global-account/react/hooks/useAuth.js +1 -0
  26. package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +2 -1
  27. package/dist/cjs/global-account/react/hooks/useAuthentication.js +1 -0
  28. package/dist/cjs/global-account/react/hooks/useFirstEOA.d.ts +4 -7666
  29. package/dist/cjs/global-account/react/hooks/useFirstEOA.js +1 -0
  30. package/dist/cjs/global-account/react/hooks/usePortfolioBalance.d.ts +15 -0
  31. package/dist/cjs/global-account/react/hooks/usePortfolioBalance.js +76 -0
  32. package/dist/cjs/global-account/react/hooks/usePortfolioCollectibles.d.ts +12 -0
  33. package/dist/cjs/global-account/react/hooks/usePortfolioCollectibles.js +178 -0
  34. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +0 -2
  35. package/dist/cjs/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  36. package/dist/cjs/shared/constants/chains/b3Chain.d.ts +24 -0
  37. package/dist/cjs/shared/constants/chains/supported.d.ts +4 -4
  38. package/dist/cjs/shared/constants/index.d.ts +4 -0
  39. package/dist/cjs/shared/utils/zerionPositions.d.ts +96 -0
  40. package/dist/cjs/shared/utils/zerionPositions.js +186 -0
  41. package/dist/cjs/wallet/utils/createWalletConfig.d.ts +2 -2
  42. package/dist/esm/anyspend/constants/rpc.d.ts +1 -0
  43. package/dist/esm/anyspend/constants/rpc.js +2 -0
  44. package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +1 -3
  45. package/dist/esm/anyspend/react/components/AnySpendDeposit.js +15 -13
  46. package/dist/esm/anyspend/react/components/AnySpendStakeB3.js +33 -14
  47. package/dist/esm/anyspend/react/components/AnySpendStakeB3ExactIn.js +33 -11
  48. package/dist/esm/anyspend/react/components/checkout/CryptoPayPanel.js +18 -10
  49. package/dist/esm/anyspend/react/components/common/TokenBalance.js +6 -7
  50. package/dist/esm/anyspend/react/hooks/useSigMint.d.ts +1 -1
  51. package/dist/esm/anyspend/utils/chain.d.ts +45 -0
  52. package/dist/esm/anyspend/utils/chain.js +50 -3
  53. package/dist/esm/global-account/react/components/B3Provider/B3Provider.d.ts +1 -2
  54. package/dist/esm/global-account/react/components/B3Provider/B3Provider.js +2 -2
  55. package/dist/esm/global-account/react/components/B3Provider/RelayKitProviderWrapper.d.ts +1 -2
  56. package/dist/esm/global-account/react/components/B3Provider/RelayKitProviderWrapper.js +4 -5
  57. package/dist/esm/global-account/react/components/ManageAccount/ContentTokens.js +24 -16
  58. package/dist/esm/global-account/react/components/ManageAccount/NFTContent.js +3 -3
  59. package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +9 -7
  60. package/dist/esm/global-account/react/components/ManageAccount/TokenContent.js +18 -9
  61. package/dist/esm/global-account/react/components/Send/Send.js +16 -12
  62. package/dist/esm/global-account/react/hooks/index.d.ts +2 -2
  63. package/dist/esm/global-account/react/hooks/index.js +2 -2
  64. package/dist/esm/global-account/react/hooks/useAuth.d.ts +2 -1
  65. package/dist/esm/global-account/react/hooks/useAuth.js +1 -0
  66. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +2 -1
  67. package/dist/esm/global-account/react/hooks/useAuthentication.js +1 -0
  68. package/dist/esm/global-account/react/hooks/useFirstEOA.d.ts +4 -7666
  69. package/dist/esm/global-account/react/hooks/useFirstEOA.js +1 -0
  70. package/dist/esm/global-account/react/hooks/usePortfolioBalance.d.ts +15 -0
  71. package/dist/esm/global-account/react/hooks/usePortfolioBalance.js +72 -0
  72. package/dist/esm/global-account/react/hooks/usePortfolioCollectibles.d.ts +12 -0
  73. package/dist/esm/global-account/react/hooks/usePortfolioCollectibles.js +175 -0
  74. package/dist/esm/global-account/react/stores/useModalStore.d.ts +0 -2
  75. package/dist/esm/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  76. package/dist/esm/shared/constants/chains/b3Chain.d.ts +24 -0
  77. package/dist/esm/shared/constants/chains/supported.d.ts +4 -4
  78. package/dist/esm/shared/constants/index.d.ts +4 -0
  79. package/dist/esm/shared/utils/zerionPositions.d.ts +96 -0
  80. package/dist/esm/shared/utils/zerionPositions.js +178 -0
  81. package/dist/esm/wallet/utils/createWalletConfig.d.ts +2 -2
  82. package/dist/types/anyspend/constants/rpc.d.ts +1 -0
  83. package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +1 -3
  84. package/dist/types/anyspend/react/hooks/useSigMint.d.ts +1 -1
  85. package/dist/types/anyspend/utils/chain.d.ts +45 -0
  86. package/dist/types/global-account/react/components/B3Provider/B3Provider.d.ts +1 -2
  87. package/dist/types/global-account/react/components/B3Provider/RelayKitProviderWrapper.d.ts +1 -2
  88. package/dist/types/global-account/react/hooks/index.d.ts +2 -2
  89. package/dist/types/global-account/react/hooks/useAuth.d.ts +2 -1
  90. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +2 -1
  91. package/dist/types/global-account/react/hooks/useFirstEOA.d.ts +4 -7666
  92. package/dist/types/global-account/react/hooks/usePortfolioBalance.d.ts +15 -0
  93. package/dist/types/global-account/react/hooks/usePortfolioCollectibles.d.ts +12 -0
  94. package/dist/types/global-account/react/stores/useModalStore.d.ts +0 -2
  95. package/dist/types/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  96. package/dist/types/shared/constants/chains/b3Chain.d.ts +24 -0
  97. package/dist/types/shared/constants/chains/supported.d.ts +4 -4
  98. package/dist/types/shared/constants/index.d.ts +4 -0
  99. package/dist/types/shared/utils/zerionPositions.d.ts +96 -0
  100. package/dist/types/wallet/utils/createWalletConfig.d.ts +2 -2
  101. package/node_modules/@b3/chain-registry/chains.json +29 -1
  102. package/node_modules/@b3/chain-registry/package.json +18 -2
  103. package/package.json +2 -2
  104. package/src/anyspend/README.md +2 -2
  105. package/src/anyspend/constants/rpc.ts +2 -0
  106. package/src/anyspend/docs/components.md +6 -6
  107. package/src/anyspend/docs/contributing.md +1 -1
  108. package/src/anyspend/docs/hooks.md +3 -3
  109. package/src/anyspend/docs/installation.md +2 -2
  110. package/src/anyspend/react/components/AnySpendDeposit.tsx +26 -19
  111. package/src/anyspend/react/components/AnySpendStakeB3.tsx +55 -14
  112. package/src/anyspend/react/components/AnySpendStakeB3ExactIn.tsx +55 -11
  113. package/src/anyspend/react/components/__tests__/QRDeposit.test.tsx +3 -4
  114. package/src/anyspend/react/components/checkout/CryptoPayPanel.tsx +25 -12
  115. package/src/anyspend/react/components/common/TokenBalance.tsx +10 -7
  116. package/src/anyspend/utils/__tests__/deprecatedChains.test.ts +44 -0
  117. package/src/anyspend/utils/__tests__/toRegistryChainId.test.ts +17 -0
  118. package/src/anyspend/utils/chain.ts +53 -2
  119. package/src/global-account/react/components/B3Provider/B3Provider.tsx +1 -3
  120. package/src/global-account/react/components/B3Provider/RelayKitProviderWrapper.tsx +4 -11
  121. package/src/global-account/react/components/ManageAccount/ContentTokens.tsx +50 -21
  122. package/src/global-account/react/components/ManageAccount/NFTContent.tsx +5 -3
  123. package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +10 -6
  124. package/src/global-account/react/components/ManageAccount/TokenContent.tsx +22 -9
  125. package/src/global-account/react/components/Send/Send.tsx +39 -15
  126. package/src/global-account/react/hooks/index.ts +2 -2
  127. package/src/global-account/react/hooks/useAuth.ts +3 -2
  128. package/src/global-account/react/hooks/useAuthentication.ts +3 -2
  129. package/src/global-account/react/hooks/useFirstEOA.tsx +9 -2
  130. package/src/global-account/react/hooks/usePortfolioBalance.ts +89 -0
  131. package/src/global-account/react/hooks/usePortfolioCollectibles.ts +212 -0
  132. package/src/global-account/react/stores/useModalStore.ts +0 -2
  133. package/src/shared/constants/index.ts +8 -0
  134. package/src/shared/utils/__tests__/zerionPositions.test.ts +226 -0
  135. package/src/shared/utils/zerionPositions.ts +244 -0
  136. package/dist/cjs/global-account/react/hooks/useSimBalance.d.ts +0 -59
  137. package/dist/cjs/global-account/react/hooks/useSimBalance.js +0 -98
  138. package/dist/cjs/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
  139. package/dist/cjs/global-account/react/hooks/useSimCollectibles.js +0 -190
  140. package/dist/cjs/global-account/react/utils/simdune.d.ts +0 -7
  141. package/dist/cjs/global-account/react/utils/simdune.js +0 -21
  142. package/dist/esm/global-account/react/hooks/useSimBalance.d.ts +0 -59
  143. package/dist/esm/global-account/react/hooks/useSimBalance.js +0 -93
  144. package/dist/esm/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
  145. package/dist/esm/global-account/react/hooks/useSimCollectibles.js +0 -187
  146. package/dist/esm/global-account/react/utils/simdune.d.ts +0 -7
  147. package/dist/esm/global-account/react/utils/simdune.js +0 -17
  148. package/dist/types/global-account/react/hooks/useSimBalance.d.ts +0 -59
  149. package/dist/types/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
  150. package/dist/types/global-account/react/utils/simdune.d.ts +0 -7
  151. package/node_modules/@b3/chain-registry/src/index.ts +0 -169
  152. package/node_modules/@b3/chain-registry/tsconfig.json +0 -16
  153. package/src/global-account/react/hooks/useSimBalance.ts +0 -164
  154. package/src/global-account/react/hooks/useSimCollectibles.ts +0 -238
  155. package/src/global-account/react/utils/simdune.ts +0 -20
@@ -5,7 +5,7 @@ import {
5
5
  toast,
6
6
  TransitionPanel,
7
7
  useAnalytics,
8
- useSimBalance,
8
+ usePortfolioBalance,
9
9
  useUnifiedChainSwitchAndExecute,
10
10
  } from "@b3dotfun/sdk/global-account/react";
11
11
  import { formatDisplayNumber, formatTokenAmount } from "@b3dotfun/sdk/shared/utils/number";
@@ -16,7 +16,7 @@ import { NumericFormat } from "react-number-format";
16
16
 
17
17
  import { useActiveAccount } from "thirdweb/react";
18
18
  import { encodeFunctionData, erc20Abi, isAddress, parseUnits } from "viem";
19
- import { SimBalanceItem } from "../../hooks/useSimBalance";
19
+ import type { B3TokenBalance } from "@b3dotfun/sdk/shared/utils/zerionPositions";
20
20
 
21
21
  // Panel view enum for managing navigation between token list and send form
22
22
  enum TokenPanelView {
@@ -45,7 +45,7 @@ export function ContentTokens({ activeTab }: ContentTokensProps) {
45
45
  const [tokenPanelView, setTokenPanelView] = useState<TokenPanelView>(TokenPanelView.LIST);
46
46
 
47
47
  // === SEND FORM STATE ===
48
- const [selectedToken, setSelectedToken] = useState<SimBalanceItem | null>(null);
48
+ const [selectedToken, setSelectedToken] = useState<B3TokenBalance | null>(null);
49
49
  const [recipientAddress, setRecipientAddress] = useState("");
50
50
  const [sendAmount, setSendAmount] = useState("");
51
51
  const [isSending, setIsSending] = useState(false);
@@ -58,7 +58,12 @@ export function ContentTokens({ activeTab }: ContentTokensProps) {
58
58
 
59
59
  // === DATA FETCHING ===
60
60
  const account = useActiveAccount();
61
- const { data: simBalance, refetch: refetchSimBalance, isLoading: isLoadingBalance } = useSimBalance(account?.address);
61
+ const {
62
+ data: balances,
63
+ refetch: refetchBalances,
64
+ isLoading: isLoadingBalance,
65
+ isError: isBalanceError,
66
+ } = usePortfolioBalance(account?.address);
62
67
 
63
68
  // === BLOCKCHAIN INTERACTION ===
64
69
  const { switchChainAndExecute } = useUnifiedChainSwitchAndExecute();
@@ -96,14 +101,14 @@ export function ContentTokens({ activeTab }: ContentTokensProps) {
96
101
  // === HELPER FUNCTION ===
97
102
  // Get current version of selected token from fresh balance data
98
103
  // 🔧 FIX: Prevents auto-navigation back to token list when balance refreshes
99
- // The useSimBalance hook refreshes data, creating new token object references
104
+ // The balance hook refreshes data, creating new token object references
100
105
  // This helper ensures we always get the fresh token data instead of stale references
101
- const getCurrentSelectedToken = (): SimBalanceItem | null => {
102
- if (!selectedToken || !simBalance?.balances) {
106
+ const getCurrentSelectedToken = (): B3TokenBalance | null => {
107
+ if (!selectedToken || !balances) {
103
108
  return null;
104
109
  }
105
110
 
106
- const found = simBalance.balances.find(
111
+ const found = balances.find(
107
112
  token => token.chain_id === selectedToken.chain_id && token.address === selectedToken.address,
108
113
  );
109
114
 
@@ -184,11 +189,16 @@ export function ContentTokens({ activeTab }: ContentTokensProps) {
184
189
  args: [recipientAddress, amountInWei],
185
190
  });
186
191
 
187
- const tx = await switchChainAndExecute(displayToken.chain_id, {
188
- to: displayToken.address === "native" ? recipientAddress : displayToken.address,
189
- data: sendTokenData,
190
- value: displayToken.address === "native" ? amountInWei : BigInt(0),
191
- });
192
+ // A native send carries value and no calldata; an ERC-20 send carries calldata and no
193
+ // value. Branch on the explicit flag, never on the address: the native sentinel is a
194
+ // display detail, and taking the ERC-20 path for a native coin spends gas without moving
195
+ // anything.
196
+ const tx = await switchChainAndExecute(
197
+ displayToken.chain_id,
198
+ displayToken.is_native
199
+ ? { to: recipientAddress, value: amountInWei }
200
+ : { to: displayToken.address, data: sendTokenData, value: BigInt(0) },
201
+ );
192
202
 
193
203
  if (tx) {
194
204
  // Track successful send
@@ -215,7 +225,7 @@ export function ContentTokens({ activeTab }: ContentTokensProps) {
215
225
  // Wait 1 second to make sure the tx is indexed on sim api.
216
226
  setTimeout(async () => {
217
227
  // Force refetch to bypass cache and get fresh balance data
218
- await refetchSimBalance();
228
+ await refetchBalances();
219
229
  }, 1000);
220
230
  setIsSending(false);
221
231
  }
@@ -409,8 +419,28 @@ export function ContentTokens({ activeTab }: ContentTokensProps) {
409
419
  return <LoadingIndicator />;
410
420
  }
411
421
 
412
- // Show empty state when no account or no balance data
413
- if (!account?.address || !simBalance) {
422
+ // A wallet IS connected here only the read failed. Prompting the user to connect would send
423
+ // them to fix something that is not broken.
424
+ // `isBalanceError` as well as the absent list: after a first success a later refetch failure
425
+ // keeps `data` populated, so `!balances` alone would show stale numbers as if they were fresh.
426
+ if (account?.address && (isBalanceError || !balances)) {
427
+ return (
428
+ <div className="flex flex-col items-center justify-center py-12 text-center">
429
+ <div className="bg-b3-line/50 mb-4 rounded-full p-4">
430
+ <Loader2 className="text-b3-foreground-muted h-8 w-8" />
431
+ </div>
432
+ <h3 className="text-b3-grey font-neue-montreal-semibold mb-2">Couldn&apos;t load your tokens</h3>
433
+ <button
434
+ onClick={() => refetchBalances()}
435
+ className="text-b3-foreground-muted font-neue-montreal-medium text-sm underline"
436
+ >
437
+ Try again
438
+ </button>
439
+ </div>
440
+ );
441
+ }
442
+
443
+ if (!account?.address) {
414
444
  return (
415
445
  <div className="flex flex-col items-center justify-center py-12 text-center">
416
446
  <div className="bg-b3-line/50 mb-4 rounded-full p-4">
@@ -426,8 +456,7 @@ export function ContentTokens({ activeTab }: ContentTokensProps) {
426
456
 
427
457
  // === SMART FILTERING LOGIC ===
428
458
  // Filter tokens with value >= $1 to reduce noise from dust tokens
429
- const filteredTokens =
430
- simBalance?.balances.filter(token => token.value_usd !== undefined && token.value_usd >= 1) || [];
459
+ const filteredTokens = balances?.filter(token => token.value_usd !== undefined && token.value_usd >= 1) || [];
431
460
 
432
461
  // 🧠 INTELLIGENT DISPLAY LOGIC:
433
462
  // Show all tokens automatically when filtering would be unhelpful:
@@ -435,11 +464,11 @@ export function ContentTokens({ activeTab }: ContentTokensProps) {
435
464
  // 2. No tokens with value >= $1 (user has only dust), OR
436
465
  // 3. 5 or fewer tokens with value >= $1 (not enough to warrant filtering)
437
466
  const shouldShowAllTokens = showAllTokens || filteredTokens.length === 0 || filteredTokens.length <= 5;
438
- const tokensToShow = shouldShowAllTokens ? simBalance?.balances || [] : filteredTokens;
439
- const hasHiddenTokens = !shouldShowAllTokens && (simBalance?.balances.length || 0) > filteredTokens.length;
467
+ const tokensToShow = shouldShowAllTokens ? balances || [] : filteredTokens;
468
+ const hasHiddenTokens = !shouldShowAllTokens && (balances?.length || 0) > filteredTokens.length;
440
469
 
441
470
  // Handle token selection and navigate to send form
442
- const handleTokenClick = (token: SimBalanceItem) => {
471
+ const handleTokenClick = (token: B3TokenBalance) => {
443
472
  setSelectedToken(token);
444
473
  animationDirection.current = "forward"; // Set animation direction BEFORE state change
445
474
  setTokenPanelView(TokenPanelView.SEND);
@@ -1,6 +1,6 @@
1
1
  import { useActiveWallet } from "thirdweb/react";
2
2
  import { AccountAssets } from "..";
3
- import { useSimCollectibles } from "../../hooks";
3
+ import { usePortfolioCollectibles } from "../../hooks";
4
4
 
5
5
  const NFTContent = () => {
6
6
  // Get active wallet state
@@ -8,11 +8,13 @@ const NFTContent = () => {
8
8
  const activeAccount = activeWallet?.getAccount();
9
9
  const activeAddress = activeAccount?.address;
10
10
 
11
- const { data: nfts, isLoading } = useSimCollectibles(activeAddress, [1, 8453], { filterSpam: true });
11
+ const { data: nfts, isLoading, isError } = usePortfolioCollectibles(activeAddress, [1, 8453], { filterSpam: true });
12
12
 
13
13
  return (
14
14
  <div style={{ minHeight: "100px" }}>
15
- {nfts ? (
15
+ {isError ? (
16
+ <div className="py-12 text-center text-gray-500">Couldn&apos;t load your NFTs</div>
17
+ ) : nfts ? (
16
18
  <AccountAssets nfts={nfts} isLoading={isLoading} />
17
19
  ) : (
18
20
  <div className="py-12 text-center text-gray-500">No NFTs found</div>
@@ -3,7 +3,7 @@ import {
3
3
  useB3Config,
4
4
  useModalStore,
5
5
  useProfile,
6
- useSimBalance,
6
+ usePortfolioBalance,
7
7
  useUser,
8
8
  } from "@b3dotfun/sdk/global-account/react";
9
9
  import { validateImageUrl } from "@b3dotfun/sdk/global-account/react/utils/profileDisplay";
@@ -32,14 +32,16 @@ const ProfileSection = () => {
32
32
  const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
33
33
  const navigateBack = useModalStore(state => state.navigateBack);
34
34
 
35
- const { data: simBalance } = useSimBalance(smartWalletAddress);
35
+ const { data: balances, isError: isBalanceError } = usePortfolioBalance(smartWalletAddress);
36
36
  const [imgError, setImgError] = useState(false);
37
37
  const handleImgError = useCallback(() => setImgError(true), []);
38
38
 
39
+ // Null while the holdings are unknown. Summing an absent list to $0.00 would report an empty
40
+ // portfolio to someone whose balances merely failed to load.
39
41
  const totalBalanceUsd = useMemo(() => {
40
- if (!simBalance?.balances) return 0;
41
- return simBalance.balances.reduce((sum, token) => sum + (token.value_usd || 0), 0);
42
- }, [simBalance]);
42
+ if (isBalanceError || !balances) return null;
43
+ return balances.reduce((sum, token) => sum + (token.value_usd || 0), 0);
44
+ }, [balances, isBalanceError]);
43
45
 
44
46
  const handleEditAvatar = () => {
45
47
  setB3ModalOpen(true);
@@ -92,7 +94,9 @@ const ProfileSection = () => {
92
94
  {!isBetterAuth && (
93
95
  <h2 className="text-b3-grey font-neue-montreal-semibold flex h-[38px] items-center gap-1 text-xl">
94
96
  <div className="text-b3-foreground-muted"> $</div>
95
- <div className="text-[30px]">{formatDisplayNumber(totalBalanceUsd, { fractionDigits: 2 })}</div>
97
+ <div className="text-[30px]">
98
+ {totalBalanceUsd === null ? "—" : formatDisplayNumber(totalBalanceUsd, { fractionDigits: 2 })}
99
+ </div>
96
100
  </h2>
97
101
  )}
98
102
  <div className="b3-modal-username font-neue-montreal-semibold text-base leading-none text-[#0B57C2]">
@@ -1,10 +1,16 @@
1
- import { useSimBalance } from "@b3dotfun/sdk/global-account/react";
1
+ import { usePortfolioBalance } from "@b3dotfun/sdk/global-account/react";
2
2
  import { getChainLogo } from "@b3dotfun/sdk/shared/constants/chains/supported";
3
3
  import { useActiveWallet } from "thirdweb/react";
4
4
  import { TokenBalanceRow } from "./TokenBalanceRow";
5
5
 
6
- // Chain logo URLs mapping
7
- const CHAIN_LOGOS: Record<number, string> = [1, 8453, 8333].map(chainId => getChainLogo(chainId));
6
+ const ETHEREUM_CHAIN_ID = 1;
7
+
8
+ // Chain logo URLs, keyed by chain id. Built with Object.fromEntries rather than `.map()`: an array
9
+ // satisfies Record<number, string> for the type checker while indexing by 0,1,2 at runtime, so
10
+ // every lookup by a real chain id would miss.
11
+ const CHAIN_LOGOS: Record<number, string> = Object.fromEntries(
12
+ [ETHEREUM_CHAIN_ID, 8453, 8333].map(chainId => [chainId, getChainLogo(chainId)]),
13
+ );
8
14
 
9
15
  const TokenContent = () => {
10
16
  // Get active wallet state
@@ -12,19 +18,26 @@ const TokenContent = () => {
12
18
  const activeAccount = activeWallet?.getAccount();
13
19
  const activeAddress = activeAccount?.address;
14
20
 
15
- const { data: simBalance } = useSimBalance(activeAddress, [8453, 8333, 1]);
16
- console.log("simBalance :", simBalance);
21
+ // Base and Ethereum only. B3 (8333) is deliberately absent: the portfolio provider does not index
22
+ // it, so asking for it returns nothing and reads as a coverage gap rather than a choice. Restoring
23
+ // B3 needs a non-provider source, the way HyperEVM and Plume are served on-chain in b3os-web.
24
+ const { data: balances, isError: isBalanceError } = usePortfolioBalance(activeAddress, [8453, ETHEREUM_CHAIN_ID]);
17
25
 
18
26
  if (!activeAddress) {
19
27
  return <div className="col-span-3 py-12 text-center text-gray-500">No tokens found</div>;
20
28
  }
21
29
 
22
- if (!simBalance?.balances || simBalance.balances.length === 0) {
30
+ // "Couldn't load" and "holds nothing" are different answers and must not share a message.
31
+ if (isBalanceError) {
32
+ return <div className="col-span-3 py-12 text-center text-gray-500">Couldn&apos;t load your tokens</div>;
33
+ }
34
+
35
+ if (!balances || balances.length === 0) {
23
36
  return <div className="col-span-3 py-12 text-center text-gray-500">No tokens found</div>;
24
37
  }
25
38
 
26
39
  // Sort by USD value descending
27
- const sortedBalances = [...simBalance.balances].sort((a, b) => {
40
+ const sortedBalances = [...balances].sort((a, b) => {
28
41
  const valueA = a.value_usd || 0;
29
42
  const valueB = b.value_usd || 0;
30
43
  return valueB - valueA;
@@ -43,8 +56,8 @@ const TokenContent = () => {
43
56
  // Determine token logo
44
57
  // For native tokens, use ETH logo instead of chain logo
45
58
  let tokenLogo = token.token_metadata?.logo || "";
46
- if (token.address === "native" && token.symbol === "ETH") {
47
- tokenLogo = CHAIN_LOGOS[0];
59
+ if (token.is_native && token.symbol === "ETH") {
60
+ tokenLogo = CHAIN_LOGOS[ETHEREUM_CHAIN_ID];
48
61
  }
49
62
 
50
63
  return (
@@ -6,7 +6,7 @@ import {
6
6
  useAnalytics,
7
7
  useModalStore,
8
8
  useProfile,
9
- useSimBalance,
9
+ usePortfolioBalance,
10
10
  useUnifiedChainSwitchAndExecute,
11
11
  } from "@b3dotfun/sdk/global-account/react";
12
12
  import { validateImageUrl } from "@b3dotfun/sdk/global-account/react/utils/profileDisplay";
@@ -18,7 +18,7 @@ import { NumericFormat } from "react-number-format";
18
18
 
19
19
  import { encodeFunctionData, erc20Abi, isAddress, parseUnits } from "viem";
20
20
 
21
- import type { SimBalanceItem } from "../../hooks/useSimBalance";
21
+ import type { B3TokenBalance } from "@b3dotfun/sdk/shared/utils/zerionPositions";
22
22
  import { useRecentAddressesStore } from "../../stores/useRecentAddressesStore";
23
23
  import ModalHeader from "../ModalHeader/ModalHeader";
24
24
  import { Button } from "../ui/button";
@@ -66,7 +66,7 @@ export function Send({ recipientAddress: initialRecipient, onSuccess }: SendModa
66
66
 
67
67
  // Wizard state
68
68
  const [step, setStep] = useState<SendStep>("recipient");
69
- const [selectedToken, setSelectedToken] = useState<SimBalanceItem | null>(null);
69
+ const [selectedToken, setSelectedToken] = useState<B3TokenBalance | null>(null);
70
70
  const [recipientAddress, setRecipientAddress] = useState(initialRecipient || "");
71
71
  const [sendAmount, setSendAmount] = useState("");
72
72
  const [isSending, setIsSending] = useState(false);
@@ -74,7 +74,12 @@ export function Send({ recipientAddress: initialRecipient, onSuccess }: SendModa
74
74
  const [showValidatedResult, setShowValidatedResult] = useState(false);
75
75
 
76
76
  // Hooks
77
- const { data: simBalance, refetch: refetchSimBalance, isLoading: isLoadingBalance } = useSimBalance(address);
77
+ const {
78
+ data: balances,
79
+ refetch: refetchBalances,
80
+ isLoading: isLoadingBalance,
81
+ isError: isBalanceError,
82
+ } = usePortfolioBalance(address);
78
83
  const { switchChainAndExecute } = useUnifiedChainSwitchAndExecute();
79
84
  const { sendAnalyticsEvent } = useAnalytics();
80
85
 
@@ -138,11 +143,11 @@ export function Send({ recipientAddress: initialRecipient, onSuccess }: SendModa
138
143
  };
139
144
 
140
145
  // Get current selected token from fresh balance
141
- const getCurrentSelectedToken = (): SimBalanceItem | null => {
142
- if (!selectedToken || !simBalance?.balances) {
146
+ const getCurrentSelectedToken = (): B3TokenBalance | null => {
147
+ if (!selectedToken || !balances) {
143
148
  return null;
144
149
  }
145
- const found = simBalance.balances.find(
150
+ const found = balances.find(
146
151
  token => token.chain_id === selectedToken.chain_id && token.address === selectedToken.address,
147
152
  );
148
153
  return found || null;
@@ -187,11 +192,15 @@ export function Send({ recipientAddress: initialRecipient, onSuccess }: SendModa
187
192
  args: [recipientAddress, amountInWei],
188
193
  });
189
194
 
190
- const tx = await switchChainAndExecute(currentToken.chain_id, {
191
- to: currentToken.address === "native" ? recipientAddress : currentToken.address,
192
- data: sendTokenData,
193
- value: currentToken.address === "native" ? amountInWei : BigInt(0),
194
- });
195
+ // A native send carries value and no calldata; an ERC-20 send carries calldata and no value.
196
+ // Branch on the explicit flag, never on the address: the native sentinel is a display detail,
197
+ // and taking the ERC-20 path for a native coin spends gas without moving anything.
198
+ const tx = await switchChainAndExecute(
199
+ currentToken.chain_id,
200
+ currentToken.is_native
201
+ ? { to: recipientAddress, value: amountInWei }
202
+ : { to: currentToken.address, data: sendTokenData, value: BigInt(0) },
203
+ );
195
204
 
196
205
  if (tx) {
197
206
  sendAnalyticsEvent("send_token_button_click", {
@@ -208,7 +217,7 @@ export function Send({ recipientAddress: initialRecipient, onSuccess }: SendModa
208
217
  }
209
218
 
210
219
  setTimeout(async () => {
211
- await refetchSimBalance();
220
+ await refetchBalances();
212
221
  }, 1000);
213
222
  }
214
223
  } catch (error: any) {
@@ -364,10 +373,10 @@ export function Send({ recipientAddress: initialRecipient, onSuccess }: SendModa
364
373
  ))}
365
374
  </div>
366
375
  </div>
367
- ) : simBalance?.balances && simBalance.balances.length > 0 ? (
376
+ ) : balances && balances.length > 0 ? (
368
377
  <div className="space-y-4">
369
378
  <div className="space-y-1">
370
- {simBalance.balances.map(token => (
379
+ {balances.map(token => (
371
380
  <div
372
381
  key={token.chain_id + "_" + token.address}
373
382
  className="hover:bg-b3-line/60 dark:hover:bg-b3-primary-wash/40 group flex cursor-pointer items-center justify-between rounded-xl p-3 transition-all duration-200"
@@ -411,6 +420,21 @@ export function Send({ recipientAddress: initialRecipient, onSuccess }: SendModa
411
420
  ))}
412
421
  </div>
413
422
  </div>
423
+ ) : isBalanceError ? (
424
+ // Telling someone they hold nothing, when the read simply failed, invites them to go
425
+ // looking for missing funds.
426
+ <div className="flex flex-col items-center justify-center py-12 text-center">
427
+ <CircleHelp className="text-b3-foreground-muted mb-4 h-8 w-8" />
428
+ <p className="text-b3-foreground-muted font-neue-montreal-medium text-sm">
429
+ Couldn&apos;t load your tokens
430
+ </p>
431
+ <button
432
+ onClick={() => refetchBalances()}
433
+ className="text-b3-foreground-muted font-neue-montreal-medium mt-2 text-sm underline"
434
+ >
435
+ Try again
436
+ </button>
437
+ </div>
414
438
  ) : (
415
439
  <div className="flex flex-col items-center justify-center py-12 text-center">
416
440
  <CircleHelp className="text-b3-foreground-muted mb-4 h-8 w-8" />
@@ -39,8 +39,8 @@ export { useQueryBSMNT } from "./useQueryBSMNT";
39
39
  export { useRemoveSessionKey } from "./useRemoveSessionKey";
40
40
  export { useRouter } from "./useRouter";
41
41
  export { useSearchParamsSSR } from "./useSearchParamsSSR";
42
- export { useSimBalance, useSimSvmBalance, useSimTokenBalance } from "./useSimBalance";
43
- export { useSimCollectibles } from "./useSimCollectibles";
42
+ export { usePortfolioBalance, usePortfolioTokenBalance } from "./usePortfolioBalance";
43
+ export { usePortfolioCollectibles } from "./usePortfolioCollectibles";
44
44
  export { useSiwe } from "./useSiwe";
45
45
  export { useTokenBalance } from "./useTokenBalance";
46
46
  export { useTokenBalanceDirect } from "./useTokenBalanceDirect";
@@ -1,7 +1,7 @@
1
1
  import app from "@b3dotfun/sdk/global-account/app";
2
2
  import { authenticateWithB3JWT } from "@b3dotfun/sdk/global-account/bsmnt";
3
3
  import { useAuthStore, useB3Config } from "@b3dotfun/sdk/global-account/react";
4
- import { ecosystemWalletId } from "@b3dotfun/sdk/shared/constants";
4
+ import { ecosystemWalletId, type EcosystemWallet } from "@b3dotfun/sdk/shared/constants";
5
5
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
6
6
  import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
7
7
  import { ConnectionOptions } from "@thirdweb-dev/wagmi-adapter";
@@ -54,7 +54,8 @@ export function useAuth() {
54
54
  const { user, setUser } = useUserQuery();
55
55
  debug("@@activeWagmiAccount", activeWagmiAccount);
56
56
 
57
- const wallet = ecosystemWallet(ecosystemWalletId, {
57
+ // Explicit annotation keeps the hook's declaration portable (TS2742).
58
+ const wallet: EcosystemWallet = ecosystemWallet(ecosystemWalletId, {
58
59
  partnerId: partnerId,
59
60
  });
60
61
 
@@ -1,7 +1,7 @@
1
1
  import app from "@b3dotfun/sdk/global-account/app";
2
2
  import { authenticateWithB3JWT } from "@b3dotfun/sdk/global-account/bsmnt";
3
3
  import { useAuthStore } from "@b3dotfun/sdk/global-account/react";
4
- import { ecosystemWalletId } from "@b3dotfun/sdk/shared/constants";
4
+ import { ecosystemWalletId, type EcosystemWallet } from "@b3dotfun/sdk/shared/constants";
5
5
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
6
6
  import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
7
7
  import { ConnectionOptions } from "@thirdweb-dev/wagmi-adapter";
@@ -63,7 +63,8 @@ export function useAuthentication(partnerId: string, { skipAutoConnect = false }
63
63
  const { switchAccount } = useSwitchAccount();
64
64
  debug("@@activeWagmiAccount", activeWagmiAccount);
65
65
 
66
- const wallet = ecosystemWallet(ecosystemWalletId, {
66
+ // Explicit annotation keeps the hook's declaration portable (TS2742).
67
+ const wallet: EcosystemWallet = ecosystemWallet(ecosystemWalletId, {
67
68
  partnerId: partnerId,
68
69
  });
69
70
 
@@ -5,10 +5,17 @@ import { useEffect, useMemo, useState } from "react";
5
5
  import { viemAdapter } from "thirdweb/adapters/viem";
6
6
  import { useConnectedWallets, useWalletInfo } from "thirdweb/react";
7
7
  import { Wallet } from "thirdweb/wallets";
8
+ import type { WalletClient } from "viem";
8
9
 
9
10
  const debug = debugB3React("useFirstEOA");
10
11
 
11
- export function useFirstEOA(chain?: { id: number; name: string; rpc: string }) {
12
+ // Explicit return annotation keeps the hook's declaration portable (TS2742).
13
+ export function useFirstEOA(chain?: { id: number; name: string; rpc: string }): {
14
+ account: Wallet | undefined;
15
+ address: string | undefined;
16
+ info: ReturnType<typeof useWalletInfo>;
17
+ walletClient: WalletClient | undefined;
18
+ } {
12
19
  const wallets = useConnectedWallets();
13
20
  const isConnected = useAuthStore(state => state.isConnected);
14
21
  const [firstEOA, setFirstEOA] = useState<Wallet | undefined>(undefined);
@@ -41,7 +48,7 @@ export function useFirstEOA(chain?: { id: number; name: string; rpc: string }) {
41
48
  autoSelectFirstEOAWallet();
42
49
  }, [isConnected, wallets]);
43
50
 
44
- const walletClient = useMemo(() => {
51
+ const walletClient = useMemo<WalletClient | undefined>(() => {
45
52
  if (!firstEOA) return undefined;
46
53
  if (!chain) return undefined;
47
54
  try {
@@ -0,0 +1,89 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import {
3
+ adaptZerionPositions,
4
+ balanceMatchesToken,
5
+ type B3TokenBalance,
6
+ type ZerionPositionLike,
7
+ } from "@b3dotfun/sdk/shared/utils/zerionPositions";
8
+
9
+ /**
10
+ * Wallet holdings, read through the b3 portfolio proxy.
11
+ *
12
+ * The proxy holds the upstream API key server-side and authorizes browsers by Origin allowlist, so
13
+ * nothing secret ships to the client and no key is accepted from it. A consumer served from an
14
+ * origin the proxy does not know will be refused — adding an origin is a proxy config change, not
15
+ * an SDK change.
16
+ */
17
+ const PORTFOLIO_API_BASE_URL = "https://portfolio-api.b3.fun";
18
+
19
+ interface ZerionPositionsResponse {
20
+ data?: ZerionPositionLike[];
21
+ }
22
+
23
+ /**
24
+ * Fetch every spendable holding for a wallet in one request.
25
+ *
26
+ * Deliberately unpaginated and unfiltered by chain. The positions endpoint returns a wallet's full
27
+ * simple-position list in a single response, and passing a chain filter is actively harmful: the
28
+ * upstream rejects a handful of otherwise-valid chain slugs and fails the WHOLE request when one
29
+ * appears, so a caller asking for five chains can lose all five. Callers narrow the result instead.
30
+ */
31
+ async function fetchWalletPositions(address: string): Promise<B3TokenBalance[]> {
32
+ const url = `${PORTFOLIO_API_BASE_URL}/v1/wallets/${encodeURIComponent(address)}/positions/?filter[positions]=only_simple`;
33
+ const response = await fetch(url, { headers: { Accept: "application/json" } });
34
+
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to fetch wallet balances: ${response.status} ${response.statusText}`);
37
+ }
38
+
39
+ const body = (await response.json()) as ZerionPositionsResponse;
40
+ return adaptZerionPositions(body.data ?? []);
41
+ }
42
+
43
+ /**
44
+ * Keyed by wallet alone so every consumer of the same wallet shares one fetch and one cache entry.
45
+ * Keying by the requested chain subset instead would refetch per caller and share nothing.
46
+ */
47
+ function walletPositionsQueryKey(address?: string) {
48
+ return ["walletPositions", address] as const;
49
+ }
50
+
51
+ /**
52
+ * Holdings for a wallet, optionally narrowed to specific chains.
53
+ *
54
+ * Read `isError` before reading `data`: a failed read and an empty wallet are different facts, and
55
+ * rendering the first as the second tells the user they hold nothing when nobody knows.
56
+ */
57
+ export function usePortfolioBalance(address?: string, chainIds?: number[]) {
58
+ const wantedChainIds = chainIds && chainIds.length > 0 ? chainIds : undefined;
59
+
60
+ return useQuery({
61
+ queryKey: walletPositionsQueryKey(address),
62
+ queryFn: () => {
63
+ if (!address) throw new Error("Address is required");
64
+ return fetchWalletPositions(address);
65
+ },
66
+ enabled: Boolean(address),
67
+ select: (balances: B3TokenBalance[]) =>
68
+ wantedChainIds ? balances.filter(balance => wantedChainIds.includes(balance.chain_id)) : balances,
69
+ });
70
+ }
71
+
72
+ /**
73
+ * One token's balance for a wallet, served from the same per-wallet fetch as {@link usePortfolioBalance}.
74
+ *
75
+ * `tokenAddress` may be a contract address or the chain's native sentinel; matching is
76
+ * case-insensitive because callers pass addresses in whatever casing their source used.
77
+ */
78
+ export function usePortfolioTokenBalance(walletAddress?: string, tokenAddress?: string, chainId?: number) {
79
+ return useQuery({
80
+ queryKey: walletPositionsQueryKey(walletAddress),
81
+ queryFn: () => {
82
+ if (!walletAddress) throw new Error("Address is required");
83
+ return fetchWalletPositions(walletAddress);
84
+ },
85
+ enabled: Boolean(walletAddress) && Boolean(tokenAddress) && Boolean(chainId),
86
+ select: (balances: B3TokenBalance[]) =>
87
+ balances.find(balance => balance.chain_id === chainId && balanceMatchesToken(balance, tokenAddress ?? "")),
88
+ });
89
+ }