@wagmi/core
Version:
VanillaJS library for Ethereum
530 lines • 24.3 kB
JavaScript
import { ResourceUnavailableRpcError, SwitchChainError, UserRejectedRequestError, getAddress, numberToHex, withRetry, withTimeout, } from 'viem';
import { ChainNotConfiguredError } from '../errors/config.js';
import { ProviderNotFoundError } from '../errors/connector.js';
import { createConnector } from './createConnector.js';
injected.type = 'injected';
export function injected(parameters = {}) {
const { shimDisconnect = true, unstable_shimAsyncInject } = parameters;
function getTarget() {
const target = parameters.target;
if (typeof target === 'function') {
const result = target();
if (result)
return result;
}
if (typeof target === 'object')
return target;
if (typeof target === 'string')
return {
...(targetMap[target] ?? {
id: target,
name: `${target[0].toUpperCase()}${target.slice(1)}`,
provider: `is${target[0].toUpperCase()}${target.slice(1)}`,
}),
};
return {
id: 'injected',
name: 'Injected',
provider(window) {
return window?.ethereum;
},
};
}
let accountsChanged;
let chainChanged;
let connect;
let disconnect;
return createConnector((config) => ({
get icon() {
return getTarget().icon;
},
get id() {
return getTarget().id;
},
get name() {
return getTarget().name;
},
/** @deprecated */
get supportsSimulation() {
return true;
},
type: injected.type,
async setup() {
const provider = await this.getProvider();
// Only start listening for events if `target` is set, otherwise `injected()` will also receive events
if (provider?.on && parameters.target) {
if (!connect) {
connect = this.onConnect.bind(this);
provider.on('connect', connect);
}
// We shouldn't need to listen for `'accountsChanged'` here since the `'connect'` event should suffice (and wallet shouldn't be connected yet).
// Some wallets, like MetaMask, do not implement the `'connect'` event and overload `'accountsChanged'` instead.
if (!accountsChanged) {
accountsChanged = this.onAccountsChanged.bind(this);
provider.on('accountsChanged', accountsChanged);
}
}
},
async connect({ chainId, isReconnecting } = {}) {
const provider = await this.getProvider();
if (!provider)
throw new ProviderNotFoundError();
let accounts = [];
if (isReconnecting)
accounts = await this.getAccounts().catch(() => []);
else if (shimDisconnect) {
// Attempt to show another prompt for selecting account if `shimDisconnect` flag is enabled
try {
const permissions = await provider.request({
method: 'wallet_requestPermissions',
params: [{ eth_accounts: {} }],
});
accounts = permissions[0]?.caveats?.[0]?.value?.map((x) => getAddress(x));
// `'wallet_requestPermissions'` can return a different order of accounts than `'eth_accounts'`
// switch to `'eth_accounts'` ordering if more than one account is connected
// https://github.com/wevm/wagmi/issues/4140
if (accounts.length > 0) {
const sortedAccounts = await this.getAccounts();
accounts = sortedAccounts;
}
}
catch (err) {
const error = err;
// Not all injected providers support `wallet_requestPermissions` (e.g. MetaMask iOS).
// Only bubble up error if user rejects request
if (error.code === UserRejectedRequestError.code)
throw new UserRejectedRequestError(error);
// Or prompt is already open
if (error.code === ResourceUnavailableRpcError.code)
throw error;
}
}
try {
if (!accounts?.length && !isReconnecting) {
const requestedAccounts = await provider.request({
method: 'eth_requestAccounts',
});
accounts = requestedAccounts.map((x) => getAddress(x));
}
// Manage EIP-1193 event listeners
// https://eips.ethereum.org/EIPS/eip-1193#events
if (connect) {
provider.removeListener('connect', connect);
connect = undefined;
}
if (!accountsChanged) {
accountsChanged = this.onAccountsChanged.bind(this);
provider.on('accountsChanged', accountsChanged);
}
if (!chainChanged) {
chainChanged = this.onChainChanged.bind(this);
provider.on('chainChanged', chainChanged);
}
if (!disconnect) {
disconnect = this.onDisconnect.bind(this);
provider.on('disconnect', disconnect);
}
// Switch to chain if provided
let currentChainId = await this.getChainId();
if (chainId && currentChainId !== chainId) {
const chain = await this.switchChain({ chainId }).catch((error) => {
if (error.code === UserRejectedRequestError.code)
throw error;
return { id: currentChainId };
});
currentChainId = chain?.id ?? currentChainId;
}
// Remove disconnected shim if it exists
if (shimDisconnect)
await config.storage?.removeItem(`${this.id}.disconnected`);
// Add connected shim if no target exists
if (!parameters.target)
await config.storage?.setItem('injected.connected', true);
return { accounts, chainId: currentChainId };
}
catch (err) {
const error = err;
if (error.code === UserRejectedRequestError.code)
throw new UserRejectedRequestError(error);
if (error.code === ResourceUnavailableRpcError.code)
throw new ResourceUnavailableRpcError(error);
throw error;
}
},
async disconnect() {
const provider = await this.getProvider();
if (!provider)
throw new ProviderNotFoundError();
// Manage EIP-1193 event listeners
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged);
chainChanged = undefined;
}
if (disconnect) {
provider.removeListener('disconnect', disconnect);
disconnect = undefined;
}
if (!connect) {
connect = this.onConnect.bind(this);
provider.on('connect', connect);
}
// Experimental support for MetaMask disconnect
// https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-2.md
try {
// Adding timeout as not all wallets support this method and can hang
// https://github.com/wevm/wagmi/issues/4064
await withTimeout(() =>
// TODO: Remove explicit type for viem@3
provider.request({
// `'wallet_revokePermissions'` added in `viem@2.10.3`
method: 'wallet_revokePermissions',
params: [{ eth_accounts: {} }],
}), { timeout: 100 });
}
catch { }
// Add shim signalling connector is disconnected
if (shimDisconnect) {
await config.storage?.setItem(`${this.id}.disconnected`, true);
}
if (!parameters.target)
await config.storage?.removeItem('injected.connected');
},
async getAccounts() {
const provider = await this.getProvider();
if (!provider)
throw new ProviderNotFoundError();
const accounts = await provider.request({ method: 'eth_accounts' });
return accounts.map((x) => getAddress(x));
},
async getChainId() {
const provider = await this.getProvider();
if (!provider)
throw new ProviderNotFoundError();
const hexChainId = await provider.request({ method: 'eth_chainId' });
return Number(hexChainId);
},
async getProvider() {
if (typeof window === 'undefined')
return undefined;
let provider;
const target = getTarget();
if (typeof target.provider === 'function')
provider = target.provider(window);
else if (typeof target.provider === 'string')
provider = findProvider(window, target.provider);
else
provider = target.provider;
// Some wallets do not conform to EIP-1193 (e.g. Trust Wallet)
// https://github.com/wevm/wagmi/issues/3526#issuecomment-1912683002
if (provider && !provider.removeListener) {
// Try using `off` handler if it exists, otherwise noop
if ('off' in provider && typeof provider.off === 'function')
provider.removeListener =
provider.off;
else
provider.removeListener = () => { };
}
return provider;
},
async isAuthorized() {
try {
const isDisconnected = shimDisconnect &&
// If shim exists in storage, connector is disconnected
(await config.storage?.getItem(`${this.id}.disconnected`));
if (isDisconnected)
return false;
// Don't allow injected connector to connect if no target is set and it hasn't already connected
// (e.g. flag in storage is not set). This prevents a targetless injected connector from connecting
// automatically whenever there is a targeted connector configured.
if (!parameters.target) {
const connected = await config.storage?.getItem('injected.connected');
if (!connected)
return false;
}
const provider = await this.getProvider();
if (!provider) {
if (unstable_shimAsyncInject !== undefined &&
unstable_shimAsyncInject !== false) {
// If no provider is found, check for async injection
// https://github.com/wevm/references/issues/167
// https://github.com/MetaMask/detect-provider
const handleEthereum = async () => {
if (typeof window !== 'undefined')
window.removeEventListener('ethereum#initialized', handleEthereum);
const provider = await this.getProvider();
return !!provider;
};
const timeout = typeof unstable_shimAsyncInject === 'number'
? unstable_shimAsyncInject
: 1_000;
const res = await Promise.race([
...(typeof window !== 'undefined'
? [
new Promise((resolve) => window.addEventListener('ethereum#initialized', () => resolve(handleEthereum()), { once: true })),
]
: []),
new Promise((resolve) => setTimeout(() => resolve(handleEthereum()), timeout)),
]);
if (res)
return true;
}
throw new ProviderNotFoundError();
}
// Use retry strategy as some injected wallets (e.g. MetaMask) fail to
// immediately resolve JSON-RPC requests on page load.
const accounts = await withRetry(() => this.getAccounts());
return !!accounts.length;
}
catch {
return false;
}
},
async switchChain({ addEthereumChainParameter, chainId }) {
const provider = await this.getProvider();
if (!provider)
throw new ProviderNotFoundError();
const chain = config.chains.find((x) => x.id === chainId);
if (!chain)
throw new SwitchChainError(new ChainNotConfiguredError());
const promise = new Promise((resolve) => {
const listener = ((data) => {
if ('chainId' in data && data.chainId === chainId) {
config.emitter.off('change', listener);
resolve();
}
});
config.emitter.on('change', listener);
});
try {
await Promise.all([
provider
.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: numberToHex(chainId) }],
})
// During `'wallet_switchEthereumChain'`, MetaMask makes a `'net_version'` RPC call to the target chain.
// If this request fails, MetaMask does not emit the `'chainChanged'` event, but will still switch the chain.
// To counter this behavior, we request and emit the current chain ID to confirm the chain switch either via
// this callback or an externally emitted `'chainChanged'` event.
// https://github.com/MetaMask/metamask-extension/issues/24247
.then(async () => {
const currentChainId = await this.getChainId();
if (currentChainId === chainId)
config.emitter.emit('change', { chainId });
}),
promise,
]);
return chain;
}
catch (err) {
const error = err;
// Indicates chain is not added to provider
if (error.code === 4902 ||
// Unwrapping for MetaMask Mobile
// https://github.com/MetaMask/metamask-mobile/issues/2944#issuecomment-976988719
error
?.data?.originalError?.code === 4902) {
try {
const { default: blockExplorer, ...blockExplorers } = chain.blockExplorers ?? {};
let blockExplorerUrls;
if (addEthereumChainParameter?.blockExplorerUrls)
blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls;
else if (blockExplorer)
blockExplorerUrls = [
blockExplorer.url,
...Object.values(blockExplorers).map((x) => x.url),
];
let rpcUrls;
if (addEthereumChainParameter?.rpcUrls?.length)
rpcUrls = addEthereumChainParameter.rpcUrls;
else
rpcUrls = [chain.rpcUrls.default?.http[0] ?? ''];
const addEthereumChain = {
blockExplorerUrls,
chainId: numberToHex(chainId),
chainName: addEthereumChainParameter?.chainName ?? chain.name,
iconUrls: addEthereumChainParameter?.iconUrls,
nativeCurrency: addEthereumChainParameter?.nativeCurrency ??
chain.nativeCurrency,
rpcUrls,
};
await Promise.all([
provider
.request({
method: 'wallet_addEthereumChain',
params: [addEthereumChain],
})
.then(async () => {
const currentChainId = await this.getChainId();
if (currentChainId === chainId)
config.emitter.emit('change', { chainId });
else
throw new UserRejectedRequestError(new Error('User rejected switch after adding network.'));
}),
promise,
]);
return chain;
}
catch (error) {
throw new UserRejectedRequestError(error);
}
}
if (error.code === UserRejectedRequestError.code)
throw new UserRejectedRequestError(error);
throw new SwitchChainError(error);
}
},
async onAccountsChanged(accounts) {
// Disconnect if there are no accounts
if (accounts.length === 0)
this.onDisconnect();
// Connect if emitter is listening for connect event (e.g. is disconnected and connects through wallet interface)
else if (config.emitter.listenerCount('connect')) {
const chainId = (await this.getChainId()).toString();
this.onConnect({ chainId });
// Remove disconnected shim if it exists
if (shimDisconnect)
await config.storage?.removeItem(`${this.id}.disconnected`);
}
// Regular change event
else
config.emitter.emit('change', {
accounts: accounts.map((x) => getAddress(x)),
});
},
onChainChanged(chain) {
const chainId = Number(chain);
config.emitter.emit('change', { chainId });
},
async onConnect(connectInfo) {
const accounts = await this.getAccounts();
if (accounts.length === 0)
return;
const chainId = Number(connectInfo.chainId);
config.emitter.emit('connect', { accounts, chainId });
// Manage EIP-1193 event listeners
const provider = await this.getProvider();
if (provider) {
if (connect) {
provider.removeListener('connect', connect);
connect = undefined;
}
if (!accountsChanged) {
accountsChanged = this.onAccountsChanged.bind(this);
provider.on('accountsChanged', accountsChanged);
}
if (!chainChanged) {
chainChanged = this.onChainChanged.bind(this);
provider.on('chainChanged', chainChanged);
}
if (!disconnect) {
disconnect = this.onDisconnect.bind(this);
provider.on('disconnect', disconnect);
}
}
},
async onDisconnect(error) {
const provider = await this.getProvider();
// If MetaMask emits a `code: 1013` error, wait for reconnection before disconnecting
// https://github.com/MetaMask/providers/pull/120
if (error && error.code === 1013) {
if (provider && !!(await this.getAccounts()).length)
return;
}
// No need to remove `${this.id}.disconnected` from storage because `onDisconnect` is typically
// only called when the wallet is disconnected through the wallet's interface, meaning the wallet
// actually disconnected and we don't need to simulate it.
config.emitter.emit('disconnect');
// Manage EIP-1193 event listeners
if (provider) {
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged);
chainChanged = undefined;
}
if (disconnect) {
provider.removeListener('disconnect', disconnect);
disconnect = undefined;
}
if (!connect) {
connect = this.onConnect.bind(this);
provider.on('connect', connect);
}
}
},
}));
}
const targetMap = {
coinbaseWallet: {
id: 'coinbaseWallet',
name: 'Coinbase Wallet',
provider(window) {
if (window?.coinbaseWalletExtension)
return window.coinbaseWalletExtension;
return findProvider(window, 'isCoinbaseWallet');
},
},
metaMask: {
id: 'metaMask',
name: 'MetaMask',
provider(window) {
return findProvider(window, (provider) => {
if (!provider.isMetaMask)
return false;
// Brave tries to make itself look like MetaMask
// Could also try RPC `web3_clientVersion` if following is unreliable
if (provider.isBraveWallet && !provider._events && !provider._state)
return false;
// Other wallets that try to look like MetaMask
const flags = [
'isApexWallet',
'isAvalanche',
'isBitKeep',
'isBlockWallet',
'isKuCoinWallet',
'isMathWallet',
'isOkxWallet',
'isOKExWallet',
'isOneInchIOSWallet',
'isOneInchAndroidWallet',
'isOpera',
'isPhantom',
'isPortal',
'isRabby',
'isTokenPocket',
'isTokenary',
'isUniswapWallet',
'isZerion',
];
for (const flag of flags)
if (provider[flag])
return false;
return true;
});
},
},
phantom: {
id: 'phantom',
name: 'Phantom',
provider(window) {
if (window?.phantom?.ethereum)
return window.phantom?.ethereum;
return findProvider(window, 'isPhantom');
},
},
};
function findProvider(window, select) {
function isProvider(provider) {
if (typeof select === 'function')
return select(provider);
if (typeof select === 'string')
return provider[select];
return true;
}
const ethereum = window.ethereum;
if (ethereum?.providers)
return ethereum.providers.find((provider) => isProvider(provider));
if (ethereum && isProvider(ethereum))
return ethereum;
return undefined;
}
//# sourceMappingURL=injected.js.map