@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.
- package/README.md +3 -3
- package/dist/cjs/anyspend/constants/rpc.d.ts +1 -0
- package/dist/cjs/anyspend/constants/rpc.js +3 -1
- package/dist/cjs/anyspend/react/components/AnySpendDeposit.d.ts +1 -3
- package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +14 -12
- package/dist/cjs/anyspend/react/components/AnySpendStakeB3.js +32 -13
- package/dist/cjs/anyspend/react/components/AnySpendStakeB3ExactIn.js +32 -10
- package/dist/cjs/anyspend/react/components/checkout/CryptoPayPanel.js +16 -8
- package/dist/cjs/anyspend/react/components/common/TokenBalance.js +4 -5
- package/dist/cjs/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/cjs/anyspend/utils/chain.d.ts +45 -0
- package/dist/cjs/anyspend/utils/chain.js +51 -2
- package/dist/cjs/global-account/react/components/B3Provider/B3Provider.d.ts +1 -2
- package/dist/cjs/global-account/react/components/B3Provider/B3Provider.js +2 -2
- package/dist/cjs/global-account/react/components/B3Provider/RelayKitProviderWrapper.d.ts +1 -2
- package/dist/cjs/global-account/react/components/B3Provider/RelayKitProviderWrapper.js +4 -5
- package/dist/cjs/global-account/react/components/ManageAccount/ContentTokens.js +23 -15
- package/dist/cjs/global-account/react/components/ManageAccount/NFTContent.js +2 -2
- package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +8 -6
- package/dist/cjs/global-account/react/components/ManageAccount/TokenContent.js +17 -8
- package/dist/cjs/global-account/react/components/Send/Send.js +15 -11
- package/dist/cjs/global-account/react/hooks/index.d.ts +2 -2
- package/dist/cjs/global-account/react/hooks/index.js +7 -8
- package/dist/cjs/global-account/react/hooks/useAuth.d.ts +2 -1
- package/dist/cjs/global-account/react/hooks/useAuth.js +1 -0
- package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +2 -1
- package/dist/cjs/global-account/react/hooks/useAuthentication.js +1 -0
- package/dist/cjs/global-account/react/hooks/useFirstEOA.d.ts +4 -7666
- package/dist/cjs/global-account/react/hooks/useFirstEOA.js +1 -0
- package/dist/cjs/global-account/react/hooks/usePortfolioBalance.d.ts +15 -0
- package/dist/cjs/global-account/react/hooks/usePortfolioBalance.js +76 -0
- package/dist/cjs/global-account/react/hooks/usePortfolioCollectibles.d.ts +12 -0
- package/dist/cjs/global-account/react/hooks/usePortfolioCollectibles.js +178 -0
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +0 -2
- package/dist/cjs/global-account/react/utils/createWagmiConfig.d.ts +2 -2
- package/dist/cjs/shared/constants/chains/b3Chain.d.ts +24 -0
- package/dist/cjs/shared/constants/chains/supported.d.ts +4 -4
- package/dist/cjs/shared/constants/index.d.ts +4 -0
- package/dist/cjs/shared/utils/zerionPositions.d.ts +96 -0
- package/dist/cjs/shared/utils/zerionPositions.js +186 -0
- package/dist/cjs/wallet/utils/createWalletConfig.d.ts +2 -2
- package/dist/esm/anyspend/constants/rpc.d.ts +1 -0
- package/dist/esm/anyspend/constants/rpc.js +2 -0
- package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +1 -3
- package/dist/esm/anyspend/react/components/AnySpendDeposit.js +15 -13
- package/dist/esm/anyspend/react/components/AnySpendStakeB3.js +33 -14
- package/dist/esm/anyspend/react/components/AnySpendStakeB3ExactIn.js +33 -11
- package/dist/esm/anyspend/react/components/checkout/CryptoPayPanel.js +18 -10
- package/dist/esm/anyspend/react/components/common/TokenBalance.js +6 -7
- package/dist/esm/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/esm/anyspend/utils/chain.d.ts +45 -0
- package/dist/esm/anyspend/utils/chain.js +50 -3
- package/dist/esm/global-account/react/components/B3Provider/B3Provider.d.ts +1 -2
- package/dist/esm/global-account/react/components/B3Provider/B3Provider.js +2 -2
- package/dist/esm/global-account/react/components/B3Provider/RelayKitProviderWrapper.d.ts +1 -2
- package/dist/esm/global-account/react/components/B3Provider/RelayKitProviderWrapper.js +4 -5
- package/dist/esm/global-account/react/components/ManageAccount/ContentTokens.js +24 -16
- package/dist/esm/global-account/react/components/ManageAccount/NFTContent.js +3 -3
- package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +9 -7
- package/dist/esm/global-account/react/components/ManageAccount/TokenContent.js +18 -9
- package/dist/esm/global-account/react/components/Send/Send.js +16 -12
- package/dist/esm/global-account/react/hooks/index.d.ts +2 -2
- package/dist/esm/global-account/react/hooks/index.js +2 -2
- package/dist/esm/global-account/react/hooks/useAuth.d.ts +2 -1
- package/dist/esm/global-account/react/hooks/useAuth.js +1 -0
- package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +2 -1
- package/dist/esm/global-account/react/hooks/useAuthentication.js +1 -0
- package/dist/esm/global-account/react/hooks/useFirstEOA.d.ts +4 -7666
- package/dist/esm/global-account/react/hooks/useFirstEOA.js +1 -0
- package/dist/esm/global-account/react/hooks/usePortfolioBalance.d.ts +15 -0
- package/dist/esm/global-account/react/hooks/usePortfolioBalance.js +72 -0
- package/dist/esm/global-account/react/hooks/usePortfolioCollectibles.d.ts +12 -0
- package/dist/esm/global-account/react/hooks/usePortfolioCollectibles.js +175 -0
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +0 -2
- package/dist/esm/global-account/react/utils/createWagmiConfig.d.ts +2 -2
- package/dist/esm/shared/constants/chains/b3Chain.d.ts +24 -0
- package/dist/esm/shared/constants/chains/supported.d.ts +4 -4
- package/dist/esm/shared/constants/index.d.ts +4 -0
- package/dist/esm/shared/utils/zerionPositions.d.ts +96 -0
- package/dist/esm/shared/utils/zerionPositions.js +178 -0
- package/dist/esm/wallet/utils/createWalletConfig.d.ts +2 -2
- package/dist/types/anyspend/constants/rpc.d.ts +1 -0
- package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +1 -3
- package/dist/types/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/types/anyspend/utils/chain.d.ts +45 -0
- package/dist/types/global-account/react/components/B3Provider/B3Provider.d.ts +1 -2
- package/dist/types/global-account/react/components/B3Provider/RelayKitProviderWrapper.d.ts +1 -2
- package/dist/types/global-account/react/hooks/index.d.ts +2 -2
- package/dist/types/global-account/react/hooks/useAuth.d.ts +2 -1
- package/dist/types/global-account/react/hooks/useAuthentication.d.ts +2 -1
- package/dist/types/global-account/react/hooks/useFirstEOA.d.ts +4 -7666
- package/dist/types/global-account/react/hooks/usePortfolioBalance.d.ts +15 -0
- package/dist/types/global-account/react/hooks/usePortfolioCollectibles.d.ts +12 -0
- package/dist/types/global-account/react/stores/useModalStore.d.ts +0 -2
- package/dist/types/global-account/react/utils/createWagmiConfig.d.ts +2 -2
- package/dist/types/shared/constants/chains/b3Chain.d.ts +24 -0
- package/dist/types/shared/constants/chains/supported.d.ts +4 -4
- package/dist/types/shared/constants/index.d.ts +4 -0
- package/dist/types/shared/utils/zerionPositions.d.ts +96 -0
- package/dist/types/wallet/utils/createWalletConfig.d.ts +2 -2
- package/node_modules/@b3/chain-registry/chains.json +29 -1
- package/node_modules/@b3/chain-registry/package.json +18 -2
- package/package.json +2 -2
- package/src/anyspend/README.md +2 -2
- package/src/anyspend/constants/rpc.ts +2 -0
- package/src/anyspend/docs/components.md +6 -6
- package/src/anyspend/docs/contributing.md +1 -1
- package/src/anyspend/docs/hooks.md +3 -3
- package/src/anyspend/docs/installation.md +2 -2
- package/src/anyspend/react/components/AnySpendDeposit.tsx +26 -19
- package/src/anyspend/react/components/AnySpendStakeB3.tsx +55 -14
- package/src/anyspend/react/components/AnySpendStakeB3ExactIn.tsx +55 -11
- package/src/anyspend/react/components/__tests__/QRDeposit.test.tsx +3 -4
- package/src/anyspend/react/components/checkout/CryptoPayPanel.tsx +25 -12
- package/src/anyspend/react/components/common/TokenBalance.tsx +10 -7
- package/src/anyspend/utils/__tests__/deprecatedChains.test.ts +44 -0
- package/src/anyspend/utils/__tests__/toRegistryChainId.test.ts +17 -0
- package/src/anyspend/utils/chain.ts +53 -2
- package/src/global-account/react/components/B3Provider/B3Provider.tsx +1 -3
- package/src/global-account/react/components/B3Provider/RelayKitProviderWrapper.tsx +4 -11
- package/src/global-account/react/components/ManageAccount/ContentTokens.tsx +50 -21
- package/src/global-account/react/components/ManageAccount/NFTContent.tsx +5 -3
- package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +10 -6
- package/src/global-account/react/components/ManageAccount/TokenContent.tsx +22 -9
- package/src/global-account/react/components/Send/Send.tsx +39 -15
- package/src/global-account/react/hooks/index.ts +2 -2
- package/src/global-account/react/hooks/useAuth.ts +3 -2
- package/src/global-account/react/hooks/useAuthentication.ts +3 -2
- package/src/global-account/react/hooks/useFirstEOA.tsx +9 -2
- package/src/global-account/react/hooks/usePortfolioBalance.ts +89 -0
- package/src/global-account/react/hooks/usePortfolioCollectibles.ts +212 -0
- package/src/global-account/react/stores/useModalStore.ts +0 -2
- package/src/shared/constants/index.ts +8 -0
- package/src/shared/utils/__tests__/zerionPositions.test.ts +226 -0
- package/src/shared/utils/zerionPositions.ts +244 -0
- package/dist/cjs/global-account/react/hooks/useSimBalance.d.ts +0 -59
- package/dist/cjs/global-account/react/hooks/useSimBalance.js +0 -98
- package/dist/cjs/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
- package/dist/cjs/global-account/react/hooks/useSimCollectibles.js +0 -190
- package/dist/cjs/global-account/react/utils/simdune.d.ts +0 -7
- package/dist/cjs/global-account/react/utils/simdune.js +0 -21
- package/dist/esm/global-account/react/hooks/useSimBalance.d.ts +0 -59
- package/dist/esm/global-account/react/hooks/useSimBalance.js +0 -93
- package/dist/esm/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
- package/dist/esm/global-account/react/hooks/useSimCollectibles.js +0 -187
- package/dist/esm/global-account/react/utils/simdune.d.ts +0 -7
- package/dist/esm/global-account/react/utils/simdune.js +0 -17
- package/dist/types/global-account/react/hooks/useSimBalance.d.ts +0 -59
- package/dist/types/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
- package/dist/types/global-account/react/utils/simdune.d.ts +0 -7
- package/node_modules/@b3/chain-registry/src/index.ts +0 -169
- package/node_modules/@b3/chain-registry/tsconfig.json +0 -16
- package/src/global-account/react/hooks/useSimBalance.ts +0 -164
- package/src/global-account/react/hooks/useSimCollectibles.ts +0 -238
- 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
|
-
|
|
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 {
|
|
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<
|
|
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 {
|
|
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
|
|
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 = ():
|
|
102
|
-
if (!selectedToken || !
|
|
106
|
+
const getCurrentSelectedToken = (): B3TokenBalance | null => {
|
|
107
|
+
if (!selectedToken || !balances) {
|
|
103
108
|
return null;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
const found =
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
-
//
|
|
413
|
-
|
|
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'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 ?
|
|
439
|
-
const hasHiddenTokens = !shouldShowAllTokens && (
|
|
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:
|
|
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 {
|
|
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 } =
|
|
11
|
+
const { data: nfts, isLoading, isError } = usePortfolioCollectibles(activeAddress, [1, 8453], { filterSpam: true });
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
14
|
<div style={{ minHeight: "100px" }}>
|
|
15
|
-
{
|
|
15
|
+
{isError ? (
|
|
16
|
+
<div className="py-12 text-center text-gray-500">Couldn'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
|
-
|
|
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:
|
|
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 (!
|
|
41
|
-
return
|
|
42
|
-
}, [
|
|
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]">
|
|
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 {
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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'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 = [...
|
|
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.
|
|
47
|
-
tokenLogo = CHAIN_LOGOS[
|
|
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
|
-
|
|
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 {
|
|
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<
|
|
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 {
|
|
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 = ():
|
|
142
|
-
if (!selectedToken || !
|
|
146
|
+
const getCurrentSelectedToken = (): B3TokenBalance | null => {
|
|
147
|
+
if (!selectedToken || !balances) {
|
|
143
148
|
return null;
|
|
144
149
|
}
|
|
145
|
-
const found =
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
) :
|
|
376
|
+
) : balances && balances.length > 0 ? (
|
|
368
377
|
<div className="space-y-4">
|
|
369
378
|
<div className="space-y-1">
|
|
370
|
-
{
|
|
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'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 {
|
|
43
|
-
export {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|