diff --git a/apps/demo-app/src/app/direct-mode/connectors/KeplrConnector.ts b/apps/demo-app/src/app/direct-mode/connectors/KeplrConnector.ts new file mode 100644 index 00000000..f20db821 --- /dev/null +++ b/apps/demo-app/src/app/direct-mode/connectors/KeplrConnector.ts @@ -0,0 +1,95 @@ +/** + * Example Keplr connector implementation + * Demonstrates how to create a custom connector for browser wallets + * + * This is an example implementation - developers can use this as a reference + * for creating their own connectors for any wallet provider. + */ + +import { Buffer } from 'buffer'; +import type { Connector, ConnectorConnectionResult, ConnectorMetadata } from '@burnt-labs/abstraxion-core'; +import { ConnectorType } from '@burnt-labs/abstraxion-core'; +import { AUTHENTICATOR_TYPE } from '@burnt-labs/abstraxion'; + +/** + * Connector for Keplr wallet + * Example implementation showing how to integrate Cosmos wallets + */ +export class KeplrConnector implements Connector { + public metadata: ConnectorMetadata; + private wallet: any = null; + + constructor() { + this.metadata = { + id: 'keplr', + name: 'Keplr', + type: ConnectorType.COSMOS_WALLET, + icon: '🔑', + }; + } + + async isAvailable(): Promise { + if (typeof window === 'undefined') { + return false; + } + return !!(window as any).keplr; + } + + async connect(chainId: string): Promise { + if (!chainId) { + throw new Error('Chain ID is required for Keplr'); + } + + const keplr = (window as any).keplr; + if (!keplr) { + throw new Error('Keplr wallet not found'); + } + + await keplr.enable(chainId); + const offlineSigner = await keplr.getOfflineSignerAuto(chainId); + const accounts = await offlineSigner.getAccounts(); + + if (accounts.length === 0) { + throw new Error('No accounts found in Keplr'); + } + + const account = accounts[0]; + const pubkey = account.pubkey; + const pubkeyHex = Buffer.from(pubkey).toString('hex'); + const pubkeyBase64 = Buffer.from(pubkey).toString('base64'); + this.wallet = keplr; + + const signMessage = async (hexMessage: string): Promise => { + if (!this.wallet) { + throw new Error('Wallet not connected'); + } + const plainText = Buffer.from(hexMessage.replace('0x', ''), 'hex').toString('utf8'); + const signature = await this.wallet.signArbitrary(chainId, account.address, plainText); + + if (typeof signature === 'string') { + const sigBytes = Buffer.from(signature, 'base64'); + return sigBytes.toString('hex'); + } else { + const sigBytes = Buffer.from((signature as any).signature, 'base64'); + return sigBytes.toString('hex'); + } + }; + + return { + authenticator: pubkeyBase64, + displayAddress: account.address, + signMessage, + metadata: { + authenticatorType: AUTHENTICATOR_TYPE.Secp256K1, + walletName: 'keplr', + pubkey: pubkeyHex, + connectionType: 'shuttle', + }, + }; + } + + async disconnect(): Promise { + this.wallet = null; + } +} + diff --git a/apps/demo-app/src/app/direct-mode/connectors/MetaMaskConnector.ts b/apps/demo-app/src/app/direct-mode/connectors/MetaMaskConnector.ts new file mode 100644 index 00000000..755211d6 --- /dev/null +++ b/apps/demo-app/src/app/direct-mode/connectors/MetaMaskConnector.ts @@ -0,0 +1,87 @@ +/** + * Example MetaMask connector implementation + * Demonstrates how to create a custom connector for Ethereum wallets + * + * This is an example implementation - developers can use this as a reference + * for creating their own connectors for any wallet provider. + */ + +import type { Connector, ConnectorConnectionResult, ConnectorMetadata } from '@burnt-labs/abstraxion'; +import { ConnectorType } from '@burnt-labs/abstraxion'; +import { AUTHENTICATOR_TYPE } from '@burnt-labs/abstraxion'; + +/** + * Connector for MetaMask wallet + * Example implementation showing how to integrate Ethereum wallets + */ +export class MetaMaskConnector implements Connector { + public metadata: ConnectorMetadata; + private ethereumProvider: any = null; + private ethereumAddress: string | null = null; + + constructor() { + this.metadata = { + id: 'metamask', + name: 'MetaMask', + type: ConnectorType.ETHEREUM_WALLET, + icon: '🦊', + }; + } + + async isAvailable(): Promise { + if (typeof window === 'undefined') { + return false; + } + return !!(window as any).ethereum; + } + + async connect(chainId?: string): Promise { + const ethereum = (window as any).ethereum; + + if (!ethereum) { + throw new Error('MetaMask wallet not found'); + } + + try { + const accounts = await ethereum.request({ method: 'eth_requestAccounts' }); + + if (!accounts || accounts.length === 0) { + throw new Error('No accounts found in MetaMask'); + } + + const address = accounts[0]; + this.ethereumProvider = ethereum; + this.ethereumAddress = address; + + const signMessage = async (hexMessage: string): Promise => { + if (!this.ethereumProvider || !this.ethereumAddress) { + throw new Error('Wallet not connected'); + } + const signature = await this.ethereumProvider.request({ + method: 'personal_sign', + params: [hexMessage, this.ethereumAddress], + }); + return signature.replace(/^0x/, ''); + }; + + return { + authenticator: address.toLowerCase(), + displayAddress: address, + signMessage, + metadata: { + authenticatorType: AUTHENTICATOR_TYPE.EthWallet, + ethereumAddress: address, + connectionType: 'metamask', + }, + }; + } catch (error: any) { + throw new Error(`Failed to connect to MetaMask: ${error.message || error}`); + } + } + + async disconnect(): Promise { + this.ethereumProvider = null; + this.ethereumAddress = null; + } +} + diff --git a/apps/demo-app/src/app/direct-mode/connectors/index.ts b/apps/demo-app/src/app/direct-mode/connectors/index.ts new file mode 100644 index 00000000..b76d3fa7 --- /dev/null +++ b/apps/demo-app/src/app/direct-mode/connectors/index.ts @@ -0,0 +1,8 @@ +/** + * Example connectors for direct mode + * These demonstrate how developers can implement their own connectors + */ + +export { KeplrConnector } from './KeplrConnector'; +export { MetaMaskConnector } from './MetaMaskConnector'; + diff --git a/apps/demo-app/src/app/direct-mode/layout.tsx b/apps/demo-app/src/app/direct-mode/layout.tsx new file mode 100644 index 00000000..18687502 --- /dev/null +++ b/apps/demo-app/src/app/direct-mode/layout.tsx @@ -0,0 +1,165 @@ +"use client"; +import { useMemo, createContext, useContext } from "react"; +import { + AbstraxionProvider, + type AbstraxionConfig, + type SignerConfig, + useConnectorSelection, +} from "@burnt-labs/abstraxion"; +import { WalletModal } from "../../components/WalletModal"; +import { KeplrConnector, MetaMaskConnector } from "./connectors"; + +// Context to share setShowModal between layout and page +interface DirectModeContextType { + setShowModal: (show: boolean) => void; +} + +const DirectModeContext = createContext(null); + +export function useDirectMode() { + const context = useContext(DirectModeContext); + if (!context) { + throw new Error("useDirectMode must be used within DirectModeLayout"); + } + return context; +} + +/** + * Direct Mode Content - manages connectors and modal state + */ +function DirectModeContent({ children }: { children: React.ReactNode }) { + // Create connector instances + const connectors = useMemo(() => [ + new KeplrConnector(), + new MetaMaskConnector(), + ], []); + + // Use connector selection hook - single source of truth for modal state + const connectorSelection = useConnectorSelection({ + connectors, + aaApiUrl: process.env.NEXT_PUBLIC_AA_API_URL, + }); + + return ( + + {children} + {/* Wallet modal - receives all hook results as props */} + + + ); +} + +export default function DirectModeLayout({ + children, +}: { + children: React.ReactNode; +}): JSX.Element { + // Build indexer config (supports both Numia and Subquery) + const indexerConfig = useMemo(() => { + if (!process.env.NEXT_PUBLIC_INDEXER_URL) return undefined; + + // If type is explicitly set to subquery, use Subquery + if (process.env.NEXT_PUBLIC_INDEXER_TYPE === 'subquery') { + if (!process.env.NEXT_PUBLIC_CODE_ID) { + throw new Error('NEXT_PUBLIC_CODE_ID is required when using Subquery indexer'); + } + return { + type: 'subquery' as const, + url: process.env.NEXT_PUBLIC_INDEXER_URL, + codeId: parseInt(process.env.NEXT_PUBLIC_CODE_ID), + }; + } + + // Otherwise, use Numia (default) + if (process.env.NEXT_PUBLIC_INDEXER_TOKEN) { + return { + type: 'numia' as const, + url: process.env.NEXT_PUBLIC_INDEXER_URL, + authToken: process.env.NEXT_PUBLIC_INDEXER_TOKEN, + }; + } + + return undefined; + }, []); + + // Smart account contract configuration (required for signer mode) + const smartAccountContractConfig = useMemo(() => { + if (!process.env.NEXT_PUBLIC_CODE_ID || !process.env.NEXT_PUBLIC_CHECKSUM) { + throw new Error('Smart account contract config is required for direct mode. Please provide NEXT_PUBLIC_CODE_ID and NEXT_PUBLIC_CHECKSUM.'); + } + return { + codeId: parseInt(process.env.NEXT_PUBLIC_CODE_ID), + checksum: process.env.NEXT_PUBLIC_CHECKSUM, + addressPrefix: process.env.NEXT_PUBLIC_ADDRESS_PREFIX || 'xion', + }; + }, []); + + // Configuration for AbstraxionProvider + // Uses signer mode with connectors - the useConnectorSelection hook handles the actual connector connection + const directModeConfig: AbstraxionConfig = useMemo(() => ({ + // REQUIRED: Chain ID + chainId: process.env.NEXT_PUBLIC_CHAIN_ID || "xion-testnet-2", + + // REQUIRED: RPC URL for blockchain connection + rpcUrl: process.env.NEXT_PUBLIC_RPC_URL!, + + // REQUIRED: REST API endpoint + restUrl: process.env.NEXT_PUBLIC_REST_URL!, + + // REQUIRED: Gas price + gasPrice: process.env.NEXT_PUBLIC_GAS_PRICE || "0.001uxion", + + // Treasury contract address (optional - for dynamic grant configs) + treasury: process.env.NEXT_PUBLIC_TREASURY_ADDRESS, + + // Fee granter address (required for grant creation and smart account creation) + feeGranter: process.env.NEXT_PUBLIC_FEE_GRANTER_ADDRESS, + + // Signer-mode configuration + authentication: { + type: "signer" as const, + + // AA API URL for account creation + aaApiUrl: process.env.NEXT_PUBLIC_AA_API_URL!, + + // Function that returns signer configuration + // Note: This is required by the type but useConnectorSelection bypasses it + // by using orchestrator directly. This function should not be called when using connectors. + getSignerConfig: async (): Promise => { + throw new Error( + 'getSignerConfig should not be called when using connectors via useConnectorSelection. ' + + 'The connection flow is handled by the useConnectorSelection hook.' + ); + }, + + // Auto-connect behavior + autoConnect: false, // Manual login - wait for user to click connect + + // Smart account contract configuration (codeId, checksum, addressPrefix) + smartAccountContract: smartAccountContractConfig, + + // Indexer configuration for account discovery (optional - falls back to RPC if not provided) + indexer: indexerConfig, + + // Treasury indexer configuration - for fetching grant configs from DaoDao indexer (fast) + // Optional - falls back to direct RPC queries if not provided + treasuryIndexer: process.env.NEXT_PUBLIC_TREASURY_INDEXER_URL ? { + url: process.env.NEXT_PUBLIC_TREASURY_INDEXER_URL, + } : undefined, + }, + }), [indexerConfig, smartAccountContractConfig]); + + return ( + + {children} + + ); +} diff --git a/apps/demo-app/src/app/direct-mode/page.tsx b/apps/demo-app/src/app/direct-mode/page.tsx new file mode 100644 index 00000000..754ac47f --- /dev/null +++ b/apps/demo-app/src/app/direct-mode/page.tsx @@ -0,0 +1,133 @@ +"use client"; +import { + useAbstraxionAccount, + useAbstraxionSigningClient, +} from "@burnt-labs/abstraxion"; +import { Button } from "@burnt-labs/ui"; +import "@burnt-labs/ui/dist/index.css"; +import "@burnt-labs/abstraxion/dist/index.css"; +import Link from "next/link"; +import { SendTokens } from "@/components/SendTokens"; +import { useDirectMode } from "./layout"; + +export default function DirectModePage(): JSX.Element { + // Get setShowModal from the layout context + const { setShowModal } = useDirectMode(); + + const { + data: account, + logout, + isConnecting, + isInitializing, + isLoading + } = useAbstraxionAccount(); + const { client } = useAbstraxionSigningClient(); + + // Show modal when connect button is clicked + // The useConnectorSelection hook handles the actual connection via orchestrator + const handleLogin = () => { + setShowModal(true); + }; + + return ( +
+ {/* Initialization Loading Overlay */} + {isInitializing && ( +
+
+
+
+
+
+
+

Initializing

+

+ Checking for existing session... +

+
+
+ )} + + {/* Wallet Connection Loading Overlay */} + {isConnecting && !isInitializing && ( +
+
+
+
+
+
+
+

Connecting Wallet

+

+ Please approve the connection in your wallet +

+
+
+ )} + +

+ Direct Mode Abstraxion Example +

+

+ This example uses custom connectors with the useConnectorSelection hook + which allows in-app wallet connections without redirecting to the dashboard. + Connect with MetaMask, Keplr, Leap, or OKX directly in this app. +

+ +
+ {!account.bech32Address && ( + + )} + + {account.bech32Address && ( + <> +
+

+ ✓ Successfully connected using direct mode! No dashboard redirect needed. +

+
+ + + + + + )} +
+ + + ← Back to examples + +
+ ); +} diff --git a/apps/demo-app/src/components/WalletModal.tsx b/apps/demo-app/src/components/WalletModal.tsx new file mode 100644 index 00000000..41722ee0 --- /dev/null +++ b/apps/demo-app/src/components/WalletModal.tsx @@ -0,0 +1,192 @@ +"use client"; + +import type { Connector } from "@burnt-labs/abstraxion"; +import { KeplrLogo } from "./icons/KeplrLogo"; +import { OKXLogo } from "./icons/OKXLogo"; +import { MetamaskLogo } from "./icons/MetamaskLogo"; +import { AbstraxionContext, type AbstraxionContextProps } from "@burnt-labs/abstraxion"; +import { useContext } from "react"; + +interface WalletModalProps { + connectors: Connector[]; + showModal: boolean; + setShowModal: (show: boolean) => void; + availableConnectors: Connector[]; + connect: (connector: Connector) => Promise; + error: string | null; + isConnecting: boolean; +} + +/** + * Wallet selection modal component. + * Displays available wallets and handles connection flow. + */ +export function WalletModal({ + connectors, + showModal, + setShowModal, + availableConnectors, + connect, + error, + isConnecting, +}: WalletModalProps) { + // Get config from context + const context = useContext(AbstraxionContext) as AbstraxionContextProps; + const { isConnected } = context; + + // Show modal when login is called and not connected + const shouldShowModal = showModal || (isConnecting && !isConnected); + + // Don't render if modal shouldn't be shown AND we're not in the middle of connecting + if (!shouldShowModal && !isConnecting) { + return null; + } + + // If no available connectors but we have connectors, show all connectors (user can install wallet) + // Otherwise, only show available connectors + const connectorsToShow = availableConnectors.length > 0 + ? availableConnectors + : connectors; // Fallback to all connectors if none are available yet + + // Map wallet IDs to logo components + const getWalletLogo = (walletId: string) => { + switch (walletId) { + case "metamask": + return ; + case "keplr": + return ; + case "okx": + return ; + default: + return null; + } + }; + + // Map connectors to display format + const walletItems = connectorsToShow.map((connector: Connector) => ({ + id: connector.metadata.id, + name: connector.metadata.name, + connector, + isAvailable: availableConnectors.some(ac => ac.metadata.id === connector.metadata.id), + logo: getWalletLogo(connector.metadata.id), + })); + + return ( +
+ {/* Backdrop - click to close (disabled when connecting) */} +
!isConnecting && setShowModal(false)} + /> + + {/* Modal Content */} +
+
+ {/* Loading Overlay */} + {isConnecting && ( +
+
+
+
+
+
+
+

Connecting Wallet

+

+ Please approve the connection and sign the grant creation transaction in your wallet +

+
+
+ )} + + {/* Header with Close Button */} +
+
+

+ Connect Wallet +

+

+ Choose a wallet to create or access your smart account +

+
+ +
+ + {/* Error Display */} + {error && ( +
+

{error}

+
+ )} + + {/* Wallet Buttons */} +
+ {walletItems.map((wallet) => ( + { + if (wallet.isAvailable) { + connect(wallet.connector); + } else { + // Show message to install wallet + alert(`${wallet.name} is not installed. Please install ${wallet.name} to continue.`); + } + }} + disabled={isConnecting || !wallet.isAvailable} + isAvailable={wallet.isAvailable} + /> + ))} +
+ + {/* Footer */} +
+

+ By connecting, you agree to the Terms of Service +

+
+
+
+
+ ); +} + +interface WalletButtonProps { + icon: React.ReactNode; + name: string; + onClick: () => void; + disabled: boolean; + isAvailable?: boolean; +} + +function WalletButton({ icon, name, onClick, disabled, isAvailable = true }: WalletButtonProps) { + return ( + + ); +} diff --git a/packages/abstraxion/src/hooks/useConnectorSelection.ts b/packages/abstraxion/src/hooks/useConnectorSelection.ts new file mode 100644 index 00000000..def22432 --- /dev/null +++ b/packages/abstraxion/src/hooks/useConnectorSelection.ts @@ -0,0 +1,303 @@ +/** + * Hook for connector selection with modal UI + * Works with custom connectors and connects to Abstraxion's state machine + */ + +import { useContext, useCallback, useState, useEffect,useRef } from "react"; +import { AbstraxionContext } from "../components/AbstraxionContext"; +import type { Connector } from "@burnt-labs/abstraxion-core"; +import { ConnectorRegistry } from "@burnt-labs/abstraxion-core"; +import { ConnectionOrchestrator } from "@burnt-labs/account-management"; +import { createCompositeAccountStrategy } from "@burnt-labs/account-management"; +import type { SessionManager } from "@burnt-labs/account-management"; +import { AbstraxionAuth } from "@burnt-labs/abstraxion-core"; +import { BrowserStorageStrategy, BrowserRedirectStrategy } from "../strategies"; + +export interface UseConnectorSelectionOptions { + /** Array of connector instances to choose from */ + connectors: Connector[]; + + /** AA API URL (required for account creation, optional - can come from env) */ + aaApiUrl?: string; +} + +export interface UseConnectorSelectionReturn { + /** Available connectors (checked via isAvailable()) */ + availableConnectors: Connector[]; + + /** Whether to show the connector selection modal */ + showModal: boolean; + + /** Set whether to show the modal */ + setShowModal: (show: boolean) => void; + + /** Connect to a specific connector */ + connect: (connector: Connector) => Promise; + + /** Connection error, if any */ + error: string | null; + + /** Whether a connection is in progress (from AbstraxionContext) */ + isConnecting: boolean; + + /** Whether initialization is in progress (from AbstraxionContext) */ + isInitializing: boolean; + + /** Whether connected (from AbstraxionContext) */ + isConnected: boolean; +} + +/** + * Hook for connector selection with modal UI + * Integrates with Abstraxion's state machine for loading states + * Reads all configuration from AbstraxionContext - only requires connectors + * + */ +export function useConnectorSelection( + options: UseConnectorSelectionOptions +): UseConnectorSelectionReturn { + const { connectors, aaApiUrl } = options; + + // Get all config and state from AbstraxionContext + const context = useContext(AbstraxionContext); + const { + chainId, + rpcUrl, + gasPrice, + treasury, + feeGranter, + contracts, + stake, + bank, + indexerUrl, + indexerAuthToken, + treasuryIndexerUrl, + authMode, + isConnecting, + isInitializing, + isConnected, + } = context; + + // Get account creation config from signer authentication if in signer mode + const smartAccountContract = authMode === 'signer' && context.authentication?.type === 'signer' + ? context.authentication.smartAccountContract + : undefined; + + // Get indexer config from signer authentication to determine type + const indexerConfig = authMode === 'signer' && context.authentication?.type === 'signer' + ? context.authentication.indexer + : undefined; + + // Local state for error, available connectors, and modal + const [error, setError] = useState(null); + const [availableConnectors, setAvailableConnectors] = useState([]); + const [showModal, setShowModal] = useState(false); + + // Use refs to persist orchestrator and connectorRegistry across renders + // Similar to how AbstraxionContext uses refs for the controller + const connectorRegistryRef = useRef(null); + const orchestratorRef = useRef(null); + const configRef = useRef<{ + chainId: string; + rpcUrl: string; + gasPrice: string; + treasury?: string; + feeGranter?: string; + contracts?: any[]; + stake?: boolean; + bank?: any[]; + indexerUrl?: string; + indexerAuthToken?: string; + treasuryIndexerUrl?: string; + smartAccountContract?: any; + aaApiUrl?: string; + indexerConfig?: any; + connectorIds?: string[]; + } | null>(null); + + // Create or update connector registry when connectors change + if (!connectorRegistryRef.current) { + connectorRegistryRef.current = new ConnectorRegistry(); + connectorRegistryRef.current.registerAll(connectors); + } else { + // Update registry if connectors changed + const currentConnectorIds = new Set(connectors.map((c: Connector) => c.metadata.id)); + const registeredIds = new Set(Array.from(connectorRegistryRef.current.getAll().map((c: Connector) => c.metadata.id))); + + // Check if connectors have changed + const connectorsChanged = + connectors.length !== registeredIds.size || + connectors.some(c => !registeredIds.has(c.metadata.id)) || + Array.from(registeredIds).some(id => !currentConnectorIds.has(id)); + + if (connectorsChanged) { + connectorRegistryRef.current.clear(); + connectorRegistryRef.current.registerAll(connectors); + } + } + + const connectorRegistry = connectorRegistryRef.current; + + // Create or update orchestrator when config changes + const currentConfig = { + chainId, + rpcUrl, + gasPrice, + treasury, + feeGranter, + contracts, + stake, + bank, + indexerUrl, + indexerAuthToken, + treasuryIndexerUrl, + smartAccountContract, + aaApiUrl, + indexerConfig, + // Note: connectors are handled separately via connectorRegistry + // so we compare connector IDs instead of the instances themselves + connectorIds: connectors.map(c => c.metadata.id).sort(), + }; + + // Check if config has changed + const configChanged = !configRef.current || + JSON.stringify(configRef.current) !== JSON.stringify(currentConfig); + + if (!orchestratorRef.current || configChanged) { + const storageStrategy = new BrowserStorageStrategy(); + const redirectStrategy = new BrowserRedirectStrategy(); + const abstraxionAuth = new AbstraxionAuth(storageStrategy, redirectStrategy); + + // Configure AbstraxionAuth instance (must be done before casting to SessionManager) + abstraxionAuth.configureAbstraxionInstance( + rpcUrl, + contracts, + stake, + bank, + undefined, // callbackUrl + treasury, + indexerUrl, + indexerAuthToken, + treasuryIndexerUrl, + ); + + // AbstraxionAuth implements SessionManager interface but setGranter is private + // Type assertion is safe here as all required methods exist + const sessionManager = abstraxionAuth as unknown as SessionManager; + + // Determine indexer type from config if available, otherwise auto-detect + // Respect the type specified in the config (numia or subquery) + const indexerType = indexerConfig?.type || (smartAccountContract?.codeId && indexerUrl ? 'subquery' : 'numia'); + + // Create account strategy + const accountStrategy = createCompositeAccountStrategy({ + indexer: indexerUrl ? (indexerType === 'subquery' && smartAccountContract?.codeId + ? { + type: 'subquery' as const, + url: indexerUrl, + codeId: smartAccountContract.codeId, + } + : { + type: 'numia' as const, + url: indexerUrl, + authToken: indexerAuthToken, + }) : undefined, + rpc: smartAccountContract && feeGranter ? { + rpcUrl, + checksum: smartAccountContract.checksum, + creator: feeGranter, // Use top-level feeGranter from context + prefix: smartAccountContract.addressPrefix, + codeId: smartAccountContract.codeId, + } : undefined, + }); + + // Create account creation config if needed + const accountCreationConfigForOrchestrator = aaApiUrl && smartAccountContract && feeGranter ? { + aaApiUrl, + smartAccountContract: { + codeId: smartAccountContract.codeId, + checksum: smartAccountContract.checksum, + addressPrefix: smartAccountContract.addressPrefix, + }, + feeGranter: feeGranter, // Use top-level feeGranter from context + } : undefined; + + // Create grant config if any grant-related config is present + const grantConfig = treasury || contracts || bank || stake ? { + treasury, + contracts, + bank, + stake, + feeGranter, + daodaoIndexerUrl: treasuryIndexerUrl, + } : undefined; + + orchestratorRef.current = new ConnectionOrchestrator({ + sessionManager, + storageStrategy, + accountStrategy, + grantConfig, + accountCreationConfig: accountCreationConfigForOrchestrator, + chainId, + rpcUrl, + gasPrice, + }); + + // Update config ref for next comparison + configRef.current = currentConfig; + } + + const orchestrator = orchestratorRef.current; + + // Check available connectors on mount and when connectors change + // Uses ConnectorRegistry.getAvailable() - same pattern as DirectController + useEffect(() => { + if (!connectorRegistry) return; + + const checkAvailable = async () => { + try { + const available = await connectorRegistry.getAvailable(); + setAvailableConnectors(available); + } catch (err) { + console.error('[useConnectorSelection] Failed to check available connectors:', err); + setAvailableConnectors([]); + } + }; + + checkAvailable(); + }, [connectorRegistry, connectors]); + + // Connect to a specific connector + const connect = useCallback(async (connector: Connector) => { + setError(null); + setShowModal(false); + + try { + // Use orchestrator to connect and setup + await orchestrator.connectAndSetup(connector); + + // Connection successful - reload page to sync state with AbstraxionContext + // The orchestrator stores the session, so AbstraxionContext will restore it on reload + if (typeof window !== 'undefined') { + window.location.reload(); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Connection failed'; + setError(errorMessage); + console.error('[useConnectorSelection] Connection error:', err); + throw err; + } + }, [orchestrator]); + + return { + availableConnectors, + showModal, + setShowModal, + connect, + error, + isConnecting, + isInitializing, + isConnected, + }; +} +