@web3auth/no-modal
Version:
Multi chain wallet aggregator for web3Auth
554 lines (547 loc) • 25 kB
JavaScript
import _objectSpread from '@babel/runtime/helpers/objectSpread2';
import _defineProperty from '@babel/runtime/helpers/defineProperty';
import { CHAIN_NAMESPACES, getErrorAnalyticsProperties, signChallenge } from '@toruslabs/base-controllers';
import Client from '@walletconnect/sign-client';
import { isValidArray, getSdkError } from '@walletconnect/utils';
import { EVM_METHOD_TYPES } from '@web3auth/ws-embed';
import deepmerge from 'deepmerge';
import { generateSiweNonce } from 'viem/siwe';
import { getWalletConnectV2Settings } from './config.js';
import { WalletConnectV2Provider } from './WalletConnectV2Provider.js';
import { WCSolanaWallet } from './wcSolanaWallet.js';
import { WALLET_CONNECTORS } from '../../base/wallet/index.js';
import { CONNECTOR_NAMESPACES } from '../../base/chain/IChainInterface.js';
import { WalletInitializationError, WalletLoginError, Web3AuthError } from '../../base/errors/index.js';
import { log } from '../../base/loglevel.js';
import { getCaipChainId, citadelServerUrl } from '../../base/utils.js';
import { ANALYTICS_EVENTS } from '../../base/analytics.js';
import { SOLANA_CAIP_CHAIN_MAP } from '../../base/constants.js';
import { BaseConnector } from '../../base/connector/baseConnector.js';
import { CONNECTOR_CATEGORY, CONNECTOR_STATUS, CONNECTOR_EVENTS } from '../../base/connector/constants.js';
import { walletSignMessage } from '../../base/wallet/solana.js';
class WalletConnectV2Connector extends BaseConnector {
constructor(options) {
var _this$connectorOption, _this$connectorOption2, _this$connectorOption3, _this$connectorOption4, _this$connectorOption5;
super(options);
_defineProperty(this, "name", WALLET_CONNECTORS.WALLET_CONNECT_V2);
_defineProperty(this, "connectorNamespace", CONNECTOR_NAMESPACES.MULTICHAIN);
_defineProperty(this, "currentChainNamespace", CHAIN_NAMESPACES.OTHER);
_defineProperty(this, "type", CONNECTOR_CATEGORY.EXTERNAL);
_defineProperty(this, "connectorOptions", void 0);
_defineProperty(this, "status", CONNECTOR_STATUS.NOT_READY);
_defineProperty(this, "connectorData", {
uri: ""
});
_defineProperty(this, "connector", null);
_defineProperty(this, "activeSession", null);
_defineProperty(this, "wcProvider", null);
_defineProperty(this, "_solanaWallet", null);
_defineProperty(this, "analytics", void 0);
this.connectorOptions = _objectSpread({}, options);
const {
qrcodeModal,
walletConnectInitOptions
} = (options === null || options === void 0 ? void 0 : options.connectorSettings) || {};
this.connectorOptions = _objectSpread(_objectSpread({}, this.connectorOptions), {}, {
connectorSettings: (_this$connectorOption = (_this$connectorOption2 = this.connectorOptions) === null || _this$connectorOption2 === void 0 ? void 0 : _this$connectorOption2.connectorSettings) !== null && _this$connectorOption !== void 0 ? _this$connectorOption : {},
loginSettings: (_this$connectorOption3 = (_this$connectorOption4 = this.connectorOptions) === null || _this$connectorOption4 === void 0 ? void 0 : _this$connectorOption4.loginSettings) !== null && _this$connectorOption3 !== void 0 ? _this$connectorOption3 : {}
});
this.analytics = options.analytics;
if (qrcodeModal) this.connectorOptions.connectorSettings.qrcodeModal = qrcodeModal;
if (walletConnectInitOptions) this.connectorOptions.connectorSettings.walletConnectInitOptions = _objectSpread(_objectSpread({}, (_this$connectorOption5 = this.connectorOptions.connectorSettings.walletConnectInitOptions) !== null && _this$connectorOption5 !== void 0 ? _this$connectorOption5 : {}), walletConnectInitOptions);
const {
loginSettings
} = options;
if (loginSettings) this.connectorOptions.loginSettings = _objectSpread(_objectSpread({}, this.connectorOptions.loginSettings || {}), loginSettings);
}
get connected() {
return !!this.activeSession;
}
get provider() {
if (this.status !== CONNECTOR_STATUS.NOT_READY && this.wcProvider) {
return this.wcProvider;
}
return null;
}
get solanaWallet() {
return this._solanaWallet;
}
set provider(_) {
throw new Error("Not implemented");
}
async init(options) {
var _this$connectorOption6;
const chainConfig = this.coreOptions.chains.find(x => x.chainId === options.chainId);
super.checkInitializationRequirements({
chainConfig
});
const projectId = (_this$connectorOption6 = this.connectorOptions.connectorSettings) === null || _this$connectorOption6 === void 0 || (_this$connectorOption6 = _this$connectorOption6.walletConnectInitOptions) === null || _this$connectorOption6 === void 0 ? void 0 : _this$connectorOption6.projectId;
const filteredChains = this.coreOptions.chains.filter(x => x.chainNamespace === CHAIN_NAMESPACES.EIP155 || x.chainNamespace === CHAIN_NAMESPACES.SOLANA);
if (filteredChains.length === 0) {
throw WalletInitializationError.invalidParams("No supported chains found");
}
const wc2Settings = await getWalletConnectV2Settings(filteredChains, projectId);
if (!this.connectorOptions.loginSettings || Object.keys(this.connectorOptions.loginSettings).length === 0) {
this.connectorOptions.loginSettings = wc2Settings.loginSettings;
}
this.connectorOptions.connectorSettings = deepmerge(wc2Settings.connectorSettings || {}, this.connectorOptions.connectorSettings || {});
const {
connectorSettings
} = this.connectorOptions;
this.connector = await Client.init(connectorSettings === null || connectorSettings === void 0 ? void 0 : connectorSettings.walletConnectInitOptions);
this.wcProvider = new WalletConnectV2Provider({
config: {
chain: chainConfig,
chains: this.coreOptions.chains
},
connector: this.connector
});
this.emit(CONNECTOR_EVENTS.READY, WALLET_CONNECTORS.WALLET_CONNECT_V2);
this.status = CONNECTOR_STATUS.READY;
log.debug("initializing wallet connect v2 connector");
if (options.autoConnect) {
await this.checkForPersistedSession();
if (this.connected) {
this.rehydrated = true;
try {
await this.onConnectHandler({
chain: chainConfig,
getAuthTokenInfo: options.getAuthTokenInfo
});
} catch (error) {
log.error("wallet auto connect", error);
this.emit(CONNECTOR_EVENTS.REHYDRATION_ERROR, error);
}
} else {
this.status = CONNECTOR_STATUS.NOT_READY;
this.emit(CONNECTOR_EVENTS.CACHE_CLEAR);
}
}
}
async connect({
chainId,
getAuthTokenInfo
}) {
super.checkConnectionRequirements();
const chainConfig = this.coreOptions.chains.find(x => x.chainId === chainId);
if (!chainConfig) throw WalletLoginError.connectionError("Chain config is not available");
if (!this.connector) throw WalletInitializationError.notReady("Wallet connector is not ready yet");
// Skip tracking for rehydration since only new connections are tracked
// Track when connection completes since it auto-initializes to generate QR code
const shouldTrack = !this.connected && !this.rehydrated;
const startTime = Date.now();
let eventData = {
connector: this.name,
connector_type: this.type,
is_injected: this.isInjected,
is_wallet_connect: true,
chain_id: getCaipChainId(chainConfig),
chain_name: chainConfig === null || chainConfig === void 0 ? void 0 : chainConfig.displayName,
chain_namespace: chainConfig === null || chainConfig === void 0 ? void 0 : chainConfig.chainNamespace
};
try {
const trackCompletionEvents = () => {
// track connection events
if (shouldTrack) {
var _this$activeSession, _this$analytics, _this$analytics2;
const {
name,
url,
redirect
} = ((_this$activeSession = this.activeSession) === null || _this$activeSession === void 0 || (_this$activeSession = _this$activeSession.peer) === null || _this$activeSession === void 0 ? void 0 : _this$activeSession.metadata) || {};
const {
native,
universal,
linkMode
} = redirect || {};
eventData = _objectSpread(_objectSpread({}, eventData), {}, {
connector: name || this.name,
wallet_url: url,
redirect_native: native,
redirect_universal: universal,
redirect_link_mode_enabled: linkMode
});
(_this$analytics = this.analytics) === null || _this$analytics === void 0 || _this$analytics.track(ANALYTICS_EVENTS.CONNECTION_STARTED, eventData);
(_this$analytics2 = this.analytics) === null || _this$analytics2 === void 0 || _this$analytics2.track(ANALYTICS_EVENTS.CONNECTION_COMPLETED, _objectSpread(_objectSpread({}, eventData), {}, {
duration: Date.now() - startTime
}));
}
};
const connectorNamespace = this.connectorNamespace;
// if already connected
if (this.connected) {
await this.onConnectHandler({
chain: chainConfig,
getAuthTokenInfo
});
return {
ethereumProvider: this.provider,
solanaWallet: this._solanaWallet,
connectorName: this.name,
connectorNamespace
};
}
if (this.status !== CONNECTOR_STATUS.CONNECTING) {
await this.createNewSession({
chainConfig,
trackCompletionEvents,
getAuthTokenInfo
});
}
return {
ethereumProvider: this.provider,
solanaWallet: this._solanaWallet,
connectorName: this.name,
connectorNamespace
};
} catch (error) {
log.error("Wallet connect v2 connector error while connecting", error);
// ready again to be connected
this.status = CONNECTOR_STATUS.READY;
this.rehydrated = true;
this.emit(CONNECTOR_EVENTS.ERRORED, error);
// track connection events
if (shouldTrack) {
var _this$analytics3, _this$analytics4;
(_this$analytics3 = this.analytics) === null || _this$analytics3 === void 0 || _this$analytics3.track(ANALYTICS_EVENTS.CONNECTION_STARTED, eventData);
(_this$analytics4 = this.analytics) === null || _this$analytics4 === void 0 || _this$analytics4.track(ANALYTICS_EVENTS.CONNECTION_FAILED, _objectSpread(_objectSpread(_objectSpread({}, eventData), getErrorAnalyticsProperties(error)), {}, {
duration: Date.now() - startTime
}));
}
const finalError = error instanceof Web3AuthError ? error : WalletLoginError.connectionError(`Failed to login with wallet connect: ${(error === null || error === void 0 ? void 0 : error.message) || ""}`, error);
throw finalError;
}
}
async switchChain(params, init = false) {
super.checkSwitchChainRequirements(params, init);
if (!this.wcProvider) throw WalletInitializationError.notReady("Wallet Connect provider is not ready yet");
try {
await this.wcProvider.switchChain({
chainId: params.chainId
});
} catch (error) {
log.error("error while switching chain", error);
throw error;
}
}
async getUserInfo() {
if (!this.canAuthorize) throw WalletLoginError.notConnectedError("Not connected with wallet, Please login/connect first");
return {};
}
async disconnect(options = {
cleanup: false,
sessionRemovedByWallet: false
}) {
var _this$activeSession2, _this$activeSession3;
const {
cleanup
} = options;
if (!this.connector || !this.connected || !((_this$activeSession2 = this.activeSession) !== null && _this$activeSession2 !== void 0 && _this$activeSession2.topic)) throw WalletLoginError.notConnectedError("Not connected with wallet");
if (!options.sessionRemovedByWallet) await this.connector.disconnect({
topic: (_this$activeSession3 = this.activeSession) === null || _this$activeSession3 === void 0 ? void 0 : _this$activeSession3.topic,
reason: getSdkError("USER_DISCONNECTED")
});
await this.clearWalletSession();
this.rehydrated = false;
this._solanaWallet = null;
if (cleanup) {
this.connector = null;
this.status = CONNECTOR_STATUS.NOT_READY;
this.wcProvider = null;
} else {
// ready to connect again
this.status = CONNECTOR_STATUS.READY;
}
this.activeSession = null;
this.emit(CONNECTOR_EVENTS.DISCONNECTED, {
connector: WALLET_CONNECTORS.WALLET_CONNECT_V2
});
}
async getAuthTokenInfo() {
if (!this.provider || !this.canAuthorize) throw WalletLoginError.notConnectedError();
this.status = CONNECTOR_STATUS.AUTHORIZING;
this.emit(CONNECTOR_EVENTS.AUTHORIZING, {
connector: WALLET_CONNECTORS.WALLET_CONNECT_V2
});
const {
chainId
} = this.provider;
const currentChainConfig = this.coreOptions.chains.find(x => x.chainId === chainId);
if (!currentChainConfig) throw WalletLoginError.connectionError("Chain config is not available");
const {
chainNamespace
} = currentChainConfig;
const accounts = chainNamespace === CHAIN_NAMESPACES.SOLANA && this._solanaWallet ? this._solanaWallet.accounts.map(a => a.address) : await this.provider.request({
method: EVM_METHOD_TYPES.GET_ACCOUNTS
});
if (accounts && accounts.length > 0) {
const cached = await this.getCachedOrNullAuthTokenInfo(accounts[0]);
if (cached) return cached;
const authServer = citadelServerUrl(this.coreOptions.authBuildEnv);
const {
challenge,
signature
} = await this.generateChallengeAndSign(authServer, accounts);
return this.verifyAndAuthorize({
chainNamespace,
signedMessage: signature,
challenge,
authServer
});
}
throw WalletLoginError.notConnectedError("Not connected with wallet, Please login/connect first");
}
async generateChallengeAndSign(authServerUrl, accounts) {
const {
chainId
} = this.provider;
const currentChainConfig = this.coreOptions.chains.find(x => x.chainId === chainId);
if (!currentChainConfig) throw WalletLoginError.connectionError("Chain config is not available");
const {
chainNamespace
} = currentChainConfig;
const accountsToUse = accounts || (chainNamespace === CHAIN_NAMESPACES.SOLANA && this._solanaWallet ? this._solanaWallet.accounts.map(a => a.address) : await this.provider.request({
method: EVM_METHOD_TYPES.GET_ACCOUNTS
}));
if (!accountsToUse || accountsToUse.length === 0) {
throw WalletLoginError.notConnectedError("No accounts found in the connected wallet");
}
const payload = {
domain: window.location.origin,
uri: window.location.href,
address: accountsToUse[0],
chainId: parseInt(chainId, 16),
version: "1",
nonce: generateSiweNonce(),
issuedAt: new Date().toISOString()
};
const authServer = authServerUrl || citadelServerUrl(this.coreOptions.authBuildEnv);
const challenge = await signChallenge(payload, chainNamespace, authServer);
const signature = await this._getSignedMessage(challenge, accountsToUse, chainNamespace);
return {
challenge,
signature,
chainNamespace
};
}
async enableMFA() {
throw new Error("Method Not implemented");
}
async manageMFA() {
throw new Error("Method Not implemented");
}
cleanupPendingPairings() {
if (!this.connector) throw WalletInitializationError.notReady("Wallet connector is not ready yet");
const inactivePairings = this.connector.pairing.getAll({
active: false
});
if (!isValidArray(inactivePairings)) return;
inactivePairings.forEach(pairing => {
if (this.connector) {
this.connector.pairing.delete(pairing.topic, getSdkError("USER_DISCONNECTED"));
}
});
}
async checkForPersistedSession() {
if (!this.connector) throw WalletInitializationError.notReady("Wallet connector is not ready yet");
if (this.connector.session.length) {
const lastKeyIndex = this.connector.session.keys.length - 1;
this.activeSession = this.connector.session.get(this.connector.session.keys[lastKeyIndex]);
}
return this.activeSession;
}
async createNewSession({
forceNewSession = false,
chainConfig,
trackCompletionEvents,
getAuthTokenInfo
}) {
try {
var _this$activeSession4, _this$connectorOption7;
if (!this.connector) throw WalletInitializationError.notReady("Wallet connector is not ready yet");
if (!this.connectorOptions.loginSettings || Object.keys(this.connectorOptions.loginSettings).length === 0) throw WalletInitializationError.notReady("login settings are not set yet");
this.status = CONNECTOR_STATUS.CONNECTING;
this.emit(CONNECTOR_EVENTS.CONNECTING, {
connector: WALLET_CONNECTORS.WALLET_CONNECT_V2
});
if (forceNewSession && (_this$activeSession4 = this.activeSession) !== null && _this$activeSession4 !== void 0 && _this$activeSession4.topic) {
var _this$activeSession5;
await this.connector.disconnect({
topic: (_this$activeSession5 = this.activeSession) === null || _this$activeSession5 === void 0 ? void 0 : _this$activeSession5.topic,
reason: getSdkError("USER_DISCONNECTED")
});
}
const {
uri,
approval
} = await this.connector.connect(this.connectorOptions.loginSettings);
const qrcodeModal = (_this$connectorOption7 = this.connectorOptions) === null || _this$connectorOption7 === void 0 || (_this$connectorOption7 = _this$connectorOption7.connectorSettings) === null || _this$connectorOption7 === void 0 ? void 0 : _this$connectorOption7.qrcodeModal;
// Open QRCode modal if a URI was returned (i.e. we're not connecting with an existing pairing).
if (uri) {
if (qrcodeModal) {
try {
await qrcodeModal.openModal({
uri
});
log.debug("EVENT", "QR Code Modal closed");
this.status = CONNECTOR_STATUS.READY;
this.emit(CONNECTOR_EVENTS.READY, WALLET_CONNECTORS.WALLET_CONNECT_V2);
} catch (error) {
log.error("unable to open qr code modal", error);
}
} else {
this.updateConnectorData({
uri
});
}
}
log.info("awaiting session approval from wallet");
// Await session approval from the wallet.
const session = await approval();
this.activeSession = session;
// Handle the returned session (e.g. update UI to "connected" state).
await this.onConnectHandler({
chain: chainConfig,
trackCompletionEvents,
getAuthTokenInfo
});
if (qrcodeModal) {
qrcodeModal.closeModal();
}
} catch (error) {
var _message;
if ((_message = error.message) !== null && _message !== void 0 && _message.toLowerCase().includes("proposal expired")) {
// Retry if connector status is still connecting
log.info("current connector status: ", this.status);
if (this.status === CONNECTOR_STATUS.CONNECTING) {
log.info("retrying to create new wallet connect session since proposal expired");
return this.createNewSession({
forceNewSession: true,
chainConfig,
getAuthTokenInfo
});
}
if (this.status === CONNECTOR_STATUS.READY) {
log.info("ignoring proposal expired error since some other connector is connected");
return;
}
}
log.error("error while creating new wallet connect session", error);
this.emit(CONNECTOR_EVENTS.ERRORED, error);
throw error;
}
}
async onConnectHandler({
chain,
trackCompletionEvents,
getAuthTokenInfo
}) {
var _this$connectorOption8, _this$activeSession$n, _this$activeSession6;
if (!this.connector || !this.wcProvider) throw WalletInitializationError.notReady("Wallet connect connector is not ready yet");
this.subscribeEvents();
if ((_this$connectorOption8 = this.connectorOptions.connectorSettings) !== null && _this$connectorOption8 !== void 0 && _this$connectorOption8.qrcodeModal) {
this.wcProvider = new WalletConnectV2Provider({
config: {
chain,
chains: this.coreOptions.chains,
skipLookupNetwork: true
},
connector: this.connector
});
}
await this.wcProvider.setupProvider(this.connector);
// Derive the active Solana chain from the WC session namespaces (genesis hash → hex chainId),
// rather than picking the first Solana chain from coreOptions which may not match the session.
const sessionSolanaChains = (_this$activeSession$n = (_this$activeSession6 = this.activeSession) === null || _this$activeSession6 === void 0 || (_this$activeSession6 = _this$activeSession6.namespaces) === null || _this$activeSession6 === void 0 || (_this$activeSession6 = _this$activeSession6.solana) === null || _this$activeSession6 === void 0 ? void 0 : _this$activeSession6.chains) !== null && _this$activeSession$n !== void 0 ? _this$activeSession$n : [];
if (sessionSolanaChains.length > 0) {
const reverseMap = Object.fromEntries(Object.entries(SOLANA_CAIP_CHAIN_MAP).map(([hex, hash]) => [hash, hex]));
const genesisHash = sessionSolanaChains[0].split(":")[1];
const hexChainId = reverseMap[genesisHash];
const solanaChain = hexChainId ? this.coreOptions.chains.find(c => c.chainId === hexChainId) : null;
if (solanaChain) {
this._solanaWallet = await WCSolanaWallet.create(this.connector, solanaChain);
}
}
this.cleanupPendingPairings();
this.status = CONNECTOR_STATUS.CONNECTED;
// track connection events
if (trackCompletionEvents) trackCompletionEvents();
this.emit(CONNECTOR_EVENTS.CONNECTED, {
connectorName: WALLET_CONNECTORS.WALLET_CONNECT_V2,
reconnected: this.rehydrated,
ethereumProvider: this.provider,
solanaWallet: this._solanaWallet,
connectorNamespace: this.connectorNamespace
});
await this.authorizeOrDisconnect(getAuthTokenInfo);
}
subscribeEvents() {
if (!this.connector) throw WalletInitializationError.notReady("Wallet connector is not ready yet");
this.connector.events.on("session_update", ({
topic,
params
}) => {
if (!this.connector) return;
const {
namespaces
} = params;
const _session = this.connector.session.get(topic);
// Overwrite the `namespaces` of the existing session with the incoming one.
const updatedSession = _objectSpread(_objectSpread({}, _session), {}, {
namespaces
});
// Integrate the updated session state into your dapp state.
this.activeSession = updatedSession;
});
this.connector.events.on("session_delete", () => {
// Session was deleted -> reset the dapp state, clean up from user session, etc.
this.disconnect({
sessionRemovedByWallet: true
});
});
this.connector.events.on("session_expire", ({
topic
}) => {
var _this$activeSession7;
// Session has expired -> clean up the session
log.info("Session expired event received for topic:", topic);
if (((_this$activeSession7 = this.activeSession) === null || _this$activeSession7 === void 0 ? void 0 : _this$activeSession7.topic) === topic) {
this.disconnect({
sessionRemovedByWallet: true
}).catch(error => {
log.error("Failed to disconnect expired session", error);
});
}
});
}
async _getSignedMessage(challenge, accounts, chainNamespace) {
if (chainNamespace === CHAIN_NAMESPACES.SOLANA && this._solanaWallet) {
return walletSignMessage(this._solanaWallet, challenge, accounts[0]);
}
return this.provider.request({
method: EVM_METHOD_TYPES.PERSONAL_SIGN,
params: [challenge, accounts[0]]
});
}
}
const walletConnectV2Connector = params => {
return ({
coreOptions,
projectConfig,
analytics
}) => {
var _params$walletConnect;
const projectId = (params === null || params === void 0 || (_params$walletConnect = params.walletConnectInitOptions) === null || _params$walletConnect === void 0 ? void 0 : _params$walletConnect.projectId) || (projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.walletConnectProjectId);
const connectorSettings = _objectSpread(_objectSpread({}, params), {}, {
walletConnectInitOptions: _objectSpread(_objectSpread({}, params === null || params === void 0 ? void 0 : params.walletConnectInitOptions), {}, {
projectId: projectId
})
});
return new WalletConnectV2Connector({
connectorSettings,
coreOptions,
analytics
});
};
};
export { walletConnectV2Connector };