@abstraxn/signer-react 3.1.4 → 3.2.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,26 @@ 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.2.0] - 2026-04-22
9
+
10
+ ### Added
11
+
12
+ - **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`.
13
+ - **EIP-7702 authorization signing support** – `smartAccountOwner` now implements `signAuthorization(...)` so 7702 flows can sign delegation authorizations through the SDK backend path.
14
+
15
+ ### Changed
16
+
17
+ - **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.
18
+ - **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.
19
+ - **7702 payload encoding alignment** – 7702 authorization signing uses Turnkey-specific parameters:
20
+ - `encoding: PAYLOAD_ENCODING_EIP7702_AUTHORIZATION`
21
+ - `hashFunction: HASH_FUNCTION_NOT_APPLICABLE`
22
+
23
+ ### Documentation
24
+
25
+ - **AA docs update** – Added `useSmartAccountOwner()` to hook list and expanded AA integration example in README.
26
+ - **Abstraxn infra example** – Updated README AA sample to use Abstraxn bundler/paymaster endpoints and dynamic chain-based URL construction.
27
+
8
28
  ## [3.1.4] - 2026-04-06
9
29
 
10
30
  ### Changed
package/README.md CHANGED
@@ -105,6 +105,78 @@ function HookConnectButton() {
105
105
  - `useExportWallet()` - Export wallet (EVM or Solana, based on current chain)
106
106
  - `usePublicClient()` / `usePrepareRawTxn()` / `useSignTxn()` / `useSignAndSendTxn()` / `useWaitForTxnReceipt()` - EVM transaction flow (prepare → sign → send → confirm)
107
107
  - `useSolanaConnection()` / `useSolanaPublicKey()` / `usePrepareSolanaTxn()` / `useSignSolanaTxn()` / `useSignAndSendSolanaTxn()` / `useWaitForSolanaConfirmation()` - Solana transaction flow (prepare → sign → send → confirm)
108
+ - `useSmartAccountOwner()` - Get an AA-compatible owner account for permissionless-style account abstraction flows
109
+
110
+ ### Example: Account Abstraction owner (permissionless-compatible)
111
+
112
+ ```tsx
113
+ import { useMemo } from 'react';
114
+ import { useSmartAccountOwner } from '@abstraxn/signer-react';
115
+ import { createPublicClient, createWalletClient, http } from 'viem';
116
+ import { sepolia } from 'viem/chains';
117
+ import { toSimpleSmartAccount } from 'permissionless/accounts';
118
+ import {
119
+ createBundlerClient,
120
+ createPaymasterClient,
121
+ entryPoint07Address,
122
+ } from 'viem/account-abstraction';
123
+
124
+ export function useAaClient({
125
+ chain = sepolia,
126
+ rpcUrl = 'https://sepolia.rpc.thirdweb.com',
127
+ abstraxnApiKey,
128
+ }: {
129
+ chain?: Chain;
130
+ rpcUrl?: string;
131
+ abstraxnApiKey: string;
132
+ }) {
133
+ const { smartAccountOwner, isReady } = useSmartAccountOwner();
134
+
135
+ return useMemo(async () => {
136
+ if (!isReady || !smartAccountOwner) {
137
+ return null;
138
+ }
139
+
140
+ const bundlerUrl = `https://bundler.abstraxn.com/api/v1/${chainId}/?apikey=${abstraxnApiKey}`;
141
+ const paymasterUrl = `https://paymaster.abstraxn.com/api/v2/${chainId}/?apikey=${abstraxnApiKey}`;
142
+
143
+ const publicClient = createPublicClient({
144
+ chain,
145
+ transport: http(rpcUrl),
146
+ });
147
+
148
+ // permissionless expects a wallet client owner
149
+ const ownerWalletClient = createWalletClient({
150
+ account: smartAccountOwner,
151
+ chain,
152
+ transport: http(rpcUrl),
153
+ });
154
+
155
+ const paymasterClient = createPaymasterClient({
156
+ transport: http(paymasterUrl),
157
+ });
158
+
159
+ const simpleSmartAccount = await toSimpleSmartAccount({
160
+ owner: ownerWalletClient,
161
+ client: publicClient,
162
+ entryPoint: {
163
+ address: entryPoint07Address,
164
+ version: '0.7',
165
+ },
166
+ });
167
+
168
+ return createBundlerClient({
169
+ client: publicClient,
170
+ transport: http(bundlerUrl),
171
+ });
172
+ }, [isReady, smartAccountOwner, chain, rpcUrl, abstraxnApiKey]);
173
+ }
174
+ ```
175
+
176
+ Notes:
177
+ - `permissionless` and AA provider clients are app-level dependencies (not bundled in `@abstraxn/signer-react`).
178
+ - The owner returned by `useSmartAccountOwner()` is signer-only; you control chain, bundler, paymaster, and entry point in your app.
179
+ - After creating the `bundlerClient`, call `sendUserOperation({ account: simpleSmartAccount, calls: [...], paymaster: paymasterClient })`.
108
180
 
109
181
  ### Example: EVM → Solana flow after social/email login
110
182
 
@@ -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");
@@ -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
  }, []);
@@ -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}`;
@@ -1,4 +1,4 @@
1
- import type { WhoamiResponse, TransactionRequest, EnableMfaResponse, VerifySetupMfaResponse, DisableMfaResponse } from '@abstraxn/signer-core';
1
+ import type { WhoamiResponse, TransactionRequest, EnableMfaResponse, VerifySetupMfaResponse, DisableMfaResponse, SupportedAuthMethod, LinkedAuthMethod } from '@abstraxn/signer-core';
2
2
  import type { Chain, PublicClient, Address, Abi, GetContractReturnType, TransactionReceipt } from 'viem';
3
3
  import { Connection as SolanaConnection, Transaction as SolanaTransaction, TransactionInstruction, type Commitment, type SendOptions } from '@solana/web3.js';
4
4
  import type { TypedData, TypedDataDomain } from 'viem';
@@ -46,6 +46,20 @@ export { useAbstraxnWallet } from './AbstraxnProvider';
46
46
  * Hook to get wallet instance (for advanced usage)
47
47
  */
48
48
  export declare function useWallet(): import("@abstraxn/signer-core").AbstraxnWallet | null;
49
+ export interface SmartAccountOwnerOptions {
50
+ /** Override the signer address. By default, uses connected wallet address from context. */
51
+ address?: Address;
52
+ }
53
+ export interface SmartAccountOwnerResult {
54
+ smartAccountOwner: any | null;
55
+ isReady: boolean;
56
+ address: Address | null;
57
+ }
58
+ /**
59
+ * Hook to get an AA-compatible owner account for permissionless-compatible SDKs.
60
+ * This does not include bundler/paymaster integration; apps can plug in any provider.
61
+ */
62
+ export declare function useSmartAccountOwner(options?: SmartAccountOwnerOptions): SmartAccountOwnerResult;
49
63
  /**
50
64
  * Hook to export wallet private key
51
65
  * Returns the export bundle containing the private key
@@ -127,6 +141,28 @@ export declare function useDisableMfa(): {
127
141
  confirmDisable: () => Promise<DisableMfaResponse | null>;
128
142
  reset: () => void;
129
143
  };
144
+ export interface UseAuthMethodLinkingReturn {
145
+ supportedMethods: SupportedAuthMethod[];
146
+ linkedMethods: LinkedAuthMethod[];
147
+ loading: boolean;
148
+ actionProvider: string | null;
149
+ error: string | null;
150
+ refreshAuthMethods: (options?: {
151
+ force?: boolean;
152
+ }) => Promise<void>;
153
+ isLinked: (provider: string) => boolean;
154
+ linkProvider: (provider: 'google' | 'discord' | 'twitter' | 'x' | 'passkey') => Promise<void>;
155
+ requestEmailLinkOtp: (email: string) => Promise<{
156
+ otpId: string;
157
+ }>;
158
+ confirmEmailLink: (otpId: string, otpCode: string) => Promise<void>;
159
+ unlinkProvider: (provider: string) => Promise<void>;
160
+ }
161
+ /**
162
+ * Hook for linking/unlinking auth methods without any UI concerns.
163
+ * It exposes state and actions only; consumers own rendering and messaging.
164
+ */
165
+ export declare function useAuthMethodLinking(): UseAuthMethodLinkingReturn;
130
166
  /**
131
167
  * Hook to access external wallet functionality
132
168
  * Returns external wallet connection state and methods
@@ -270,7 +306,6 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
270
306
  request?: (parameters: import("viem").CcipRequestParameters) => Promise<import("viem/_types/utils/ccip").CcipRequestReturnType>;
271
307
  } | undefined;
272
308
  chain: Chain | undefined;
273
- dataSuffix?: import("viem").DataSuffix | undefined;
274
309
  experimental_blockTag?: import("viem").BlockTag | undefined;
275
310
  key: string;
276
311
  name: string;
@@ -453,7 +488,6 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
453
488
  getChainId: () => Promise<import("viem").GetChainIdReturnType>;
454
489
  getCode: (args: import("viem").GetBytecodeParameters) => Promise<import("viem").GetBytecodeReturnType>;
455
490
  getContractEvents: <const abi extends Abi | readonly unknown[], eventName extends import("viem").ContractEventName<abi> | undefined = undefined, strict extends boolean | undefined = undefined, fromBlock extends import("viem").BlockNumber | import("viem").BlockTag | undefined = undefined, toBlock extends import("viem").BlockNumber | import("viem").BlockTag | undefined = undefined>(args: import("viem").GetContractEventsParameters<abi, eventName, strict, fromBlock, toBlock>) => Promise<import("viem").GetContractEventsReturnType<abi, eventName, strict, fromBlock, toBlock>>;
456
- getDelegation: (args: import("viem").GetDelegationParameters) => Promise<import("viem").GetDelegationReturnType>;
457
491
  getEip712Domain: (args: import("viem").GetEip712DomainParameters) => Promise<import("viem").GetEip712DomainReturnType>;
458
492
  getEnsAddress: (args: import("viem").GetEnsAddressParameters) => Promise<import("viem").GetEnsAddressReturnType>;
459
493
  getEnsAvatar: (args: import("viem").GetEnsAvatarParameters) => Promise<import("viem").GetEnsAvatarReturnType>;
@@ -3879,7 +3913,6 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
3879
3913
  cacheTime?: undefined;
3880
3914
  ccipRead?: undefined;
3881
3915
  chain?: undefined;
3882
- dataSuffix?: undefined;
3883
3916
  experimental_blockTag?: undefined;
3884
3917
  key?: undefined;
3885
3918
  name?: undefined;
@@ -3929,7 +3962,6 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
3929
3962
  request?: (parameters: import("viem").CcipRequestParameters) => Promise<import("viem/_types/utils/ccip").CcipRequestReturnType>;
3930
3963
  } | undefined;
3931
3964
  chain: Chain | undefined;
3932
- dataSuffix?: import("viem").DataSuffix | undefined;
3933
3965
  experimental_blockTag?: import("viem").BlockTag | undefined;
3934
3966
  key: string;
3935
3967
  name: string;
@@ -8341,7 +8373,6 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8341
8373
  cacheTime?: undefined;
8342
8374
  ccipRead?: undefined;
8343
8375
  chain?: undefined;
8344
- dataSuffix?: undefined;
8345
8376
  experimental_blockTag?: undefined;
8346
8377
  key?: undefined;
8347
8378
  name?: undefined;
@@ -8868,7 +8899,6 @@ export declare function useExternalWalletClient(): {
8868
8899
  request?: (parameters: import("viem").CcipRequestParameters) => Promise<import("viem/_types/utils/ccip").CcipRequestReturnType>;
8869
8900
  } | undefined;
8870
8901
  chain: Chain;
8871
- dataSuffix?: import("viem").DataSuffix | undefined;
8872
8902
  experimental_blockTag?: import("viem").BlockTag | undefined;
8873
8903
  key: string;
8874
8904
  name: string;
@@ -13280,7 +13310,6 @@ export declare function useExternalWalletClient(): {
13280
13310
  cacheTime?: undefined;
13281
13311
  ccipRead?: undefined;
13282
13312
  chain?: undefined;
13283
- dataSuffix?: undefined;
13284
13313
  experimental_blockTag?: undefined;
13285
13314
  key?: undefined;
13286
13315
  name?: undefined;
package/dist/src/hooks.js CHANGED
@@ -4,7 +4,8 @@
4
4
  import { useState, useCallback, useEffect, useMemo } from 'react';
5
5
  import { useAbstraxnWallet } from './AbstraxnProvider';
6
6
  import { InvalidDeviceIdError, SignMfaRequiredError } from '@abstraxn/signer-core';
7
- import { createPublicClient, createWalletClient, http, getContract, serializeTransaction, encodeFunctionData, hashTypedData } from 'viem';
7
+ import { createPublicClient, createWalletClient, http, getContract, serializeTransaction, encodeFunctionData, hashTypedData, hashMessage, parseSignature } from 'viem';
8
+ import { toAccount } from 'viem/accounts';
8
9
  import { Connection as SolanaConnection, Transaction as SolanaTransaction, SystemProgram, PublicKey, LAMPORTS_PER_SOL, TransactionInstruction, } from '@solana/web3.js';
9
10
  import { useWalletClient as useWagmiWalletClient, useAccount, useConfig, useChainId as useWagmiChainId, useSwitchChain as useWagmiSwitchChain, useSignMessage as useWagmiSignMessage, useReconnect as useWagmiReconnect } from 'wagmi';
10
11
  import { getWalletClient, switchChain } from '@wagmi/core';
@@ -72,6 +73,67 @@ export function useWallet() {
72
73
  const { wallet } = useAbstraxnWallet();
73
74
  return wallet;
74
75
  }
76
+ /**
77
+ * Hook to get an AA-compatible owner account for permissionless-compatible SDKs.
78
+ * This does not include bundler/paymaster integration; apps can plug in any provider.
79
+ */
80
+ export function useSmartAccountOwner(options = {}) {
81
+ const { isConnected, address: connectedAddress, signTransactionViaAPI, signTypedTxViaAPI, verifySignMfa, } = useAbstraxnWallet();
82
+ const ownerAddress = (options.address ?? connectedAddress ?? null);
83
+ const smartAccountOwner = useMemo(() => {
84
+ if (!isConnected || !ownerAddress)
85
+ return null;
86
+ return toAccount({
87
+ address: ownerAddress,
88
+ /**
89
+ * ERC-4337 SimpleAccount (permissionless) signs UserOperations via viem `signMessage` with
90
+ * `message: { raw: userOpHash }`. That requires the same digest as viem local accounts:
91
+ * EIP-191 `hashMessage(message)`, then ECDSA sign that 32-byte digest — not UTF-8 text hashing.
92
+ */
93
+ async signMessage({ message }) {
94
+ const digest = hashMessage(message);
95
+ const signature = await runWithSignStepUpRetry(() => signTypedTxViaAPI(ownerAddress, digest, 'PAYLOAD_ENCODING_HEXADECIMAL', 'HASH_FUNCTION_NO_OP').then((res) => res.signature), verifySignMfa);
96
+ return (signature.startsWith('0x') ? signature : `0x${signature}`);
97
+ },
98
+ async signTransaction(transaction) {
99
+ const unsignedTransaction = serializeTransaction(transaction);
100
+ const result = await runWithSignStepUpRetry(() => signTransactionViaAPI(unsignedTransaction, ownerAddress), verifySignMfa);
101
+ return result.signedTransaction;
102
+ },
103
+ async signTypedData(typedData) {
104
+ const digest = hashTypedData(typedData);
105
+ const result = await runWithSignStepUpRetry(() => signTypedTxViaAPI(ownerAddress, digest, 'PAYLOAD_ENCODING_EIP712', 'HASH_FUNCTION_NO_OP'), verifySignMfa);
106
+ const signature = result.signature;
107
+ return (signature.startsWith('0x') ? signature : `0x${signature}`);
108
+ },
109
+ /** EIP-7702: sign delegation authorization (`keccak256(0x05 || rlp(...))`) via backend, same as raw ECDSA over digest. */
110
+ async signAuthorization(parameters) {
111
+ const authorizationAddress = parameters.contractAddress ?? parameters.address;
112
+ const authorizationPayload = JSON.stringify({
113
+ chainId: parameters.chainId,
114
+ address: authorizationAddress,
115
+ nonce: parameters.nonce,
116
+ });
117
+ const sig = await runWithSignStepUpRetry(() => signTypedTxViaAPI(ownerAddress, authorizationPayload, 'PAYLOAD_ENCODING_EIP7702_AUTHORIZATION', 'HASH_FUNCTION_NOT_APPLICABLE').then((res) => res.signature), verifySignMfa);
118
+ const hex = (sig.startsWith('0x') ? sig : `0x${sig}`);
119
+ const { r, s, yParity } = parseSignature(hex);
120
+ return {
121
+ address: authorizationAddress,
122
+ chainId: parameters.chainId,
123
+ nonce: parameters.nonce,
124
+ r,
125
+ s,
126
+ yParity,
127
+ };
128
+ },
129
+ });
130
+ }, [isConnected, ownerAddress, signTransactionViaAPI, signTypedTxViaAPI, verifySignMfa]);
131
+ return {
132
+ smartAccountOwner,
133
+ isReady: !!smartAccountOwner,
134
+ address: ownerAddress,
135
+ };
136
+ }
75
137
  /**
76
138
  * Hook to export wallet private key
77
139
  * Returns the export bundle containing the private key
@@ -414,6 +476,282 @@ export function useDisableMfa() {
414
476
  reset,
415
477
  };
416
478
  }
479
+ const AUTH_METHOD_LINK_PROVIDER_ALLOWLIST = new Set([
480
+ 'google',
481
+ 'discord',
482
+ 'x',
483
+ 'passkey',
484
+ ]);
485
+ const AUTH_METHOD_UNLINK_PROVIDER_ALLOWLIST = new Set([
486
+ 'google',
487
+ 'discord',
488
+ 'x',
489
+ 'email',
490
+ 'passkey',
491
+ ]);
492
+ function normalizeAuthMethodProviderForApi(provider) {
493
+ const normalized = provider.trim().toLowerCase();
494
+ if (normalized === 'otp')
495
+ return 'email';
496
+ return normalized;
497
+ }
498
+ function normalizeAuthMethodProviderForMatch(provider) {
499
+ const normalized = normalizeAuthMethodProviderForApi(provider);
500
+ if (normalized === 'twitter' || normalized === 'x')
501
+ return 'x';
502
+ return normalized;
503
+ }
504
+ function normalizeAuthMethodErrorMessage(message) {
505
+ return message.replace(/^network error:\s*/i, '').trim();
506
+ }
507
+ function toAuthMethodError(error, fallbackMessage) {
508
+ if (error instanceof Error) {
509
+ return new Error(normalizeAuthMethodErrorMessage(error.message || fallbackMessage));
510
+ }
511
+ if (typeof error === 'string' && error.trim() !== '') {
512
+ return new Error(normalizeAuthMethodErrorMessage(error));
513
+ }
514
+ return new Error(fallbackMessage);
515
+ }
516
+ /**
517
+ * Hook for linking/unlinking auth methods without any UI concerns.
518
+ * It exposes state and actions only; consumers own rendering and messaging.
519
+ */
520
+ export function useAuthMethodLinking() {
521
+ const { wallet, whoami, getSupportedAuthMethods, getLinkedAuthMethods, requestOtpForEmailLink, loginWithOTP, } = useAbstraxnWallet();
522
+ const [supportedMethods, setSupportedMethods] = useState([]);
523
+ const [linkedMethods, setLinkedMethods] = useState([]);
524
+ const [queryLoading, setQueryLoading] = useState(false);
525
+ const [actionProvider, setActionProvider] = useState(null);
526
+ const [error, setError] = useState(null);
527
+ const requireConnectedWallet = useCallback(() => {
528
+ if (!wallet?.isConnected) {
529
+ throw new Error('Wallet not connected');
530
+ }
531
+ return wallet;
532
+ }, [wallet]);
533
+ const refreshAuthMethods = useCallback(async (_options) => {
534
+ const activeWallet = requireConnectedWallet();
535
+ setQueryLoading(true);
536
+ setError(null);
537
+ try {
538
+ const supportedRequest = getSupportedAuthMethods != null
539
+ ? getSupportedAuthMethods()
540
+ : activeWallet.getSupportedAuthMethods();
541
+ const linkedRequest = getLinkedAuthMethods != null
542
+ ? getLinkedAuthMethods()
543
+ : activeWallet.getLinkedAuthMethods();
544
+ const [supportedResponse, linkedResponse] = await Promise.all([
545
+ supportedRequest,
546
+ linkedRequest,
547
+ ]);
548
+ const loginProvider = normalizeAuthMethodProviderForMatch(String(whoami?.loginProvider ?? ''));
549
+ const allSupportedMethods = supportedResponse.supportedMethods ?? [];
550
+ const filteredMethods = loginProvider !== ''
551
+ ? allSupportedMethods.filter((method) => normalizeAuthMethodProviderForMatch(method.provider) !== loginProvider)
552
+ : allSupportedMethods;
553
+ setSupportedMethods(filteredMethods);
554
+ setLinkedMethods(linkedResponse.linkedMethods ?? []);
555
+ }
556
+ catch (err) {
557
+ const nextError = toAuthMethodError(err, 'Failed to load auth methods');
558
+ setSupportedMethods([]);
559
+ setLinkedMethods([]);
560
+ setError(nextError.message);
561
+ throw nextError;
562
+ }
563
+ finally {
564
+ setQueryLoading(false);
565
+ }
566
+ }, [getLinkedAuthMethods, getSupportedAuthMethods, requireConnectedWallet, whoami]);
567
+ const isLinked = useCallback((provider) => {
568
+ const normalizedProvider = normalizeAuthMethodProviderForMatch(provider);
569
+ return linkedMethods.some((method) => normalizeAuthMethodProviderForMatch(method.authProvider) === normalizedProvider);
570
+ }, [linkedMethods]);
571
+ const runOAuthPopupLink = useCallback(async (activeWallet, provider) => {
572
+ if (typeof window === 'undefined') {
573
+ throw new Error('OAuth linking requires a browser environment');
574
+ }
575
+ const { authUrl } = await activeWallet.initiateLinkAuth(provider);
576
+ const width = 500;
577
+ const height = 600;
578
+ const left = Math.round((window.screen.width - width) / 2);
579
+ const top = Math.round((window.screen.height - height) / 2);
580
+ const popup = window.open(authUrl, 'abstraxn-auth-link', `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`);
581
+ if (!popup) {
582
+ throw new Error('Popup blocked. Please allow popups for this site.');
583
+ }
584
+ const timeoutMs = 120000;
585
+ await new Promise((resolve, reject) => {
586
+ let settled = false;
587
+ let closeCheckId = null;
588
+ let timeoutId = null;
589
+ const finalizeReject = (reason) => {
590
+ if (settled)
591
+ return;
592
+ settled = true;
593
+ cleanup();
594
+ reject(new Error(reason));
595
+ };
596
+ const cleanup = () => {
597
+ window.removeEventListener('message', onMessage);
598
+ if (closeCheckId != null)
599
+ clearInterval(closeCheckId);
600
+ if (timeoutId != null)
601
+ clearTimeout(timeoutId);
602
+ };
603
+ const onMessage = (event) => {
604
+ if (event.origin !== window.location.origin)
605
+ return;
606
+ if (event.source !== popup)
607
+ return;
608
+ const data = event.data;
609
+ if (!data?.type)
610
+ return;
611
+ const eventProvider = normalizeAuthMethodProviderForMatch(String(data.provider ?? ''));
612
+ const targetProvider = normalizeAuthMethodProviderForMatch(provider);
613
+ if (eventProvider !== targetProvider)
614
+ return;
615
+ if (data.type === 'abstraxn-auth-link-error') {
616
+ finalizeReject(normalizeAuthMethodErrorMessage(String(data.error ?? 'Failed to link auth method')));
617
+ return;
618
+ }
619
+ if (data.type === 'abstraxn-auth-link-done') {
620
+ if (!data.activityBody || data.activityBody.trim() === '') {
621
+ finalizeReject('Invalid linking response');
622
+ return;
623
+ }
624
+ settled = true;
625
+ cleanup();
626
+ activeWallet
627
+ .submitLinkAuthStamped(provider, data.activityBody)
628
+ .then(resolve)
629
+ .catch((submitError) => {
630
+ reject(toAuthMethodError(submitError, 'Failed to finalize auth method link'));
631
+ });
632
+ }
633
+ };
634
+ window.addEventListener('message', onMessage);
635
+ closeCheckId = setInterval(() => {
636
+ if (popup.closed) {
637
+ finalizeReject('Linking was cancelled');
638
+ }
639
+ }, 200);
640
+ timeoutId = setTimeout(() => {
641
+ finalizeReject('Linking timed out. Please try again.');
642
+ }, timeoutMs);
643
+ });
644
+ }, [wallet]);
645
+ const linkProvider = useCallback(async (provider) => {
646
+ const activeWallet = requireConnectedWallet();
647
+ const normalizedProvider = provider.toLowerCase();
648
+ if (!AUTH_METHOD_LINK_PROVIDER_ALLOWLIST.has(normalizedProvider)) {
649
+ throw new Error(`Unsupported provider for linking: ${provider}`);
650
+ }
651
+ setActionProvider(normalizedProvider);
652
+ setError(null);
653
+ try {
654
+ if (normalizedProvider === 'passkey') {
655
+ await activeWallet.linkPasskey();
656
+ }
657
+ else {
658
+ await runOAuthPopupLink(activeWallet, normalizedProvider);
659
+ }
660
+ await refreshAuthMethods({ force: true });
661
+ }
662
+ catch (err) {
663
+ const nextError = toAuthMethodError(err, 'Failed to link auth method');
664
+ setError(nextError.message);
665
+ throw nextError;
666
+ }
667
+ finally {
668
+ setActionProvider(null);
669
+ }
670
+ }, [refreshAuthMethods, requireConnectedWallet, runOAuthPopupLink]);
671
+ const requestEmailLinkOtp = useCallback(async (email) => {
672
+ const activeWallet = requireConnectedWallet();
673
+ const trimmedEmail = email.trim();
674
+ if (!trimmedEmail) {
675
+ throw new Error('Email is required');
676
+ }
677
+ setActionProvider('email');
678
+ setError(null);
679
+ try {
680
+ if (requestOtpForEmailLink) {
681
+ return await requestOtpForEmailLink(trimmedEmail);
682
+ }
683
+ if (loginWithOTP) {
684
+ return await loginWithOTP(trimmedEmail);
685
+ }
686
+ // Fallback to core method directly if context methods are unavailable.
687
+ return await activeWallet.requestOtpForEmailLink(trimmedEmail);
688
+ }
689
+ catch (err) {
690
+ const nextError = toAuthMethodError(err, 'Failed to send email verification code');
691
+ setError(nextError.message);
692
+ throw nextError;
693
+ }
694
+ finally {
695
+ setActionProvider(null);
696
+ }
697
+ }, [loginWithOTP, requestOtpForEmailLink, requireConnectedWallet]);
698
+ const confirmEmailLink = useCallback(async (otpId, otpCode) => {
699
+ const activeWallet = requireConnectedWallet();
700
+ const trimmedOtpId = otpId.trim();
701
+ const trimmedOtpCode = otpCode.trim();
702
+ if (!trimmedOtpId || !trimmedOtpCode) {
703
+ throw new Error('otpId and otpCode are required');
704
+ }
705
+ setActionProvider('email');
706
+ setError(null);
707
+ try {
708
+ await activeWallet.linkEmail(trimmedOtpId, trimmedOtpCode);
709
+ await refreshAuthMethods({ force: true });
710
+ }
711
+ catch (err) {
712
+ const nextError = toAuthMethodError(err, 'Failed to link email');
713
+ setError(nextError.message);
714
+ throw nextError;
715
+ }
716
+ finally {
717
+ setActionProvider(null);
718
+ }
719
+ }, [refreshAuthMethods, requireConnectedWallet]);
720
+ const unlinkProvider = useCallback(async (provider) => {
721
+ const activeWallet = requireConnectedWallet();
722
+ const normalizedProvider = normalizeAuthMethodProviderForApi(provider);
723
+ if (!AUTH_METHOD_UNLINK_PROVIDER_ALLOWLIST.has(normalizedProvider)) {
724
+ throw new Error(`Unsupported provider for unlinking: ${provider}`);
725
+ }
726
+ setActionProvider(normalizedProvider);
727
+ setError(null);
728
+ try {
729
+ await activeWallet.unlinkAuthMethod(normalizedProvider);
730
+ await refreshAuthMethods({ force: true });
731
+ }
732
+ catch (err) {
733
+ const nextError = toAuthMethodError(err, 'Failed to unlink auth method');
734
+ setError(nextError.message);
735
+ throw nextError;
736
+ }
737
+ finally {
738
+ setActionProvider(null);
739
+ }
740
+ }, [refreshAuthMethods, requireConnectedWallet]);
741
+ return {
742
+ supportedMethods,
743
+ linkedMethods,
744
+ loading: queryLoading || actionProvider !== null,
745
+ actionProvider,
746
+ error,
747
+ refreshAuthMethods,
748
+ isLinked,
749
+ linkProvider,
750
+ requestEmailLinkOtp,
751
+ confirmEmailLink,
752
+ unlinkProvider,
753
+ };
754
+ }
417
755
  /**
418
756
  * Hook to access external wallet functionality
419
757
  * Returns external wallet connection state and methods
@@ -4,14 +4,14 @@
4
4
  * when used in a React project
5
5
  */
6
6
  export { AbstraxnProvider, useAbstraxnWallet } from './AbstraxnProvider';
7
- export { useIsConnected, useAddress, useAuthContext, useChainId, useError, useEnableMfa, useDisableMfa, useEmailForOtp, useWallet, useExportWallet, useWhoami, useExternalWallet, useExternalWalletBalance, useExternalWalletChain, useExternalWalletInfo, usePublicClient, useWalletClient, useContract, usePrepareRawTxn, useSignTxn, useSignTypedTx, useSignAndSendTxn, useWaitForTxnReceipt, useReadContract, useExternalWalletClient, useWriteContract, useConnectionType, useSwitchChain, useSignMessage, useEstimateGas, useGetGasPrice, useSolanaConnection, useSolanaPublicKey, useSolanaBalance, usePrepareSolanaTxn, useSolanaExternalWallet, usePrepareSolanaProgramTxn, useSolanaProgramTransaction, useSignSolanaTxn, useSignAndSendSolanaTxn, useWaitForSolanaConfirmation, } from './hooks';
7
+ export { useIsConnected, useAddress, useAuthContext, useChainId, useError, useEnableMfa, useDisableMfa, useAuthMethodLinking, useEmailForOtp, useWallet, useSmartAccountOwner, useExportWallet, useWhoami, useExternalWallet, useExternalWalletBalance, useExternalWalletChain, useExternalWalletInfo, usePublicClient, useWalletClient, useContract, usePrepareRawTxn, useSignTxn, useSignTypedTx, useSignAndSendTxn, useWaitForTxnReceipt, useReadContract, useExternalWalletClient, useWriteContract, useConnectionType, useSwitchChain, useSignMessage, useEstimateGas, useGetGasPrice, useSolanaConnection, useSolanaPublicKey, useSolanaBalance, usePrepareSolanaTxn, useSolanaExternalWallet, usePrepareSolanaProgramTxn, useSolanaProgramTransaction, useSignSolanaTxn, useSignAndSendSolanaTxn, useWaitForSolanaConfirmation, } from './hooks';
8
8
  export { ConnectButton } from './ConnectButton';
9
9
  export type { ConnectButtonProps, ConnectButtonVariant, ConnectButtonSize } from './ConnectButton';
10
10
  export { WalletModal } from './WalletModal';
11
11
  export type { WalletModalProps } from './WalletModal';
12
12
  export { OnboardingUIComponent as OnboardingUI } from './OnboardingUI';
13
13
  export type { AbstraxnProviderConfig, AbstraxnUIConfig, AbstraxnContextValue, ChainConfig, } from './types';
14
- export type { PayloadEncoding, HashFunction, SignTypedTxParams, SolanaAccountMetaInput, SolanaInstructionDataInput, } from './hooks';
14
+ export type { PayloadEncoding, HashFunction, SignTypedTxParams, SmartAccountOwnerOptions, SmartAccountOwnerResult, SolanaAccountMetaInput, SolanaInstructionDataInput, } from './hooks';
15
15
  export type { OnboardingUIComponentProps, OnboardingUIComponentRef } from './OnboardingUI';
16
16
  export { ALL_CHAINS, EVM_CHAINS, SOLANA_CHAINS, getChainById, getChainsByType, getDefaultChains, toCoreChain, type ChainData, } from './chains';
17
17
  export { CONNECTORS, getConnectorMeta, isOAuthConnector, isWalletConnector, isAuthConnector, type ConnectorType, type ConnectorMeta, } from './connectors';
package/dist/src/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * when used in a React project
5
5
  */
6
6
  export { AbstraxnProvider, useAbstraxnWallet } from './AbstraxnProvider';
7
- export { useIsConnected, useAddress, useAuthContext, useChainId, useError, useEnableMfa, useDisableMfa, useEmailForOtp, useWallet, useExportWallet, useWhoami, useExternalWallet, useExternalWalletBalance, useExternalWalletChain, useExternalWalletInfo, usePublicClient, useWalletClient, useContract, usePrepareRawTxn, useSignTxn, useSignTypedTx, useSignAndSendTxn, useWaitForTxnReceipt, useReadContract, useExternalWalletClient, useWriteContract, useConnectionType, useSwitchChain, useSignMessage, useEstimateGas, useGetGasPrice, useSolanaConnection, useSolanaPublicKey, useSolanaBalance, usePrepareSolanaTxn, useSolanaExternalWallet, usePrepareSolanaProgramTxn, useSolanaProgramTransaction, useSignSolanaTxn, useSignAndSendSolanaTxn, useWaitForSolanaConfirmation, } from './hooks';
7
+ export { useIsConnected, useAddress, useAuthContext, useChainId, useError, useEnableMfa, useDisableMfa, useAuthMethodLinking, useEmailForOtp, useWallet, useSmartAccountOwner, useExportWallet, useWhoami, useExternalWallet, useExternalWalletBalance, useExternalWalletChain, useExternalWalletInfo, usePublicClient, useWalletClient, useContract, usePrepareRawTxn, useSignTxn, useSignTypedTx, useSignAndSendTxn, useWaitForTxnReceipt, useReadContract, useExternalWalletClient, useWriteContract, useConnectionType, useSwitchChain, useSignMessage, useEstimateGas, useGetGasPrice, useSolanaConnection, useSolanaPublicKey, useSolanaBalance, usePrepareSolanaTxn, useSolanaExternalWallet, usePrepareSolanaProgramTxn, useSolanaProgramTransaction, useSignSolanaTxn, useSignAndSendSolanaTxn, useWaitForSolanaConfirmation, } from './hooks';
8
8
  export { ConnectButton } from './ConnectButton';
9
9
  export { WalletModal } from './WalletModal';
10
10
  export { OnboardingUIComponent as OnboardingUI } from './OnboardingUI';
@@ -273,7 +273,7 @@ export interface AbstraxnContextValue {
273
273
  availableChains?: import("./chains").ChainData[];
274
274
  walletBalance?: bigint | null;
275
275
  /** Refetch wallet balance from RPC (Abstraxn or external). Call when UI screen changes (e.g. back button, modal open/close). */
276
- refetchBalance?: () => void;
276
+ refetchBalance?: () => Promise<void>;
277
277
  connectionType?: string | null;
278
278
  emailForOtp?: string;
279
279
  setEmailForOtp?: (email: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abstraxn/signer-react",
3
- "version": "3.1.4",
3
+ "version": "3.2.0",
4
4
  "description": "React SDK for Abstraxn Wallet - React components, hooks, and providers for seamless Web3 wallet integration",
5
5
  "main": "./dist/src/index.js",
6
6
  "module": "./dist/src/index.js",