@wagmi/connectors
Version:
Collection of connectors for Wagmi
310 lines (292 loc) • 10.7 kB
text/typescript
import type {
createEVMClient,
EIP1193Provider,
MetamaskConnectEVM,
} from '@metamask/connect-evm'
import { ChainNotConfiguredError, createConnector } from '@wagmi/core'
import type { ExactPartial, OneOf, UnionCompute } from '@wagmi/core/internal'
import {
type Address,
getAddress,
numberToHex,
type ProviderConnectInfo,
ResourceUnavailableRpcError,
type RpcError,
SwitchChainError,
UserRejectedRequestError,
withRetry,
withTimeout,
} from 'viem'
export type MetaMaskParameters = UnionCompute<
ExactPartial<Omit<CreateEVMClientParameters, 'api' | 'eventHandlers'>> & {
/**
* @deprecated Use `dapp` instead.
*
* Metadata is used to fill details for the UX on confirmation screens in MetaMask.
*/
dappMetadata?: { name?: string; url?: string; iconUrl?: string } | undefined
/**
* @deprecated Use `debug` instead.
*/
logging?: { sdk?: boolean } | undefined
/**
* @deprecated Use `ui.headless` instead.
*/
headless?: boolean | undefined
} & OneOf<
| {
/* Shortcut to connect and sign a message */
connectAndSign?: string | undefined
}
| {
// TODO: Strongly type `method` and `params`
/* Allow `connectWith` any rpc method */
connectWith?: { method: string; params: unknown[] } | undefined
}
>
>
type CreateEVMClientParameters = Parameters<typeof createEVMClient>[0]
metaMask.type = 'metaMask' as const
export function metaMask(parameters: MetaMaskParameters = {}) {
type Provider = EIP1193Provider
type Properties = {
onConnect(connectInfo: ProviderConnectInfo): void
onDisplayUri(uri: string): void
getInstance(): Promise<MetamaskConnectEVM>
}
let metamask: MetamaskConnectEVM | undefined
let metamaskPromise: Promise<MetamaskConnectEVM> | undefined
return createConnector<Provider, Properties>((config) => ({
id: 'metaMaskSDK',
name: 'MetaMask',
rdns: ['io.metamask', 'io.metamask.mobile'],
type: metaMask.type,
async connect({ chainId, isReconnecting, withCapabilities } = {}) {
const instance = await this.getInstance()
const provider = instance.getProvider()
let accounts: readonly Address[] = []
if (isReconnecting) accounts = await this.getAccounts().catch(() => [])
try {
let signResponse: string | undefined
let connectWithResponse: unknown | undefined
if (!accounts?.length) {
const chainIds = config.chains.map((chain) => numberToHex(chain.id))
if (parameters.connectAndSign || parameters.connectWith) {
if (parameters.connectAndSign)
signResponse = await instance.connectAndSign({
chainIds,
message: parameters.connectAndSign,
})
else if (parameters.connectWith)
connectWithResponse = await instance.connectWith({
chainIds,
method: parameters.connectWith.method,
params: parameters.connectWith.params,
})
accounts = await this.getAccounts()
} else {
const result = await instance.connect({ chainIds })
accounts = result.accounts.map((x) => getAddress(x))
}
}
// 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
}
if (signResponse)
provider.emit('connectAndSign', {
accounts,
chainId: numberToHex(currentChainId),
signResponse,
})
else if (connectWithResponse)
provider.emit('connectWith', {
accounts,
chainId: numberToHex(currentChainId),
connectWithResponse,
})
return {
// TODO(v3): Make `withCapabilities: true` default behavior
accounts: (withCapabilities
? accounts.map((address) => ({ address, capabilities: {} }))
: accounts) as never,
chainId: currentChainId,
}
} catch (err) {
const error = err as RpcError
if (error.code === UserRejectedRequestError.code)
throw new UserRejectedRequestError(error)
if (error.code === ResourceUnavailableRpcError.code)
throw new ResourceUnavailableRpcError(error)
throw error
}
},
async disconnect() {
const instance = await this.getInstance()
return instance.disconnect()
},
async getAccounts() {
const instance = await this.getInstance()
if (instance.accounts.length)
return instance.accounts.map((x) => getAddress(x))
// Fallback to provider if SDK doesn't return accounts
const provider = instance.getProvider()
const accounts = (await provider.request({
method: 'eth_accounts',
})) as string[]
return accounts.map((x) => getAddress(x))
},
async getChainId() {
const instance = await this.getInstance()
if (instance.getChainId()) return Number(instance.getChainId())
// Fallback to provider if SDK doesn't return chainId
const provider = instance.getProvider()
const chainId = await provider.request({ method: 'eth_chainId' })
return Number(chainId)
},
async getProvider() {
const instance = await this.getInstance()
return instance.getProvider()
},
async isAuthorized() {
try {
// MetaMask mobile provider sometimes fails to immediately resolve
// JSON-RPC requests on page load
const timeout = 10
const accounts = await withRetry(
async () =>
withTimeout(
async () => {
const accounts = await this.getAccounts()
if (!accounts.length) throw new Error('try again')
return accounts
},
{ timeout },
),
{ delay: timeout + 1, retryCount: 3 },
)
return Boolean(accounts.length)
} catch {
return false
}
},
async switchChain({ addEthereumChainParameter, chainId }) {
const chain = config.chains.find(({ id }) => id === Number(chainId))
if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
const hexChainId = numberToHex(chainId)
try {
const instance = await this.getInstance()
await instance.switchChain({
chainId: hexChainId,
chainConfiguration: {
blockExplorerUrls: addEthereumChainParameter?.blockExplorerUrls
? [...addEthereumChainParameter.blockExplorerUrls]
: chain.blockExplorers?.default.url
? [chain.blockExplorers.default.url]
: undefined,
chainId: hexChainId,
chainName: addEthereumChainParameter?.chainName ?? chain.name,
iconUrls: addEthereumChainParameter?.iconUrls,
nativeCurrency:
addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency,
rpcUrls: addEthereumChainParameter?.rpcUrls
? [...addEthereumChainParameter.rpcUrls]
: chain.rpcUrls.default?.http
? [...chain.rpcUrls.default.http]
: undefined,
},
})
return chain
} catch (err) {
const error = err as RpcError
if (error.code === UserRejectedRequestError.code)
throw new UserRejectedRequestError(error)
throw new SwitchChainError(error)
}
},
async onAccountsChanged(accounts) {
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 })
},
async onDisconnect(error) {
// If MetaMask emits a `code: 1013` error, wait for reconnection before disconnecting
// https://github.com/MetaMask/providers/pull/120
if (error && (error as RpcError<1013>).code === 1013) {
const provider = await this.getProvider()
if (provider && Boolean((await this.getAccounts()).length)) return
}
config.emitter.emit('disconnect')
},
onDisplayUri(uri) {
config.emitter.emit('message', { type: 'display_uri', data: uri })
},
async getInstance() {
if (!metamask) {
if (!metamaskPromise) {
const { createEVMClient } = await (async () => {
try {
return import('@metamask/connect-evm')
} catch {
throw new Error('dependency "@metamask/connect-evm" not found')
}
})()
const defaultDappParams =
typeof window === 'undefined'
? { name: 'wagmi' }
: { name: window.location.hostname, url: window.location.href }
metamaskPromise = createEVMClient({
...parameters,
api: {
supportedNetworks: Object.fromEntries(
config.chains.map((chain) => [
numberToHex(chain.id),
chain.rpcUrls.default?.http[0] ?? '',
]),
),
},
dapp: parameters.dapp ?? {
...defaultDappParams,
...parameters.dappMetadata,
},
debug: parameters.debug ?? parameters.logging?.sdk,
eventHandlers: {
accountsChanged: this.onAccountsChanged.bind(this),
chainChanged: this.onChainChanged.bind(this),
connect: this.onConnect.bind(this),
disconnect: this.onDisconnect.bind(this),
displayUri: this.onDisplayUri.bind(this),
},
analytics: {
integrationType: 'wagmi',
},
ui: {
...parameters.ui,
...(parameters.headless != null && {
headless: parameters.headless,
}),
},
...(parameters.mobile && { mobile: parameters.mobile }),
})
}
metamask = await metamaskPromise
}
return metamask
},
}))
}