UNPKG

@reown/appkit-controllers

Version:

The full stack toolkit to build onchain app UX.

451 lines • 18.4 kB
/* eslint-disable no-console */ import { proxy, ref, subscribe as sub } from 'valtio/vanilla'; import { subscribeKey as subKey } from 'valtio/vanilla/utils'; import { ConstantsUtil as CommonConstantsUtil, ParseUtil } from '@reown/appkit-common'; import { getPreferredAccountType } from '../utils/ChainControllerUtil.js'; import { ConnectionControllerUtil } from '../utils/ConnectionControllerUtil.js'; import { ConnectorControllerUtil } from '../utils/ConnectorControllerUtil.js'; import { CoreHelperUtil } from '../utils/CoreHelperUtil.js'; import { StorageUtil } from '../utils/StorageUtil.js'; import { AppKitError, withErrorBoundary } from '../utils/withErrorBoundary.js'; import { ChainController } from './ChainController.js'; import { ConnectorController } from './ConnectorController.js'; import { EventsController } from './EventsController.js'; import { ModalController } from './ModalController.js'; import { PublicStateController } from './PublicStateController.js'; import { RouterController } from './RouterController.js'; import { TransactionsController } from './TransactionsController.js'; // -- State --------------------------------------------- // const state = proxy({ connections: new Map(), recentConnections: new Map(), isSwitchingConnection: false, wcError: false, wcFetchingUri: false, buffering: false, status: 'disconnected' }); // eslint-disable-next-line init-declarations let wcConnectionPromise; // -- Controller ---------------------------------------- // const controller = { state, subscribe(callback) { return sub(state, () => callback(state)); }, subscribeKey(key, callback) { return subKey(state, key, callback); }, _getClient() { return state._client; }, setClient(client) { state._client = ref(client); }, initialize(adapters) { const namespaces = adapters .filter((a) => Boolean(a.namespace)) .map(a => a.namespace); ConnectionController.syncStorageConnections(namespaces); }, syncStorageConnections(namespaces) { const storageConnections = StorageUtil.getConnections(); const namespacesToSync = namespaces ?? Array.from(ChainController.state.chains.keys()); for (const namespace of namespacesToSync) { const storageConnectionsByNamespace = storageConnections[namespace] ?? []; const recentConnectionsMap = new Map(state.recentConnections); recentConnectionsMap.set(namespace, storageConnectionsByNamespace); state.recentConnections = recentConnectionsMap; } }, getConnections(namespace) { return namespace ? (state.connections.get(namespace) ?? []) : []; }, hasAnyConnection(connectorId) { const connections = ConnectionController.state.connections; return Array.from(connections.values()) .flatMap(_connections => _connections) .some(({ connectorId: _connectorId }) => _connectorId === connectorId); }, async connectWalletConnect({ cache = 'auto' } = {}) { state.wcFetchingUri = true; const isInTelegramOrSafariIos = CoreHelperUtil.isTelegram() || (CoreHelperUtil.isSafari() && CoreHelperUtil.isIos()); try { if (cache === 'always' || (cache === 'auto' && isInTelegramOrSafariIos)) { if (wcConnectionPromise) { await wcConnectionPromise; wcConnectionPromise = undefined; return; } if (!CoreHelperUtil.isPairingExpired(state?.wcPairingExpiry)) { const link = state.wcUri; state.wcUri = link; return; } wcConnectionPromise = ConnectionController._getClient()?.connectWalletConnect?.(); ConnectionController.state.status = 'connecting'; await wcConnectionPromise; wcConnectionPromise = undefined; state.wcPairingExpiry = undefined; ConnectionController.state.status = 'connected'; } else { await ConnectionController._getClient()?.connectWalletConnect?.(); } } catch (error) { state.wcError = true; state.wcFetchingUri = false; state.status = 'disconnected'; wcConnectionPromise = undefined; throw error; } }, async connectExternal(options, chain, setChain = true) { const connectData = await ConnectionController._getClient()?.connectExternal?.(options); if (setChain) { ChainController.setActiveNamespace(chain); } const connector = ConnectorController.state.allConnectors.find(c => c.id === options?.id); const connectSuccessEventMethod = options.type === 'AUTH' ? 'email' : 'browser'; EventsController.sendEvent({ type: 'track', event: 'CONNECT_SUCCESS', properties: { method: connectSuccessEventMethod, name: connector?.name || 'Unknown', view: RouterController.state.view, walletRank: connector?.explorerWallet?.order } }); return connectData; }, async reconnectExternal(options) { await ConnectionController._getClient()?.reconnectExternal?.(options); const namespace = options.chain || ChainController.state.activeChain; if (namespace) { ConnectorController.setConnectorId(options.id, namespace); } }, async setPreferredAccountType(accountType, namespace) { if (!namespace) { return; } ModalController.setLoading(true, ChainController.state.activeChain); const authConnector = ConnectorController.getAuthConnector(); if (!authConnector) { return; } ChainController.setAccountProp('preferredAccountType', accountType, namespace); await authConnector.provider.setPreferredAccount(accountType); StorageUtil.setPreferredAccountTypes(Object.entries(ChainController.state.chains).reduce((acc, [key, _]) => { const namespace = key; const accountType = getPreferredAccountType(namespace); if (accountType !== undefined) { ; acc[namespace] = accountType; } return acc; }, {})); await ConnectionController.reconnectExternal(authConnector); ModalController.setLoading(false, ChainController.state.activeChain); EventsController.sendEvent({ type: 'track', event: 'SET_PREFERRED_ACCOUNT_TYPE', properties: { accountType, network: ChainController.state.activeCaipNetwork?.caipNetworkId || '' } }); }, async signMessage(message) { return ConnectionController._getClient()?.signMessage(message); }, parseUnits(value, decimals) { return ConnectionController._getClient()?.parseUnits(value, decimals); }, formatUnits(value, decimals) { return ConnectionController._getClient()?.formatUnits(value, decimals); }, updateBalance(namespace) { return ConnectionController._getClient()?.updateBalance(namespace); }, async sendTransaction(args) { return ConnectionController._getClient()?.sendTransaction(args); }, async getCapabilities(params) { return ConnectionController._getClient()?.getCapabilities(params); }, async grantPermissions(params) { return ConnectionController._getClient()?.grantPermissions(params); }, async walletGetAssets(params) { return ConnectionController._getClient()?.walletGetAssets(params) ?? {}; }, async estimateGas(args) { return ConnectionController._getClient()?.estimateGas(args); }, async writeContract(args) { return ConnectionController._getClient()?.writeContract(args); }, async writeSolanaTransaction(args) { return ConnectionController._getClient()?.writeSolanaTransaction(args); }, async getEnsAddress(value) { return ConnectionController._getClient()?.getEnsAddress(value); }, async getEnsAvatar(value) { return ConnectionController._getClient()?.getEnsAvatar(value); }, checkInstalled(ids) { return ConnectionController._getClient()?.checkInstalled?.(ids) || false; }, resetWcConnection() { state.wcUri = undefined; state.wcPairingExpiry = undefined; state.wcLinking = undefined; state.recentWallet = undefined; state.wcFetchingUri = false; state.status = 'disconnected'; TransactionsController.resetTransactions(); StorageUtil.deleteWalletConnectDeepLink(); StorageUtil.deleteRecentWallet(); PublicStateController.set({ connectingWallet: undefined }); }, resetUri() { state.wcUri = undefined; state.wcPairingExpiry = undefined; wcConnectionPromise = undefined; state.wcFetchingUri = false; PublicStateController.set({ connectingWallet: undefined }); }, finalizeWcConnection(address) { const { wcLinking, recentWallet } = ConnectionController.state; if (wcLinking) { StorageUtil.setWalletConnectDeepLink(wcLinking); } if (recentWallet) { StorageUtil.setAppKitRecent(recentWallet); } if (address) { EventsController.sendEvent({ type: 'track', event: 'CONNECT_SUCCESS', address, properties: { method: wcLinking ? 'mobile' : 'qrcode', name: RouterController.state.data?.wallet?.name || 'Unknown', view: RouterController.state.view, walletRank: recentWallet?.order } }); } }, setWcBasic(wcBasic) { state.wcBasic = wcBasic; }, setUri(uri) { state.wcUri = uri; state.wcFetchingUri = false; state.wcPairingExpiry = CoreHelperUtil.getPairingExpiry(); }, setWcLinking(wcLinking) { state.wcLinking = wcLinking; }, setWcError(wcError) { state.wcError = wcError; state.wcFetchingUri = false; state.buffering = false; }, setRecentWallet(wallet) { state.recentWallet = wallet; }, setBuffering(buffering) { state.buffering = buffering; }, setStatus(status) { state.status = status; }, setIsSwitchingConnection(isSwitchingConnection) { state.isSwitchingConnection = isSwitchingConnection; }, async disconnect({ id, namespace, initialDisconnect } = {}) { try { await ConnectionController._getClient()?.disconnect({ id, chainNamespace: namespace, initialDisconnect }); } catch (error) { throw new AppKitError('Failed to disconnect', 'INTERNAL_SDK_ERROR', error); } }, async disconnectConnector({ id, namespace }) { try { await ConnectionController._getClient()?.disconnectConnector({ id, namespace }); } catch (error) { throw new AppKitError('Failed to disconnect connector', 'INTERNAL_SDK_ERROR', error); } }, setConnections(connections, chainNamespace) { const connectionsMap = new Map(state.connections); connectionsMap.set(chainNamespace, connections); state.connections = connectionsMap; }, async handleAuthAccountSwitch({ address, namespace }) { const accountData = ChainController.getAccountData(namespace); const smartAccount = accountData?.user?.accounts?.find(c => c.type === 'smartAccount'); const accountType = smartAccount && smartAccount.address.toLowerCase() === address.toLowerCase() && ConnectorControllerUtil.canSwitchToSmartAccount(namespace) ? 'smartAccount' : 'eoa'; await ConnectionController.setPreferredAccountType(accountType, namespace); }, async handleActiveConnection({ connection, namespace, address }) { const connector = ConnectorController.getConnectorById(connection.connectorId); const isAuthConnector = connection.connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH; if (!connector) { throw new Error(`No connector found for connection: ${connection.connectorId}`); } if (!isAuthConnector) { const connectData = await ConnectionController.connectExternal({ id: connector.id, type: connector.type, provider: connector.provider, address, chain: namespace }, namespace); return connectData?.address; } else if (address) { await ConnectionController.handleAuthAccountSwitch({ address, namespace }); } return address; }, async handleDisconnectedConnection({ connection, namespace, address, closeModalOnConnect }) { const connector = ConnectorController.getConnectorById(connection.connectorId); const authName = connection.auth?.name?.toLowerCase(); const isAuthConnector = connection.connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH; const isWCConnector = connection.connectorId === CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT; if (!connector) { throw new Error(`No connector found for connection: ${connection.connectorId}`); } let newAddress = undefined; if (isAuthConnector) { if (authName && ConnectorControllerUtil.isSocialProvider(authName)) { const { address: socialAddress } = await ConnectorControllerUtil.connectSocial({ social: authName, closeModalOnConnect, onOpenFarcaster() { ModalController.open({ view: 'ConnectingFarcaster' }); }, onConnect() { RouterController.replace('ProfileWallets'); } }); newAddress = socialAddress; } else { const { address: emailAddress } = await ConnectorControllerUtil.connectEmail({ closeModalOnConnect, onOpen() { ModalController.open({ view: 'EmailLogin' }); }, onConnect() { RouterController.replace('ProfileWallets'); } }); newAddress = emailAddress; } } else if (isWCConnector) { const { address: wcAddress } = await ConnectorControllerUtil.connectWalletConnect({ walletConnect: true, connector, closeModalOnConnect, onOpen(isMobile) { const view = isMobile ? 'AllWallets' : 'ConnectingWalletConnect'; if (ModalController.state.open) { RouterController.push(view); } else { ModalController.open({ view }); } }, onConnect() { RouterController.replace('ProfileWallets'); } }); newAddress = wcAddress; } else { const connectData = await ConnectionController.connectExternal({ id: connector.id, type: connector.type, provider: connector.provider, chain: namespace }, namespace); if (connectData) { newAddress = connectData.address; } } if (isAuthConnector && address) { await ConnectionController.handleAuthAccountSwitch({ address, namespace }); } return newAddress; }, async switchConnection({ connection, address, namespace, closeModalOnConnect, onChange }) { let currentAddress = undefined; const caipAddress = ChainController.getAccountData(namespace)?.caipAddress; if (caipAddress) { const { address: currentAddressParsed } = ParseUtil.parseCaipAddress(caipAddress); currentAddress = currentAddressParsed; } const status = ConnectionControllerUtil.getConnectionStatus(connection, namespace); switch (status) { case 'connected': case 'active': { const newAddress = await ConnectionController.handleActiveConnection({ connection, namespace, address }); if (currentAddress && newAddress) { const hasSwitchedAccount = newAddress.toLowerCase() !== currentAddress.toLowerCase(); onChange?.({ address: newAddress, namespace, hasSwitchedAccount, hasSwitchedWallet: status === 'active' }); } break; } case 'disconnected': { const newAddress = await ConnectionController.handleDisconnectedConnection({ connection, namespace, address, closeModalOnConnect }); if (newAddress) { onChange?.({ address: newAddress, namespace, hasSwitchedAccount: true, hasSwitchedWallet: true }); } break; } default: throw new Error(`Invalid connection status: ${status}`); } } }; // Export the controller wrapped with our error boundary export const ConnectionController = withErrorBoundary(controller); //# sourceMappingURL=ConnectionController.js.map