@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
@@ -8,7 +8,7 @@ import {
8
8
  toast,
9
9
  useHasMounted,
10
10
  useModalStore,
11
- useSimBalance,
11
+ usePortfolioBalance,
12
12
  useUnifiedChainSwitchAndExecute,
13
13
  } from "@b3dotfun/sdk/global-account/react";
14
14
  import { PUBLIC_BASE_RPC_URL } from "@b3dotfun/sdk/shared/constants";
@@ -75,12 +75,32 @@ export function AnySpendStakeB3({
75
75
  const { address } = useAccount();
76
76
  const { switchChainAndExecute, isSwitchingOrExecuting } = useUnifiedChainSwitchAndExecute();
77
77
 
78
- // Fetch B3 token balance
79
- const { data: simBalance, isLoading: isBalanceLoading } = useSimBalance(address, [base.id]);
80
- const b3RawBalanceStr = simBalance?.balances.find(b => eqci(b.address, B3_TOKEN.address))?.amount || "0";
78
+ // Fetch B3 token balance. `isBalanceUnknown` must gate every use of `b3RawBalance`: a failed read
79
+ // yields the same 0 as an empty wallet, and acting on it pushes a holder into the swap flow to
80
+ // buy a token they already have.
81
+ const {
82
+ data: balances,
83
+ isLoading: isBalanceLoading,
84
+ isError: isBalanceError,
85
+ refetch: refetchBalance,
86
+ } = usePortfolioBalance(address, [base.id]);
87
+ // Guarded on `address`: with no wallet connected the query never runs, and a query that was
88
+ // never asked is not a query that failed. Without this the screen tells a visitor their
89
+ // balance is unavailable and refuses to continue, when they simply have not connected yet.
90
+ const isBalanceUnknown = Boolean(address) && (isBalanceError || (!isBalanceLoading && balances === undefined));
91
+ const b3RawBalanceStr = balances?.find(b => eqci(b.address, B3_TOKEN.address))?.amount || "0";
81
92
  const b3RawBalance = BigInt(b3RawBalanceStr);
82
93
  const b3Balance = formatTokenAmount(b3RawBalance, B3_TOKEN.decimals);
83
94
 
95
+ /**
96
+ * Whether the wallet demonstrably holds enough B3 to stake `amountRaw` directly.
97
+ *
98
+ * False while the balance is unknown — but callers must not read that as a shortfall. An unknown
99
+ * balance is neither enough nor short, and the two need different UI: a shortfall offers the swap
100
+ * flow, an unknown offers a retry. Check `isBalanceUnknown` first.
101
+ */
102
+ const hasEnoughB3 = (amountRaw: string) => !isBalanceUnknown && BigInt(amountRaw || "0") <= b3RawBalance;
103
+
84
104
  // State for direct staking flow
85
105
  const [isStaking, setIsStaking] = useState(false);
86
106
  const [stakingTxHash, setStakingTxHash] = useState<string>("");
@@ -273,10 +293,12 @@ export function AnySpendStakeB3({
273
293
  return;
274
294
  }
275
295
 
276
- // Check if user has sufficient B3 balance for direct staking
277
- const hasEnoughBalance = b3RawBalance && BigInt(userStakeAmount) <= b3RawBalance;
296
+ if (isBalanceUnknown) {
297
+ toast.error("Can't read your B3 balance right now. Please try again.");
298
+ return;
299
+ }
278
300
 
279
- if (hasEnoughBalance) {
301
+ if (hasEnoughB3(userStakeAmount)) {
280
302
  // User has enough B3, proceed with direct staking
281
303
  handleDirectStaking();
282
304
  } else {
@@ -334,10 +356,9 @@ export function AnySpendStakeB3({
334
356
  transition={{ duration: 0.3, delay: 0.1, ease: "easeInOut" }}
335
357
  >
336
358
  <h2 className="font-sf-rounded font-neue-montreal-medium mb-1 text-center text-2xl font-semibold">
337
- {(() => {
338
- const hasEnoughBalance = b3RawBalance && BigInt(debouncedUserStakeAmount || "0") <= b3RawBalance;
339
- return hasEnoughBalance || !debouncedAmount ? "Stake B3" : "Swap & Stake B3";
340
- })()}
359
+ {isBalanceUnknown || !debouncedAmount || hasEnoughB3(debouncedUserStakeAmount)
360
+ ? "Stake B3"
361
+ : "Swap & Stake B3"}
341
362
  </h2>
342
363
  </motion.div>
343
364
  </div>
@@ -356,7 +377,14 @@ export function AnySpendStakeB3({
356
377
  <div className="flex items-center justify-between">
357
378
  <p className="text-as-primary/70 text-sm font-medium">I want to stake</p>
358
379
  <span className="text-as-primary/50 flex items-center gap-1 text-sm">
359
- Available: {isBalanceLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : `${b3Balance} B3`}
380
+ Available:{" "}
381
+ {isBalanceLoading ? (
382
+ <Loader2 className="h-3 w-3 animate-spin" />
383
+ ) : isBalanceUnknown ? (
384
+ "unavailable"
385
+ ) : (
386
+ `${b3Balance} B3`
387
+ )}
360
388
  </span>
361
389
  </div>
362
390
  </div>
@@ -379,9 +407,22 @@ export function AnySpendStakeB3({
379
407
 
380
408
  <div className="mt-4">
381
409
  {(() => {
382
- const hasEnoughBalance = b3RawBalance && BigInt(debouncedUserStakeAmount || "0") <= b3RawBalance;
410
+ // An unreadable balance is not a shortfall, so it must not open the swap upsell —
411
+ // that would sell a swap to someone who may already hold the tokens.
412
+ if (isBalanceUnknown) {
413
+ return (
414
+ <div className="bg-as-brand/10 flex flex-col items-center gap-2 rounded-lg p-4 pb-5">
415
+ <p className="text-as-primary/70 text-sm font-medium">
416
+ We couldn&apos;t read your B3 balance just now.
417
+ </p>
418
+ <Button variant="ghost" onClick={() => refetchBalance()} className="text-as-primary text-sm">
419
+ Try again
420
+ </Button>
421
+ </div>
422
+ );
423
+ }
383
424
 
384
- if (!hasEnoughBalance || !debouncedAmount) {
425
+ if (!hasEnoughB3(debouncedUserStakeAmount) || !debouncedAmount) {
385
426
  return (
386
427
  <div className="bg-as-brand/10 flex flex-col items-center gap-2 rounded-lg p-4 pb-5">
387
428
  <div className="flex items-center justify-center gap-2">
@@ -9,7 +9,7 @@ import {
9
9
  toast,
10
10
  useHasMounted,
11
11
  useModalStore,
12
- useSimBalance,
12
+ usePortfolioBalance,
13
13
  useUnifiedChainSwitchAndExecute,
14
14
  } from "@b3dotfun/sdk/global-account/react";
15
15
  import { PUBLIC_BASE_RPC_URL } from "@b3dotfun/sdk/shared/constants";
@@ -82,12 +82,32 @@ export function AnySpendStakeB3ExactIn({
82
82
  const { address } = useAccount();
83
83
  const { switchChainAndExecute, isSwitchingOrExecuting } = useUnifiedChainSwitchAndExecute();
84
84
 
85
- // Fetch B3 token balance
86
- const { data: simBalance, isLoading: isBalanceLoading } = useSimBalance(address, [base.id]);
87
- const b3RawBalanceStr = simBalance?.balances.find(b => eqci(b.address, B3_TOKEN.address))?.amount || "0";
85
+ // Fetch B3 token balance. `isBalanceUnknown` must gate every use of `b3RawBalance`: a failed read
86
+ // yields the same 0 as an empty wallet, and acting on it pushes a holder into the swap flow to
87
+ // buy a token they already have.
88
+ const {
89
+ data: balances,
90
+ isLoading: isBalanceLoading,
91
+ isError: isBalanceError,
92
+ refetch: refetchBalance,
93
+ } = usePortfolioBalance(address, [base.id]);
94
+ // Guarded on `address`: with no wallet connected the query never runs, and a query that was
95
+ // never asked is not a query that failed. Without this the screen tells a visitor their
96
+ // balance is unavailable and refuses to continue, when they simply have not connected yet.
97
+ const isBalanceUnknown = Boolean(address) && (isBalanceError || (!isBalanceLoading && balances === undefined));
98
+ const b3RawBalanceStr = balances?.find(b => eqci(b.address, B3_TOKEN.address))?.amount || "0";
88
99
  const b3RawBalance = BigInt(b3RawBalanceStr);
89
100
  const b3Balance = formatTokenAmount(b3RawBalance, B3_TOKEN.decimals);
90
101
 
102
+ /**
103
+ * Whether the wallet demonstrably holds enough B3 to stake `amountRaw` directly.
104
+ *
105
+ * False while the balance is unknown — but callers must not read that as a shortfall. An unknown
106
+ * balance is neither enough nor short, and the two need different UI: a shortfall offers the swap
107
+ * flow, an unknown offers a retry. Check `isBalanceUnknown` first.
108
+ */
109
+ const hasEnoughB3 = (amountRaw: string) => !isBalanceUnknown && BigInt(amountRaw || "0") <= b3RawBalance;
110
+
91
111
  // State for direct staking flow
92
112
  const [isStaking, setIsStaking] = useState(false);
93
113
  const [stakingTxHash, setStakingTxHash] = useState<string>("");
@@ -283,9 +303,12 @@ export function AnySpendStakeB3ExactIn({
283
303
  }
284
304
 
285
305
  // Check if user has sufficient B3 balance for direct staking
286
- const hasEnoughBalance = b3RawBalance && BigInt(userStakeAmount) <= b3RawBalance;
306
+ if (isBalanceUnknown) {
307
+ toast.error("Can't read your B3 balance right now. Please try again.");
308
+ return;
309
+ }
287
310
 
288
- if (hasEnoughBalance) {
311
+ if (hasEnoughB3(userStakeAmount)) {
289
312
  // User has enough B3, proceed with direct staking
290
313
  handleDirectStaking();
291
314
  } else {
@@ -353,8 +376,9 @@ export function AnySpendStakeB3ExactIn({
353
376
  >
354
377
  <h2 className="font-sf-rounded font-neue-montreal-medium mb-1 text-center text-2xl font-semibold">
355
378
  {(() => {
356
- const hasEnoughBalance = b3RawBalance && BigInt(debouncedUserStakeAmount || "0") <= b3RawBalance;
357
- return hasEnoughBalance || !debouncedAmount ? "Stake B3" : "Swap & Stake B3";
379
+ return isBalanceUnknown || !debouncedAmount || hasEnoughB3(debouncedUserStakeAmount)
380
+ ? "Stake B3"
381
+ : "Swap & Stake B3";
358
382
  })()}
359
383
  </h2>
360
384
  </motion.div>
@@ -374,7 +398,14 @@ export function AnySpendStakeB3ExactIn({
374
398
  <div className="flex items-center justify-between">
375
399
  <p className="text-as-primary/70 text-sm font-medium">I want to stake</p>
376
400
  <span className="text-as-primary/50 flex items-center gap-1 text-sm">
377
- Available: {isBalanceLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : `${b3Balance} B3`}
401
+ Available:{" "}
402
+ {isBalanceLoading ? (
403
+ <Loader2 className="h-3 w-3 animate-spin" />
404
+ ) : isBalanceUnknown ? (
405
+ "unavailable"
406
+ ) : (
407
+ `${b3Balance} B3`
408
+ )}
378
409
  </span>
379
410
  </div>
380
411
  </div>
@@ -397,9 +428,22 @@ export function AnySpendStakeB3ExactIn({
397
428
 
398
429
  <div className="mt-4">
399
430
  {(() => {
400
- const hasEnoughBalance = b3RawBalance && BigInt(debouncedUserStakeAmount || "0") <= b3RawBalance;
431
+ // An unreadable balance is not a shortfall, so it must not open the swap upsell —
432
+ // that would sell a swap to someone who may already hold the tokens.
433
+ if (isBalanceUnknown) {
434
+ return (
435
+ <div className="bg-as-brand/10 flex flex-col items-center gap-2 rounded-lg p-4 pb-5">
436
+ <p className="text-as-primary/70 text-sm font-medium">
437
+ We couldn&apos;t read your B3 balance just now.
438
+ </p>
439
+ <Button variant="ghost" onClick={() => refetchBalance()} className="text-as-primary text-sm">
440
+ Try again
441
+ </Button>
442
+ </div>
443
+ );
444
+ }
401
445
 
402
- if (!hasEnoughBalance || !debouncedAmount) {
446
+ if (!hasEnoughB3(debouncedUserStakeAmount) || !debouncedAmount) {
403
447
  return (
404
448
  <div className="bg-as-brand/10 flex flex-col items-center gap-2 rounded-lg p-4 pb-5">
405
449
  <div className="flex items-center justify-center gap-2">
@@ -89,10 +89,9 @@ vi.mock("@b3dotfun/sdk/global-account/react/hooks/useAccountWallet", () => ({
89
89
  useAccountWallet: () => ({ connectedEOAWallet: undefined }),
90
90
  }));
91
91
 
92
- vi.mock("@b3dotfun/sdk/global-account/react/hooks/useSimBalance", () => ({
93
- useSimBalance: () => ({ data: undefined, isLoading: false }),
94
- useSimSvmBalance: () => ({ data: undefined, isLoading: false }),
95
- useSimTokenBalance: () => ({ data: undefined, isLoading: false }),
92
+ vi.mock("@b3dotfun/sdk/global-account/react/hooks/usePortfolioBalance", () => ({
93
+ usePortfolioBalance: () => ({ data: undefined, isLoading: false, isError: false }),
94
+ usePortfolioTokenBalance: () => ({ data: undefined, isLoading: false, isError: false }),
96
95
  }));
97
96
 
98
97
  vi.mock("@b3dotfun/sdk/global-account/react/hooks/useTokenData", () => ({
@@ -7,12 +7,12 @@ import { useAnyspendOrderAndTransactions } from "@b3dotfun/sdk/anyspend/react/ho
7
7
  import { useCreateDepositFirstOrder } from "@b3dotfun/sdk/anyspend/react/hooks/useCreateDepositFirstOrder";
8
8
  import { useOnOrderSuccess } from "@b3dotfun/sdk/anyspend/react/hooks/useOnOrderSuccess";
9
9
  import { ALL_CHAINS, RELAY_SOLANA_MAINNET_CHAIN_ID, getAvailableChainIds } from "@b3dotfun/sdk/anyspend";
10
- import { getPaymentUrl } from "@b3dotfun/sdk/anyspend/utils/chain";
10
+ import { getPaymentUrl, toRegistryChainId } from "@b3dotfun/sdk/anyspend/utils/chain";
11
11
  import { isNativeToken } from "@b3dotfun/sdk/anyspend/utils/token";
12
12
  import {
13
13
  useAccountWallet,
14
14
  useIsMobile,
15
- useSimTokenBalance,
15
+ usePortfolioTokenBalance,
16
16
  useTokenData,
17
17
  useUnifiedChainSwitchAndExecute,
18
18
  } from "@b3dotfun/sdk/global-account/react";
@@ -116,13 +116,21 @@ export function CryptoPayPanel({
116
116
  ? "native"
117
117
  : selectedSrcToken.address
118
118
  : undefined;
119
- const { data: balanceData } = useSimTokenBalance(effectiveAddress, tokenAddress, selectedSrcChainId);
119
+ // `selectedSrcChainId` is Relay-numbered; balance rows are keyed by the chain registry's id.
120
+ const { data: balanceRow, isError: isBalanceError } = usePortfolioTokenBalance(
121
+ effectiveAddress,
122
+ tokenAddress,
123
+ toRegistryChainId(selectedSrcChainId),
124
+ );
120
125
 
121
126
  const balance = useMemo(() => {
122
- const b = balanceData?.balances?.[0];
123
- if (!b?.amount) return { raw: BigInt(0), formatted: "0", decimals: 18 };
124
- return { raw: BigInt(b.amount), formatted: formatTokenAmount(BigInt(b.amount), b.decimals), decimals: b.decimals };
125
- }, [balanceData]);
127
+ if (!balanceRow?.amount) return { raw: BigInt(0), formatted: "0", decimals: 18 };
128
+ return {
129
+ raw: BigInt(balanceRow.amount),
130
+ formatted: formatTokenAmount(BigInt(balanceRow.amount), balanceRow.decimals),
131
+ decimals: balanceRow.decimals,
132
+ };
133
+ }, [balanceRow]);
126
134
 
127
135
  const srcAmount = useMemo(() => {
128
136
  if (isSameToken) return totalAmount;
@@ -134,7 +142,9 @@ export function CryptoPayPanel({
134
142
  return formatTokenAmount(BigInt(srcAmount || "0"), selectedSrcToken.decimals || 18);
135
143
  }, [srcAmount, selectedSrcToken]);
136
144
 
137
- const hasEnoughBalance = balance.raw >= BigInt(srcAmount || "0");
145
+ // A failed balance read must not masquerade as a shortfall: it would disable Pay and tell a
146
+ // funded user they are short. Unknown blocks auto-pay but leaves the manual path open.
147
+ const hasEnoughBalance = !isBalanceError && balance.raw >= BigInt(srcAmount || "0");
138
148
 
139
149
  /* ------------------------------------------------------------------ */
140
150
  /* Destination token object (shared by both flows) */
@@ -385,7 +395,8 @@ export function CryptoPayPanel({
385
395
 
386
396
  const isLoading = isLoadingAnyspendQuote;
387
397
  const isPending = isCreatingSwapOrder || isSendingDeposit || isWaitingForExecution;
388
- const canPay = connectedAddress && selectedSrcToken && hasEnoughBalance && !isLoading && !isPending;
398
+ const canPay =
399
+ connectedAddress && selectedSrcToken && (hasEnoughBalance || isBalanceError) && !isLoading && !isPending;
389
400
 
390
401
  const chainName = ALL_CHAINS[selectedSrcChainId]?.name || "the specified chain";
391
402
  const chainLogoUrl = ALL_CHAINS[selectedSrcChainId]?.logoUrl;
@@ -494,7 +505,7 @@ export function CryptoPayPanel({
494
505
  </div>
495
506
  </motion.div>
496
507
 
497
- {/* ---- Insufficient balance warning ---- */}
508
+ {/* ---- Balance warning ---- */}
498
509
  <AnimatePresence>
499
510
  {connectedAddress && selectedSrcToken && !hasEnoughBalance && !isLoading && (
500
511
  <motion.p
@@ -503,9 +514,11 @@ export function CryptoPayPanel({
503
514
  animate={{ opacity: 1, height: "auto" }}
504
515
  exit={{ opacity: 0, height: 0 }}
505
516
  transition={{ duration: 0.2, ease: "easeOut" }}
506
- className="text-center text-sm text-red-500"
517
+ className={`text-center text-sm ${isBalanceError ? "text-as-primary/50" : "text-red-500"}`}
507
518
  >
508
- Insufficient {selectedSrcToken.symbol} balance
519
+ {isBalanceError
520
+ ? `Couldn't check your ${selectedSrcToken.symbol} balance`
521
+ : `Insufficient ${selectedSrcToken.symbol} balance`}
509
522
  </motion.p>
510
523
  )}
511
524
  </AnimatePresence>
@@ -1,7 +1,7 @@
1
1
  import { components } from "@b3dotfun/sdk/anyspend/types/api";
2
- import { getNativeRequired } from "@b3dotfun/sdk/anyspend/utils/chain";
2
+ import { getNativeRequired, toRegistryChainId } from "@b3dotfun/sdk/anyspend/utils/chain";
3
3
  import { isNativeToken } from "@b3dotfun/sdk/anyspend/utils/token";
4
- import { useSimTokenBalance } from "@b3dotfun/sdk/global-account/react";
4
+ import { usePortfolioTokenBalance } from "@b3dotfun/sdk/global-account/react";
5
5
  import { formatTokenAmount } from "@b3dotfun/sdk/shared/utils/number";
6
6
  import { useMemo } from "react";
7
7
  import { formatUnits } from "viem";
@@ -15,11 +15,14 @@ export function TokenBalance({
15
15
  walletAddress: string | undefined;
16
16
  onChangeInput: (value: string) => void;
17
17
  }) {
18
- const tokenAddress = isNativeToken(token.address) ? "native" : token.address;
19
- const { data, isLoading } = useSimTokenBalance(walletAddress, tokenAddress, token.chainId);
18
+ // `token.chainId` is Relay-numbered; balance rows are keyed by the chain registry's id.
19
+ const {
20
+ data: balance,
21
+ isLoading,
22
+ isError,
23
+ } = usePortfolioTokenBalance(walletAddress, token.address, toRegistryChainId(token.chainId));
20
24
 
21
25
  const { rawBalance, formattedBalance } = useMemo(() => {
22
- const balance = data?.balances?.[0];
23
26
  if (!balance?.amount) {
24
27
  return { rawBalance: null, formattedBalance: "0" };
25
28
  }
@@ -28,7 +31,7 @@ export function TokenBalance({
28
31
  rawBalance: raw,
29
32
  formattedBalance: formatTokenAmount(raw, balance.decimals),
30
33
  };
31
- }, [data]);
34
+ }, [balance]);
32
35
 
33
36
  const handlePercentageClick = (percentage: number) => {
34
37
  if (!rawBalance) return;
@@ -59,7 +62,7 @@ export function TokenBalance({
59
62
  {!isLoading && (
60
63
  <>
61
64
  <div className="text-as-primary/50 inline-flex rounded-lg text-sm">
62
- {rawBalance ? `Balance: ${formattedBalance}` : `Balance: 0`}
65
+ {isError ? "Balance: —" : rawBalance ? `Balance: ${formattedBalance}` : `Balance: 0`}
63
66
  </div>
64
67
 
65
68
  {!!rawBalance && (
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Deprecated chains must disappear from the chain picker but stay in ALL_CHAINS.
3
+ *
4
+ * Those are two different questions. ALL_CHAINS answers "what is this chain" and is needed to
5
+ * render every order that ever existed — anyspend has ~1,400 historical orders on B3 and Plume, and
6
+ * getChainName throws on an unknown id. getAvailableChainIds answers "can a new order use this",
7
+ * which is what the deprecation actually changes.
8
+ */
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import { ALL_CHAINS, DEPRECATED_CHAIN_IDS, getAvailableChainIds, isDeprecatedChain } from "../chain";
12
+
13
+ describe("deprecated chains", () => {
14
+ it("covers B3 and Plume", () => {
15
+ expect(DEPRECATED_CHAIN_IDS).toContain(8333);
16
+ expect(DEPRECATED_CHAIN_IDS).toContain(98866);
17
+ expect(isDeprecatedChain(8333)).toBe(true);
18
+ expect(isDeprecatedChain(98866)).toBe(true);
19
+ expect(isDeprecatedChain(8453)).toBe(false);
20
+ expect(isDeprecatedChain(undefined)).toBe(false);
21
+ });
22
+
23
+ // Offering a chain the API rejects lets a user pick something that then fails at order creation.
24
+ it("are absent from the picker in both directions", () => {
25
+ for (const context of ["from", "to"] as const) {
26
+ const available = getAvailableChainIds(context);
27
+ for (const chainId of DEPRECATED_CHAIN_IDS) {
28
+ expect(available).not.toContain(chainId);
29
+ }
30
+ }
31
+ });
32
+
33
+ it("still leaves the picker with the chains that do work", () => {
34
+ expect(getAvailableChainIds("to")).toContain(8453);
35
+ expect(getAvailableChainIds("from")).toContain(1);
36
+ });
37
+
38
+ // Removing the entry would make getChainName and friends throw while rendering old orders.
39
+ it("remain in ALL_CHAINS so historical orders still render", () => {
40
+ for (const chainId of DEPRECATED_CHAIN_IDS) {
41
+ expect(ALL_CHAINS[chainId]).toBeDefined();
42
+ }
43
+ });
44
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { RELAY_SOLANA_MAINNET_CHAIN_ID } from "@b3dotfun/sdk/anyspend/constants";
3
+ import { toRegistryChainId } from "../chain";
4
+ import { SOLANA_CHAIN_ID } from "@b3dotfun/sdk/shared/utils/zerionPositions";
5
+
6
+ describe("toRegistryChainId", () => {
7
+ it("translates Relay's Solana id to the registry's", () => {
8
+ expect(RELAY_SOLANA_MAINNET_CHAIN_ID).not.toBe(SOLANA_CHAIN_ID);
9
+ expect(toRegistryChainId(RELAY_SOLANA_MAINNET_CHAIN_ID)).toBe(SOLANA_CHAIN_ID);
10
+ });
11
+
12
+ it("leaves EVM chain ids alone — those are universal", () => {
13
+ for (const chainId of [1, 8453, 42161, 137, 999]) {
14
+ expect(toRegistryChainId(chainId)).toBe(chainId);
15
+ }
16
+ });
17
+ });
@@ -1,4 +1,5 @@
1
1
  import { RELAY_SOLANA_MAINNET_CHAIN_ID } from "@b3dotfun/sdk/anyspend/constants";
2
+ import { SOLANA_CHAIN_ID } from "@b3dotfun/sdk/shared/utils/zerionPositions";
2
3
  import {
3
4
  ABSTRACT_PUBLIC_RPC,
4
5
  ARBITRUM_PUBLIC_RPC,
@@ -10,6 +11,7 @@ import {
10
11
  OPTIMISM_PUBLIC_RPC,
11
12
  PLUME_PUBLIC_RPC,
12
13
  POLYGON_PUBLIC_RPC,
14
+ ROBINHOOD_PUBLIC_RPC,
13
15
  } from "@b3dotfun/sdk/anyspend/constants/rpc";
14
16
  import { components } from "@b3dotfun/sdk/anyspend/types/api";
15
17
  import { b3Viem } from "@b3dotfun/sdk/shared/constants/chains/b3Viem";
@@ -26,7 +28,7 @@ import {
26
28
  Transport,
27
29
  WalletClient,
28
30
  } from "viem";
29
- import { abstract, arbitrum, avalanche, base, bsc, mainnet, optimism, polygon } from "viem/chains";
31
+ import { abstract, arbitrum, avalanche, base, bsc, mainnet, optimism, polygon, robinhood } from "viem/chains";
30
32
  import { ChainType, IBaseChain, IEVMChain, IHyperliquidChain, ISolanaChain } from "../types/chain";
31
33
  import {
32
34
  getAvaxToken,
@@ -272,6 +274,20 @@ export const EVM_MAINNET: Record<number, IEVMChain> = {
272
274
  coingeckoName: "plume-network",
273
275
  wethAddress: "0xea237441c92cae6fc17caaf9a7acb3f953be4bd1", // WPLUME (wrapped native)
274
276
  },
277
+ [robinhood.id]: {
278
+ id: robinhood.id,
279
+ name: robinhood.name,
280
+ logoUrl: "https://assets.relay.link/icons/square/4663/light.png",
281
+ type: ChainType.EVM,
282
+ nativeRequired: parseEther("0.0001"),
283
+ canDepositNative: true,
284
+ defaultToken: getEthToken(robinhood.id),
285
+ nativeToken: getEthToken(robinhood.id),
286
+ viem: getCustomEvmChain(robinhood, ROBINHOOD_PUBLIC_RPC),
287
+ pollingInterval: 1000, // 1 second for Robinhood Chain (Arbitrum Orbit fast blocks)
288
+ coingeckoName: "robinhood",
289
+ wethAddress: "0x0bd7d308f8e1639fab988df18a8011f41eacad73", // wrapped native ETH (registry wrappedNative)
290
+ },
275
291
  };
276
292
 
277
293
  export const EVM_TESTNET: Record<number, IEVMChain> = {
@@ -356,6 +372,17 @@ export const EVM_CHAINS: Record<number, IEVMChain> = { ...EVM_MAINNET, ...EVM_TE
356
372
 
357
373
  export const SOLANA_CHAINS: Record<number, ISolanaChain> = { [RELAY_SOLANA_MAINNET_CHAIN_ID]: SOLANA_MAINNET };
358
374
 
375
+ /**
376
+ * Translate a Relay-numbered chain id into the id the chain registry uses.
377
+ *
378
+ * Relay numbers Solana 792703809; every other chain shares the universal EVM id. Balance rows are
379
+ * keyed by the registry's id, so handing Relay's number to a balance lookup matches nothing at all
380
+ * — and a funded Solana wallet then reads as empty rather than as unreadable.
381
+ */
382
+ export function toRegistryChainId(chainId: number): number {
383
+ return chainId === RELAY_SOLANA_MAINNET_CHAIN_ID ? SOLANA_CHAIN_ID : chainId;
384
+ }
385
+
359
386
  export const HYPERLIQUID_CHAINS: Record<number, IHyperliquidChain> = {
360
387
  [HYPERLIQUID_CHAIN_ID]: HYPERLIQUID_MAINNET,
361
388
  };
@@ -680,6 +707,26 @@ export function getHyperliquidChain(chainId: number): IHyperliquidChain {
680
707
  return HYPERLIQUID_CHAINS[chainId];
681
708
  }
682
709
 
710
+ /**
711
+ * Chains AnySpend no longer accepts orders on, as either source or destination.
712
+ *
713
+ * They stay in ALL_CHAINS on purpose. That map answers "what is this chain" — name, logo, token,
714
+ * decimals — which every order that ever existed still needs in order to render; removing an entry
715
+ * would make getChainName and friends throw on historical rows. Whether new orders are accepted is
716
+ * this separate question.
717
+ *
718
+ * anyspend-service imports this list rather than keeping its own, so the API and the chain picker
719
+ * can never disagree about what is on offer.
720
+ */
721
+ export const DEPRECATED_CHAIN_IDS: readonly number[] = [
722
+ 8333, // B3
723
+ 98866, // Plume
724
+ ];
725
+
726
+ export function isDeprecatedChain(chainId?: number): boolean {
727
+ return chainId !== undefined && DEPRECATED_CHAIN_IDS.includes(chainId);
728
+ }
729
+
683
730
  /**
684
731
  * Get available chain IDs for AnySpend based on context (source or destination).
685
732
  * Filters out chains that shouldn't be available for the given context.
@@ -695,7 +742,11 @@ export function getHyperliquidChain(chainId: number): IHyperliquidChain {
695
742
  * const destChains = getAvailableChainIds("to") // [1, 8453, 137, ..., 1337]
696
743
  */
697
744
  export function getAvailableChainIds(context: "from" | "to"): number[] {
698
- const allChainIds = Object.values(ALL_CHAINS).map(chain => chain.id);
745
+ // Deprecated chains are excluded from both directions: the API rejects them, so offering them in
746
+ // a picker would let a user select something that then fails at order creation.
747
+ const allChainIds = Object.values(ALL_CHAINS)
748
+ .map(chain => chain.id)
749
+ .filter(chainId => !isDeprecatedChain(chainId));
699
750
 
700
751
  if (context === "from") {
701
752
  // Hyperliquid is only supported as destination chain, not source chain
@@ -37,7 +37,6 @@ export function B3Provider({
37
37
  environment,
38
38
  automaticallySetFirstEoa,
39
39
  defaultEoaProvider,
40
- simDuneApiKey,
41
40
  // deprecated since v0.0.87
42
41
  toaster: _toaster,
43
42
  clientType = "rest",
@@ -63,7 +62,6 @@ export function B3Provider({
63
62
  automaticallySetFirstEoa?: boolean;
64
63
  /** EIP-1193 provider to auto-connect as the default EOA wallet (e.g., Farcaster frame wallet) */
65
64
  defaultEoaProvider?: EIP1193.EIP1193Provider;
66
- simDuneApiKey?: string;
67
65
  toaster?: {
68
66
  position?: "top-center" | "top-right" | "bottom-center" | "bottom-right";
69
67
  style?: React.CSSProperties;
@@ -150,7 +148,7 @@ export function B3Provider({
150
148
  authStrategy={authStrategy}
151
149
  >
152
150
  <ToastContextConnector />
153
- <RelayKitProviderWrapper simDuneApiKey={simDuneApiKey}>
151
+ <RelayKitProviderWrapper>
154
152
  {children}
155
153
  {/* For the modal https://github.com/b3-fun/b3/blob/main/packages/sdk/src/global-account/react/components/ui/dialog.tsx#L46 */}
156
154
  <StyleRoot id="b3-root" />
@@ -2,13 +2,7 @@ import { RelayKitProvider } from "@relayprotocol/relay-kit-ui";
2
2
  import { fetchChainConfigs, MAINNET_RELAY_API, RelayChain } from "@relayprotocol/relay-sdk";
3
3
  import { useEffect, useState } from "react";
4
4
 
5
- export function RelayKitProviderWrapper({
6
- children,
7
- simDuneApiKey,
8
- }: {
9
- children: React.ReactNode;
10
- simDuneApiKey?: string;
11
- }) {
5
+ export function RelayKitProviderWrapper({ children }: { children: React.ReactNode }) {
12
6
  const [relayChains, setRelayChains] = useState<RelayChain[]>([]);
13
7
 
14
8
  useEffect(() => {
@@ -19,15 +13,14 @@ export function RelayKitProviderWrapper({
19
13
  fetchChains();
20
14
  }, []);
21
15
 
16
+ // No balance provider is configured for Relay's own token selector. Relay reads balances through
17
+ // its Dune integration, which we no longer hold a key for, and it accepts no substitute — so the
18
+ // selector lists tokens without balances rather than showing figures nothing can refresh.
22
19
  return (
23
20
  <RelayKitProvider
24
21
  options={{
25
22
  baseApiUrl: MAINNET_RELAY_API,
26
23
  source: "anyspend",
27
- duneConfig: {
28
- apiKey: simDuneApiKey,
29
- apiBaseUrl: "https://api.sim.dune.com",
30
- },
31
24
  chains: relayChains,
32
25
  privateChainIds: undefined,
33
26
  appName: "AnySpend",