@b3dotfun/sdk 0.1.70-alpha.17 → 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 (162) 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/hooks/useUnifiedChainSwitchAndExecute.d.ts +1 -0
  35. package/dist/cjs/global-account/react/hooks/useUnifiedChainSwitchAndExecute.js +18 -11
  36. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +0 -2
  37. package/dist/cjs/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  38. package/dist/cjs/shared/constants/chains/b3Chain.d.ts +24 -0
  39. package/dist/cjs/shared/constants/chains/supported.d.ts +4 -4
  40. package/dist/cjs/shared/constants/index.d.ts +4 -0
  41. package/dist/cjs/shared/utils/zerionPositions.d.ts +96 -0
  42. package/dist/cjs/shared/utils/zerionPositions.js +186 -0
  43. package/dist/cjs/wallet/utils/createWalletConfig.d.ts +2 -2
  44. package/dist/esm/anyspend/constants/rpc.d.ts +1 -0
  45. package/dist/esm/anyspend/constants/rpc.js +2 -0
  46. package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +1 -3
  47. package/dist/esm/anyspend/react/components/AnySpendDeposit.js +15 -13
  48. package/dist/esm/anyspend/react/components/AnySpendStakeB3.js +33 -14
  49. package/dist/esm/anyspend/react/components/AnySpendStakeB3ExactIn.js +33 -11
  50. package/dist/esm/anyspend/react/components/checkout/CryptoPayPanel.js +18 -10
  51. package/dist/esm/anyspend/react/components/common/TokenBalance.js +6 -7
  52. package/dist/esm/anyspend/react/hooks/useSigMint.d.ts +1 -1
  53. package/dist/esm/anyspend/utils/chain.d.ts +45 -0
  54. package/dist/esm/anyspend/utils/chain.js +50 -3
  55. package/dist/esm/global-account/react/components/B3Provider/B3Provider.d.ts +1 -2
  56. package/dist/esm/global-account/react/components/B3Provider/B3Provider.js +2 -2
  57. package/dist/esm/global-account/react/components/B3Provider/RelayKitProviderWrapper.d.ts +1 -2
  58. package/dist/esm/global-account/react/components/B3Provider/RelayKitProviderWrapper.js +4 -5
  59. package/dist/esm/global-account/react/components/ManageAccount/ContentTokens.js +24 -16
  60. package/dist/esm/global-account/react/components/ManageAccount/NFTContent.js +3 -3
  61. package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +9 -7
  62. package/dist/esm/global-account/react/components/ManageAccount/TokenContent.js +18 -9
  63. package/dist/esm/global-account/react/components/Send/Send.js +16 -12
  64. package/dist/esm/global-account/react/hooks/index.d.ts +2 -2
  65. package/dist/esm/global-account/react/hooks/index.js +2 -2
  66. package/dist/esm/global-account/react/hooks/useAuth.d.ts +2 -1
  67. package/dist/esm/global-account/react/hooks/useAuth.js +1 -0
  68. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +2 -1
  69. package/dist/esm/global-account/react/hooks/useAuthentication.js +1 -0
  70. package/dist/esm/global-account/react/hooks/useFirstEOA.d.ts +4 -7666
  71. package/dist/esm/global-account/react/hooks/useFirstEOA.js +1 -0
  72. package/dist/esm/global-account/react/hooks/usePortfolioBalance.d.ts +15 -0
  73. package/dist/esm/global-account/react/hooks/usePortfolioBalance.js +72 -0
  74. package/dist/esm/global-account/react/hooks/usePortfolioCollectibles.d.ts +12 -0
  75. package/dist/esm/global-account/react/hooks/usePortfolioCollectibles.js +175 -0
  76. package/dist/esm/global-account/react/hooks/useUnifiedChainSwitchAndExecute.d.ts +1 -0
  77. package/dist/esm/global-account/react/hooks/useUnifiedChainSwitchAndExecute.js +18 -11
  78. package/dist/esm/global-account/react/stores/useModalStore.d.ts +0 -2
  79. package/dist/esm/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  80. package/dist/esm/shared/constants/chains/b3Chain.d.ts +24 -0
  81. package/dist/esm/shared/constants/chains/supported.d.ts +4 -4
  82. package/dist/esm/shared/constants/index.d.ts +4 -0
  83. package/dist/esm/shared/utils/zerionPositions.d.ts +96 -0
  84. package/dist/esm/shared/utils/zerionPositions.js +178 -0
  85. package/dist/esm/wallet/utils/createWalletConfig.d.ts +2 -2
  86. package/dist/types/anyspend/constants/rpc.d.ts +1 -0
  87. package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +1 -3
  88. package/dist/types/anyspend/react/hooks/useSigMint.d.ts +1 -1
  89. package/dist/types/anyspend/utils/chain.d.ts +45 -0
  90. package/dist/types/global-account/react/components/B3Provider/B3Provider.d.ts +1 -2
  91. package/dist/types/global-account/react/components/B3Provider/RelayKitProviderWrapper.d.ts +1 -2
  92. package/dist/types/global-account/react/hooks/index.d.ts +2 -2
  93. package/dist/types/global-account/react/hooks/useAuth.d.ts +2 -1
  94. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +2 -1
  95. package/dist/types/global-account/react/hooks/useFirstEOA.d.ts +4 -7666
  96. package/dist/types/global-account/react/hooks/usePortfolioBalance.d.ts +15 -0
  97. package/dist/types/global-account/react/hooks/usePortfolioCollectibles.d.ts +12 -0
  98. package/dist/types/global-account/react/hooks/useUnifiedChainSwitchAndExecute.d.ts +1 -0
  99. package/dist/types/global-account/react/stores/useModalStore.d.ts +0 -2
  100. package/dist/types/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  101. package/dist/types/shared/constants/chains/b3Chain.d.ts +24 -0
  102. package/dist/types/shared/constants/chains/supported.d.ts +4 -4
  103. package/dist/types/shared/constants/index.d.ts +4 -0
  104. package/dist/types/shared/utils/zerionPositions.d.ts +96 -0
  105. package/dist/types/wallet/utils/createWalletConfig.d.ts +2 -2
  106. package/node_modules/@b3/chain-registry/chains.json +29 -1
  107. package/node_modules/@b3/chain-registry/package.json +18 -2
  108. package/package.json +2 -2
  109. package/src/anyspend/README.md +2 -2
  110. package/src/anyspend/constants/rpc.ts +2 -0
  111. package/src/anyspend/docs/components.md +6 -6
  112. package/src/anyspend/docs/contributing.md +1 -1
  113. package/src/anyspend/docs/hooks.md +3 -3
  114. package/src/anyspend/docs/installation.md +2 -2
  115. package/src/anyspend/react/components/AnySpendDeposit.tsx +26 -19
  116. package/src/anyspend/react/components/AnySpendStakeB3.tsx +55 -14
  117. package/src/anyspend/react/components/AnySpendStakeB3ExactIn.tsx +55 -11
  118. package/src/anyspend/react/components/__tests__/QRDeposit.test.tsx +3 -4
  119. package/src/anyspend/react/components/checkout/CryptoPayPanel.tsx +25 -12
  120. package/src/anyspend/react/components/common/TokenBalance.tsx +10 -7
  121. package/src/anyspend/utils/__tests__/deprecatedChains.test.ts +44 -0
  122. package/src/anyspend/utils/__tests__/toRegistryChainId.test.ts +17 -0
  123. package/src/anyspend/utils/chain.ts +53 -2
  124. package/src/global-account/react/components/B3Provider/B3Provider.tsx +1 -3
  125. package/src/global-account/react/components/B3Provider/RelayKitProviderWrapper.tsx +4 -11
  126. package/src/global-account/react/components/ManageAccount/ContentTokens.tsx +50 -21
  127. package/src/global-account/react/components/ManageAccount/NFTContent.tsx +5 -3
  128. package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +10 -6
  129. package/src/global-account/react/components/ManageAccount/TokenContent.tsx +22 -9
  130. package/src/global-account/react/components/Send/Send.tsx +39 -15
  131. package/src/global-account/react/hooks/__tests__/useUnifiedChainSwitchAndExecute.test.tsx +156 -0
  132. package/src/global-account/react/hooks/index.ts +2 -2
  133. package/src/global-account/react/hooks/useAuth.ts +3 -2
  134. package/src/global-account/react/hooks/useAuthentication.ts +3 -2
  135. package/src/global-account/react/hooks/useFirstEOA.tsx +9 -2
  136. package/src/global-account/react/hooks/usePortfolioBalance.ts +89 -0
  137. package/src/global-account/react/hooks/usePortfolioCollectibles.ts +212 -0
  138. package/src/global-account/react/hooks/useUnifiedChainSwitchAndExecute.ts +26 -12
  139. package/src/global-account/react/stores/useModalStore.ts +0 -2
  140. package/src/shared/constants/index.ts +8 -0
  141. package/src/shared/utils/__tests__/zerionPositions.test.ts +226 -0
  142. package/src/shared/utils/zerionPositions.ts +244 -0
  143. package/dist/cjs/global-account/react/hooks/useSimBalance.d.ts +0 -59
  144. package/dist/cjs/global-account/react/hooks/useSimBalance.js +0 -98
  145. package/dist/cjs/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
  146. package/dist/cjs/global-account/react/hooks/useSimCollectibles.js +0 -190
  147. package/dist/cjs/global-account/react/utils/simdune.d.ts +0 -7
  148. package/dist/cjs/global-account/react/utils/simdune.js +0 -21
  149. package/dist/esm/global-account/react/hooks/useSimBalance.d.ts +0 -59
  150. package/dist/esm/global-account/react/hooks/useSimBalance.js +0 -93
  151. package/dist/esm/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
  152. package/dist/esm/global-account/react/hooks/useSimCollectibles.js +0 -187
  153. package/dist/esm/global-account/react/utils/simdune.d.ts +0 -7
  154. package/dist/esm/global-account/react/utils/simdune.js +0 -17
  155. package/dist/types/global-account/react/hooks/useSimBalance.d.ts +0 -59
  156. package/dist/types/global-account/react/hooks/useSimCollectibles.d.ts +0 -45
  157. package/dist/types/global-account/react/utils/simdune.d.ts +0 -7
  158. package/node_modules/@b3/chain-registry/src/index.ts +0 -169
  159. package/node_modules/@b3/chain-registry/tsconfig.json +0 -16
  160. package/src/global-account/react/hooks/useSimBalance.ts +0 -164
  161. package/src/global-account/react/hooks/useSimCollectibles.ts +0 -238
  162. package/src/global-account/react/utils/simdune.ts +0 -20
@@ -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
+ }
@@ -0,0 +1,212 @@
1
+ import type { NFT, SimpleHashNFTResponse } from "@b3dotfun/sdk/global-account/types/simplehash.types";
2
+ import { zerionSlugToChainId } from "@b3dotfun/sdk/shared/utils/zerionPositions";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ const PORTFOLIO_API_BASE_URL = "https://portfolio-api.b3.fun";
6
+
7
+ interface ZerionNftPosition {
8
+ id?: string;
9
+ attributes?: {
10
+ amount?: string;
11
+ changed_at?: string;
12
+ nft_info?: {
13
+ contract_address?: string;
14
+ token_id?: string;
15
+ name?: string;
16
+ interface?: string;
17
+ content?: { preview?: { url?: string }; detail?: { url?: string } };
18
+ flags?: { is_spam?: boolean };
19
+ };
20
+ collection_info?: {
21
+ name?: string;
22
+ description?: string;
23
+ content?: { icon?: { url?: string } };
24
+ };
25
+ };
26
+ relationships?: { chain?: { data?: { id?: string } } };
27
+ }
28
+
29
+ interface ZerionNftPositionsResponse {
30
+ data?: ZerionNftPosition[];
31
+ }
32
+
33
+ /** Caps an ERC-1155 balance string at MAX_SAFE_INTEGER so a huge supply cannot overflow. */
34
+ function safeParseBalance(balance: string | undefined): number {
35
+ if (!balance) return 1;
36
+ const parsed = parseInt(balance, 10);
37
+ if (Number.isNaN(parsed)) return 1;
38
+ return Math.min(parsed, Number.MAX_SAFE_INTEGER);
39
+ }
40
+
41
+ /**
42
+ * Reshape one Zerion NFT position into the SimpleHash record `AccountAssets` renders.
43
+ *
44
+ * Fields Zerion does not carry — traits, rarity, royalties, sale history — stay empty rather than
45
+ * being synthesised from something adjacent. In particular the position's `price` is a current
46
+ * estimate, not a sale, so it must not be written into `last_sale`.
47
+ */
48
+ function toSimpleHashNft(position: ZerionNftPosition, ownerAddress: string): NFT | null {
49
+ const attributes = position.attributes ?? {};
50
+ const nftInfo = attributes.nft_info;
51
+ const slug = position.relationships?.chain?.data?.id;
52
+ if (!nftInfo?.contract_address || !nftInfo.token_id || !slug) return null;
53
+
54
+ const imageUrl = nftInfo.content?.detail?.url ?? nftInfo.content?.preview?.url ?? "";
55
+ const collectionInfo = attributes.collection_info ?? {};
56
+ const acquiredAt = attributes.changed_at ?? "";
57
+
58
+ return {
59
+ nft_id: `${slug}.${nftInfo.contract_address}.${nftInfo.token_id}`,
60
+ chain: slug,
61
+ contract_address: nftInfo.contract_address,
62
+ token_id: nftInfo.token_id,
63
+ name: nftInfo.name || "",
64
+ description: collectionInfo.description || "",
65
+ previews: {
66
+ image_small_url: nftInfo.content?.preview?.url ?? imageUrl,
67
+ image_medium_url: imageUrl,
68
+ image_large_url: imageUrl,
69
+ image_opengraph_url: imageUrl,
70
+ blurhash: "",
71
+ predominant_color: "",
72
+ },
73
+ image_url: imageUrl,
74
+ image_properties: { width: 0, height: 0, size: 0, mime_type: "", exif_orientation: null },
75
+ video_url: null,
76
+ video_properties: null,
77
+ audio_url: null,
78
+ audio_properties: null,
79
+ model_url: null,
80
+ model_properties: null,
81
+ other_url: null,
82
+ other_properties: null,
83
+ background_color: null,
84
+ external_url: null,
85
+ created_date: "",
86
+ status: "minted",
87
+ token_count: 1,
88
+ owner_count: 1,
89
+ owners: [
90
+ {
91
+ owner_address: ownerAddress,
92
+ quantity: safeParseBalance(attributes.amount),
93
+ quantity_string: attributes.amount || "1",
94
+ first_acquired_date: acquiredAt,
95
+ last_acquired_date: acquiredAt,
96
+ },
97
+ ],
98
+ contract: {
99
+ type: nftInfo.interface === "ERC1155" ? "ERC1155" : "ERC721",
100
+ name: collectionInfo.name || "",
101
+ symbol: null,
102
+ deployed_by: "",
103
+ deployed_via_contract: "",
104
+ owned_by: "",
105
+ has_multiple_collections: false,
106
+ },
107
+ collection: {
108
+ collection_id: nftInfo.contract_address,
109
+ name: collectionInfo.name || "Unknown Collection",
110
+ description: collectionInfo.description ?? null,
111
+ image_url: collectionInfo.content?.icon?.url ?? "",
112
+ image_properties: { width: 0, height: 0, mime_type: "" },
113
+ banner_image_url: null,
114
+ category: null,
115
+ is_nsfw: null,
116
+ external_url: null,
117
+ twitter_username: null,
118
+ discord_url: null,
119
+ instagram_username: null,
120
+ medium_username: null,
121
+ telegram_url: null,
122
+ marketplace_pages: [],
123
+ metaplex_mint: null,
124
+ metaplex_candy_machine: null,
125
+ metaplex_first_verified_creator: null,
126
+ floor_prices: [],
127
+ top_bids: [],
128
+ distinct_owner_count: 0,
129
+ distinct_nft_count: 0,
130
+ total_quantity: 0,
131
+ chains: [slug],
132
+ top_contracts: [nftInfo.contract_address],
133
+ collection_royalties: [],
134
+ },
135
+ last_sale: null,
136
+ primary_sale: null,
137
+ first_created: {
138
+ minted_to: ownerAddress,
139
+ quantity: 1,
140
+ quantity_string: "1",
141
+ timestamp: "",
142
+ block_number: 0,
143
+ transaction: "",
144
+ transaction_initiator: "",
145
+ },
146
+ rarity: { rank: null, score: null, unique_attributes: null },
147
+ royalty: [],
148
+ extra_metadata: {
149
+ attributes: [],
150
+ image_original_url: imageUrl,
151
+ animation_original_url: null,
152
+ metadata_original_url: "",
153
+ },
154
+ };
155
+ }
156
+
157
+ /**
158
+ * NFTs held by a wallet, in the SimpleHash shape `AccountAssets` already renders.
159
+ *
160
+ * Chain narrowing happens here rather than in the request: the upstream rejects some valid chain
161
+ * slugs outright and fails the whole request when one is present, so asking for a subset can cost
162
+ * you the whole answer.
163
+ */
164
+ export function usePortfolioCollectibles(
165
+ address?: string,
166
+ chainIds?: number[],
167
+ options?: { limit?: number; filterSpam?: boolean },
168
+ ) {
169
+ const limit = options?.limit;
170
+ const filterSpam = options?.filterSpam;
171
+ const wantedChainIds = chainIds && chainIds.length > 0 ? chainIds : undefined;
172
+
173
+ return useQuery({
174
+ queryKey: ["walletCollectibles", address, limit, filterSpam],
175
+ queryFn: async (): Promise<SimpleHashNFTResponse> => {
176
+ if (!address) throw new Error("Address is required");
177
+
178
+ const params = new URLSearchParams();
179
+ if (limit) params.set("page[size]", String(limit));
180
+ const query = params.toString();
181
+ const url = `${PORTFOLIO_API_BASE_URL}/v1/wallets/${encodeURIComponent(address)}/nft-positions/${query ? `?${query}` : ""}`;
182
+
183
+ const response = await fetch(url, { headers: { Accept: "application/json" } });
184
+ if (!response.ok) {
185
+ throw new Error(`Failed to fetch collectibles: ${response.status} ${response.statusText}`);
186
+ }
187
+
188
+ const body = (await response.json()) as ZerionNftPositionsResponse;
189
+ // Spam is dropped here, not in `select`: the flag lives on the raw position and the
190
+ // SimpleHash shape has nowhere to carry it downstream.
191
+ const nfts = (body.data ?? [])
192
+ .filter(position => !(filterSpam && position.attributes?.nft_info?.flags?.is_spam === true))
193
+ .map(position => toSimpleHashNft(position, address))
194
+ .filter((nft): nft is NFT => nft !== null);
195
+
196
+ return { next_cursor: null, next: null, previous: null, nfts };
197
+ },
198
+ enabled: Boolean(address),
199
+ staleTime: 30 * 1000,
200
+ gcTime: 5 * 60 * 1000,
201
+ select: (data: SimpleHashNFTResponse) => {
202
+ if (!wantedChainIds) return data;
203
+
204
+ const all = data.nfts ?? [];
205
+ const nfts = all.filter(nft => {
206
+ const chainId = zerionSlugToChainId(nft.chain);
207
+ return chainId !== undefined && wantedChainIds.includes(chainId);
208
+ });
209
+ return nfts.length === all.length ? data : { ...data, nfts };
210
+ },
211
+ });
212
+ }
@@ -10,7 +10,6 @@ import { prepareTransaction, sendTransaction as twSendTransaction } from "thirdw
10
10
  import { isAddress } from "viem";
11
11
  import { useSwitchChain } from "wagmi";
12
12
  import { useB3Config } from "../components/B3Provider/B3ConfigProvider";
13
- import { useB3Account } from "../components/B3Provider/useB3Account";
14
13
  import { useAccountWallet } from "./useAccountWallet";
15
14
 
16
15
  export interface UnifiedTransactionParams {
@@ -23,8 +22,7 @@ export function useUnifiedChainSwitchAndExecute() {
23
22
  const { switchChainAsync } = useSwitchChain();
24
23
  const [isSwitchingOrExecuting, setIsSwitchingOrExecuting] = useState(false);
25
24
 
26
- const { isActiveSmartWallet, isActiveEOAWallet, connectedEOAWallet } = useAccountWallet();
27
- const aaAccount = useB3Account();
25
+ const { isActiveSmartWallet, isActiveEOAWallet, connectedSmartWallet, connectedEOAWallet } = useAccountWallet();
28
26
 
29
27
  // From context, not process.env: partnerId is hashed into the server intent key and must match the ecosystem wallet.
30
28
  const { partnerId } = useB3Config();
@@ -119,11 +117,26 @@ export function useUnifiedChainSwitchAndExecute() {
119
117
  [connectedEOAWallet, switchChainAsync],
120
118
  );
121
119
 
122
- // Handle AA wallet transaction (no chain switch needed for AA)
123
- const handleAASendTransaction = useCallback(
124
- async (targetChainId: number, params: UnifiedTransactionParams): Promise<string | undefined> => {
125
- if (!aaAccount) {
126
- toast.error("Smart wallet not connected");
120
+ // Handle Global Account transaction (no chain switch needed for AA).
121
+ // Resolve the signer from the connected ecosystem wallet rather than the active
122
+ // wallet so callers can explicitly require the Global Account.
123
+ const handleGlobalAccountSendTransaction = useCallback(
124
+ async (
125
+ targetChainId: number,
126
+ params: UnifiedTransactionParams,
127
+ expectedGlobalAccountAddress?: string,
128
+ ): Promise<string | undefined> => {
129
+ const globalAccount = connectedSmartWallet?.getAccount();
130
+ if (!globalAccount) {
131
+ toast.error("Global Account not connected");
132
+ return;
133
+ }
134
+
135
+ if (
136
+ expectedGlobalAccountAddress &&
137
+ globalAccount.address.toLowerCase() !== expectedGlobalAccountAddress.toLowerCase()
138
+ ) {
139
+ toast.error("Connected Global Account does not match the authenticated account");
127
140
  return;
128
141
  }
129
142
 
@@ -162,7 +175,7 @@ export function useUnifiedChainSwitchAndExecute() {
162
175
  toast.info("Sending transaction…");
163
176
  const start = performance.now();
164
177
  const sendTxResponse = await twSendTransaction({
165
- account: aaAccount,
178
+ account: globalAccount,
166
179
  transaction,
167
180
  });
168
181
  const end = performance.now();
@@ -178,7 +191,7 @@ export function useUnifiedChainSwitchAndExecute() {
178
191
  setIsSwitchingOrExecuting(false);
179
192
  }
180
193
  },
181
- [aaAccount, partnerId],
194
+ [connectedSmartWallet, partnerId],
182
195
  );
183
196
 
184
197
  // Unified switch chain and execute function
@@ -186,15 +199,16 @@ export function useUnifiedChainSwitchAndExecute() {
186
199
  async (targetChainId: number, params: UnifiedTransactionParams): Promise<string | undefined> => {
187
200
  // Check which wallet type is active
188
201
  if (isActiveSmartWallet) {
189
- return handleAASendTransaction(targetChainId, params);
202
+ return handleGlobalAccountSendTransaction(targetChainId, params);
190
203
  }
191
204
  return handleEOASwitchChainAndSendTransaction(targetChainId, params);
192
205
  },
193
- [isActiveSmartWallet, handleAASendTransaction, handleEOASwitchChainAndSendTransaction],
206
+ [isActiveSmartWallet, handleGlobalAccountSendTransaction, handleEOASwitchChainAndSendTransaction],
194
207
  );
195
208
 
196
209
  return {
197
210
  switchChainAndExecute,
211
+ switchChainAndExecuteWithGlobalAccount: handleGlobalAccountSendTransaction,
198
212
  switchChainAndExecuteWithEOA: handleEOASwitchChainAndSendTransaction,
199
213
  isSwitchingOrExecuting,
200
214
  isActiveSmartWallet,
@@ -675,8 +675,6 @@ export interface AnySpendDepositModalProps extends BaseModalProps {
675
675
  showChainSelection?: boolean;
676
676
  /** Whether to show the "Fund with Fiat" option in the deposit options list. Defaults to true */
677
677
  showFiatOption?: boolean;
678
- /** Minimum pool size for filtering tokens (default: 1,000,000) */
679
- minPoolSize?: number;
680
678
  /** Custom title for chain selection step */
681
679
  chainSelectionTitle?: string;
682
680
  /** Custom description for chain selection step */
@@ -1,3 +1,5 @@
1
+ import type { Wallet } from "thirdweb/wallets";
2
+
1
3
  export * from "./currency";
2
4
 
3
5
  export const siteURL = "https://basement.fun";
@@ -8,6 +10,12 @@ export const ecosystemWalletId = (process.env.NEXT_PUBLIC_THIRDWEB_ECOSYSTEM_ID
8
10
  process.env.EXPO_PUBLIC_THIRDWEB_ECOSYSTEM_ID ||
9
11
  "ecosystem.b3-open-gaming") as `ecosystem.${string}`; // Fallback to prod key lookup
10
12
 
13
+ // Portable equivalent of thirdweb's `EcosystemWallet`, which thirdweb does not export
14
+ // from any public entry point — declaration emit cannot name the internal type (TS2742).
15
+ export type EcosystemWallet = Wallet<`ecosystem.${string}`> & {
16
+ getAuthToken: () => string | null;
17
+ };
18
+
11
19
  export const tokenIcons = {
12
20
  ETH: "https://cdn.b3.fun/ethereum.svg",
13
21
  WETH: "https://cdn.b3.fun/wrapped-ethereum.svg",
@@ -0,0 +1,226 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ adaptZerionPositions,
4
+ chainIdToZerionSlug,
5
+ EVM_NATIVE_TOKEN_ADDRESS,
6
+ zerionSlugToChainId,
7
+ type ZerionPositionLike,
8
+ } from "../zerionPositions";
9
+
10
+ // Slugs and ids below are shaped after live Zerion responses.
11
+ const SLUG_TO_CHAIN_ID: Record<string, number> = {
12
+ ethereum: 1,
13
+ base: 8453,
14
+ polygon: 137,
15
+ celo: 42220,
16
+ hyperevm: 999,
17
+ };
18
+ const slugToChainId = (slug: string) => SLUG_TO_CHAIN_ID[slug];
19
+
20
+ function position(overrides: {
21
+ id: string;
22
+ slug: string;
23
+ symbol: string;
24
+ implAddress?: string | null;
25
+ implDecimals?: number;
26
+ quantityDecimals?: number;
27
+ int?: string;
28
+ }): ZerionPositionLike {
29
+ const { id, slug, symbol, implAddress = null, implDecimals = 18, quantityDecimals = 18, int = "1000" } = overrides;
30
+ return {
31
+ id,
32
+ attributes: {
33
+ quantity: { int, decimals: quantityDecimals },
34
+ fungible_info: {
35
+ symbol,
36
+ implementations: [{ chain_id: slug, address: implAddress, decimals: implDecimals }],
37
+ },
38
+ },
39
+ relationships: { chain: { data: { id: slug } } },
40
+ };
41
+ }
42
+
43
+ describe("adaptZerionPositions native detection", () => {
44
+ it("treats a null implementation address as the chain's native coin", () => {
45
+ const [row] = adaptZerionPositions(
46
+ [position({ id: "base-base-asset-asset", slug: "base", symbol: "ETH" })],
47
+ slugToChainId,
48
+ );
49
+
50
+ expect(row.is_native).toBe(true);
51
+ expect(row.address).toBe(EVM_NATIVE_TOKEN_ADDRESS);
52
+ });
53
+
54
+ it("treats Polygon's native coin as native despite its system-contract address", () => {
55
+ const [row] = adaptZerionPositions(
56
+ [
57
+ position({
58
+ id: "base-polygon-asset-asset",
59
+ slug: "polygon",
60
+ symbol: "POL",
61
+ implAddress: "0x0000000000000000000000000000000000001010",
62
+ }),
63
+ ],
64
+ slugToChainId,
65
+ );
66
+
67
+ expect(row.is_native).toBe(true);
68
+ expect(row.address).toBe(EVM_NATIVE_TOKEN_ADDRESS);
69
+ });
70
+
71
+ it("treats Celo's native coin as native despite its ERC-20 address", () => {
72
+ const [row] = adaptZerionPositions(
73
+ [
74
+ position({
75
+ id: "base-celo-asset-asset",
76
+ slug: "celo",
77
+ symbol: "celo",
78
+ implAddress: "0x471ece3750da237f93b8e339c536989b8978a438",
79
+ }),
80
+ ],
81
+ slugToChainId,
82
+ );
83
+
84
+ expect(row.is_native).toBe(true);
85
+ });
86
+
87
+ it("keeps a same-symbol non-native fungible distinct from the native coin", () => {
88
+ const rows = adaptZerionPositions(
89
+ [
90
+ position({
91
+ id: "base-polygon-asset-asset",
92
+ slug: "polygon",
93
+ symbol: "POL",
94
+ implAddress: "0x0000000000000000000000000000000000001010",
95
+ }),
96
+ position({
97
+ id: "0x89299a4c719ebe9fe6018aed81eb1d48b4f78d56-polygon-asset-asset",
98
+ slug: "polygon",
99
+ symbol: "POL",
100
+ implAddress: "0x89299a4c719ebe9fe6018aed81eb1d48b4f78d56",
101
+ }),
102
+ ],
103
+ slugToChainId,
104
+ );
105
+
106
+ expect(rows.map(r => r.is_native)).toEqual([true, false]);
107
+ expect(rows[1].address).toBe("0x89299a4c719ebe9fe6018aed81eb1d48b4f78d56");
108
+ });
109
+ });
110
+
111
+ describe("adaptZerionPositions row filtering", () => {
112
+ it("drops a row whose chain is unmapped", () => {
113
+ const rows = adaptZerionPositions(
114
+ [position({ id: "base-unknownchain-asset-asset", slug: "unknownchain", symbol: "???" })],
115
+ slugToChainId,
116
+ );
117
+
118
+ expect(rows).toEqual([]);
119
+ });
120
+
121
+ it("drops a row whose fungible has no implementation on its chain, rather than calling it native", () => {
122
+ const rows = adaptZerionPositions(
123
+ [
124
+ {
125
+ id: "0xabc-ethereum-asset-asset",
126
+ attributes: {
127
+ quantity: { int: "1000", decimals: 18 },
128
+ fungible_info: {
129
+ symbol: "ORPHAN",
130
+ // implementations exist, but none for the position's own chain
131
+ implementations: [{ chain_id: "polygon", address: "0xabc", decimals: 18 }],
132
+ },
133
+ },
134
+ relationships: { chain: { data: { id: "ethereum" } } },
135
+ },
136
+ ],
137
+ slugToChainId,
138
+ );
139
+
140
+ expect(rows).toEqual([]);
141
+ });
142
+
143
+ it("drops a row that carries no quantity rather than reporting a zero balance", () => {
144
+ const rows = adaptZerionPositions(
145
+ [
146
+ {
147
+ id: "base-base-asset-asset",
148
+ attributes: {
149
+ quantity: { decimals: 18 },
150
+ fungible_info: { symbol: "ETH", implementations: [{ chain_id: "base", address: null, decimals: 18 }] },
151
+ },
152
+ relationships: { chain: { data: { id: "base" } } },
153
+ },
154
+ ],
155
+ slugToChainId,
156
+ );
157
+
158
+ expect(rows).toEqual([]);
159
+ });
160
+
161
+ it("drops a row that carries no decimals rather than guessing a scale", () => {
162
+ const rows = adaptZerionPositions(
163
+ [
164
+ {
165
+ id: "0xabc-ethereum-asset-asset",
166
+ attributes: {
167
+ quantity: { int: "1000" },
168
+ fungible_info: { symbol: "MYSTERY", implementations: [{ chain_id: "ethereum", address: "0xabc" }] },
169
+ },
170
+ relationships: { chain: { data: { id: "ethereum" } } },
171
+ },
172
+ ],
173
+ slugToChainId,
174
+ );
175
+
176
+ expect(rows).toEqual([]);
177
+ });
178
+
179
+ it("drops trash and non-displayable rows", () => {
180
+ const base = position({ id: "0xspam-ethereum-asset-asset", slug: "ethereum", symbol: "SPAM" });
181
+ const spam: ZerionPositionLike = { ...base, attributes: { ...base.attributes, flags: { is_trash: true } } };
182
+ const hidden: ZerionPositionLike = {
183
+ ...base,
184
+ attributes: { ...base.attributes, flags: { displayable: false } },
185
+ };
186
+
187
+ expect(adaptZerionPositions([spam, hidden], slugToChainId)).toEqual([]);
188
+ });
189
+
190
+ it("drops any quantity that is not a base-10 integer, since all of them format to NaN", () => {
191
+ const rows = adaptZerionPositions(
192
+ [
193
+ position({ id: "0xa-ethereum-asset-asset", slug: "ethereum", symbol: "A", int: "" }),
194
+ position({ id: "0xb-ethereum-asset-asset", slug: "ethereum", symbol: "B", int: null as unknown as string }),
195
+ position({ id: "0xc-ethereum-asset-asset", slug: "ethereum", symbol: "C", int: "not-a-number" }),
196
+ position({ id: "0xd-ethereum-asset-asset", slug: "ethereum", symbol: "D", int: "0x1f" }),
197
+ position({ id: "0xe-ethereum-asset-asset", slug: "ethereum", symbol: "E", int: "1.5" }),
198
+ ],
199
+ slugToChainId,
200
+ );
201
+
202
+ expect(rows).toEqual([]);
203
+ });
204
+
205
+ it("keeps a genuine zero balance", () => {
206
+ const [row] = adaptZerionPositions(
207
+ [position({ id: "base-ethereum-asset-asset", slug: "ethereum", symbol: "ETH", int: "0" })],
208
+ slugToChainId,
209
+ );
210
+
211
+ expect(row.amount).toBe("0");
212
+ });
213
+ });
214
+
215
+ describe("zerionSlugToChainId", () => {
216
+ it("reports an inherited object key as an unknown chain", () => {
217
+ // Slugs come off the wire; an object lookup would answer `constructor` with a function.
218
+ expect(zerionSlugToChainId("constructor")).toBeUndefined();
219
+ expect(zerionSlugToChainId("toString")).toBeUndefined();
220
+ });
221
+
222
+ it("still resolves a real slug", () => {
223
+ expect(zerionSlugToChainId("ethereum")).toBe(1);
224
+ expect(chainIdToZerionSlug(8453)).toBe("base");
225
+ });
226
+ });