@reown/appkit-controllers
Version:
The full stack toolkit to build onchain app UX.
330 lines • 16.1 kB
JavaScript
import { ConstantsUtil as CommonConstantsUtil, HelpersUtil } from '@reown/appkit-common';
import { ApiController } from '../controllers/ApiController.js';
import { ChainController } from '../controllers/ChainController.js';
import { ConnectionController } from '../controllers/ConnectionController.js';
import { ConnectorController } from '../controllers/ConnectorController.js';
import { OptionsController } from '../controllers/OptionsController.js';
import { CoreHelperUtil } from './CoreHelperUtil.js';
import { OptionsUtil } from './OptionsUtil.js';
import { StorageUtil } from './StorageUtil.js';
import { WalletUtil } from './WalletUtil.js';
export const ConnectorUtil = {
getConnectorsByType(connectors, recommended, featured) {
const { customWallets } = OptionsController.state;
const recent = StorageUtil.getRecentWallets();
const filteredRecommended = WalletUtil.filterOutDuplicateWallets(recommended);
const filteredFeatured = WalletUtil.filterOutDuplicateWallets(featured);
const multiChain = connectors.filter(connector => connector.type === 'MULTI_CHAIN');
const announced = connectors.filter(connector => connector.type === 'ANNOUNCED');
const injected = connectors.filter(connector => connector.type === 'INJECTED');
const external = connectors.filter(connector => connector.type === 'EXTERNAL');
return {
custom: customWallets,
recent,
external,
multiChain,
announced,
injected,
recommended: filteredRecommended,
featured: filteredFeatured
};
},
showConnector(connector) {
const rdns = connector.info?.rdns;
const isRDNSExcluded = Boolean(rdns) &&
ApiController.state.excludedWallets.some(wallet => Boolean(wallet.rdns) && wallet.rdns === rdns);
const isNameExcluded = Boolean(connector.name) &&
ApiController.state.excludedWallets.some(wallet => HelpersUtil.isLowerCaseMatch(wallet.name, connector.name));
if (connector.type === 'INJECTED') {
const isBrowserWallet = connector.name === 'Browser Wallet';
if (isBrowserWallet) {
if (!CoreHelperUtil.isMobile()) {
return false;
}
if (CoreHelperUtil.isMobile() && !rdns && !ConnectionController.checkInstalled()) {
return false;
}
}
if (isRDNSExcluded || isNameExcluded) {
return false;
}
}
if ((connector.type === 'ANNOUNCED' || connector.type === 'EXTERNAL') &&
(isRDNSExcluded || isNameExcluded)) {
return false;
}
const { includeWalletIds, excludeWalletIds } = OptionsController.state;
const isFilterableConnectorType = connector.type === 'INJECTED' ||
connector.type === 'ANNOUNCED' ||
connector.type === 'MULTI_CHAIN';
if (isFilterableConnectorType) {
const connectorWalletId = connector.explorerId || connector.explorerWallet?.id;
if (excludeWalletIds?.length &&
connectorWalletId &&
excludeWalletIds.includes(connectorWalletId)) {
return false;
}
if (includeWalletIds?.length &&
(!connectorWalletId || !includeWalletIds.includes(connectorWalletId))) {
return false;
}
}
return true;
},
/**
* Returns true if the user is connected to a WalletConnect connector in the any of the available namespaces.
* @returns boolean
*/
getIsConnectedWithWC() {
const chains = Array.from(ChainController.state.chains.values());
const isConnectedWithWC = chains.some(chain => {
const connectorId = ConnectorController.getConnectorId(chain.namespace);
return connectorId === CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT;
});
return isConnectedWithWC;
},
/**
* Returns the connector positions in the order of the user's preference.
* @returns ConnectorTypeOrder[]
*/
getConnectorTypeOrder({ recommended, featured, custom, recent, announced, injected, multiChain, external, overriddenConnectors = OptionsController.state.features?.connectorTypeOrder ?? [] }) {
const allConnectors = [
{ type: 'walletConnect', isEnabled: true },
{ type: 'recent', isEnabled: recent.length > 0 },
{ type: 'injected', isEnabled: [...injected, ...announced, ...multiChain].length > 0 },
{ type: 'featured', isEnabled: featured.length > 0 },
{ type: 'custom', isEnabled: custom && custom.length > 0 },
{ type: 'external', isEnabled: external.length > 0 },
{ type: 'recommended', isEnabled: recommended.length > 0 }
];
const enabledConnectors = allConnectors.filter(option => option.isEnabled);
const enabledConnectorTypes = new Set(enabledConnectors.map(option => option.type));
const prioritizedConnectors = overriddenConnectors
.filter(type => enabledConnectorTypes.has(type))
.map(type => ({ type, isEnabled: true }));
const remainingConnectors = enabledConnectors.filter(({ type: enabledConnectorType }) => {
const hasPrioritizedConnector = prioritizedConnectors.some(({ type: prioritizedConnectorType }) => prioritizedConnectorType === enabledConnectorType);
return !hasPrioritizedConnector;
});
return Array.from(new Set([...prioritizedConnectors, ...remainingConnectors].map(({ type }) => type)));
},
sortConnectorsByExplorerWallet(connectors) {
return [...connectors].sort((a, b) => {
if (a.explorerWallet && b.explorerWallet) {
return (a.explorerWallet.order ?? 0) - (b.explorerWallet.order ?? 0);
}
if (a.explorerWallet) {
return -1;
}
if (b.explorerWallet) {
return 1;
}
return 0;
});
},
/**
* Returns the priority of a connector. Base Account has highest priority, followed by Coinbase then the rest.
*
* This is needed because Base Account and Coinbase share the same explorer wallet ID.
* Without prioritization, selecting Base Account could incorrectly trigger the Coinbase Wallet extension.
*
* @param connector - The connector to get the priority of.
* @returns The priority of the connector.
*/
getPriority(connector) {
if (connector.id === CommonConstantsUtil.CONNECTOR_ID.BASE_ACCOUNT) {
return 0;
}
if (connector.id === CommonConstantsUtil.CONNECTOR_ID.COINBASE ||
connector.id === CommonConstantsUtil.CONNECTOR_ID.COINBASE_SDK) {
return 1;
}
return 2;
},
/**
* Sorts connectors by priority.
* @param connectors - The connectors to sort.
* @returns Sorted connectors.
*/
sortConnectorsByPriority(connectors) {
return [...connectors].sort((a, b) => ConnectorUtil.getPriority(a) - ConnectorUtil.getPriority(b));
},
getAuthName({ email, socialUsername, socialProvider }) {
if (socialUsername) {
if (socialProvider && socialProvider === 'discord' && socialUsername.endsWith('0')) {
return socialUsername.slice(0, -1);
}
return socialUsername;
}
return email.length > 30 ? `${email.slice(0, -3)}...` : email;
},
async fetchProviderData(connector) {
try {
if (connector.name === 'Browser Wallet' && !CoreHelperUtil.isMobile()) {
return { accounts: [], chainId: undefined };
}
if (connector.id === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
return { accounts: [], chainId: undefined };
}
const [accounts, chainId] = await Promise.all([
connector.provider?.request({ method: 'eth_accounts' }),
connector.provider
?.request({ method: 'eth_chainId' })
.then(hexChainId => Number(hexChainId))
]);
return { accounts, chainId };
}
catch (err) {
// eslint-disable-next-line no-console
console.warn(`Failed to fetch provider data for ${connector.name}`, err);
return { accounts: [], chainId: undefined };
}
},
/**
* Filter out duplicate custom wallets by RDNS
* @param wallets
*/
getFilteredCustomWallets(wallets) {
const recent = StorageUtil.getRecentWallets();
const connectorRDNSs = ConnectorController.state.connectors
.map(connector => connector.info?.rdns)
.filter(Boolean);
const recentRDNSs = recent.map(wallet => wallet.rdns).filter(Boolean);
const allRDNSs = connectorRDNSs.concat(recentRDNSs);
if (allRDNSs.includes('io.metamask.mobile') && CoreHelperUtil.isMobile()) {
const index = allRDNSs.indexOf('io.metamask.mobile');
allRDNSs[index] = 'io.metamask';
}
const filtered = wallets.filter(wallet => !allRDNSs.includes(String(wallet?.rdns)));
return filtered;
},
hasWalletConnector(wallet) {
return ConnectorController.state.connectors.some(connector => connector.id === wallet.id || connector.name === wallet.name);
},
isWalletCompatibleWithCurrentChain(wallet) {
const currentNamespace = ChainController.state.activeChain;
if (currentNamespace && wallet.chains) {
return wallet.chains.some(c => {
const chainNamespace = c.split(':')[0];
return currentNamespace === chainNamespace;
});
}
return true;
},
getFilteredRecentWallets() {
const recentWallets = StorageUtil.getRecentWallets();
const filteredRecentWallets = recentWallets
.filter(wallet => !WalletUtil.isExcluded(wallet))
.filter(wallet => !this.hasWalletConnector(wallet))
.filter(wallet => this.isWalletCompatibleWithCurrentChain(wallet));
return filteredRecentWallets;
},
getCappedRecommendedWallets(wallets) {
const { connectors } = ConnectorController.state;
const { customWallets, featuredWalletIds } = OptionsController.state;
const wcConnector = connectors.find(c => c.id === 'walletConnect');
const injectedConnectors = connectors.filter(c => c.type === 'INJECTED' || c.type === 'ANNOUNCED' || c.type === 'MULTI_CHAIN');
if (!wcConnector && !injectedConnectors.length && !customWallets?.length) {
return [];
}
const isEmailEnabled = OptionsUtil.isEmailEnabled();
const isSocialsEnabled = OptionsUtil.isSocialsEnabled();
const injectedWallets = injectedConnectors.filter(i => i.name !== 'Browser Wallet' && i.name !== 'WalletConnect');
const featuredWalletAmount = featuredWalletIds?.length || 0;
const customWalletAmount = customWallets?.length || 0;
const injectedWalletAmount = injectedWallets.length || 0;
const emailWalletAmount = isEmailEnabled ? 1 : 0;
const socialWalletAmount = isSocialsEnabled ? 1 : 0;
const walletsDisplayed = featuredWalletAmount +
customWalletAmount +
injectedWalletAmount +
emailWalletAmount +
socialWalletAmount;
const DISPLAYED_WALLETS_AMOUNT = 4;
const sliceAmount = Math.max(0, DISPLAYED_WALLETS_AMOUNT - walletsDisplayed);
if (sliceAmount <= 0) {
return [];
}
const filtered = WalletUtil.filterOutDuplicateWallets(wallets);
return filtered.slice(0, sliceAmount);
},
processConnectorsByType(connectors, shouldFilter = true) {
const sorted = ConnectorUtil.sortConnectorsByExplorerWallet([...connectors]);
return shouldFilter ? sorted.filter(ConnectorUtil.showConnector) : sorted;
},
connectorList() {
const byType = ConnectorUtil.getConnectorsByType(ConnectorController.state.connectors, ApiController.state.recommended, ApiController.state.featured);
// Build per-type lists with existing filtering/sorting rules
const announced = this.processConnectorsByType(byType.announced.filter(c => c.id !== 'walletConnect'));
const injected = this.processConnectorsByType(byType.injected);
const multiChain = this.processConnectorsByType(byType.multiChain.filter(c => c.name !== 'WalletConnect'), false);
const custom = byType.custom;
const recent = byType.recent;
const external = this.processConnectorsByType(byType.external.filter(c => c.id !== CommonConstantsUtil.CONNECTOR_ID.COINBASE_SDK &&
c.id !== CommonConstantsUtil.CONNECTOR_ID.BASE_ACCOUNT));
const recommended = byType.recommended;
const featured = byType.featured;
const connectorTypeOrder = ConnectorUtil.getConnectorTypeOrder({
custom,
recent,
announced,
injected,
multiChain,
recommended,
featured,
external
});
const wcConnector = ConnectorController.state.connectors.find(c => c.id === 'walletConnect');
const isMobile = CoreHelperUtil.isMobile();
const items = [];
for (const type of connectorTypeOrder) {
switch (type) {
case 'walletConnect': {
if (!isMobile && wcConnector) {
items.push({ kind: 'connector', subtype: 'walletConnect', connector: wcConnector });
}
break;
}
case 'recent': {
const filteredRecent = ConnectorUtil.getFilteredRecentWallets();
filteredRecent.forEach(w => items.push({ kind: 'wallet', subtype: 'recent', wallet: w }));
break;
}
case 'injected': {
multiChain.forEach(c => items.push({ kind: 'connector', subtype: 'multiChain', connector: c }));
announced.forEach(c => items.push({ kind: 'connector', subtype: 'announced', connector: c }));
injected.forEach(c => items.push({ kind: 'connector', subtype: 'injected', connector: c }));
break;
}
case 'featured': {
featured.forEach(w => items.push({ kind: 'wallet', subtype: 'featured', wallet: w }));
break;
}
case 'custom': {
const filteredCustom = ConnectorUtil.getFilteredCustomWallets(custom ?? []);
filteredCustom.forEach(w => items.push({ kind: 'wallet', subtype: 'custom', wallet: w }));
break;
}
case 'external': {
external.forEach(c => items.push({ kind: 'connector', subtype: 'external', connector: c }));
break;
}
case 'recommended': {
const cappedRecommended = ConnectorUtil.getCappedRecommendedWallets(recommended);
cappedRecommended.forEach(w => items.push({ kind: 'wallet', subtype: 'recommended', wallet: w }));
break;
}
default:
// eslint-disable-next-line no-console
console.warn(`Unknown connector type: ${type}`);
}
}
return items;
},
hasInjectedConnectors() {
return ConnectorController.state.connectors.filter(c => (c.type === 'INJECTED' || c.type === 'ANNOUNCED' || c.type === 'MULTI_CHAIN') &&
c.name !== 'Browser Wallet' &&
c.name !== 'WalletConnect').length;
}
};
//# sourceMappingURL=ConnectorUtil.js.map