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

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 (50) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.js +1 -2
  2. package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +4 -9
  3. package/dist/cjs/anyspend/react/components/checkout/CryptoPayPanel.js +2 -3
  4. package/dist/cjs/anyspend/utils/address.d.ts +2 -1
  5. package/dist/cjs/anyspend/utils/address.js +12 -9
  6. package/dist/cjs/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  7. package/dist/cjs/shared/constants/chains/supported.d.ts +4 -4
  8. package/dist/cjs/shared/constants/chains/supported.js +4 -4
  9. package/dist/cjs/shared/utils/zerionPositions.d.ts +3 -3
  10. package/dist/cjs/shared/utils/zerionPositions.js +12 -10
  11. package/dist/cjs/wallet/utils/createWalletConfig.d.ts +2 -2
  12. package/dist/esm/anyspend/react/components/AnySpend.js +2 -3
  13. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +5 -10
  14. package/dist/esm/anyspend/react/components/checkout/CryptoPayPanel.js +3 -4
  15. package/dist/esm/anyspend/utils/address.d.ts +2 -1
  16. package/dist/esm/anyspend/utils/address.js +9 -6
  17. package/dist/esm/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  18. package/dist/esm/shared/constants/chains/supported.d.ts +4 -4
  19. package/dist/esm/shared/constants/chains/supported.js +1 -1
  20. package/dist/esm/shared/utils/zerionPositions.d.ts +3 -3
  21. package/dist/esm/shared/utils/zerionPositions.js +5 -3
  22. package/dist/esm/wallet/utils/createWalletConfig.d.ts +2 -2
  23. package/dist/types/anyspend/utils/address.d.ts +2 -1
  24. package/dist/types/global-account/react/utils/createWagmiConfig.d.ts +2 -2
  25. package/dist/types/shared/constants/chains/supported.d.ts +4 -4
  26. package/dist/types/shared/utils/zerionPositions.d.ts +3 -3
  27. package/dist/types/wallet/utils/createWalletConfig.d.ts +2 -2
  28. package/node_modules/@b3/chain-registry/chains.json +38 -1
  29. package/node_modules/@b3/chain-registry/dist/chains.json +1225 -0
  30. package/node_modules/@b3/chain-registry/dist/index.cjs +1676 -0
  31. package/node_modules/@b3/chain-registry/dist/index.d.cts +195 -0
  32. package/node_modules/@b3/chain-registry/dist/index.d.ts +195 -0
  33. package/node_modules/@b3/chain-registry/dist/index.mjs +1604 -0
  34. package/node_modules/@b3/chain-registry/dist/src/index.d.ts +193 -0
  35. package/node_modules/@b3/chain-registry/dist/src/index.js +408 -0
  36. package/node_modules/@b3/chain-registry/package.json +44 -8
  37. package/node_modules/@b3/chain-registry/src/index.test.ts +432 -0
  38. package/node_modules/@b3/chain-registry/src/index.ts +672 -0
  39. package/package.json +5 -2
  40. package/src/anyspend/react/components/AnySpend.tsx +2 -2
  41. package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +5 -10
  42. package/src/anyspend/react/components/__tests__/QRDeposit.test.tsx +39 -1
  43. package/src/anyspend/react/components/checkout/CryptoPayPanel.tsx +13 -4
  44. package/src/anyspend/services/gas.test.ts +95 -18
  45. package/src/anyspend/utils/__tests__/address.test.ts +39 -0
  46. package/src/anyspend/utils/address.ts +8 -6
  47. package/src/shared/constants/chains/supported.ts +1 -1
  48. package/src/shared/react/hooks/__tests__/useCurrencyConversion.test.ts +54 -43
  49. package/src/shared/utils/__tests__/zerionPositions.test.ts +23 -1
  50. package/src/shared/utils/zerionPositions.ts +6 -3
@@ -0,0 +1,432 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ CANONICAL_NAME_ASSETS,
4
+ chains,
5
+ looksLikeProviderTokenAddress,
6
+ isProviderTokenAddress,
7
+ isRegistryResolvedTokenIdentity,
8
+ isRegistryTokenIdentityCandidate,
9
+ isResolvedTokenIdentity,
10
+ isTokenIdentityCandidate,
11
+ matchCanonicalNativeAssetText,
12
+ nativeAssets,
13
+ looksLikeTokenAddress,
14
+ resolveChainIdentifier,
15
+ resolveDexScreenerChainIdentifier,
16
+ tokenAddressFamily,
17
+ tokenAddressKey,
18
+ validateChainDefinitions,
19
+ type ChainDefinition,
20
+ type NativeAssetDefinition,
21
+ type ResolvedTokenIdentity,
22
+ type TokenIdentityCandidate,
23
+ } from "./index";
24
+
25
+ const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
26
+
27
+ function cloneChains(): ChainDefinition[] {
28
+ return structuredClone(chains);
29
+ }
30
+
31
+ describe("chain identifier resolution", () => {
32
+ it("maps names, keys, normalized aliases, and numeric ids to one chain", () => {
33
+ const canonical = resolveChainIdentifier("solana");
34
+
35
+ expect(canonical?.network).toBe("solana");
36
+ expect(resolveChainIdentifier(" SOLANA ")).toBe(canonical);
37
+ expect(resolveChainIdentifier("Solana Mainnet")).toBe(canonical);
38
+ expect(resolveChainIdentifier("7565164")).toBe(canonical);
39
+ expect(resolveChainIdentifier("unknown-chain")).toBeNull();
40
+ });
41
+
42
+ it("maps registry aliases to provider ids and preserves bounded unknown provider ids", () => {
43
+ expect(resolveDexScreenerChainIdentifier("eth")).toEqual({
44
+ identityChainId: "eth",
45
+ providerChainId: "ethereum",
46
+ chainType: "evm",
47
+ registryChainId: 1,
48
+ });
49
+ expect(resolveDexScreenerChainIdentifier("1")?.providerChainId).toBe("ethereum");
50
+ expect(resolveDexScreenerChainIdentifier("Ethereum")?.providerChainId).toBe("ethereum");
51
+ expect(resolveDexScreenerChainIdentifier("137")?.providerChainId).toBe("polygon");
52
+ expect(resolveDexScreenerChainIdentifier("43114")?.providerChainId).toBe("avalanche");
53
+ expect(resolveDexScreenerChainIdentifier("polygon-zkevm")?.providerChainId).toBe("polygonzkevm");
54
+ expect(resolveDexScreenerChainIdentifier("42170")?.providerChainId).toBe("arbitrumnova");
55
+ expect(resolveDexScreenerChainIdentifier("degen")?.providerChainId).toBe("degenchain");
56
+ expect(resolveDexScreenerChainIdentifier("metis")).toEqual({
57
+ identityChainId: "metis",
58
+ providerChainId: "metis",
59
+ chainType: "evm",
60
+ registryChainId: 1088,
61
+ });
62
+ expect(
63
+ Object.fromEntries(
64
+ chains
65
+ .filter(chain => [10, 4663, 8453, 42161].includes(chain.chainId))
66
+ .map(chain => [chain.chainId, chain.dexscreenerNetwork]),
67
+ ),
68
+ ).toEqual({
69
+ 10: "optimism",
70
+ 4663: "robinhood",
71
+ 8453: "base",
72
+ 42161: "arbitrum",
73
+ });
74
+ expect(resolveDexScreenerChainIdentifier("sui")).toEqual({
75
+ identityChainId: "sui",
76
+ providerChainId: "sui",
77
+ chainType: null,
78
+ registryChainId: null,
79
+ });
80
+ expect(resolveDexScreenerChainIdentifier("bad/provider")).toBeNull();
81
+ expect(resolveDexScreenerChainIdentifier("x".repeat(65))).toBeNull();
82
+ });
83
+
84
+ it("pins every registry chain with declared DexScreener support", () => {
85
+ expect(
86
+ Object.fromEntries(
87
+ chains.filter(chain => chain.dexscreenerNetwork).map(chain => [chain.network, chain.dexscreenerNetwork]),
88
+ ),
89
+ ).toEqual({
90
+ abstract: "abstract",
91
+ arbitrum: "arbitrum",
92
+ arbitrum_nova: "arbitrumnova",
93
+ avax: "avalanche",
94
+ base: "base",
95
+ berachain: "berachain",
96
+ blast: "blast",
97
+ bsc: "bsc",
98
+ celo: "celo",
99
+ cro: "cronos",
100
+ degen: "degenchain",
101
+ eth: "ethereum",
102
+ ftm: "fantom",
103
+ glmr: "moonbeam",
104
+ hyperevm: "hyperevm",
105
+ linea: "linea",
106
+ mantle: "mantle",
107
+ matic: "polygon",
108
+ metis: "metis",
109
+ mode: "mode",
110
+ optimism: "optimism",
111
+ "polygon-zkevm": "polygonzkevm",
112
+ robinhood: "robinhood",
113
+ scroll: "scroll",
114
+ solana: "solana",
115
+ soneium: "soneium",
116
+ sonic: "sonic",
117
+ unichain: "unichain",
118
+ worldchain: "worldchain",
119
+ xdai: "gnosis",
120
+ zksync: "zksync",
121
+ });
122
+ });
123
+
124
+ it("fails closed for registry chains without declared DexScreener support", () => {
125
+ const undeclaredIdentifiers = [
126
+ "ronin",
127
+ "2020",
128
+ "b3",
129
+ "8333",
130
+ "zircuit",
131
+ "48900",
132
+ "plume",
133
+ "98866",
134
+ "taiko",
135
+ "167000",
136
+ "zora",
137
+ "7777777",
138
+ "aurora",
139
+ "1313161554",
140
+ ];
141
+
142
+ expect(
143
+ Object.fromEntries(
144
+ undeclaredIdentifiers.map(identifier => [identifier, resolveDexScreenerChainIdentifier(identifier)]),
145
+ ),
146
+ ).toEqual(Object.fromEntries(undeclaredIdentifiers.map(identifier => [identifier, null])));
147
+ });
148
+
149
+ it("rejects an identifier claimed by two different chains", () => {
150
+ const definitions = cloneChains();
151
+ definitions.push({
152
+ ...definitions[1],
153
+ chainId: 999_999_999,
154
+ key: "OTHER",
155
+ name: "Other",
156
+ network: "ethereum",
157
+ });
158
+
159
+ expect(() => validateChainDefinitions(definitions)).toThrow(
160
+ 'Chain identifier "ethereum" is claimed by both "eth" and "ethereum"',
161
+ );
162
+ });
163
+
164
+ it("rejects a provider id claimed by two different chains", () => {
165
+ const duplicateProviderId = cloneChains();
166
+ duplicateProviderId[1] = { ...duplicateProviderId[1], dexscreenerNetwork: "ethereum" };
167
+ expect(() => validateChainDefinitions(duplicateProviderId)).toThrow(
168
+ 'Chain identifier "ethereum" is claimed by both "eth" and "optimism"',
169
+ );
170
+ });
171
+
172
+ it("rejects duplicate numeric ids and incomplete wrapped-native metadata", () => {
173
+ const duplicateId = cloneChains();
174
+ duplicateId[1] = { ...duplicateId[1], chainId: duplicateId[0].chainId };
175
+ expect(() => validateChainDefinitions(duplicateId)).toThrow(/Chain id 1 is claimed/);
176
+
177
+ const incompleteWrapped = cloneChains();
178
+ const solanaIndex = incompleteWrapped.findIndex(chain => chain.network === "solana");
179
+ incompleteWrapped[solanaIndex] = { ...incompleteWrapped[solanaIndex], dexscreenerNetwork: undefined };
180
+ expect(() => validateChainDefinitions(incompleteWrapped)).toThrow(/Incomplete wrapped-native metadata/);
181
+ });
182
+
183
+ it("rejects invalid canonical SOL and wrapped-address metadata", () => {
184
+ const invalidSol = cloneChains();
185
+ const solanaIndex = invalidSol.findIndex(chain => chain.network === "solana");
186
+ invalidSol[solanaIndex] = {
187
+ ...invalidSol[solanaIndex],
188
+ wrappedNativeCurrency: { name: "Wrapped SOL", symbol: "WSOL", decimals: 9 },
189
+ };
190
+ expect(() => validateChainDefinitions(invalidSol)).toThrow("Canonical Solana SOL/wSOL metadata is invalid");
191
+
192
+ const wrongFamily = cloneChains();
193
+ wrongFamily[0] = { ...wrongFamily[0], wrappedNative: "So11111111111111111111111111111111111111112" };
194
+ expect(() => validateChainDefinitions(wrongFamily)).toThrow(/Invalid wrapped-native address/);
195
+ });
196
+ });
197
+
198
+ describe("token identity trust boundaries", () => {
199
+ const futureWrappedIdentity: ResolvedTokenIdentity = {
200
+ kind: "wrapped-native",
201
+ matchedBy: "canonical",
202
+ chainId: "eth",
203
+ address: WETH,
204
+ marketAddress: WETH,
205
+ name: "Wrapped Ether",
206
+ symbol: "WETH",
207
+ nativeSymbol: "ETH",
208
+ };
209
+ const futureWrappedCandidate: TokenIdentityCandidate = {
210
+ chainId: "eth",
211
+ providerChainId: "ethereum",
212
+ tokenAddress: WETH,
213
+ name: "Wrapped Ether",
214
+ symbol: "WETH",
215
+ pairSide: "base",
216
+ identity: futureWrappedIdentity,
217
+ };
218
+
219
+ it("accepts a structurally valid future wrapped identity without local canonical metadata", () => {
220
+ expect(isResolvedTokenIdentity(futureWrappedIdentity)).toBe(true);
221
+ expect(isTokenIdentityCandidate(futureWrappedCandidate)).toBe(true);
222
+ expect(isRegistryResolvedTokenIdentity(futureWrappedIdentity)).toBe(false);
223
+ expect(isRegistryTokenIdentityCandidate(futureWrappedCandidate)).toBe(false);
224
+ });
225
+
226
+ it("folds token-address case only for EVM identity evidence", () => {
227
+ const lowercaseWeth = WETH.toLowerCase();
228
+ expect(
229
+ isResolvedTokenIdentity({
230
+ ...futureWrappedIdentity,
231
+ address: lowercaseWeth,
232
+ }),
233
+ ).toBe(true);
234
+ expect(
235
+ isTokenIdentityCandidate({
236
+ ...futureWrappedCandidate,
237
+ tokenAddress: lowercaseWeth,
238
+ }),
239
+ ).toBe(true);
240
+
241
+ const futureNativeAsset: NativeAssetDefinition = {
242
+ chainId: 1,
243
+ chain: "eth",
244
+ dexscreenerChain: "ethereum",
245
+ name: "Ethereum",
246
+ symbol: "ETH",
247
+ decimals: 18,
248
+ wrapped: {
249
+ address: WETH,
250
+ name: "Wrapped Ether",
251
+ symbol: "WETH",
252
+ decimals: 18,
253
+ },
254
+ };
255
+ const mutableNativeAssets = nativeAssets as NativeAssetDefinition[];
256
+ const futureAssetIndex = mutableNativeAssets.push(futureNativeAsset) - 1;
257
+ try {
258
+ const lowercaseIdentity: ResolvedTokenIdentity = {
259
+ ...futureWrappedIdentity,
260
+ address: lowercaseWeth,
261
+ marketAddress: lowercaseWeth,
262
+ };
263
+ expect(isRegistryResolvedTokenIdentity(lowercaseIdentity)).toBe(true);
264
+ expect(
265
+ isRegistryTokenIdentityCandidate({
266
+ ...futureWrappedCandidate,
267
+ tokenAddress: lowercaseWeth,
268
+ identity: lowercaseIdentity,
269
+ }),
270
+ ).toBe(true);
271
+ } finally {
272
+ mutableNativeAssets.splice(futureAssetIndex, 1);
273
+ }
274
+ const invalidUppercasePrefix = `0X${WETH.slice(2)}`;
275
+ expect(
276
+ isResolvedTokenIdentity({
277
+ ...futureWrappedIdentity,
278
+ address: invalidUppercasePrefix,
279
+ }),
280
+ ).toBe(false);
281
+ expect(
282
+ isTokenIdentityCandidate({
283
+ ...futureWrappedCandidate,
284
+ tokenAddress: invalidUppercasePrefix,
285
+ }),
286
+ ).toBe(false);
287
+
288
+ const wrappedSol = "So11111111111111111111111111111111111111112";
289
+ const caseMutatedWrappedSol = `s${wrappedSol.slice(1)}`;
290
+ const wrappedSolIdentity: ResolvedTokenIdentity = {
291
+ kind: "wrapped-native",
292
+ matchedBy: "canonical",
293
+ chainId: "solana",
294
+ address: wrappedSol,
295
+ marketAddress: wrappedSol,
296
+ name: "Wrapped SOL",
297
+ symbol: "WSOL",
298
+ nativeSymbol: "SOL",
299
+ };
300
+
301
+ expect(
302
+ isResolvedTokenIdentity({
303
+ ...wrappedSolIdentity,
304
+ address: caseMutatedWrappedSol,
305
+ }),
306
+ ).toBe(false);
307
+ expect(
308
+ isTokenIdentityCandidate({
309
+ chainId: "solana",
310
+ providerChainId: "solana",
311
+ tokenAddress: caseMutatedWrappedSol,
312
+ name: "Wrapped SOL",
313
+ symbol: "WSOL",
314
+ pairSide: "base",
315
+ identity: wrappedSolIdentity,
316
+ }),
317
+ ).toBe(false);
318
+ });
319
+
320
+ it("never promotes case-mutated wrapped Solana addresses to registry-native identity", () => {
321
+ const canonical = "So11111111111111111111111111111111111111112";
322
+ const mutation = `s${canonical.slice(1)}`;
323
+ const mutatedIdentity: ResolvedTokenIdentity = {
324
+ kind: "wrapped-native",
325
+ matchedBy: "canonical",
326
+ chainId: "solana",
327
+ address: mutation,
328
+ marketAddress: mutation,
329
+ name: "Wrapped SOL",
330
+ symbol: "WSOL",
331
+ nativeSymbol: "SOL",
332
+ };
333
+
334
+ expect(isResolvedTokenIdentity(mutatedIdentity)).toBe(true);
335
+ expect(isRegistryResolvedTokenIdentity(mutatedIdentity)).toBe(false);
336
+ });
337
+
338
+ it("rejects a known chain whose address belongs to another family", () => {
339
+ expect(
340
+ isResolvedTokenIdentity({
341
+ kind: "contract",
342
+ matchedBy: "exact",
343
+ chainId: "solana",
344
+ address: WETH,
345
+ marketAddress: WETH,
346
+ name: "Wrong family",
347
+ symbol: "WRONG",
348
+ nativeSymbol: null,
349
+ }),
350
+ ).toBe(false);
351
+ });
352
+
353
+ it("allows structurally valid contracts on provider chains absent from the local registry", () => {
354
+ expect(
355
+ isResolvedTokenIdentity({
356
+ kind: "contract",
357
+ matchedBy: "ranked",
358
+ chainId: "future-evm",
359
+ address: WETH,
360
+ marketAddress: WETH,
361
+ name: "Future token",
362
+ symbol: "FUTURE",
363
+ nativeSymbol: null,
364
+ }),
365
+ ).toBe(true);
366
+ });
367
+
368
+ it("classifies only complete EVM and 32-byte Solana token addresses", () => {
369
+ expect(tokenAddressFamily(WETH)).toBe("evm");
370
+ expect(tokenAddressFamily("So11111111111111111111111111111111111111112")).toBe("solana");
371
+ expect(tokenAddressFamily("0xabc")).toBeNull();
372
+ });
373
+
374
+ it("centralizes canonical SOL and wSOL text matching in the registry", () => {
375
+ expect(CANONICAL_NAME_ASSETS).toHaveLength(1);
376
+ const [solana] = CANONICAL_NAME_ASSETS;
377
+ expect(matchCanonicalNativeAssetText(solana, "$SOL")).toBe("native");
378
+ expect(matchCanonicalNativeAssetText(solana, "Solana")).toBe("native");
379
+ expect(matchCanonicalNativeAssetText(solana, "wSOL")).toBe("wrapped-native");
380
+ expect(matchCanonicalNativeAssetText(solana, "Wrapped SOL")).toBe("wrapped-native");
381
+ expect(matchCanonicalNativeAssetText(solana, "ETH")).toBeNull();
382
+ });
383
+
384
+ it("distinguishes explicit provider coin types from arbitrary opaque user input", () => {
385
+ const suiCoinType = "0x2::sui::SUI";
386
+ const longOpaqueAddress = `EQ${"a".repeat(46)}`;
387
+
388
+ expect(isProviderTokenAddress(suiCoinType)).toBe(true);
389
+ expect(looksLikeTokenAddress(suiCoinType)).toBe(false);
390
+ expect(looksLikeProviderTokenAddress(suiCoinType)).toBe(true);
391
+ expect(looksLikeProviderTokenAddress(longOpaqueAddress)).toBe(false);
392
+ expect(isProviderTokenAddress(longOpaqueAddress)).toBe(true);
393
+ expect(isProviderTokenAddress("not-token")).toBe(false);
394
+ expect(isProviderTokenAddress(`0x2::${"a".repeat(257)}`)).toBe(false);
395
+ expect(tokenAddressKey("sui", "AbCd")).toBe("AbCd");
396
+ expect(tokenAddressKey("future-evm", WETH)).toBe(WETH.toLowerCase());
397
+ expect(tokenAddressKey("eth", WETH)).toBe(WETH.toLowerCase());
398
+ expect(
399
+ isResolvedTokenIdentity({
400
+ kind: "contract",
401
+ matchedBy: "exact",
402
+ chainId: "sui",
403
+ address: suiCoinType,
404
+ marketAddress: suiCoinType,
405
+ name: "Sui",
406
+ symbol: "SUI",
407
+ nativeSymbol: null,
408
+ }),
409
+ ).toBe(true);
410
+ const candidate = {
411
+ chainId: "sui",
412
+ providerChainId: "sui",
413
+ tokenAddress: suiCoinType,
414
+ name: "Sui",
415
+ symbol: "SUI",
416
+ pairSide: "base",
417
+ identity: {
418
+ kind: "contract",
419
+ matchedBy: "exact",
420
+ chainId: "sui",
421
+ address: suiCoinType,
422
+ marketAddress: suiCoinType,
423
+ name: "Sui",
424
+ symbol: "SUI",
425
+ nativeSymbol: null,
426
+ },
427
+ };
428
+ expect(isRegistryTokenIdentityCandidate(candidate)).toBe(true);
429
+ expect(isTokenIdentityCandidate(candidate)).toBe(true);
430
+ expect(isTokenIdentityCandidate({ ...candidate, providerChainId: undefined })).toBe(false);
431
+ });
432
+ });