@web3-wallet/core
Version:
web3-wallet core
388 lines (387 loc) • 13.9 kB
JavaScript
import { createWalletStoreAndActions } from './createWalletStore';
import { isAddChainParameter, ProviderNoFoundError } from './types';
import { parseChainId, toHexChainId } from './utils';
export class Connector {
/**
*
* @param walletName - {@link Connector#WalletName}
* @param actions - {@link WalletStoreActions}
*/
constructor(options) {
const { store, actions } = createWalletStoreAndActions();
this.store = store;
this.actions = actions;
this.options = options;
}
/**
*
* ProviderNoFoundError should be thrown in the following cases
* 1. detectProvider failed to retrieve the provider from a host environment.
* 2. calling functions that requires a provider, but we have been unable to retrieve the provider
*/
// protected providerNotFoundError: ProviderNoFoundError;
get providerNotFoundError() {
return new ProviderNoFoundError(`${this.walletName} provider not found`);
}
/**
* Detect provider in the host environment.
*
* @param providerFilter - providerFilter is provided the detected provider as it's input
* and providerFilter returns a boolean to indicated wether the detected provider can be used.
* 1. detectProvider should throw the ProviderNoFoundError if providerFilter returns false
* 2. detectProvider should throw the ProviderNoFoundError if providerFilter returns false
*
* detectProvider is internally called by {@link Connector#lazyInitialize}
*
* @return Promise<p> -
* 1. resolve with the provider if the detection succeeded.
* 2. reject with an ProviderNotFoundError if it failed to retrieve the provider from the host environment.
*/
async detectProvider(providerFilter, options) {
if (this.provider)
return this.provider;
const m = await import('@web3-wallet/detect-provider');
const injectedProvider = (await m.detectProvider(options ?? this.options?.detectProviderOptions));
if (!injectedProvider)
throw this.providerNotFoundError;
let provider = injectedProvider;
providerFilter =
providerFilter ?? this.options?.providerFilter ?? (() => true);
/**
* handle the case when e.g. metamask and coinbase wallet are both installed
* */
if (injectedProvider.providers?.length) {
provider = injectedProvider.providers?.find(providerFilter);
}
else {
provider = provider && providerFilter(provider) ? provider : undefined;
}
if (!provider) {
throw this.providerNotFoundError;
}
this.provider = provider;
return provider;
}
/**
* `lazyInitialize` does the following two things:
* 1. triggers the provider detection process {@link Connector#detectProvider}
* 2. Register event listeners to the provider {@link Connector#addEventListeners}
*
* `lazyInitialize` is internally called by the following public methods
* - {@link Connector#connect}
* - {@link Connector#autoConnect}
*
* @returns Promise<void>
*/
async lazyInitialize() {
await this.detectProvider();
this.removeEventListeners = this.addEventListeners();
}
/**
* Try to connect to wallet.
*
* autoConnect never reject, it will always resolve.
*
* autoConnect only try to connect to wallet, if it don't need any further
* user interaction with the wallet in the connecting process.
*
* @return Promise<boolean> -
* 1. resolve with `true` if the connection succeeded.
* 2. resolve with `false` if the connection failed.
*
*/
async autoConnect() {
const endConnection = this.actions.startConnection();
try {
await this.lazyInitialize();
const [chainId, accounts] = await Promise.all([
this.requestChainId(),
this.requestAccounts(),
]);
if (!accounts.length)
throw new Error('No accounts returned');
this.updateChainId(chainId);
this.updateAccounts(accounts);
}
catch (e) {
console.debug(`Could not auto connect`, e);
return false;
}
finally {
endConnection();
}
return true;
}
/**
* Initiates a connection.
*
* @param chain - If defined, indicates the desired chain to connect to. If the user is
* already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch
* to the chain, if one of two conditions is met: either they already have it added in their extension, or the
* argument is of type AddEthereumChainParameter, in which case the user will be prompted to add the chain with the
* specified parameters first, before being prompted to switch.
*
* @returns Promise<void>
*/
async connect(chain) {
const endConnection = this.actions.startConnection();
try {
await this.lazyInitialize();
const [chainId, accounts] = await Promise.all([
this.requestChainId(),
this.requestAccounts(),
]);
const receivedChainId = parseChainId(chainId);
const desiredChainId = typeof chain === 'number' ? chain : chain?.chainId;
/**
* there's no desired chain, or it's equal to the received chain
*/
if (!desiredChainId || receivedChainId === desiredChainId) {
this.updateChainId(receivedChainId);
this.updateAccounts(accounts);
/**
* The connection process completed successfully, we can stop from here.
*/
return;
}
/**
* the desiredChainId does match the receivedChainId, try to switch chain
*/
try {
await this.switchChain(desiredChainId);
this.updateChainId(desiredChainId);
this.updateAccounts(accounts);
}
catch (err) {
const error = err;
/**
* switch chain failed, try to add chain
*/
const shouldTryToAddChain = isAddChainParameter(chain);
// const shouldTryToAddChain =
// isAddChainParameter(chain) &&
// (error.code === 4902 ||
// error.message.code === 4902 ||
// error.code === -32603);
/**
* don't know how to handle the error, throw the error again
* and stop from here, the whole connection process failed.
*/
if (!this.addChain || !shouldTryToAddChain)
throw error;
/**
* try to add a new chain to wallet
*/
await this.addChain(chain);
/**
* switch to the added chainId again
*/
await this.switchChain(chain.chainId);
}
}
finally {
endConnection();
}
}
/**
* Disconnect wallet
*
* Wallet connector implementors should override this method if the wallet supports
* force disconnect.
*
* What is force disconnect?
* - force disconnect will actually disconnect the wallet.
* - non-disconnect only reset the wallet store to it's initial state.
*
* For some wallets, MetaMask for example, there're not ways to force disconnect MetaMask.
* For some wallets, Walletconnect for example, we are able to force disconnect Walletconnect.
* @param _force - wether to force disconnect to wallet, default is false.
*
*/
async disconnect(_force) {
this.actions.resetState();
}
/**
* Add an asset to the wallet assets list
*
* @param asset - {@link WatchAssetParameters}
* @return Promise<void>
*/
async watchAsset(asset) {
if (!this.provider)
throw this.providerNotFoundError;
const success = await this.provider.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC20',
options: asset,
},
});
if (!success)
throw new Error(`Failed to watch ${asset.symbol}`);
}
/**
* Update the wallet store with new the chainId
*
* @param chainId - the chainId to update
* @return void
*/
updateChainId(chainId) {
this.actions.update({
chainId: parseChainId(chainId),
});
}
/**
* Update the wallet store with new the new accounts
*
* @param accounts
* @return void
*/
updateAccounts(accounts) {
this.actions.update({ accounts });
}
/**
* Wallet connect listener
*
* @param info - the connect info
* @return void
*/
onConnect(info) {
this.updateChainId(info.chainId);
}
/**
* Wallet disconnect listener
*
* @param error - the disconnect ProviderRpcError
* @return void
*/
onDisconnect(_) {
this.actions.resetState();
}
/**
* Wallet chainId change listener
*
* @param chainId - the new chainId
* @return void
*/
onChainChanged(chainId) {
this.updateChainId(chainId);
}
/**
* wallet account change listener
*/
onAccountsChanged(accounts) {
this.updateAccounts(accounts);
}
/**
* Register event listeners to the provider
*
* @return removeEventListeners - a function to remove the registered event listeners {@link Connector#removeEventListeners}
*/
addEventListeners() {
if (!this.provider)
return;
// return if event listeners are already added
if (this.removeEventListeners)
return;
const onConnect = this.onConnect.bind(this);
const onDisconnect = this.onDisconnect.bind(this);
const onChainChanged = this.onChainChanged.bind(this);
const onAccountsChanged = this.onAccountsChanged.bind(this);
if (typeof this.provider.on === 'function') {
this.provider.on('connect', onConnect);
this.provider.on('disconnect', onDisconnect);
this.provider.on('chainChanged', onChainChanged);
this.provider.on('accountsChanged', onAccountsChanged);
}
else {
this.provider.addListener('connect', onConnect);
this.provider.addListener('disconnect', onDisconnect);
this.provider.addListener('chainChanged', onChainChanged);
this.provider.addListener('accountsChanged', onAccountsChanged);
}
return () => {
if (!this.provider)
return;
if (typeof this.provider.off === 'function') {
this.provider.off('connect', onConnect);
this.provider.off('disconnect', onDisconnect);
this.provider.off('chainChanged', onChainChanged);
this.provider.off('accountsChanged', onAccountsChanged);
}
if (typeof this.provider.removeListener === 'function') {
this.provider.removeListener('connect', onConnect);
this.provider.removeListener('disconnect', onDisconnect);
this.provider.removeListener('chainChanged', onChainChanged);
this.provider.removeListener('accountsChanged', onAccountsChanged);
}
};
}
/**
* Switch network
*
* - {@link SwitchEthereumChainParameter}
*
* @param chainId - the if the the chain to switch to
* @returns Promise<void>
*/
async switchChain(chainId) {
if (!this.provider)
throw this.providerNotFoundError;
await this.provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: toHexChainId(chainId) }],
});
}
/**
* Add a new network/chain to wallet
*
* @param - {@link AddEthereumChainParameter}
* @returns Promise<void>
*/
async addChain(addChainParameter) {
if (!this.provider)
throw this.providerNotFoundError;
await this.provider.request({
method: 'wallet_addEthereumChain',
params: [
{
...addChainParameter,
chainId: toHexChainId(addChainParameter.chainId),
},
],
});
}
/**
* Fetch wallet accounts via the wallet provider
*
* @return Promise<string[]> - the fetched accounts
*/
async requestAccounts() {
if (!this.provider)
throw this.providerNotFoundError;
try {
const accounts = await this.provider.request({
method: 'eth_requestAccounts',
});
return accounts;
}
catch (error) {
console.debug(`Failed to request accounts with 'eth_requestAccounts', try to fallback to 'eth_accounts'`);
const accounts = await this.provider.request({
method: 'eth_accounts',
});
return accounts;
}
}
/**
* Fetch wallet chainId via the wallet provider
*
* @return Promise<string> - the fetched chainId
*/
async requestChainId() {
if (!this.provider)
throw this.providerNotFoundError;
return await this.provider.request({ method: 'eth_chainId' });
}
}