UNPKG

@biuauth/wallet-connect-v2-adapter

Version:
303 lines (260 loc) 11.7 kB
import { WalletConnectV2Provider } from "@biuauth/ethereum-provider"; import SignClient from "@walletconnect/sign-client"; import { SessionTypes } from "@walletconnect/types"; import { getSdkError, isValidArray } from "@walletconnect/utils"; import { ADAPTER_CATEGORY, ADAPTER_CATEGORY_TYPE, ADAPTER_EVENTS, ADAPTER_NAMESPACES, ADAPTER_STATUS, ADAPTER_STATUS_TYPE, AdapterInitOptions, AdapterNamespaceType, CHAIN_NAMESPACES, ChainNamespaceType, CONNECTED_EVENT_DATA, CustomChainConfig, log, SafeEventEmitterProvider, UserInfo, WALLET_ADAPTERS, WalletConnectV2Data, WalletInitializationError, WalletLoginError, WalletOperationsError, Web3AuthError, } from "@web3auth/base"; import { BaseEvmAdapter } from "@web3auth/base-evm-adapter"; import merge from "lodash.merge"; import { getWalletConnectV2Settings, WALLET_CONNECT_EXTENSION_ADAPTERS } from "./config"; import { WalletConnectV2AdapterOptions } from "./interface"; import { isChainIdSupported } from "./utils"; class WalletConnectV2Adapter extends BaseEvmAdapter<void> { readonly name: string = WALLET_ADAPTERS.WALLET_CONNECT_V2; readonly adapterNamespace: AdapterNamespaceType = ADAPTER_NAMESPACES.EIP155; readonly currentChainNamespace: ChainNamespaceType = CHAIN_NAMESPACES.EIP155; readonly type: ADAPTER_CATEGORY_TYPE = ADAPTER_CATEGORY.EXTERNAL; readonly adapterOptions: WalletConnectV2AdapterOptions; public status: ADAPTER_STATUS_TYPE = ADAPTER_STATUS.NOT_READY; public adapterData: WalletConnectV2Data = { uri: "", extensionAdapters: WALLET_CONNECT_EXTENSION_ADAPTERS, }; public connector: SignClient | null = null; public activeSession: SessionTypes.Struct | null = null; private wcProvider: WalletConnectV2Provider | null = null; constructor(options: WalletConnectV2AdapterOptions = {}) { super(options); this.adapterOptions = { ...options }; } get connected(): boolean { return !!this.activeSession; } get provider(): SafeEventEmitterProvider | null { if (this.status !== ADAPTER_STATUS.NOT_READY && this.wcProvider) { return this.wcProvider.provider; } return null; } set provider(_: SafeEventEmitterProvider | null) { throw new Error("Not implemented"); } async init(options: AdapterInitOptions): Promise<void> { await super.init(); super.checkInitializationRequirements(); const projectId = this.adapterOptions.adapterSettings?.walletConnectInitOptions?.projectId; if (!projectId) { throw WalletInitializationError.invalidParams("Wallet connect project id is required in wallet connect v2 adapter"); } const wc2Settings = await getWalletConnectV2Settings( this.chainConfig?.chainNamespace as ChainNamespaceType, [parseInt(this.chainConfig?.chainId as string, 16)], projectId ); if (!this.adapterOptions.loginSettings) { this.adapterOptions.loginSettings = wc2Settings.loginSettings; } this.adapterOptions.adapterSettings = merge(wc2Settings.adapterSettings, this.adapterOptions.adapterSettings); const { adapterSettings } = this.adapterOptions; this.connector = await SignClient.init(adapterSettings?.walletConnectInitOptions); this.wcProvider = new WalletConnectV2Provider({ config: { chainConfig: this.chainConfig as CustomChainConfig }, connector: this.connector }); this.emit(ADAPTER_EVENTS.READY, WALLET_ADAPTERS.WALLET_CONNECT_V2); this.status = ADAPTER_STATUS.READY; log.debug("initializing wallet connect v2 adapter"); if (options.autoConnect) { await this.checkForPersistedSession(); if (this.connected) { this.rehydrated = true; try { await this.onConnectHandler(); } catch (error) { log.error("wallet auto connect", error); this.emit(ADAPTER_EVENTS.ERRORED, error); } } else { this.status = ADAPTER_STATUS.NOT_READY; this.emit(ADAPTER_EVENTS.CACHE_CLEAR); } } } async connect(): Promise<SafeEventEmitterProvider | null> { super.checkConnectionRequirements(); if (!this.connector) throw WalletInitializationError.notReady("Wallet adapter is not ready yet"); try { // if already connected if (this.connected) { await this.onConnectHandler(); return this.provider; } if (this.status !== ADAPTER_STATUS.CONNECTING) { await this.createNewSession(); } return this.provider; } catch (error) { log.error("Wallet connect v2 adapter error while connecting", error); // ready again to be connected this.status = ADAPTER_STATUS.READY; this.rehydrated = true; this.emit(ADAPTER_EVENTS.ERRORED, error); const finalError = error instanceof Web3AuthError ? error : WalletLoginError.connectionError(`Failed to login with wallet connect: ${(error as Error)?.message || ""}`); throw finalError; } } public async addChain(chainConfig: CustomChainConfig, init = false): Promise<void> { super.checkAddChainRequirements(chainConfig, init); if (!isChainIdSupported(this.currentChainNamespace, parseInt(chainConfig.chainId, 16), this.adapterOptions.loginSettings)) { throw WalletOperationsError.chainIDNotAllowed(`Unsupported chainID: ${chainConfig.chainId}`); } await this.wcProvider?.addChain(chainConfig); this.addChainConfig(chainConfig); } public async switchChain(params: { chainId: string }, init = false): Promise<void> { super.checkSwitchChainRequirements(params, init); if (!isChainIdSupported(this.currentChainNamespace, parseInt(params.chainId, 16), this.adapterOptions.loginSettings)) { throw WalletOperationsError.chainIDNotAllowed(`Unsupported chainID: ${params.chainId}`); } await this.wcProvider?.switchChain({ chainId: params.chainId }); this.setAdapterSettings({ chainConfig: this.getChainConfig(params.chainId) as CustomChainConfig }); } async getUserInfo(): Promise<Partial<UserInfo>> { if (!this.connected) throw WalletLoginError.notConnectedError("Not connected with wallet, Please login/connect first"); return {}; } async disconnect(options: { cleanup: boolean } = { cleanup: false }): Promise<void> { await super.disconnectSession(); const { cleanup } = options; if (!this.connector || !this.connected || !this.activeSession?.topic) throw WalletLoginError.notConnectedError("Not connected with wallet"); await this.connector.disconnect({ topic: this.activeSession?.topic, reason: getSdkError("USER_DISCONNECTED") }); this.rehydrated = false; if (cleanup) { this.connector = null; this.status = ADAPTER_STATUS.NOT_READY; this.wcProvider = null; } else { // ready to connect again this.status = ADAPTER_STATUS.READY; } this.activeSession = null; await super.disconnect(); } private cleanupPendingPairings(): void { if (!this.connector) throw WalletInitializationError.notReady("Wallet adapter 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")); } }); } private async checkForPersistedSession(): Promise<SessionTypes.Struct | null> { if (!this.connector) throw WalletInitializationError.notReady("Wallet adapter 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; } private async createNewSession(opts: { forceNewSession: boolean } = { forceNewSession: false }): Promise<void> { try { if (!this.connector) throw WalletInitializationError.notReady("Wallet adapter is not ready yet"); if (!this.adapterOptions.loginSettings) throw WalletInitializationError.notReady("login settings are not set yet"); this.status = ADAPTER_STATUS.CONNECTING; this.emit(ADAPTER_EVENTS.CONNECTING, { adapter: WALLET_ADAPTERS.WALLET_CONNECT_V2 }); if (opts.forceNewSession && this.activeSession?.topic) { await this.connector.disconnect({ topic: this.activeSession?.topic, reason: getSdkError("USER_DISCONNECTED") }); } log.debug("creating new session for web3auth wallet connect"); const { uri, approval } = await this.connector.connect(this.adapterOptions.loginSettings); const qrcodeModal = this.adapterOptions?.adapterSettings?.qrcodeModal; // Open QRCode modal if a URI was returned (i.e. we're not connecting with an existing pairing). if (uri) { if (qrcodeModal) { qrcodeModal.openModal({ uri }); log.debug("EVENT", "QR Code Modal closed"); this.status = ADAPTER_STATUS.READY; this.emit(ADAPTER_EVENTS.READY, WALLET_ADAPTERS.WALLET_CONNECT_V2); } else { this.updateAdapterData({ uri, extensionAdapters: WALLET_CONNECT_EXTENSION_ADAPTERS } as WalletConnectV2Data); } } this.connector.events.once("proposal_expire", (args) => { log.info("proposal expired", args); // Handle proposal expiration this.createNewSession({ forceNewSession: true }); }); 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(); if (qrcodeModal) { qrcodeModal.closeModal(); } } catch (error) { log.error("error while creating new wallet connect session", error); this.emit(ADAPTER_EVENTS.ERRORED, error); throw error; } } private async onConnectHandler() { if (!this.connector || !this.wcProvider) throw WalletInitializationError.notReady("Wallet adapter is not ready yet"); if (!this.chainConfig) throw WalletInitializationError.invalidParams("Chain config is not set"); if (this.adapterOptions.adapterSettings?.qrcodeModal) { this.wcProvider = new WalletConnectV2Provider({ config: { chainConfig: this.chainConfig as CustomChainConfig, skipLookupNetwork: true, }, connector: this.connector, }); } await this.wcProvider.setupProvider(this.connector); this.subscribeEvents(); this.cleanupPendingPairings(); this.status = ADAPTER_STATUS.CONNECTED; this.emit(ADAPTER_EVENTS.CONNECTED, { adapter: WALLET_ADAPTERS.WALLET_CONNECT_V2, reconnected: this.rehydrated } as CONNECTED_EVENT_DATA); } private subscribeEvents(): void { if (!this.connector) throw WalletInitializationError.notReady("Wallet adapter 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 = { ..._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(); }); } } export { WalletConnectV2Adapter };