@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
@@ -6,6 +6,7 @@ import {
6
6
  getDefaultToken,
7
7
  getExplorerTxUrl,
8
8
  getHyperliquidUSDCToken,
9
+ isSameChainAndToken,
9
10
  HYPERLIQUID_CHAIN_ID,
10
11
  HYPERLIQUID_USDC_ADDRESS,
11
12
  USDC_BASE,
@@ -902,8 +903,7 @@ function AnySpendInner({
902
903
  const isSameChainSameToken = useMemo(() => {
903
904
  return (
904
905
  activeTab === "crypto" &&
905
- selectedSrcChainId === selectedDstChainId &&
906
- selectedSrcToken.address.toLowerCase() === selectedDstToken.address.toLowerCase()
906
+ isSameChainAndToken(selectedSrcChainId, selectedSrcToken.address, selectedDstChainId, selectedDstToken.address)
907
907
  );
908
908
  }, [activeTab, selectedSrcChainId, selectedDstChainId, selectedSrcToken.address, selectedDstToken.address]);
909
909
 
@@ -2,7 +2,7 @@ import { getChainName, getExplorerTxUrl } from "@b3dotfun/sdk/anyspend";
2
2
  import { useGasPrice } from "@b3dotfun/sdk/anyspend/react";
3
3
  import { components } from "@b3dotfun/sdk/anyspend/types/api";
4
4
  import { GetQuoteResponse } from "@b3dotfun/sdk/anyspend/types/api_req_res";
5
- import { normalizeAddress } from "@b3dotfun/sdk/anyspend/utils";
5
+ import { isSameChainAndToken, normalizeAddress } from "@b3dotfun/sdk/anyspend/utils";
6
6
  import {
7
7
  Button,
8
8
  ShinyButton,
@@ -218,16 +218,11 @@ function AnySpendCustomExactInInner({
218
218
  const isSameChainSameToken = useMemo(() => {
219
219
  return (
220
220
  effectivePaymentType === "crypto" &&
221
- selectedSrcChainId === selectedDstChainId &&
222
- selectedSrcToken?.address?.toLowerCase() === selectedDstToken?.address?.toLowerCase()
221
+ selectedSrcToken != null &&
222
+ selectedDstToken != null &&
223
+ isSameChainAndToken(selectedSrcChainId, selectedSrcToken.address, selectedDstChainId, selectedDstToken.address)
223
224
  );
224
- }, [
225
- effectivePaymentType,
226
- selectedSrcChainId,
227
- selectedDstChainId,
228
- selectedSrcToken?.address,
229
- selectedDstToken?.address,
230
- ]);
225
+ }, [effectivePaymentType, selectedSrcChainId, selectedDstChainId, selectedSrcToken, selectedDstToken]);
231
226
 
232
227
  // Check if this is a direct transfer
233
228
  const isDirectTransfer = isSameChainSameToken && allowDirectTransfer;
@@ -1,4 +1,4 @@
1
- import { USDC_BASE } from "@b3dotfun/sdk/anyspend";
1
+ import { RELAY_SOLANA_MAINNET_CHAIN_ID, USDC_BASE } from "@b3dotfun/sdk/anyspend";
2
2
  import { fireEvent, render, screen } from "@testing-library/react";
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
4
 
@@ -74,6 +74,10 @@ vi.mock("@relayprotocol/relay-kit-ui", () => ({
74
74
  },
75
75
  }));
76
76
 
77
+ vi.mock("@stripe/stripe-js", () => ({
78
+ loadStripe: vi.fn(() => Promise.resolve(null)),
79
+ }));
80
+
77
81
  // Spread the real global-account/react (Button, Badge, etc. are needed by transitively
78
82
  // imported sub-components and render fine under happy-dom). Override only `toast` so the
79
83
  // error path doesn't depend on a toast provider.
@@ -240,6 +244,40 @@ describe("QRDeposit pureTransferOnly (Fund Wallet)", () => {
240
244
  expect(container.textContent).toContain("Target ≈ 9.365547 USDC");
241
245
  });
242
246
 
247
+ it("Test C3 — case-mutated Solana mints require a swap order", async () => {
248
+ const QRDeposit = await loadQRDeposit();
249
+ const wrappedSol = "So11111111111111111111111111111111111111112";
250
+ const sourceToken = {
251
+ chainId: RELAY_SOLANA_MAINNET_CHAIN_ID,
252
+ address: wrappedSol,
253
+ symbol: "wSOL",
254
+ name: "Wrapped SOL",
255
+ decimals: 9,
256
+ metadata: {},
257
+ };
258
+ const destinationToken = {
259
+ ...sourceToken,
260
+ address: `s${wrappedSol.slice(1)}`,
261
+ symbol: "OTHER",
262
+ name: "Distinct Solana Mint",
263
+ };
264
+
265
+ render(
266
+ <QRDeposit
267
+ recipientAddress={RECIPIENT}
268
+ sourceToken={sourceToken}
269
+ sourceChainId={RELAY_SOLANA_MAINNET_CHAIN_ID}
270
+ destinationToken={destinationToken}
271
+ destinationChainId={RELAY_SOLANA_MAINNET_CHAIN_ID}
272
+ destinationAmountDisplay="1"
273
+ />,
274
+ );
275
+
276
+ expect(createOrderSpy).toHaveBeenCalledTimes(1);
277
+ expect(createOrderSpy.mock.calls[0][0].dstToken.address).toBe(destinationToken.address);
278
+ expect(watchTransferArgs.enabled).toBe(false);
279
+ });
280
+
243
281
  it("Test C2 — swap mode without a fixed amount shows the Receive row but no target hint", async () => {
244
282
  const QRDeposit = await loadQRDeposit();
245
283
 
@@ -6,7 +6,12 @@ import { useAnyspendCreateOrder } from "@b3dotfun/sdk/anyspend/react/hooks/useAn
6
6
  import { useAnyspendOrderAndTransactions } from "@b3dotfun/sdk/anyspend/react/hooks/useAnyspendOrderAndTransactions";
7
7
  import { useCreateDepositFirstOrder } from "@b3dotfun/sdk/anyspend/react/hooks/useCreateDepositFirstOrder";
8
8
  import { useOnOrderSuccess } from "@b3dotfun/sdk/anyspend/react/hooks/useOnOrderSuccess";
9
- import { ALL_CHAINS, RELAY_SOLANA_MAINNET_CHAIN_ID, getAvailableChainIds } from "@b3dotfun/sdk/anyspend";
9
+ import {
10
+ ALL_CHAINS,
11
+ RELAY_SOLANA_MAINNET_CHAIN_ID,
12
+ getAvailableChainIds,
13
+ isSameChainAndToken,
14
+ } from "@b3dotfun/sdk/anyspend";
10
15
  import { getPaymentUrl, toRegistryChainId } from "@b3dotfun/sdk/anyspend/utils/chain";
11
16
  import { isNativeToken } from "@b3dotfun/sdk/anyspend/utils/token";
12
17
  import {
@@ -97,9 +102,13 @@ export function CryptoPayPanel({
97
102
  }, [selectedSrcToken, dstTokenData, destinationTokenAddress, destinationTokenChainId]);
98
103
 
99
104
  const isSameToken =
100
- selectedSrcToken &&
101
- selectedSrcToken.address.toLowerCase() === destinationTokenAddress.toLowerCase() &&
102
- selectedSrcToken.chainId === destinationTokenChainId;
105
+ selectedSrcToken !== null &&
106
+ isSameChainAndToken(
107
+ selectedSrcToken.chainId,
108
+ selectedSrcToken.address,
109
+ destinationTokenChainId,
110
+ destinationTokenAddress,
111
+ );
103
112
 
104
113
  const { anyspendQuote, isLoadingAnyspendQuote } = useAnyspendQuote({
105
114
  type: "swap",
@@ -1,31 +1,108 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { GasOracleResponse } from "../types/gas";
2
3
  import { gasService, isGasOracleSupported } from "./gas";
3
4
 
5
+ const RESPONSES: Readonly<Record<number, GasOracleResponse>> = {
6
+ 1: {
7
+ chainId: 1,
8
+ chainName: "Ethereum",
9
+ timestamp: "2026-01-01T00:00:00.000Z",
10
+ source: "blocknative",
11
+ gasPrice: { low: "7", standard: "8", fast: "9" },
12
+ eip1559: {
13
+ baseFee: "6",
14
+ maxPriorityFee: { low: "1", standard: "2", fast: "3" },
15
+ maxFee: { low: "8", standard: "10", fast: "12" },
16
+ },
17
+ analysis: {
18
+ level: "elevated",
19
+ percentile: 80,
20
+ vs1h: 1.5,
21
+ vs4h: 1.4,
22
+ vs24h: 1.3,
23
+ recommendation: "Wait if possible.",
24
+ },
25
+ cached: false,
26
+ },
27
+ 8453: {
28
+ chainId: 8453,
29
+ chainName: "Base",
30
+ timestamp: "2026-01-01T00:00:00.000Z",
31
+ source: "rpc",
32
+ gasPrice: { low: "0.004", standard: "0.005", fast: "0.006" },
33
+ eip1559: {
34
+ baseFee: "0.003",
35
+ maxPriorityFee: { low: "0.001", standard: "0.002", fast: "0.003" },
36
+ maxFee: { low: "0.005", standard: "0.007", fast: "0.009" },
37
+ },
38
+ analysis: {
39
+ level: "low",
40
+ percentile: 20,
41
+ vs1h: 0.8,
42
+ vs4h: 0.9,
43
+ vs24h: 0.7,
44
+ recommendation: "Good time to transact.",
45
+ },
46
+ cached: true,
47
+ cacheAge: 10,
48
+ },
49
+ };
50
+
4
51
  describe("gasService", () => {
5
- it("should check supported chains", () => {
6
- expect(isGasOracleSupported(8453)).toBe(true); // Base
7
- expect(isGasOracleSupported(1)).toBe(true); // Ethereum
8
- expect(isGasOracleSupported(999999)).toBe(false); // Unknown
52
+ beforeEach(() => {
53
+ vi.stubGlobal(
54
+ "fetch",
55
+ vi.fn(async (input: string | URL | Request) => {
56
+ const url = new URL(input instanceof Request ? input.url : String(input));
57
+ const chainId = Number(url.pathname.split("/").at(-1));
58
+ const response = RESPONSES[chainId];
59
+
60
+ return response ? Response.json(response) : new Response(null, { status: 404 });
61
+ }),
62
+ );
9
63
  });
10
64
 
11
- it("should fetch gas price for Base", async () => {
12
- const gas = await gasService.fetch(8453);
65
+ afterEach(() => {
66
+ vi.unstubAllGlobals();
67
+ });
13
68
 
14
- console.log("Gas data:", gas);
69
+ it("checks supported chains", () => {
70
+ expect(isGasOracleSupported(8453)).toBe(true);
71
+ expect(isGasOracleSupported(1)).toBe(true);
72
+ expect(isGasOracleSupported(999999)).toBe(false);
73
+ });
74
+
75
+ it("fetches and transforms Base gas data", async () => {
76
+ const gas = await gasService.fetch(8453);
15
77
 
16
- expect(gas.chainId).toBe(8453);
17
- expect(gas.chainName).toBe("Base");
18
- expect(gas.level).toMatch(/low|normal|elevated|high|spike/);
19
- expect(typeof gas.gasPriceGwei).toBe("string");
20
- expect(typeof gas.isSpike).toBe("boolean");
78
+ expect(gas).toEqual({
79
+ chainId: 8453,
80
+ chainName: "Base",
81
+ gasPriceGwei: "0.005",
82
+ baseFeeGwei: "0.003",
83
+ level: "low",
84
+ isSpike: false,
85
+ recommendation: "Good time to transact.",
86
+ vsMedian: 0.8,
87
+ source: "rpc",
88
+ timestamp: "2026-01-01T00:00:00.000Z",
89
+ });
21
90
  });
22
91
 
23
- it("should fetch gas price for Ethereum", async () => {
92
+ it("fetches and transforms Ethereum gas data", async () => {
24
93
  const gas = await gasService.fetch(1);
25
94
 
26
- console.log("ETH Gas:", gas);
27
-
28
- expect(gas.chainId).toBe(1);
29
- expect(gas.source).toBe("blocknative"); // ETH uses Blocknative
95
+ expect(gas).toEqual({
96
+ chainId: 1,
97
+ chainName: "Ethereum",
98
+ gasPriceGwei: "8",
99
+ baseFeeGwei: "6",
100
+ level: "elevated",
101
+ isSpike: true,
102
+ recommendation: "Wait if possible.",
103
+ vsMedian: 1.5,
104
+ source: "blocknative",
105
+ timestamp: "2026-01-01T00:00:00.000Z",
106
+ });
30
107
  });
31
108
  });
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { RELAY_SOLANA_MAINNET_CHAIN_ID } from "../../constants";
3
+ import { HYPERLIQUID_CHAIN_ID } from "../token";
4
+ import { isSameChainAndToken } from "../address";
5
+
6
+ const SOLANA_CHAIN_ID = 7565164;
7
+ const WRAPPED_SOL = "So11111111111111111111111111111111111111112";
8
+
9
+ describe("isSameChainAndToken", () => {
10
+ it("compares Solana mint addresses byte-for-byte", () => {
11
+ const caseMutatedMint = `s${WRAPPED_SOL.slice(1)}`;
12
+
13
+ expect(isSameChainAndToken(SOLANA_CHAIN_ID, WRAPPED_SOL, SOLANA_CHAIN_ID, caseMutatedMint)).toBe(false);
14
+ const invalidBase58Mutation = `SO${WRAPPED_SOL.slice(2)}`;
15
+ expect(
16
+ isSameChainAndToken(
17
+ RELAY_SOLANA_MAINNET_CHAIN_ID,
18
+ invalidBase58Mutation,
19
+ RELAY_SOLANA_MAINNET_CHAIN_ID,
20
+ invalidBase58Mutation.toLowerCase(),
21
+ ),
22
+ ).toBe(false);
23
+ });
24
+
25
+ it("compares EVM contract addresses case-insensitively on the same chain", () => {
26
+ const checksumAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
27
+
28
+ expect(isSameChainAndToken(1, checksumAddress, 1, checksumAddress.toLowerCase())).toBe(true);
29
+ expect(isSameChainAndToken(1, checksumAddress, 8453, checksumAddress)).toBe(false);
30
+ });
31
+
32
+ it("preserves case-insensitive token matching on Hyperliquid", () => {
33
+ const mixedCaseAddress = "0xAbCdEf0123456789AbCdEf0123456789";
34
+
35
+ expect(
36
+ isSameChainAndToken(HYPERLIQUID_CHAIN_ID, mixedCaseAddress, HYPERLIQUID_CHAIN_ID, mixedCaseAddress.toLowerCase()),
37
+ ).toBe(true);
38
+ });
39
+ });
@@ -1,11 +1,9 @@
1
+ import { chainTypeForIdentifier, isSolanaAddress } from "@b3/chain-registry/runtime";
1
2
  import { isAddress } from "viem";
3
+ import { RELAY_SOLANA_MAINNET_CHAIN_ID } from "../constants";
2
4
  import { HYPERLIQUID_CHAIN_ID } from "./token";
3
5
 
4
- export function isSolanaAddress(address: string): boolean {
5
- // Solana addresses are 32-byte base58 strings (usually 32-44 characters)
6
- const solanaAddressRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
7
- return solanaAddressRegex.test(address);
8
- }
6
+ export { isSolanaAddress };
9
7
 
10
8
  export function isEvmOrSolanaAddress(address: string): boolean {
11
9
  return isAddress(address) || isSolanaAddress(address);
@@ -49,5 +47,9 @@ export function isSameChainAndToken(
49
47
  destinationChainId: number,
50
48
  destinationTokenAddress: string,
51
49
  ): boolean {
52
- return sourceChainId === destinationChainId && eqci(sourceTokenAddress, destinationTokenAddress);
50
+ if (sourceChainId !== destinationChainId) return false;
51
+ if (chainTypeForIdentifier(String(sourceChainId)) === "solana" || sourceChainId === RELAY_SOLANA_MAINNET_CHAIN_ID) {
52
+ return sourceTokenAddress === destinationTokenAddress;
53
+ }
54
+ return sourceTokenAddress.toLowerCase() === destinationTokenAddress.toLowerCase();
53
55
  }
@@ -1,4 +1,4 @@
1
- import { ANYSPEND_EVM_CHAINS } from "@b3/chain-registry";
1
+ import { ANYSPEND_EVM_CHAINS } from "@b3/chain-registry/runtime";
2
2
  import { toThirdwebChain, toViemChain } from "@b3dotfun/sdk/shared/utils/chain-transformers";
3
3
  import invariant from "invariant";
4
4
  import type { Chain as ThirdwebChain } from "thirdweb";
@@ -1,25 +1,27 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
1
  import { renderHook } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { useCurrencyConversion } from "../useCurrencyConversion";
4
4
 
5
5
  // Mock the external dependencies
6
6
  // Store mock rates for different quote currencies
7
7
  const mockRates: Record<string, number | undefined> = {};
8
8
 
9
- // Mock store state
10
9
  const mockStoreState: any = {
11
10
  selectedCurrency: "B3",
12
11
  baseCurrency: "B3",
12
+ customCurrencies: {},
13
+ getExchangeRate: vi.fn(() => undefined),
13
14
  setSelectedCurrency: vi.fn(),
14
15
  setBaseCurrency: vi.fn(),
15
16
  };
16
17
 
17
- vi.mock("@b3dotfun/sdk/global-account/react", () => ({
18
- useExchangeRate: vi.fn((params: any) => {
19
- const rate = mockRates[params?.quoteCurrency];
20
- return { rate };
21
- }),
22
- }));
18
+ vi.mock("@tanstack/react-query", async importOriginal => {
19
+ const actual = await importOriginal<typeof import("@tanstack/react-query")>();
20
+ return {
21
+ ...actual,
22
+ useQuery: vi.fn(() => ({ data: mockRates })),
23
+ };
24
+ });
23
25
 
24
26
  vi.mock("@b3dotfun/sdk/shared/utils/number", () => ({
25
27
  formatDisplayNumber: vi.fn((value: any) => {
@@ -29,14 +31,8 @@ vi.mock("@b3dotfun/sdk/shared/utils/number", () => ({
29
31
  }),
30
32
  }));
31
33
 
32
- vi.mock("../../stores/currencyStore", () => ({
33
- useCurrencyStore: vi.fn((selector: any) => {
34
- if (selector) {
35
- return selector(mockStoreState);
36
- }
37
- return mockStoreState;
38
- }),
39
- CURRENCY_SYMBOLS: {
34
+ vi.mock("../../stores/currencyStore", () => {
35
+ const symbols: Record<string, string> = {
40
36
  B3: "B3",
41
37
  USD: "$",
42
38
  EUR: "€",
@@ -47,8 +43,23 @@ vi.mock("../../stores/currencyStore", () => ({
47
43
  ETH: "ETH",
48
44
  SOL: "SOL",
49
45
  KRW: "₩",
50
- },
51
- }));
46
+ };
47
+
48
+ return {
49
+ useCurrencyStore: vi.fn((selector: any) => {
50
+ if (selector) {
51
+ return selector(mockStoreState);
52
+ }
53
+ return mockStoreState;
54
+ }),
55
+ getCurrencySymbol: vi.fn((currency: string) => symbols[currency] ?? currency),
56
+ getCurrencyMetadata: vi.fn(() => undefined),
57
+ };
58
+ });
59
+
60
+ function renderCurrencyConversion() {
61
+ return renderHook(() => useCurrencyConversion());
62
+ }
52
63
 
53
64
  describe("useCurrencyConversion", () => {
54
65
  beforeEach(() => {
@@ -66,7 +77,7 @@ describe("useCurrencyConversion", () => {
66
77
  mockStoreState.selectedCurrency = "B3";
67
78
  mockStoreState.baseCurrency = "B3";
68
79
 
69
- const { result } = renderHook(() => useCurrencyConversion());
80
+ const { result } = renderCurrencyConversion();
70
81
  const formatted = result.current.formatCurrencyValue(100, "B3");
71
82
 
72
83
  expect(formatted).toContain("B3");
@@ -78,7 +89,7 @@ describe("useCurrencyConversion", () => {
78
89
  mockStoreState.selectedCurrency = "USD";
79
90
  mockStoreState.baseCurrency = "B3";
80
91
 
81
- const { result } = renderHook(() => useCurrencyConversion());
92
+ const { result } = renderCurrencyConversion();
82
93
  const formatted = result.current.formatCurrencyValue(100, "B3");
83
94
 
84
95
  expect(formatted).toContain("B3");
@@ -90,7 +101,7 @@ describe("useCurrencyConversion", () => {
90
101
  mockStoreState.selectedCurrency = "USD";
91
102
  mockStoreState.baseCurrency = "B3";
92
103
 
93
- const { result } = renderHook(() => useCurrencyConversion());
104
+ const { result } = renderCurrencyConversion();
94
105
  const formatted = result.current.formatCurrencyValue(100, "B3");
95
106
 
96
107
  expect(formatted).toMatch(/^\$/);
@@ -103,7 +114,7 @@ describe("useCurrencyConversion", () => {
103
114
  mockStoreState.selectedCurrency = "EUR";
104
115
  mockStoreState.baseCurrency = "B3";
105
116
 
106
- const { result } = renderHook(() => useCurrencyConversion());
117
+ const { result } = renderCurrencyConversion();
107
118
  const formatted = result.current.formatCurrencyValue(100, "B3");
108
119
 
109
120
  expect(formatted).toMatch(/^€/);
@@ -115,7 +126,7 @@ describe("useCurrencyConversion", () => {
115
126
  mockStoreState.selectedCurrency = "JPY";
116
127
  mockStoreState.baseCurrency = "B3";
117
128
 
118
- const { result } = renderHook(() => useCurrencyConversion());
129
+ const { result } = renderCurrencyConversion();
119
130
  const formatted = result.current.formatCurrencyValue(100, "B3");
120
131
 
121
132
  expect(formatted).toContain("¥");
@@ -128,7 +139,7 @@ describe("useCurrencyConversion", () => {
128
139
  mockStoreState.selectedCurrency = "KRW";
129
140
  mockStoreState.baseCurrency = "B3";
130
141
 
131
- const { result } = renderHook(() => useCurrencyConversion());
142
+ const { result } = renderCurrencyConversion();
132
143
  const formatted = result.current.formatCurrencyValue(100, "B3");
133
144
 
134
145
  expect(formatted).toContain("₩");
@@ -141,7 +152,7 @@ describe("useCurrencyConversion", () => {
141
152
  mockStoreState.selectedCurrency = "ETH";
142
153
  mockStoreState.baseCurrency = "B3";
143
154
 
144
- const { result } = renderHook(() => useCurrencyConversion());
155
+ const { result } = renderCurrencyConversion();
145
156
  const formatted = result.current.formatCurrencyValue(100, "B3");
146
157
 
147
158
  expect(formatted).toContain("ETH");
@@ -154,7 +165,7 @@ describe("useCurrencyConversion", () => {
154
165
  mockStoreState.selectedCurrency = "SOL";
155
166
  mockStoreState.baseCurrency = "B3";
156
167
 
157
- const { result } = renderHook(() => useCurrencyConversion());
168
+ const { result } = renderCurrencyConversion();
158
169
  const formatted = result.current.formatCurrencyValue(100, "B3");
159
170
 
160
171
  expect(formatted).toContain("SOL");
@@ -166,7 +177,7 @@ describe("useCurrencyConversion", () => {
166
177
  mockStoreState.selectedCurrency = "USD";
167
178
  mockStoreState.baseCurrency = "B3";
168
179
 
169
- const { result } = renderHook(() => useCurrencyConversion());
180
+ const { result } = renderCurrencyConversion();
170
181
  const formatted = result.current.formatCurrencyValue(10, "B3");
171
182
 
172
183
  // 10 * 1.5 = 15
@@ -180,7 +191,7 @@ describe("useCurrencyConversion", () => {
180
191
  mockStoreState.selectedCurrency = "USD";
181
192
  mockStoreState.baseCurrency = "B3";
182
193
 
183
- const { result } = renderHook(() => useCurrencyConversion());
194
+ const { result } = renderCurrencyConversion();
184
195
  const inputValue = 100;
185
196
  const formatted = result.current.formatCurrencyValue(inputValue, "B3");
186
197
 
@@ -193,7 +204,7 @@ describe("useCurrencyConversion", () => {
193
204
  mockStoreState.selectedCurrency = "USD";
194
205
  mockStoreState.baseCurrency = "B3";
195
206
 
196
- const { result } = renderHook(() => useCurrencyConversion());
207
+ const { result } = renderCurrencyConversion();
197
208
 
198
209
  expect(result.current.selectedCurrency).toBe("USD");
199
210
  });
@@ -202,7 +213,7 @@ describe("useCurrencyConversion", () => {
202
213
  mockStoreState.selectedCurrency = "USD";
203
214
  mockStoreState.baseCurrency = "B3";
204
215
 
205
- const { result } = renderHook(() => useCurrencyConversion());
216
+ const { result } = renderCurrencyConversion();
206
217
 
207
218
  expect(result.current.baseCurrency).toBe("B3");
208
219
  });
@@ -213,7 +224,7 @@ describe("useCurrencyConversion", () => {
213
224
  mockStoreState.selectedCurrency = "USD";
214
225
  mockStoreState.baseCurrency = "B3";
215
226
 
216
- const { result } = renderHook(() => useCurrencyConversion());
227
+ const { result } = renderCurrencyConversion();
217
228
 
218
229
  expect(result.current.exchangeRate).toBe(testRate);
219
230
  });
@@ -222,7 +233,7 @@ describe("useCurrencyConversion", () => {
222
233
  mockStoreState.selectedCurrency = "EUR";
223
234
  mockStoreState.baseCurrency = "B3";
224
235
 
225
- const { result } = renderHook(() => useCurrencyConversion());
236
+ const { result } = renderCurrencyConversion();
226
237
 
227
238
  expect(result.current.selectedCurrencySymbol).toBe("€");
228
239
  expect(result.current.baseCurrencySymbol).toBe("B3");
@@ -235,24 +246,24 @@ describe("useCurrencyConversion", () => {
235
246
  mockStoreState.selectedCurrency = "B3";
236
247
  mockStoreState.baseCurrency = "B3";
237
248
 
238
- const { result } = renderHook(() => useCurrencyConversion());
249
+ const { result } = renderCurrencyConversion();
239
250
  const tooltip = result.current.formatTooltipValue(100, "B3");
240
251
 
241
252
  expect(tooltip).toContain("USD");
242
253
  expect(tooltip).toContain("150");
243
254
  });
244
255
 
245
- it("should show base currency when displaying other currency", () => {
256
+ it("shows the USD equivalent regardless of selected display currency", () => {
246
257
  mockRates.EUR = 0.9;
247
258
  mockRates.USD = 1.2;
248
259
  mockStoreState.selectedCurrency = "EUR";
249
260
  mockStoreState.baseCurrency = "B3";
250
261
 
251
- const { result } = renderHook(() => useCurrencyConversion());
262
+ const { result } = renderCurrencyConversion();
252
263
  const tooltip = result.current.formatTooltipValue(100, "B3");
253
264
 
254
- expect(tooltip).toContain("B3");
255
- expect(tooltip).toContain("100");
265
+ expect(tooltip).toContain("USD");
266
+ expect(tooltip).toContain("120");
256
267
  });
257
268
 
258
269
  it("should handle custom currency for base currency", () => {
@@ -261,7 +272,7 @@ describe("useCurrencyConversion", () => {
261
272
  mockStoreState.selectedCurrency = "EUR";
262
273
  mockStoreState.baseCurrency = "B3";
263
274
 
264
- const { result } = renderHook(() => useCurrencyConversion());
275
+ const { result } = renderCurrencyConversion();
265
276
  const tooltip = result.current.formatTooltipValue(100, "B3");
266
277
 
267
278
  expect(tooltip).toContain("USD");
@@ -273,7 +284,7 @@ describe("useCurrencyConversion", () => {
273
284
  mockStoreState.selectedCurrency = "USD";
274
285
  mockStoreState.baseCurrency = "B3";
275
286
 
276
- const { result } = renderHook(() => useCurrencyConversion());
287
+ const { result } = renderCurrencyConversion();
277
288
  const tooltip = result.current.formatTooltipValue(50, "ETH");
278
289
 
279
290
  expect(tooltip).toContain("ETH");
@@ -285,7 +296,7 @@ describe("useCurrencyConversion", () => {
285
296
  mockStoreState.selectedCurrency = "B3";
286
297
  mockStoreState.baseCurrency = "B3";
287
298
 
288
- const { result } = renderHook(() => useCurrencyConversion());
299
+ const { result } = renderCurrencyConversion();
289
300
  const tooltip = result.current.formatTooltipValue(-100, "B3");
290
301
 
291
302
  expect(tooltip).toContain("USD");
@@ -293,15 +304,15 @@ describe("useCurrencyConversion", () => {
293
304
  expect(tooltip).not.toContain("-");
294
305
  });
295
306
 
296
- it("should handle exchange rate unavailable", () => {
307
+ it("falls back to the source currency when its USD rate is unavailable", () => {
297
308
  mockRates.USD = undefined;
298
309
  mockStoreState.selectedCurrency = "B3";
299
310
  mockStoreState.baseCurrency = "B3";
300
311
 
301
- const { result } = renderHook(() => useCurrencyConversion());
312
+ const { result } = renderCurrencyConversion();
302
313
  const tooltip = result.current.formatTooltipValue(100, "B3");
303
314
 
304
- expect(tooltip).toContain("USD");
315
+ expect(tooltip).toContain("B3");
305
316
  expect(tooltip).toContain("100");
306
317
  });
307
318
  });
@@ -1,8 +1,13 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { isSolanaAddress as registryIsSolanaAddress, SOLANA_NATIVE_ASSET } from "@b3/chain-registry/runtime";
2
+ import { describe, expect, expectTypeOf, it } from "vitest";
3
+ import { isSolanaAddress as sdkIsSolanaAddress } from "../../../anyspend/utils/address";
2
4
  import {
3
5
  adaptZerionPositions,
4
6
  chainIdToZerionSlug,
5
7
  EVM_NATIVE_TOKEN_ADDRESS,
8
+ SOLANA_CHAIN_ID,
9
+ SOLANA_NATIVE_TOKEN_ADDRESS,
10
+ SOLANA_ZERION_SLUG,
6
11
  zerionSlugToChainId,
7
12
  type ZerionPositionLike,
8
13
  } from "../zerionPositions";
@@ -224,3 +229,20 @@ describe("zerionSlugToChainId", () => {
224
229
  expect(chainIdToZerionSlug(8453)).toBe("base");
225
230
  });
226
231
  });
232
+
233
+ describe("canonical Solana registry compatibility", () => {
234
+ it("preserves published SDK values and literal types", () => {
235
+ expect(SOLANA_NATIVE_TOKEN_ADDRESS).toBe(SOLANA_NATIVE_ASSET.wrapped.address);
236
+ expect(SOLANA_ZERION_SLUG).toBe(SOLANA_NATIVE_ASSET.chain);
237
+ expect(SOLANA_CHAIN_ID).toBe(SOLANA_NATIVE_ASSET.chainId);
238
+ expectTypeOf(SOLANA_NATIVE_TOKEN_ADDRESS).toEqualTypeOf<"So11111111111111111111111111111111111111112">();
239
+ expectTypeOf(SOLANA_ZERION_SLUG).toEqualTypeOf<"solana">();
240
+ expectTypeOf(SOLANA_CHAIN_ID).toEqualTypeOf<7565164>();
241
+ });
242
+
243
+ it("re-exports the registry Solana address guard unchanged", () => {
244
+ expect(sdkIsSolanaAddress).toBe(registryIsSolanaAddress);
245
+ expect(sdkIsSolanaAddress(SOLANA_NATIVE_TOKEN_ADDRESS)).toBe(true);
246
+ expect(sdkIsSolanaAddress("So1111111111111111111111111111111111111111O")).toBe(false);
247
+ });
248
+ });