@reown/appkit-controllers
Version:
#### 🔗 [Website](https://reown.com/appkit)
365 lines • 15 kB
JavaScript
/* 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 { 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 { AccountController } from './AccountController.js';
import { ChainController } from './ChainController.js';
import { ConnectorController } from './ConnectorController.js';
import { EventsController } from './EventsController.js';
import { ModalController } from './ModalController.js';
import { RouterController } from './RouterController.js';
import { TransactionsController } from './TransactionsController.js';
// -- State --------------------------------------------- //
const state = proxy({
connections: new Map(),
isSwitchingConnection: false,
wcError: 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 storageConnections = StorageUtil.getConnections();
for (const adapter of adapters) {
const namespace = adapter.namespace;
if (namespace) {
const existingConnections = state.connections.get(namespace) ?? [];
const storageConnectionsByNamespace = storageConnections[namespace] ?? [];
const allConnections = [...existingConnections, ...storageConnectionsByNamespace];
state.connections.set(namespace, allConnections);
}
}
},
async connectWalletConnect() {
if (CoreHelperUtil.isTelegram() || (CoreHelperUtil.isSafari() && CoreHelperUtil.isIos())) {
if (wcConnectionPromise) {
await wcConnectionPromise;
wcConnectionPromise = undefined;
return;
}
if (!CoreHelperUtil.isPairingExpired(state?.wcPairingExpiry)) {
const link = state.wcUri;
state.wcUri = link;
return;
}
wcConnectionPromise = ConnectionController._getClient()
?.connectWalletConnect?.()
.catch(() => undefined);
ConnectionController.state.status = 'connecting';
await wcConnectionPromise;
wcConnectionPromise = undefined;
state.wcPairingExpiry = undefined;
ConnectionController.state.status = 'connected';
}
else {
await ConnectionController._getClient()?.connectWalletConnect?.();
}
},
async connectExternal(options, chain, setChain = true) {
const connectData = await ConnectionController._getClient()?.connectExternal?.(options);
if (setChain) {
ChainController.setActiveNamespace(chain);
}
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) {
ModalController.setLoading(true, ChainController.state.activeChain);
const authConnector = ConnectorController.getAuthConnector();
if (!authConnector) {
return;
}
AccountController.setPreferredAccountType(accountType, namespace);
await authConnector.provider.setPreferredAccount(accountType);
StorageUtil.setPreferredAccountTypes(AccountController.state.preferredAccountTypes ?? { [namespace]: accountType });
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);
},
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 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.status = 'disconnected';
TransactionsController.resetTransactions();
StorageUtil.deleteWalletConnectDeepLink();
},
resetUri() {
state.wcUri = undefined;
state.wcPairingExpiry = undefined;
wcConnectionPromise = undefined;
},
finalizeWcConnection() {
const { wcLinking, recentWallet } = ConnectionController.state;
if (wcLinking) {
StorageUtil.setWalletConnectDeepLink(wcLinking);
}
if (recentWallet) {
StorageUtil.setAppKitRecent(recentWallet);
}
EventsController.sendEvent({
type: 'track',
event: 'CONNECT_SUCCESS',
properties: {
method: wcLinking ? 'mobile' : 'qrcode',
name: RouterController.state.data?.wallet?.name || 'Unknown'
}
});
},
setWcBasic(wcBasic) {
state.wcBasic = wcBasic;
},
setUri(uri) {
state.wcUri = uri;
state.wcPairingExpiry = CoreHelperUtil.getPairingExpiry();
},
setWcLinking(wcLinking) {
state.wcLinking = wcLinking;
},
setWcError(wcError) {
state.wcError = wcError;
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 } = {}) {
try {
await ConnectionController._getClient()?.disconnect({
id,
chainNamespace: namespace
});
}
catch (error) {
throw new AppKitError('Failed to disconnect', 'INTERNAL_SDK_ERROR', error);
}
},
setConnections(connections, chainNamespace) {
state.connections = new Map(state.connections.set(chainNamespace, connections));
},
async handleAuthAccountSwitch({ address, connection, namespace }) {
const smartAccountAddress = connection.accounts.find(c => c.type === 'smartAccount')?.address;
const isAddressSmartAccount = smartAccountAddress?.toLowerCase() === address.toLowerCase();
const accountType = isAddressSmartAccount && 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}`);
}
const connectData = await ConnectionController.connectExternal({
id: connector.id,
type: connector.type,
provider: connector.provider,
address,
chain: namespace
}, namespace);
if (isAuthConnector && address) {
await ConnectionController.handleAuthAccountSwitch({ address, connection, namespace });
}
return connectData?.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) {
ModalController.open({ view: isMobile ? 'AllWallets' : 'ConnectingWalletConnect' });
},
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, connection, namespace });
}
return newAddress;
},
async switchConnection({ connection, address, namespace, closeModalOnConnect, onChange }) {
// Validate the account switch
ConnectionControllerUtil.validateAccountSwitch({ namespace, connection, address });
let currentAddress = undefined;
const caipAddress = AccountController.getCaipAddress(namespace);
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