@wagmi/core
Version:
VanillaJS library for Ethereum
435 lines (391 loc) • 13.8 kB
text/typescript
import type {
Provider as AccountsProvider,
Rpc as AccountsRpc,
dangerous_secp256k1 as accountsDangerousSecp256k1,
dialog as accountsDialog,
webAuthn as accountsWebAuthn,
} from 'accounts'
import {
type Address,
numberToHex,
type ProviderConnectInfo,
type RpcError,
SwitchChainError,
UserRejectedRequestError,
withRetry,
} from 'viem'
import { parseAccount } from 'viem/utils'
import { createConnector } from '../connectors/createConnector.js'
import type { Connector } from '../createConfig.js'
import { ChainNotConfiguredError } from '../errors/config.js'
type AccountsModule = {
dialog: typeof accountsDialog
Provider: typeof AccountsProvider
dangerous_secp256k1: typeof accountsDangerousSecp256k1
webAuthn: typeof accountsWebAuthn
}
type AccountsDialogParameters = NonNullable<
Parameters<typeof accountsDialog>[0]
>
type AccountsProviderParameters = NonNullable<
Parameters<typeof AccountsProvider.create>[0]
>
type AccountsAdapter = NonNullable<AccountsProviderParameters['adapter']>
type AccountsDangerousSecp256k1Parameters = NonNullable<
Parameters<typeof accountsDangerousSecp256k1>[0]
>
type AccountsWebAuthnParameters = NonNullable<
Parameters<typeof accountsWebAuthn>[0]
>
type Provider = Pick<
ReturnType<typeof AccountsProvider.create>,
'getAccount' | 'getClient' | 'on' | 'removeListener' | 'request'
>
type AccountsConnectParameters = NonNullable<
AccountsRpc.wallet_connect.Decoded['params']
>[0]
type CapabilitiesRequest = AccountsConnectParameters['capabilities']
type InternalAccount =
AccountsRpc.wallet_connect.Encoded['returns']['accounts'][number]
const tempoWalletIcon =
'data:image/svg+xml,<svg width="269" height="269" viewBox="0 0 269 269" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="269" height="269" fill="black"/><path d="M123.273 190.794H93.445L121.09 105.318H85.7334L93.445 80.2642H191.95L184.238 105.318H150.773L123.273 190.794Z" fill="white"/></svg>'
/** @deprecated use `tempoWallet.Parameters` instead */
export type TempoWalletParameters = tempoWallet.Parameters
/**
* Connector for the Tempo Wallet dialog.
*/
export function tempoWallet(parameters: tempoWallet.Parameters = {}) {
const {
dialog: dialogOption,
host,
icon = tempoWalletIcon,
name,
rdns,
theme,
...providerParameters
} = parameters
return _setup({
createAdapter(accounts) {
return accounts.dialog({
dialog: dialogOption,
host,
icon,
name,
rdns,
theme,
})
},
icon,
id: rdns ?? 'xyz.tempo',
name: name ?? 'Tempo Wallet',
providerParameters,
rdns: rdns ?? 'xyz.tempo',
type: 'injected',
})
}
export declare namespace tempoWallet {
export type Parameters = Omit<
AccountsProviderParameters,
'adapter' | 'chains'
> &
AccountsDialogParameters
export type ConnectParameters<withCapabilities extends boolean = false> =
setup.ConnectParameters<withCapabilities>
export type ConnectReturnType<withCapabilities extends boolean = false> =
setup.ConnectReturnType<withCapabilities>
}
/** @deprecated use `webAuthn.Parameters` instead */
export type WebAuthnParameters = webAuthn.Parameters
webAuthn.type = 'webAuthn' as const
/**
* Connector for a WebAuthn EOA.
*/
export function webAuthn(parameters: webAuthn.Parameters = {}) {
const { authUrl, ceremony, icon, name, rdns, ...providerParameters } =
parameters
return _setup({
createAdapter(accounts) {
return ceremony
? accounts.webAuthn({ ceremony, icon, name, rdns })
: accounts.webAuthn({ authUrl, icon, name, rdns })
},
icon,
id: 'webAuthn',
name: name ?? 'EOA (WebAuthn)',
providerParameters,
rdns,
type: 'webAuthn',
})
}
export declare namespace webAuthn {
export type Parameters = AccountsWebAuthnParameters &
Omit<AccountsProviderParameters, 'adapter' | 'chains'>
}
/** @deprecated use `dangerous_secp256k1.Parameters` instead */
export type Dangerous_Secp256k1Parameters = dangerous_secp256k1.Parameters
dangerous_secp256k1.type = 'dangerous_secp256k1' as const
/**
* Connector for a Secp256k1 EOA.
*
* WARNING: NOT RECOMMENDED FOR PRODUCTION USAGE.
* This connector stores private keys in clear text, and are bound to the session
* length of the storage used.
*/
export function dangerous_secp256k1(
parameters: dangerous_secp256k1.Parameters = {},
) {
const { icon, name, privateKey, rdns, ...providerParameters } = parameters
return _setup({
createAdapter(accounts) {
return accounts.dangerous_secp256k1({ icon, name, privateKey, rdns })
},
icon,
id: 'secp256k1',
name: name ?? 'EOA (Secp256k1)',
providerParameters,
rdns,
type: 'secp256k1',
})
}
export declare namespace dangerous_secp256k1 {
export type Parameters = AccountsDangerousSecp256k1Parameters &
Omit<AccountsProviderParameters, 'adapter' | 'chains'>
}
function _setup(parameters: setup.Parameters) {
type Properties = {
connect<withCapabilities extends boolean = false>(
parameters?: setup.ConnectParameters<withCapabilities>,
): Promise<setup.ConnectReturnType<withCapabilities>>
}
return createConnector<Provider, Properties>((config) => {
const chains = config.chains
let providerPromise: Promise<Provider> | undefined
let accountsChanged: Connector['onAccountsChanged'] | undefined
let chainChanged: ((chain: string) => void) | undefined
let connect: ((connectInfo: ProviderConnectInfo) => void) | undefined
let disconnect: ((error?: Error | undefined) => void) | undefined
async function getAccountsModule() {
return await import('accounts').catch(() => {
throw new Error('dependency "accounts" not found')
})
}
async function getProvider() {
providerPromise ??= (async () => {
const accounts = await getAccountsModule()
return accounts.Provider.create({
...parameters.providerParameters,
adapter: parameters.createAdapter(accounts),
chains: config.chains as never,
transports: config.transports as never,
}) as unknown as Provider
})()
return await providerPromise
}
return {
icon: parameters.icon,
id: parameters.id,
name: parameters.name,
rdns: parameters.rdns,
type: parameters.type,
async connect(connectParameters = {}) {
const { chainId, isReconnecting, withCapabilities } = connectParameters
const capabilities =
'capabilities' in connectParameters
? connectParameters.capabilities
: undefined
let accounts: readonly InternalAccount[] = []
let currentChainId: number | undefined
if (isReconnecting) {
accounts = await this.getAccounts()
.then((accounts) =>
accounts.map((address) => ({ address, capabilities: {} })),
)
.catch(() => [])
}
try {
if (!accounts.length && !isReconnecting) {
const provider = await getProvider()
const response = (await provider.request({
method: 'wallet_connect',
params: [
{
...(chainId ? { chainId } : {}),
...(capabilities ? { capabilities } : {}),
},
] as never,
})) as AccountsRpc.wallet_connect.Encoded['returns']
accounts = response.accounts
}
currentChainId ??= await this.getChainId()
if (!currentChainId) throw new ChainNotConfiguredError()
const provider = await getProvider()
if (connect) {
provider.removeListener('connect', connect)
connect = undefined
}
if (!accountsChanged) {
accountsChanged = this.onAccountsChanged.bind(this)
provider.on('accountsChanged', accountsChanged as never)
}
if (!chainChanged) {
chainChanged = this.onChainChanged.bind(this)
provider.on('chainChanged', chainChanged)
}
if (!disconnect) {
disconnect = this.onDisconnect.bind(this)
provider.on('disconnect', disconnect)
}
return {
accounts: (withCapabilities
? accounts
: accounts.map((account) => account.address)) as never,
chainId: currentChainId,
}
} catch (error) {
const rpcError = error as RpcError
if (rpcError.code === UserRejectedRequestError.code)
throw new UserRejectedRequestError(rpcError)
throw rpcError
}
},
async disconnect() {
const provider = await getProvider()
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged)
chainChanged = undefined
}
if (disconnect) {
provider.removeListener('disconnect', disconnect)
disconnect = undefined
}
if (!connect) {
connect = this.onConnect?.bind(this)
if (connect) provider.on('connect', connect)
}
await provider.request({ method: 'wallet_disconnect' })
},
async getAccounts() {
const provider = await getProvider()
return await provider.request({ method: 'eth_accounts' })
},
async getChainId() {
const provider = await getProvider()
return Number(await provider.request({ method: 'eth_chainId' }))
},
async getClient({ chainId } = {}) {
const provider = await getProvider()
// Always provide a JSON-RPC account; the SDK provider performs
// access key orchestration internally before signing.
const { address } = provider.getAccount({ accessKey: false })
return Object.assign(provider.getClient({ chainId }), {
account: parseAccount(address),
}) as never
},
async getProvider() {
return await getProvider()
},
async isAuthorized() {
try {
const accounts = await withRetry(() => this.getAccounts())
return !!accounts.length
} catch {
return false
}
},
onAccountsChanged(accounts) {
config.emitter.emit('change', {
accounts: accounts as readonly Address[],
})
},
onChainChanged(chain) {
config.emitter.emit('change', { chainId: Number(chain) })
},
async onConnect(connectInfo) {
const accounts = await this.getAccounts()
if (accounts.length === 0) return
const chainId = Number(connectInfo.chainId)
config.emitter.emit('connect', { accounts, chainId })
const provider = await getProvider()
if (connect) {
provider.removeListener('connect', connect)
connect = undefined
}
if (!accountsChanged) {
accountsChanged = this.onAccountsChanged.bind(this)
provider.on('accountsChanged', accountsChanged as never)
}
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 getProvider()
config.emitter.emit('disconnect')
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged)
chainChanged = undefined
}
if (disconnect) {
provider.removeListener('disconnect', disconnect)
disconnect = undefined
}
if (!connect) {
connect = this.onConnect?.bind(this)
if (connect) provider.on('connect', connect)
}
},
async setup() {
if (!connect) {
const provider = await getProvider()
connect = this.onConnect?.bind(this)
if (connect) provider.on('connect', connect)
}
},
async switchChain({ chainId }) {
const chain = chains.find((chain) => chain.id === chainId)
if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
const provider = await getProvider()
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: numberToHex(chainId) }],
})
return chain
},
}
})
}
export declare namespace setup {
export type Parameters = {
/** Builds the underlying `accounts` adapter (dialog, webAuthn, secp256k1) the connector wraps. */
createAdapter: (accounts: AccountsModule) => AccountsAdapter
/** Optional connector icon (data URL or remote URL). Surfaced via EIP-6963 announcement. */
icon?: string | undefined
/** Connector ID. */
id: string
/** Human-readable connector name. */
name: string
/** Forwarded to `Provider.create`. */
providerParameters: Omit<AccountsProviderParameters, 'adapter' | 'chains'>
/** EIP-6963 reverse-DNS ID(s) for the connector. */
rdns?: string | readonly string[] | undefined
/** Connector type. */
type: string
}
export type ConnectParameters<withCapabilities extends boolean = false> = {
capabilities?: CapabilitiesRequest | undefined
chainId?: number | undefined
isReconnecting?: boolean | undefined
withCapabilities?: withCapabilities | boolean | undefined
}
export type ConnectReturnType<withCapabilities extends boolean = false> = {
accounts: withCapabilities extends true
? readonly InternalAccount[]
: readonly Address[]
chainId: number
}
}