@web3-wallet/core
Version:
web3-wallet core
415 lines (414 loc) • 15.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Connector = void 0;
const createWalletStore_1 = require("./createWalletStore");
const types_1 = require("./types");
const utils_1 = require("./utils");
class Connector {
/**
*
* @param walletName - {@link Connector#WalletName}
* @param actions - {@link WalletStoreActions}
*/
constructor(options) {
const { store, actions } = (0, createWalletStore_1.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 types_1.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 Promise.resolve().then(() => __importStar(require('@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 = (0, utils_1.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 = (0, types_1.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: (0, utils_1.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: (0, utils_1.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: (0, utils_1.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' });
}
}
exports.Connector = Connector;