@wagmi/connectors
Version:
Collection of connectors for Wagmi
547 lines (498 loc) • 17.1 kB
text/typescript
import type {
Preference,
ProviderInterface,
createCoinbaseWalletSDK,
} from '@coinbase/wallet-sdk'
import {
ChainNotConfiguredError,
type Connector,
createConnector,
} from '@wagmi/core'
import type { Compute, Mutable, Omit } from '@wagmi/core/internal'
import type {
CoinbaseWalletProvider as CBW_Provider,
CoinbaseWalletSDK as CBW_SDK,
} from 'cbw-sdk'
import {
type AddEthereumChainParameter,
type Address,
type Hex,
type ProviderRpcError,
SwitchChainError,
UserRejectedRequestError,
getAddress,
numberToHex,
} from 'viem'
type Version = '3' | '4'
export type CoinbaseWalletParameters<version extends Version = '3'> =
version extends '4'
? Compute<
{
headlessMode?: false | undefined
/** Coinbase Wallet SDK version */
version?: version | '3' | undefined
} & Version4Parameters
>
: Compute<
{
/**
* @deprecated `headlessMode` will be removed in the next major version. Upgrade to `version: '4'`.
*/
headlessMode?: true | undefined
/**
* Coinbase Wallet SDK version
* @deprecated Version 3 will be removed in the next major version. Upgrade to `version: '4'`.
* @default '4'
*/
version?: version | '4' | undefined
} & Version3Parameters
>
coinbaseWallet.type = 'coinbaseWallet' as const
export function coinbaseWallet<version extends Version>(
parameters: CoinbaseWalletParameters<version> = {} as any,
): version extends '4'
? ReturnType<typeof version4>
: ReturnType<typeof version3> {
if (parameters.version === '3' || parameters.headlessMode)
return version3(parameters as Version3Parameters) as any
return version4(parameters as Version4Parameters) as any
}
type Version4Parameters = Mutable<
Omit<
Parameters<typeof createCoinbaseWalletSDK>[0],
| 'appChainIds' // set via wagmi config
| 'preference'
> & {
// TODO(v3): Remove `Preference['options']`
/**
* Preference for the type of wallet to display.
* @default 'all'
*/
preference?: Preference['options'] | Compute<Preference> | undefined
}
>
function version4(parameters: Version4Parameters) {
type Provider = ProviderInterface & {
// for backwards compatibility
close?(): void
}
type Properties = {
connect(parameters?: {
chainId?: number | undefined
instantOnboarding?: boolean | undefined
isReconnecting?: boolean | undefined
}): Promise<{
accounts: readonly Address[]
chainId: number
}>
}
let walletProvider: Provider | undefined
let accountsChanged: Connector['onAccountsChanged'] | undefined
let chainChanged: Connector['onChainChanged'] | undefined
let disconnect: Connector['onDisconnect'] | undefined
return createConnector<Provider, Properties>((config) => ({
id: 'coinbaseWalletSDK',
name: 'Coinbase Wallet',
rdns: 'com.coinbase.wallet',
type: coinbaseWallet.type,
async connect({ chainId, ...rest } = {}) {
try {
const provider = await this.getProvider()
const accounts = (
(await provider.request({
method: 'eth_requestAccounts',
params:
'instantOnboarding' in rest && rest.instantOnboarding
? [{ onboarding: 'instant' }]
: [],
})) as string[]
).map((x) => getAddress(x))
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
}
return { accounts, chainId: currentChainId }
} catch (error) {
if (
/(user closed modal|accounts received is empty|user denied account|request rejected)/i.test(
(error as Error).message,
)
)
throw new UserRejectedRequestError(error as Error)
throw error
}
},
async disconnect() {
const provider = await this.getProvider()
if (accountsChanged) {
provider.removeListener('accountsChanged', accountsChanged)
accountsChanged = undefined
}
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged)
chainChanged = undefined
}
if (disconnect) {
provider.removeListener('disconnect', disconnect)
disconnect = undefined
}
provider.disconnect()
provider.close?.()
},
async getAccounts() {
const provider = await this.getProvider()
return (
(await provider.request({
method: 'eth_accounts',
})) as string[]
).map((x) => getAddress(x))
},
async getChainId() {
const provider = await this.getProvider()
const chainId = (await provider.request({
method: 'eth_chainId',
})) as Hex
return Number(chainId)
},
async getProvider() {
if (!walletProvider) {
const preference = (() => {
if (typeof parameters.preference === 'string')
return { options: parameters.preference }
return {
...parameters.preference,
options: parameters.preference?.options ?? 'all',
}
})()
const { createCoinbaseWalletSDK } = await import('@coinbase/wallet-sdk')
const sdk = createCoinbaseWalletSDK({
...parameters,
appChainIds: config.chains.map((x) => x.id),
preference,
})
walletProvider = sdk.getProvider()
}
return walletProvider
},
async isAuthorized() {
try {
const accounts = await this.getAccounts()
return !!accounts.length
} catch {
return false
}
},
async switchChain({ addEthereumChainParameter, chainId }) {
const chain = config.chains.find((chain) => chain.id === chainId)
if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
const provider = await this.getProvider()
try {
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: numberToHex(chain.id) }],
})
return chain
} catch (error) {
// Indicates chain is not added to provider
if ((error as ProviderRpcError).code === 4902) {
try {
let blockExplorerUrls: string[] | undefined
if (addEthereumChainParameter?.blockExplorerUrls)
blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
else
blockExplorerUrls = chain.blockExplorers?.default.url
? [chain.blockExplorers?.default.url]
: []
let rpcUrls: readonly string[]
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,
} satisfies AddEthereumChainParameter
await provider.request({
method: 'wallet_addEthereumChain',
params: [addEthereumChain],
})
return chain
} catch (error) {
throw new UserRejectedRequestError(error as Error)
}
}
throw new SwitchChainError(error as Error)
}
},
onAccountsChanged(accounts) {
if (accounts.length === 0) this.onDisconnect()
else
config.emitter.emit('change', {
accounts: accounts.map((x) => getAddress(x)),
})
},
onChainChanged(chain) {
const chainId = Number(chain)
config.emitter.emit('change', { chainId })
},
async onDisconnect(_error) {
config.emitter.emit('disconnect')
const provider = await this.getProvider()
if (accountsChanged) {
provider.removeListener('accountsChanged', accountsChanged)
accountsChanged = undefined
}
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged)
chainChanged = undefined
}
if (disconnect) {
provider.removeListener('disconnect', disconnect)
disconnect = undefined
}
},
}))
}
type Version3Parameters = Mutable<
Omit<
ConstructorParameters<typeof CBW_SDK>[0],
'reloadOnDisconnect' // remove property since TSDoc says default is `true`
>
> & {
/**
* Fallback Ethereum JSON RPC URL
* @default ""
*/
jsonRpcUrl?: string | undefined
/**
* Fallback Ethereum Chain ID
* @default 1
*/
chainId?: number | undefined
/**
* Whether or not to reload dapp automatically after disconnect.
* @default false
*/
reloadOnDisconnect?: boolean | undefined
}
function version3(parameters: Version3Parameters) {
const reloadOnDisconnect = false
type Provider = CBW_Provider
let sdk: CBW_SDK | undefined
let walletProvider: Provider | undefined
let accountsChanged: Connector['onAccountsChanged'] | undefined
let chainChanged: Connector['onChainChanged'] | undefined
let disconnect: Connector['onDisconnect'] | undefined
return createConnector<Provider>((config) => ({
id: 'coinbaseWalletSDK',
name: 'Coinbase Wallet',
rdns: 'com.coinbase.wallet',
type: coinbaseWallet.type,
async connect({ chainId } = {}) {
try {
const provider = await this.getProvider()
const accounts = (
(await provider.request({
method: 'eth_requestAccounts',
})) as string[]
).map((x) => getAddress(x))
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
}
return { accounts, chainId: currentChainId }
} catch (error) {
if (
/(user closed modal|accounts received is empty|user denied account)/i.test(
(error as Error).message,
)
)
throw new UserRejectedRequestError(error as Error)
throw error
}
},
async disconnect() {
const provider = await this.getProvider()
if (accountsChanged) {
provider.removeListener('accountsChanged', accountsChanged)
accountsChanged = undefined
}
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged)
chainChanged = undefined
}
if (disconnect) {
provider.removeListener('disconnect', disconnect)
disconnect = undefined
}
provider.disconnect()
provider.close()
},
async getAccounts() {
const provider = await this.getProvider()
return (
await provider.request<string[]>({
method: 'eth_accounts',
})
).map((x) => getAddress(x))
},
async getChainId() {
const provider = await this.getProvider()
const chainId = await provider.request<Hex>({
method: 'eth_chainId',
})
return Number(chainId)
},
async getProvider() {
if (!walletProvider) {
// Unwrapping import for Vite compatibility.
// See: https://github.com/vitejs/vite/issues/9703
const CoinbaseWalletSDK = await (async () => {
const { default: SDK } = await import('cbw-sdk')
if (typeof SDK !== 'function' && typeof SDK.default === 'function')
return SDK.default
return SDK as unknown as typeof SDK.default
})()
sdk = new CoinbaseWalletSDK({ ...parameters, reloadOnDisconnect })
// Force types to retrieve private `walletExtension` method from the Coinbase Wallet SDK.
const walletExtensionChainId = (
sdk as unknown as {
get walletExtension(): { getChainId(): number } | undefined
}
).walletExtension?.getChainId()
const chain =
config.chains.find((chain) =>
parameters.chainId
? chain.id === parameters.chainId
: chain.id === walletExtensionChainId,
) || config.chains[0]
const chainId = parameters.chainId || chain?.id
const jsonRpcUrl =
parameters.jsonRpcUrl || chain?.rpcUrls.default.http[0]
walletProvider = sdk.makeWeb3Provider(jsonRpcUrl, chainId)
}
return walletProvider
},
async isAuthorized() {
try {
const accounts = await this.getAccounts()
return !!accounts.length
} catch {
return false
}
},
async switchChain({ addEthereumChainParameter, chainId }) {
const chain = config.chains.find((chain) => chain.id === chainId)
if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
const provider = await this.getProvider()
try {
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: numberToHex(chain.id) }],
})
return chain
} catch (error) {
// Indicates chain is not added to provider
if ((error as ProviderRpcError).code === 4902) {
try {
let blockExplorerUrls: string[] | undefined
if (addEthereumChainParameter?.blockExplorerUrls)
blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
else
blockExplorerUrls = chain.blockExplorers?.default.url
? [chain.blockExplorers?.default.url]
: []
let rpcUrls: readonly string[]
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,
} satisfies AddEthereumChainParameter
await provider.request({
method: 'wallet_addEthereumChain',
params: [addEthereumChain],
})
return chain
} catch (error) {
throw new UserRejectedRequestError(error as Error)
}
}
throw new SwitchChainError(error as Error)
}
},
onAccountsChanged(accounts) {
if (accounts.length === 0) this.onDisconnect()
else
config.emitter.emit('change', {
accounts: accounts.map((x) => getAddress(x)),
})
},
onChainChanged(chain) {
const chainId = Number(chain)
config.emitter.emit('change', { chainId })
},
async onDisconnect(_error) {
config.emitter.emit('disconnect')
const provider = await this.getProvider()
if (accountsChanged) {
provider.removeListener('accountsChanged', accountsChanged)
accountsChanged = undefined
}
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged)
chainChanged = undefined
}
if (disconnect) {
provider.removeListener('disconnect', disconnect)
disconnect = undefined
}
},
}))
}