@web3auth-mpc/no-modal
Version:
Multi chain wallet aggregator for web3Auth
342 lines (297 loc) • 13.7 kB
text/typescript
import { SafeEventEmitter } from "@toruslabs/openlogin-jrpc";
import type { OPENLOGIN_NETWORK_TYPE } from "@toruslabs/openlogin-utils";
import {
ADAPTER_EVENTS,
ADAPTER_NAMESPACES,
ADAPTER_STATUS,
ADAPTER_STATUS_TYPE,
CHAIN_NAMESPACES,
CONNECTED_EVENT_DATA,
CustomChainConfig,
getChainConfig,
IAdapter,
IBaseProvider,
IProvider,
IWeb3Auth,
log,
storageAvailable,
UserAuthInfo,
UserInfo,
WALLET_ADAPTER_TYPE,
WalletInitializationError,
WalletLoginError,
Web3AuthError,
} from "@web3auth-mpc/base";
import { IPlugin, PLUGIN_NAMESPACES } from "@web3auth-mpc/base-plugin";
import { CommonJRPCProvider } from "@web3auth-mpc/base-provider";
export interface Web3AuthNoModalOptions {
/**
* Client id for web3auth.
* You can obtain your client id from the web3auth developer dashboard.
* You can set any random string for this on localhost.
*/
clientId: string;
/**
* custom chain configuration for chainNamespace
*
* @defaultValue mainnet config of provided chainNamespace
*/
chainConfig: Partial<CustomChainConfig> & Pick<CustomChainConfig, "chainNamespace">;
/**
* setting to true will enable logs
*
* @defaultValue false
*/
enableLogging?: boolean;
/**
* setting to "local" will persist social login session accross browser tabs.
*
* @defaultValue "local"
*/
storageKey?: "session" | "local";
/**
* sessionTime (in seconds) for idToken issued by Web3Auth for server side verification.
* @defaultValue 86400
*
* Note: max value can be 7 days (86400 * 7) and min can be 1 day (86400)
*/
sessionTime?: number;
/**
* Web3Auth Network to use for the session & the issued idToken
* @defaultValue mainnet
*/
web3AuthNetwork?: OPENLOGIN_NETWORK_TYPE;
/**
* Uses core-kit key with web3auth provider
* @defaultValue false
*/
useCoreKitKey?: boolean;
}
const ADAPTER_CACHE_KEY = "Web3Auth-cachedAdapter";
export class Web3AuthNoModal extends SafeEventEmitter implements IWeb3Auth {
readonly coreOptions: Web3AuthNoModalOptions;
public connectedAdapterName: WALLET_ADAPTER_TYPE | null = null;
public status: ADAPTER_STATUS_TYPE = ADAPTER_STATUS.NOT_READY;
public cachedAdapter: string | null = null;
protected walletAdapters: Record<string, IAdapter<unknown>> = {};
protected commonJRPCProvider: CommonJRPCProvider | null = null;
private plugins: Record<string, IPlugin> = {};
private storage: "sessionStorage" | "localStorage" = "localStorage";
constructor(options: Web3AuthNoModalOptions) {
super();
if (!options.clientId) throw WalletInitializationError.invalidParams("Please provide a valid clientId in constructor");
if (options.enableLogging) log.enableAll();
else log.setLevel("error");
if (!options.chainConfig?.chainNamespace || !Object.values(CHAIN_NAMESPACES).includes(options.chainConfig?.chainNamespace))
throw WalletInitializationError.invalidParams("Please provide a valid chainNamespace in chainConfig");
if (options.storageKey === "session") this.storage = "sessionStorage";
this.cachedAdapter = storageAvailable(this.storage) ? window[this.storage].getItem(ADAPTER_CACHE_KEY) : null;
this.coreOptions = {
...options,
chainConfig: {
...(getChainConfig(options.chainConfig?.chainNamespace, options.chainConfig?.chainId) || {}),
...options.chainConfig,
},
};
this.subscribeToAdapterEvents = this.subscribeToAdapterEvents.bind(this);
}
get connected(): boolean {
return Boolean(this.connectedAdapterName);
}
get provider(): IProvider | null {
if (this.status !== ADAPTER_STATUS.NOT_READY && this.commonJRPCProvider) {
return this.commonJRPCProvider;
}
return null;
}
set provider(_: IProvider | null) {
throw new Error("Not implemented");
}
public async init(): Promise<void> {
this.commonJRPCProvider = await CommonJRPCProvider.getProviderInstance({ chainConfig: this.coreOptions.chainConfig as CustomChainConfig });
const initPromises = Object.keys(this.walletAdapters).map((adapterName) => {
this.subscribeToAdapterEvents(this.walletAdapters[adapterName]);
// if adapter doesn't have any chain config yet thn set it based on provided namespace and chainId.
// if no chainNamespace or chainId is being provided, it will connect with mainnet.
if (!this.walletAdapters[adapterName].chainConfigProxy) {
const providedChainConfig = this.coreOptions.chainConfig;
if (!providedChainConfig.chainNamespace) throw WalletInitializationError.invalidParams("Please provide chainNamespace in chainConfig");
this.walletAdapters[adapterName].setAdapterSettings({
chainConfig: providedChainConfig,
sessionTime: this.coreOptions.sessionTime,
clientId: this.coreOptions.clientId,
web3AuthNetwork: this.coreOptions.web3AuthNetwork,
useCoreKitKey: this.coreOptions.useCoreKitKey,
});
} else {
this.walletAdapters[adapterName].setAdapterSettings({
sessionTime: this.coreOptions.sessionTime,
clientId: this.coreOptions.clientId,
web3AuthNetwork: this.coreOptions.web3AuthNetwork,
useCoreKitKey: this.coreOptions.useCoreKitKey,
});
}
return this.walletAdapters[adapterName].init({ autoConnect: this.cachedAdapter === adapterName }).catch((e) => log.error(e));
});
this.status = ADAPTER_STATUS.READY;
await Promise.all(initPromises);
}
public configureAdapter(adapter: IAdapter<unknown>): Web3AuthNoModal {
this.checkInitRequirements();
const providedChainConfig = this.coreOptions.chainConfig;
if (!providedChainConfig.chainNamespace) throw WalletInitializationError.invalidParams("Please provide chainNamespace in chainConfig");
const adapterAlreadyExists = this.walletAdapters[adapter.name];
if (adapterAlreadyExists) throw WalletInitializationError.duplicateAdapterError(`Wallet adapter for ${adapter.name} already exists`);
if (adapter.adapterNamespace !== ADAPTER_NAMESPACES.MULTICHAIN && adapter.adapterNamespace !== providedChainConfig.chainNamespace)
throw WalletInitializationError.incompatibleChainNameSpace(
`This wallet adapter belongs to ${adapter.adapterNamespace} which is incompatible with currently used namespace: ${providedChainConfig.chainNamespace}`
);
if (
adapter.adapterNamespace === ADAPTER_NAMESPACES.MULTICHAIN &&
adapter.currentChainNamespace &&
providedChainConfig.chainNamespace !== adapter.currentChainNamespace
) {
// chainConfig checks are already validated in constructor so using typecast is safe here.
adapter.setAdapterSettings({ chainConfig: providedChainConfig as CustomChainConfig });
}
this.walletAdapters[adapter.name] = adapter;
return this;
}
public clearCache() {
if (!storageAvailable(this.storage)) return;
window[this.storage].removeItem(ADAPTER_CACHE_KEY);
this.cachedAdapter = null;
}
public async addChain(chainConfig: CustomChainConfig): Promise<void> {
if (this.status === ADAPTER_STATUS.CONNECTED && this.connectedAdapterName)
return this.walletAdapters[this.connectedAdapterName].addChain(chainConfig);
if (this.commonJRPCProvider) {
return this.commonJRPCProvider.addChain(chainConfig);
}
throw WalletInitializationError.notReady(`No wallet is ready`);
}
public async switchChain(params: { chainId: string }): Promise<void> {
if (this.status === ADAPTER_STATUS.CONNECTED && this.connectedAdapterName)
return this.walletAdapters[this.connectedAdapterName].switchChain(params);
if (this.commonJRPCProvider) {
return this.commonJRPCProvider.switchChain(params);
}
throw WalletInitializationError.notReady(`No wallet is ready`);
}
/**
* Connect to a specific wallet adapter
* @param walletName - Key of the walletAdapter to use.
*/
async connectTo<T>(walletName: WALLET_ADAPTER_TYPE, loginParams?: T): Promise<IProvider | null> {
if (!this.walletAdapters[walletName] || !this.commonJRPCProvider)
throw WalletInitializationError.notFound(`Please add wallet adapter for ${walletName} wallet, before connecting`);
const provider = await this.walletAdapters[walletName].connect(loginParams);
this.commonJRPCProvider.updateProviderEngineProxy((provider as IBaseProvider<unknown>).provider || provider);
return this.provider;
}
async logout(options: { cleanup: boolean } = { cleanup: false }): Promise<void> {
if (this.status !== ADAPTER_STATUS.CONNECTED || !this.connectedAdapterName) throw WalletLoginError.notConnectedError(`No wallet is connected`);
await this.walletAdapters[this.connectedAdapterName].disconnect(options);
}
async getUserInfo(): Promise<Partial<UserInfo>> {
log.debug("Getting user info", this.status, this.connectedAdapterName);
if (this.status !== ADAPTER_STATUS.CONNECTED || !this.connectedAdapterName) throw WalletLoginError.notConnectedError(`No wallet is connected`);
return this.walletAdapters[this.connectedAdapterName].getUserInfo();
}
async authenticateUser(): Promise<UserAuthInfo> {
if (this.status !== ADAPTER_STATUS.CONNECTED || !this.connectedAdapterName) throw WalletLoginError.notConnectedError(`No wallet is connected`);
return this.walletAdapters[this.connectedAdapterName].authenticateUser();
}
public async addPlugin(plugin: IPlugin): Promise<IWeb3Auth> {
if (this.plugins[plugin.name]) throw new Error(`Plugin ${plugin.name} already exist`);
if (plugin.pluginNamespace !== PLUGIN_NAMESPACES.MULTICHAIN && plugin.pluginNamespace !== this.coreOptions.chainConfig.chainNamespace)
throw new Error(
`This plugin belongs to ${plugin.pluginNamespace} namespace which is incompatible with currently used namespace: ${this.coreOptions.chainConfig.chainNamespace}`
);
this.plugins[plugin.name] = plugin;
return this;
}
protected subscribeToAdapterEvents(walletAdapter: IAdapter<unknown>): void {
walletAdapter.on(ADAPTER_EVENTS.CONNECTED, async (data: CONNECTED_EVENT_DATA) => {
if (!this.commonJRPCProvider) throw WalletInitializationError.notFound(`CommonJrpcProvider not found`);
const { provider } = this.walletAdapters[data.adapter];
this.commonJRPCProvider.updateProviderEngineProxy((provider as IBaseProvider<unknown>).provider || provider);
this.status = ADAPTER_STATUS.CONNECTED;
this.connectedAdapterName = data.adapter;
this.cacheWallet(data.adapter);
log.debug("connected", this.status, this.connectedAdapterName);
Object.values(this.plugins).map(async (plugin) => {
try {
if (!plugin.SUPPORTED_ADAPTERS.includes(data.adapter)) {
return;
}
await plugin.initWithWeb3Auth(this);
await plugin.connect();
} catch (error: unknown) {
// swallow error if connector adapter doesn't supports this plugin.
if ((error as Web3AuthError).code === 5211) {
return;
}
log.error(error);
}
});
this.emit(ADAPTER_EVENTS.CONNECTED, { ...data } as CONNECTED_EVENT_DATA);
});
walletAdapter.on(ADAPTER_EVENTS.DISCONNECTED, async (data) => {
// get back to ready state for rehydrating.
this.status = ADAPTER_STATUS.READY;
if (storageAvailable(this.storage)) {
const cachedAdapter = window[this.storage].getItem(ADAPTER_CACHE_KEY);
if (this.connectedAdapterName === cachedAdapter) {
this.clearCache();
}
}
log.debug("disconnected", this.status, this.connectedAdapterName);
await Promise.all(
Object.values(this.plugins).map((plugin) => {
return plugin.disconnect().catch((error: Web3AuthError) => {
// swallow error if adapter doesn't supports this plugin.
if (error.code === 5211) {
return;
}
// throw error;
log.error(error);
});
})
);
this.connectedAdapterName = null;
this.emit(ADAPTER_EVENTS.DISCONNECTED, data);
});
walletAdapter.on(ADAPTER_EVENTS.CONNECTING, (data) => {
this.status = ADAPTER_STATUS.CONNECTING;
this.emit(ADAPTER_EVENTS.CONNECTING, data);
log.debug("connecting", this.status, this.connectedAdapterName);
});
walletAdapter.on(ADAPTER_EVENTS.ERRORED, (data) => {
this.status = ADAPTER_STATUS.ERRORED;
this.clearCache();
this.emit(ADAPTER_EVENTS.ERRORED, data);
log.debug("errored", this.status, this.connectedAdapterName);
});
walletAdapter.on(ADAPTER_EVENTS.ADAPTER_DATA_UPDATED, (data) => {
log.debug("adapter data updated", data);
this.emit(ADAPTER_EVENTS.ADAPTER_DATA_UPDATED, data);
});
walletAdapter.on(ADAPTER_EVENTS.CACHE_CLEAR, (data) => {
log.debug("adapter cache clear", data);
if (storageAvailable(this.storage)) {
this.clearCache();
}
});
}
protected checkInitRequirements(): void {
if (this.status === ADAPTER_STATUS.CONNECTING) throw WalletInitializationError.notReady("Already pending connection");
if (this.status === ADAPTER_STATUS.CONNECTED) throw WalletInitializationError.notReady("Already connected");
if (this.status === ADAPTER_STATUS.READY) throw WalletInitializationError.notReady("Adapter is already initialized");
}
private cacheWallet(walletName: string) {
if (!storageAvailable(this.storage)) return;
window[this.storage].setItem(ADAPTER_CACHE_KEY, walletName);
this.cachedAdapter = walletName;
}
}