@abstraxn/signer-react 3.1.4 → 3.3.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.3.0] - 2026-04-28
9
+
10
+ ### Added
11
+
12
+ - **Solana gasless relayer hooks** – Added `useSolanaGasless()` and `useSignAndSendSolanaGaslessTxn()` to support build/sign/send gasless flow from React apps.
13
+ - **Solana gasless provider config** – Added `solanaGasless` in `AbstraxnProviderConfig` for relayer settings (`enabled`, `relayerBaseUrl`, `apiKey`, `networkChainId`, and optional polling defaults).
14
+
15
+ ### Changed
16
+
17
+ - **Gasless signing path uses existing signer-react backend signing** – Gasless transaction signing now reuses `signSolanaTransactionViaAPI` (+ MFA step-up retry) so signatures continue to come from the existing Turnkey-backed signer flow instead of client-side signing.
18
+ - **Public exports extended** – Exported new Solana gasless hooks and related types from `@abstraxn/signer-react`.
19
+
20
+ ## [3.2.0] - 2026-04-22
21
+
22
+ ### Added
23
+
24
+ - **Account Abstraction owner hook** – Added `useSmartAccountOwner()` to expose a viem-compatible custom local account for AA integrations (e.g. permissionless). The hook returns `{ smartAccountOwner, isReady, address }`, supports optional address override, and is exported from `@abstraxn/signer-react`.
25
+ - **EIP-7702 authorization signing support** – `smartAccountOwner` now implements `signAuthorization(...)` so 7702 flows can sign delegation authorizations through the SDK backend path.
26
+
27
+ ### Changed
28
+
29
+ - **AA signing now uses backend signing APIs** – AA owner signing routes through existing authenticated backend signing methods (`signTransactionViaAPI` / `signTypedTxViaAPI`) instead of requiring app-level Turnkey account creation.
30
+ - **UserOperation message signing compatibility** – `signMessage` in AA owner flow now follows viem EIP-191 semantics (`hashMessage(...)` then sign digest) to match permissionless/simple account UserOperation signature expectations.
31
+ - **7702 payload encoding alignment** – 7702 authorization signing uses Turnkey-specific parameters:
32
+ - `encoding: PAYLOAD_ENCODING_EIP7702_AUTHORIZATION`
33
+ - `hashFunction: HASH_FUNCTION_NOT_APPLICABLE`
34
+
35
+ ### Documentation
36
+
37
+ - **AA docs update** – Added `useSmartAccountOwner()` to hook list and expanded AA integration example in README.
38
+ - **Abstraxn infra example** – Updated README AA sample to use Abstraxn bundler/paymaster endpoints and dynamic chain-based URL construction.
39
+
8
40
  ## [3.1.4] - 2026-04-06
9
41
 
10
42
  ### Changed
package/README.md CHANGED
@@ -27,6 +27,12 @@ import App from './App';
27
27
 
28
28
  const providerConfig: AbstraxnProviderConfig = {
29
29
  apiKey: 'your-api-key-here',
30
+ solanaGasless: {
31
+ enabled: true,
32
+ relayerBaseUrl: 'https://solana-hub.abstraxn.com',
33
+ apiKey: 'your-solana-relayer-api-key',
34
+ networkChainId: 101, // Solana mainnet
35
+ },
30
36
  };
31
37
 
32
38
  createRoot(document.getElementById('root')!).render(
@@ -105,6 +111,79 @@ function HookConnectButton() {
105
111
  - `useExportWallet()` - Export wallet (EVM or Solana, based on current chain)
106
112
  - `usePublicClient()` / `usePrepareRawTxn()` / `useSignTxn()` / `useSignAndSendTxn()` / `useWaitForTxnReceipt()` - EVM transaction flow (prepare → sign → send → confirm)
107
113
  - `useSolanaConnection()` / `useSolanaPublicKey()` / `usePrepareSolanaTxn()` / `useSignSolanaTxn()` / `useSignAndSendSolanaTxn()` / `useWaitForSolanaConfirmation()` - Solana transaction flow (prepare → sign → send → confirm)
114
+ - `useSolanaGasless()` / `useSignAndSendSolanaGaslessTxn()` - Solana gasless flow using relayer build/send with Turnkey signing through signer-react
115
+ - `useSmartAccountOwner()` - Get an AA-compatible owner account for permissionless-style account abstraction flows
116
+
117
+ ### Example: Account Abstraction owner (permissionless-compatible)
118
+
119
+ ```tsx
120
+ import { useMemo } from 'react';
121
+ import { useSmartAccountOwner } from '@abstraxn/signer-react';
122
+ import { createPublicClient, createWalletClient, http } from 'viem';
123
+ import { sepolia } from 'viem/chains';
124
+ import { toSimpleSmartAccount } from 'permissionless/accounts';
125
+ import {
126
+ createBundlerClient,
127
+ createPaymasterClient,
128
+ entryPoint07Address,
129
+ } from 'viem/account-abstraction';
130
+
131
+ export function useAaClient({
132
+ chain = sepolia,
133
+ rpcUrl = 'https://sepolia.rpc.thirdweb.com',
134
+ abstraxnApiKey,
135
+ }: {
136
+ chain?: Chain;
137
+ rpcUrl?: string;
138
+ abstraxnApiKey: string;
139
+ }) {
140
+ const { smartAccountOwner, isReady } = useSmartAccountOwner();
141
+
142
+ return useMemo(async () => {
143
+ if (!isReady || !smartAccountOwner) {
144
+ return null;
145
+ }
146
+
147
+ const bundlerUrl = `https://bundler.abstraxn.com/api/v1/${chainId}/?apikey=${abstraxnApiKey}`;
148
+ const paymasterUrl = `https://paymaster.abstraxn.com/api/v2/${chainId}/?apikey=${abstraxnApiKey}`;
149
+
150
+ const publicClient = createPublicClient({
151
+ chain,
152
+ transport: http(rpcUrl),
153
+ });
154
+
155
+ // permissionless expects a wallet client owner
156
+ const ownerWalletClient = createWalletClient({
157
+ account: smartAccountOwner,
158
+ chain,
159
+ transport: http(rpcUrl),
160
+ });
161
+
162
+ const paymasterClient = createPaymasterClient({
163
+ transport: http(paymasterUrl),
164
+ });
165
+
166
+ const simpleSmartAccount = await toSimpleSmartAccount({
167
+ owner: ownerWalletClient,
168
+ client: publicClient,
169
+ entryPoint: {
170
+ address: entryPoint07Address,
171
+ version: '0.7',
172
+ },
173
+ });
174
+
175
+ return createBundlerClient({
176
+ client: publicClient,
177
+ transport: http(bundlerUrl),
178
+ });
179
+ }, [isReady, smartAccountOwner, chain, rpcUrl, abstraxnApiKey]);
180
+ }
181
+ ```
182
+
183
+ Notes:
184
+ - `permissionless` and AA provider clients are app-level dependencies (not bundled in `@abstraxn/signer-react`).
185
+ - The owner returned by `useSmartAccountOwner()` is signer-only; you control chain, bundler, paymaster, and entry point in your app.
186
+ - After creating the `bundlerClient`, call `sendUserOperation({ account: simpleSmartAccount, calls: [...], paymaster: paymasterClient })`.
108
187
 
109
188
  ### Example: EVM → Solana flow after social/email login
110
189
 
@@ -531,6 +531,52 @@
531
531
  color: #6b7280;
532
532
  }
533
533
 
534
+ .wallet-modal-asset-balance-skeleton {
535
+ display: inline-block;
536
+ width: 140px;
537
+ max-width: 100%;
538
+ height: 14px;
539
+ border-radius: 999px;
540
+ background: linear-gradient(90deg, rgba(156, 163, 175, 0.22), rgba(156, 163, 175, 0.44), rgba(156, 163, 175, 0.22));
541
+ background-size: 200% 100%;
542
+ animation: wallet-modal-balance-shimmer 1.2s ease-in-out infinite;
543
+ }
544
+
545
+ .wallet-modal-theme-dark .wallet-modal-asset-balance-skeleton {
546
+ background: linear-gradient(90deg, rgba(75, 85, 99, 0.3), rgba(107, 114, 128, 0.55), rgba(75, 85, 99, 0.3));
547
+ background-size: 200% 100%;
548
+ }
549
+
550
+ .wallet-modal-asset-explorer-link {
551
+ display: inline-flex;
552
+ width: fit-content;
553
+ margin-top: 2px;
554
+ font-size: 12px;
555
+ font-weight: 500;
556
+ text-decoration: none;
557
+ }
558
+
559
+ .wallet-modal-theme-dark .wallet-modal-asset-explorer-link {
560
+ color: #a855f7;
561
+ }
562
+
563
+ .wallet-modal-theme-light .wallet-modal-asset-explorer-link {
564
+ color: #9333ea;
565
+ }
566
+
567
+ .wallet-modal-asset-explorer-link:hover {
568
+ text-decoration: underline;
569
+ }
570
+
571
+ @keyframes wallet-modal-balance-shimmer {
572
+ 0% {
573
+ background-position: 100% 0;
574
+ }
575
+ 100% {
576
+ background-position: -100% 0;
577
+ }
578
+ }
579
+
534
580
  .wallet-modal-asset-status {
535
581
  position: absolute;
536
582
  bottom: 0;
@@ -36,6 +36,7 @@ export function WalletModal({ isOpen, onClose, onRampUrl }) {
36
36
  const [showSendModal, setShowSendModal] = useState(false);
37
37
  const [showManageModal, setShowManageModal] = useState(false);
38
38
  const [showChainSelector, setShowChainSelector] = useState(false);
39
+ const [isBalanceRefreshing, setIsBalanceRefreshing] = useState(false);
39
40
  const [selectedChainType, setSelectedChainType] = useState("evm");
40
41
  // Export states
41
42
  const [showExportWarning, setShowExportWarning] = useState(false);
@@ -59,19 +60,30 @@ export function WalletModal({ isOpen, onClose, onRampUrl }) {
59
60
  // If no chain selected, show default
60
61
  if (!balance || balance === 0n)
61
62
  return "0";
62
- return formatBalance(balance, 18, "ETH");
63
+ return formatBalance(balance, 18, "ETH", 4);
63
64
  }
64
65
  const symbol = currentChain.nativeCurrency?.symbol || "ETH";
65
66
  const decimals = currentChain.nativeCurrency?.decimals || 18;
66
67
  if (!balance || balance === 0n) {
67
68
  return `0 ${symbol}`;
68
69
  }
69
- return formatBalance(balance, decimals, symbol);
70
+ return formatBalance(balance, decimals, symbol, 4);
70
71
  }, [currentChain, externalWalletBalance, walletBalance]);
72
+ const chainExplorer = useMemo(() => {
73
+ if (!currentChain)
74
+ return null;
75
+ const url = currentChain.blockExplorer?.url || currentChain.explorerUrl;
76
+ if (!url)
77
+ return null;
78
+ return {
79
+ url,
80
+ label: currentChain.blockExplorer?.name || "Explorer",
81
+ };
82
+ }, [currentChain]);
71
83
  // Refetch balance from RPC when UI screen changes (back button, modal open/close, etc.)
72
84
  useEffect(() => {
73
85
  if (isOpen && refetchBalance) {
74
- refetchBalance();
86
+ void refetchBalance();
75
87
  }
76
88
  }, [isOpen, showReceiveModal, showSendModal, showManageModal, showChainSelector, refetchBalance]);
77
89
  /** Load MFA status once per session (in-memory) when this wallet modal opens; not persisted. */
@@ -300,7 +312,7 @@ export function WalletModal({ isOpen, onClose, onRampUrl }) {
300
312
  return (_jsx(ManageWalletModal, { isOpen: true, onClose: () => setShowManageModal(false), displayAddress: displayAddress, userEmail: user?.email, organizationName: whoami?.organizationName, isExternalWalletConnected: isExternalWalletConnected || false, onExportPrivateKey: handleExportPrivateKey, theme: theme }));
301
313
  }
302
314
  // Main Modal
303
- return (_jsxs(_Fragment, { children: [_jsx("div", { "data-abstraxn-sdk-root": true, className: `wallet-modal-overlay wallet-modal-theme-${theme}`, onClick: showChainSelector ? undefined : onClose, style: showChainSelector ? { pointerEvents: "none" } : undefined, children: _jsxs("div", { className: `wallet-modal-content wallet-modal-theme-${theme} ${showChainSelector ? "selector-open" : ""}`, onClick: (e) => e.stopPropagation(), style: showChainSelector ? { pointerEvents: "auto" } : undefined, children: [_jsxs("div", { className: "wallet-modal-header", children: [_jsx("div", { className: "wallet-modal-header-left", children: _jsxs("div", { className: "wallet-modal-user-info", children: [_jsx("div", { className: "wallet-modal-avatar", children: _jsx(UserAvatar, { user: user, isExternalWalletConnected: isExternalWalletConnected || false }) }), _jsxs("div", { className: "wallet-modal-user-details", children: [_jsxs("div", { className: "wallet-modal-address", onClick: handleCopyAddress, children: [formatAddress(displayAddress || address), copied ? (_jsx("span", { className: "wallet-modal-copy-notification", children: "Copied!" })) : (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", className: "wallet-modal-copy-icon", children: [_jsx("path", { d: "M5.5 3.5H3.5C2.67157 3.5 2 4.17157 2 5V12.5C2 13.3284 2.67157 14 3.5 14H11C11.8284 14 12.5 13.3284 12.5 12.5V10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M5.5 3.5C5.5 2.67157 6.17157 2 7 2H12.5C13.3284 2 14 2.67157 14 3.5V9C14 9.82843 13.3284 10.5 12.5 10.5H7C6.17157 10.5 5.5 9.82843 5.5 9V3.5Z", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }))] }), user?.email && (_jsx("div", { className: "wallet-modal-email", children: user.email }))] })] }) }), _jsx("div", { className: "wallet-modal-header-right", children: _jsx("button", { className: "wallet-modal-close", onClick: onClose, "aria-label": "Close", children: _jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6L18 18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) })] }), !isExternalWalletConnected && (_jsxs("div", { className: "wallet-modal-actions", children: [_jsxs("button", { className: "wallet-modal-action-btn", onClick: handleSend, children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), _jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })] }), _jsx("span", { children: "Send" })] }), _jsxs("button", { className: "wallet-modal-action-btn", onClick: handleReceive, children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), _jsx("polyline", { points: "7 10 12 15 17 10" }), _jsx("line", { x1: "12", y1: "15", x2: "12", y2: "3" })] }), _jsx("span", { children: "Receive" })] }), _jsxs("button", { className: "wallet-modal-action-btn", onClick: handleBuy, children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }), _jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })] }), _jsx("span", { children: "Buy" })] })] })), !isExternalWalletConnected && currentChain && (_jsxs("div", { className: "wallet-modal-asset", onClick: () => setShowChainSelector(true), children: [_jsxs("div", { className: "wallet-modal-asset-info", children: [_jsxs("div", { className: "wallet-modal-asset-icon-wrapper", children: [currentChain.iconUrl ? (_jsx("img", { src: currentChain.iconUrl, alt: currentChain.displayName, className: "wallet-modal-asset-icon-img" })) : (_jsx("div", { className: "wallet-modal-asset-icon", children: _jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M12 2v20M2 12h20" })] }) })), _jsx("span", { className: "wallet-modal-asset-status" })] }), _jsxs("div", { className: "wallet-modal-asset-details", children: [_jsx("div", { className: "wallet-modal-asset-name", children: currentChain.displayName }), _jsx("div", { className: "wallet-modal-asset-balance", children: balanceDisplay })] })] }), _jsx("div", { className: "wallet-modal-asset-arrow", children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "9 18 15 12 9 6" }) }) })] })), _jsxs("div", { className: "wallet-modal-menu", children: [_jsxs("button", { className: "wallet-modal-menu-item", onClick: handleManageWallet, children: [_jsx(LuWallet, { size: 20 }), _jsx("span", { children: "Manage Wallet" })] }), _jsxs("button", { className: "wallet-modal-menu-item wallet-modal-menu-item-disconnect", onClick: handleDisconnect, children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [_jsx("path", { d: "M7 3H4C3.44772 3 3 3.44772 3 4V16C3 16.5523 3.44772 17 4 17H7", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M13 7L17 10L13 13", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M17 10H9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })] }), _jsx("span", { children: "Disconnect" })] })] })] }) }), showChainSelector && (_jsx("div", { "data-abstraxn-sdk-root": true, className: `wallet-modal-overlay wallet-modal-theme-${theme}`, style: { zIndex: 10006, pointerEvents: "auto" }, onClick: (e) => {
315
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "data-abstraxn-sdk-root": true, className: `wallet-modal-overlay wallet-modal-theme-${theme}`, onClick: showChainSelector ? undefined : onClose, style: showChainSelector ? { pointerEvents: "none" } : undefined, children: _jsxs("div", { className: `wallet-modal-content wallet-modal-theme-${theme} ${showChainSelector ? "selector-open" : ""}`, onClick: (e) => e.stopPropagation(), style: showChainSelector ? { pointerEvents: "auto" } : undefined, children: [_jsxs("div", { className: "wallet-modal-header", children: [_jsx("div", { className: "wallet-modal-header-left", children: _jsxs("div", { className: "wallet-modal-user-info", children: [_jsx("div", { className: "wallet-modal-avatar", children: _jsx(UserAvatar, { user: user, isExternalWalletConnected: isExternalWalletConnected || false }) }), _jsxs("div", { className: "wallet-modal-user-details", children: [_jsxs("div", { className: "wallet-modal-address", onClick: handleCopyAddress, children: [formatAddress(displayAddress || address), copied ? (_jsx("span", { className: "wallet-modal-copy-notification", children: "Copied!" })) : (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", className: "wallet-modal-copy-icon", children: [_jsx("path", { d: "M5.5 3.5H3.5C2.67157 3.5 2 4.17157 2 5V12.5C2 13.3284 2.67157 14 3.5 14H11C11.8284 14 12.5 13.3284 12.5 12.5V10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M5.5 3.5C5.5 2.67157 6.17157 2 7 2H12.5C13.3284 2 14 2.67157 14 3.5V9C14 9.82843 13.3284 10.5 12.5 10.5H7C6.17157 10.5 5.5 9.82843 5.5 9V3.5Z", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }))] }), user?.email && (_jsx("div", { className: "wallet-modal-email", children: user.email }))] })] }) }), _jsx("div", { className: "wallet-modal-header-right", children: _jsx("button", { className: "wallet-modal-close", onClick: onClose, "aria-label": "Close", children: _jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6L18 18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) })] }), !isExternalWalletConnected && (_jsxs("div", { className: "wallet-modal-actions", children: [_jsxs("button", { className: "wallet-modal-action-btn", onClick: handleSend, children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), _jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })] }), _jsx("span", { children: "Send" })] }), _jsxs("button", { className: "wallet-modal-action-btn", onClick: handleReceive, children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), _jsx("polyline", { points: "7 10 12 15 17 10" }), _jsx("line", { x1: "12", y1: "15", x2: "12", y2: "3" })] }), _jsx("span", { children: "Receive" })] }), _jsxs("button", { className: "wallet-modal-action-btn", onClick: handleBuy, children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }), _jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })] }), _jsx("span", { children: "Buy" })] })] })), !isExternalWalletConnected && currentChain && (_jsxs("div", { className: "wallet-modal-asset", onClick: () => setShowChainSelector(true), children: [_jsxs("div", { className: "wallet-modal-asset-info", children: [_jsxs("div", { className: "wallet-modal-asset-icon-wrapper", children: [currentChain.iconUrl ? (_jsx("img", { src: currentChain.iconUrl, alt: currentChain.displayName, className: "wallet-modal-asset-icon-img" })) : (_jsx("div", { className: "wallet-modal-asset-icon", children: _jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M12 2v20M2 12h20" })] }) })), _jsx("span", { className: "wallet-modal-asset-status" })] }), _jsxs("div", { className: "wallet-modal-asset-details", children: [_jsx("div", { className: "wallet-modal-asset-name", children: currentChain.displayName }), _jsx("div", { className: "wallet-modal-asset-balance", children: isBalanceRefreshing ? (_jsx("span", { className: "wallet-modal-asset-balance-skeleton", "aria-label": "Loading balance" })) : (balanceDisplay) }), chainExplorer?.url && (_jsx("a", { href: chainExplorer.url, target: "_blank", rel: "noopener noreferrer", className: "wallet-modal-asset-explorer-link", onClick: (e) => e.stopPropagation(), children: chainExplorer.label }))] })] }), _jsx("div", { className: "wallet-modal-asset-arrow", children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "9 18 15 12 9 6" }) }) })] })), _jsxs("div", { className: "wallet-modal-menu", children: [_jsxs("button", { className: "wallet-modal-menu-item", onClick: handleManageWallet, children: [_jsx(LuWallet, { size: 20 }), _jsx("span", { children: "Manage Wallet" })] }), _jsxs("button", { className: "wallet-modal-menu-item wallet-modal-menu-item-disconnect", onClick: handleDisconnect, children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [_jsx("path", { d: "M7 3H4C3.44772 3 3 3.44772 3 4V16C3 16.5523 3.44772 17 4 17H7", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M13 7L17 10L13 13", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M17 10H9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })] }), _jsx("span", { children: "Disconnect" })] })] })] }) }), showChainSelector && (_jsx("div", { "data-abstraxn-sdk-root": true, className: `wallet-modal-overlay wallet-modal-theme-${theme}`, style: { zIndex: 10006, pointerEvents: "auto" }, onClick: (e) => {
304
316
  e.stopPropagation();
305
317
  setShowChainSelector(false);
306
318
  }, children: _jsx("div", { className: `wallet-modal-content wallet-modal-theme-${theme}`, onClick: (e) => e.stopPropagation(), children: _jsx(ChainSelector, { isOpen: showChainSelector, onClose: () => setShowChainSelector(false), currentChain: currentChain || null, availableChains: availableChains || [], onChainSelect: async (chainId) => {
@@ -317,7 +329,11 @@ export function WalletModal({ isOpen, onClose, onRampUrl }) {
317
329
  // Switch to a different chain
318
330
  if (switchChainEnhanced) {
319
331
  try {
332
+ setIsBalanceRefreshing(true);
320
333
  await switchChainEnhanced(chainId);
334
+ if (refetchBalance) {
335
+ await refetchBalance();
336
+ }
321
337
  // Only close if switch was successful
322
338
  setShowChainSelector(false);
323
339
  }
@@ -325,6 +341,9 @@ export function WalletModal({ isOpen, onClose, onRampUrl }) {
325
341
  console.error("Failed to switch chain:", error);
326
342
  // Don't close the selector on error so user can try again
327
343
  }
344
+ finally {
345
+ setIsBalanceRefreshing(false);
346
+ }
328
347
  }
329
348
  else {
330
349
  console.warn("switchChainEnhanced not available");
@@ -14,6 +14,9 @@ import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react
14
14
  import { PhantomWalletAdapter, SolflareWalletAdapter } from "@solana/wallet-adapter-wallets";
15
15
  export function AbstraxnProvider({ config, children }) {
16
16
  const externalWalletsEnabled = config.externalWallets?.enabled ?? false;
17
+ const solanaEndpoint = config.chains?.solanaEndpoint ??
18
+ config.solanaEndpoint ??
19
+ "https://api.mainnet-beta.solana.com";
17
20
  const externalSsr = config.externalWallets?.ssr ?? false;
18
21
  // Get or create QueryClient - tries to use existing one from context first
19
22
  // This ensures we use the app's QueryClient instance if it exists, preventing multiple instances
@@ -197,7 +200,7 @@ export function AbstraxnProvider({ config, children }) {
197
200
  }
198
201
  // Do NOT pass key to WagmiProvider. Remounting loses connection state and causes
199
202
  // "Connector not connected" in Next.js (e.g. when config loads after hydration).
200
- return (_jsx(QueryClientWrapper, { queryClient: queryClient, children: _jsx(ConnectionProvider, { endpoint: config.chains?.solanaEndpoint || "https://api.mainnet-beta.solana.com", children: _jsx(WalletProvider, { wallets: solanaWallets, autoConnect: true, children: _jsx(WagmiProvider, { config: wagmiConfig, initialState: config.initialState, children: _jsx(AbstraxnProviderWithWagmi, { config: config, children: children }) }) }) }) }));
203
+ return (_jsx(QueryClientWrapper, { queryClient: queryClient, children: _jsx(ConnectionProvider, { endpoint: solanaEndpoint, children: _jsx(WalletProvider, { wallets: solanaWallets, autoConnect: true, children: _jsx(WagmiProvider, { config: wagmiConfig, initialState: config.initialState, children: _jsx(AbstraxnProviderWithWagmi, { config: config, children: children }) }) }) }) }));
201
204
  }
202
205
  // If external wallets are disabled, use the provider without wagmi
203
206
  return (_jsx(AbstraxnProviderWithoutWagmi, { config: config, children: children }));
@@ -3225,6 +3225,60 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3225
3225
  const currentChainId = isExternalWalletConnected && externalWalletChainId
3226
3226
  ? externalWalletChainId
3227
3227
  : chainId;
3228
+ // Custom chains passed via `config.chains` (array form) are not part of the static `EVM_CHAINS` map.
3229
+ // Normalize them into `ChainData` so the wallet UI can resolve chain + balance correctly.
3230
+ const customChainsById = useMemo(() => {
3231
+ const chainConfig = config.chains;
3232
+ const map = new Map();
3233
+ if (!Array.isArray(chainConfig) || chainConfig.length === 0)
3234
+ return map;
3235
+ for (const raw of chainConfig) {
3236
+ if (!raw || typeof raw.id !== "number" || !raw.nativeCurrency)
3237
+ continue;
3238
+ // Extract RPC URL (support viem shape + sdk-integrate custom shape)
3239
+ let rpcUrl;
3240
+ if (raw.rpcUrls?.default?.http) {
3241
+ rpcUrl = Array.isArray(raw.rpcUrls.default.http)
3242
+ ? raw.rpcUrls.default.http[0]
3243
+ : raw.rpcUrls.default.http;
3244
+ }
3245
+ else if (raw.rpcUrls?.public?.http) {
3246
+ rpcUrl = Array.isArray(raw.rpcUrls.public.http)
3247
+ ? raw.rpcUrls.public.http[0]
3248
+ : raw.rpcUrls.public.http;
3249
+ }
3250
+ else if (typeof raw.rpcUrl === "string") {
3251
+ rpcUrl = raw.rpcUrl;
3252
+ }
3253
+ if (!rpcUrl || rpcUrl.trim() === "")
3254
+ continue;
3255
+ const explorerUrl = raw.blockExplorers?.default?.url ||
3256
+ raw.explorerUrl || '';
3257
+ const displayName = raw.displayName || raw.name || `Chain ${raw.id}`;
3258
+ const name = (raw.name ? String(raw.name) : `chain-${raw.id}`)
3259
+ .toLowerCase()
3260
+ .replace(/\s+/g, "-");
3261
+ const isTestnet = raw.testnet === true ||
3262
+ raw.isTestnet === true ||
3263
+ // Heuristic fallback for unknown custom chains
3264
+ (raw.id !== 1 && raw.id !== 10 && raw.id !== 137 && raw.id !== 8453 && raw.id !== 42161 && raw.id !== 2741);
3265
+ const chainData = {
3266
+ id: raw.id,
3267
+ name,
3268
+ displayName,
3269
+ rpcUrl,
3270
+ explorerUrl,
3271
+ iconUrl: raw.iconUrl,
3272
+ icon: raw.icon,
3273
+ nativeCurrency: raw.nativeCurrency,
3274
+ type: "evm",
3275
+ isTestnet,
3276
+ blockExplorer: raw.blockExplorer,
3277
+ };
3278
+ map.set(chainData.id, chainData);
3279
+ }
3280
+ return map;
3281
+ }, [config.chains]);
3228
3282
  // Fetch balance for Abstraxn wallet (not external wallet) - reusable for on-demand refetch
3229
3283
  const refetchWalletBalance = useCallback(async () => {
3230
3284
  if (isExternalWalletConnected || !address || !currentChainId) {
@@ -3232,7 +3286,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3232
3286
  return;
3233
3287
  }
3234
3288
  try {
3235
- const currentChain = getChainById(currentChainId);
3289
+ const currentChain = getChainById(currentChainId) || customChainsById.get(currentChainId);
3236
3290
  if (!currentChain || currentChain.type !== "evm") {
3237
3291
  setWalletBalance(null);
3238
3292
  return;
@@ -3259,24 +3313,35 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3259
3313
  console.error("Failed to fetch balance:", error);
3260
3314
  setWalletBalance(null);
3261
3315
  }
3262
- }, [address, currentChainId, isExternalWalletConnected]);
3316
+ }, [address, currentChainId, isExternalWalletConnected, customChainsById]);
3263
3317
  useEffect(() => {
3264
3318
  refetchWalletBalance();
3265
3319
  }, [refetchWalletBalance]);
3266
3320
  // Single refetch for UI: Abstraxn wallet = RPC call, external wallet = wagmi refetch
3267
- const refetchBalance = useCallback(() => {
3321
+ const refetchBalance = useCallback(async () => {
3268
3322
  if (isExternalWalletConnected && wagmiBalance?.refetch) {
3269
- void wagmiBalance.refetch();
3323
+ await wagmiBalance.refetch();
3270
3324
  return;
3271
3325
  }
3272
- void refetchWalletBalance();
3326
+ await refetchWalletBalance();
3273
3327
  }, [isExternalWalletConnected, wagmiBalance, refetchWalletBalance]);
3274
3328
  // Compute available chains from config - supports both legacy and new format
3275
3329
  const availableChains = useMemo(() => {
3276
3330
  const chains = [];
3277
3331
  const chainConfig = config.chains;
3278
- // Priority 1: Check new chains config format
3279
- if (chainConfig?.supportedEvmChains &&
3332
+ // Priority 1: Check if chains is an array of viem/custom chain objects
3333
+ if (Array.isArray(chainConfig) && chainConfig.length > 0) {
3334
+ for (const raw of chainConfig) {
3335
+ const normalized = raw && typeof raw.id === "number"
3336
+ ? customChainsById.get(raw.id)
3337
+ : undefined;
3338
+ if (normalized)
3339
+ chains.push(normalized);
3340
+ }
3341
+ }
3342
+ // Priority 2: Check new chains config format
3343
+ if (!Array.isArray(chainConfig) &&
3344
+ chainConfig?.supportedEvmChains &&
3280
3345
  chainConfig.supportedEvmChains.length > 0) {
3281
3346
  // Use configured chains from new format
3282
3347
  chainConfig.supportedEvmChains.forEach((chainName) => {
@@ -3286,8 +3351,10 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3286
3351
  }
3287
3352
  });
3288
3353
  }
3289
- // Priority 2: Check legacy supportedChains format
3290
- else if (config.supportedChains && config.supportedChains.length > 0) {
3354
+ // Priority 3: Check legacy supportedChains format
3355
+ else if (!Array.isArray(chainConfig) &&
3356
+ config.supportedChains &&
3357
+ config.supportedChains.length > 0) {
3291
3358
  // Convert legacy Chain format to ChainData
3292
3359
  config.supportedChains.forEach((legacyChain) => {
3293
3360
  // Try to find matching chain by ID
@@ -3313,7 +3380,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3313
3380
  }
3314
3381
  });
3315
3382
  }
3316
- // Priority 3: Default chains only if nothing configured at all
3383
+ // Priority 4: Default chains only if nothing configured at all
3317
3384
  // Only show defaults if both config.chains and config.supportedChains are missing
3318
3385
  else if (!chainConfig && !config.supportedChains) {
3319
3386
  chains.push(EVM_CHAINS.ethereum, EVM_CHAINS.polygon, EVM_CHAINS.base);
@@ -3330,13 +3397,15 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3330
3397
  : !chainConfig && !config.supportedChains
3331
3398
  ? [EVM_CHAINS.ethereum, EVM_CHAINS.polygon, EVM_CHAINS.base]
3332
3399
  : [];
3333
- }, [config.chains, config.supportedChains]);
3400
+ }, [config.chains, config.supportedChains, customChainsById]);
3334
3401
  // Get current chain data
3335
3402
  const currentChain = useMemo(() => {
3336
3403
  if (!currentChainId)
3337
3404
  return null;
3338
- return getChainById(currentChainId) || null;
3339
- }, [currentChainId]);
3405
+ return (getChainById(currentChainId) ||
3406
+ customChainsById.get(currentChainId) ||
3407
+ null);
3408
+ }, [currentChainId, customChainsById]);
3340
3409
  // Enhanced switchChain that works for both EVM and Solana
3341
3410
  const switchChainEnhanced = useCallback(async (targetChainId) => {
3342
3411
  if (!walletRef.current) {
@@ -3347,7 +3416,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3347
3416
  if (!targetChainInAvailable) {
3348
3417
  throw new Error(`Chain with ID ${targetChainId} is not available. Please configure it in your SDK setup.`);
3349
3418
  }
3350
- const targetChain = getChainById(targetChainId);
3419
+ const targetChain = getChainById(targetChainId) || customChainsById.get(targetChainId);
3351
3420
  if (!targetChain) {
3352
3421
  throw new Error(`Chain with ID ${targetChainId} is not supported`);
3353
3422
  }
@@ -3407,7 +3476,13 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3407
3476
  finally {
3408
3477
  setLoading(false);
3409
3478
  }
3410
- }, [isExternalWalletConnected, wagmiSwitchChain, switchChain, availableChains]);
3479
+ }, [
3480
+ isExternalWalletConnected,
3481
+ wagmiSwitchChain,
3482
+ switchChain,
3483
+ availableChains,
3484
+ customChainsById,
3485
+ ]);
3411
3486
  const setEmailForOtp = useCallback((email) => {
3412
3487
  setEmailForOtpState(email);
3413
3488
  }, []);
@@ -3611,6 +3686,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3611
3686
  setEmailForOtp: setEmailForOtp,
3612
3687
  // Global config flags
3613
3688
  enableSolana: config.chains?.enableSolana,
3689
+ solanaGaslessConfig: config.solanaGasless,
3614
3690
  };
3615
3691
  // Ref to store latest value to avoid dependency issues (defined after value)
3616
3692
  const valueRef = useRef(value);
@@ -95,7 +95,7 @@ export function useSendTransaction({ sendTransaction, currentChain, fromAddress,
95
95
  data: '0x',
96
96
  chainId: currentChain.id,
97
97
  gas: {
98
- gasLimit,
98
+ gasLimit: gasLimit,
99
99
  maxFeePerGas: gasFees.maxFeePerGas,
100
100
  maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas,
101
101
  gasPrice: gasFees.gasPrice,
@@ -9,7 +9,7 @@ export declare function formatAddress(addr: string | null | undefined): string;
9
9
  * Formats a balance with proper decimals and symbol
10
10
  * Removes trailing zeros and unnecessary decimals
11
11
  */
12
- export declare function formatBalance(balance: bigint | null | undefined, decimals: number, symbol: string): string;
12
+ export declare function formatBalance(balance: bigint | null | undefined, decimals: number, symbol: string, maxFractionDigits?: number): string;
13
13
  /**
14
14
  * Formats a balance value (number) with proper decimals
15
15
  */
@@ -15,13 +15,12 @@ export function formatAddress(addr) {
15
15
  * Formats a balance with proper decimals and symbol
16
16
  * Removes trailing zeros and unnecessary decimals
17
17
  */
18
- export function formatBalance(balance, decimals, symbol) {
18
+ export function formatBalance(balance, decimals, symbol, maxFractionDigits = decimals) {
19
19
  if (!balance || balance === 0n)
20
20
  return `0 ${symbol}`;
21
21
  const balanceValue = Number(balance) / Math.pow(10, decimals);
22
- // Remove trailing zeros and unnecessary decimals
23
- // Use toFixed with max decimals, then remove trailing zeros
24
- const formatted = balanceValue.toFixed(decimals);
22
+ const safeFractionDigits = Math.max(0, Math.min(maxFractionDigits, decimals));
23
+ const formatted = balanceValue.toFixed(safeFractionDigits);
25
24
  // Remove trailing zeros and decimal point if not needed
26
25
  const trimmed = formatted.replace(/\.?0+$/, '');
27
26
  return `${trimmed} ${symbol}`;