@wagmi/connectors
Version:
Collection of connectors for Wagmi
146 lines (130 loc) • 4.85 kB
text/typescript
import type { SafeAppProvider } from '@safe-global/safe-apps-provider'
import type { Opts } from '@safe-global/safe-apps-sdk'
import {
type Connector,
ProviderNotFoundError,
createConnector,
} from '@wagmi/core'
import type { Compute } from '@wagmi/core/internal'
import { getAddress, withTimeout } from 'viem'
export type SafeParameters = Compute<
Opts & {
/**
* Connector automatically connects when used as Safe App.
*
* This flag simulates the disconnect behavior by keeping track of connection status in storage
* and only autoconnecting when previously connected by user action (e.g. explicitly choosing to connect).
*
* @default false
*/
shimDisconnect?: boolean | undefined
/**
* Timeout in milliseconds for `getInfo` (from the Safe SDK) to resolve.
*
* `getInfo` does not resolve when not used in Safe App iFrame. This allows the connector to force a timeout.
* @default 10
*/
unstable_getInfoTimeout?: number | undefined
}
>
safe.type = 'safe' as const
export function safe(parameters: SafeParameters = {}) {
const { shimDisconnect = false } = parameters
type Provider = SafeAppProvider | undefined
type Properties = Record<string, unknown>
type StorageItem = { 'safe.disconnected': true }
let provider_: Provider | undefined
let disconnect: Connector['onDisconnect'] | undefined
return createConnector<Provider, Properties, StorageItem>((config) => ({
id: 'safe',
name: 'Safe',
type: safe.type,
async connect() {
const provider = await this.getProvider()
if (!provider) throw new ProviderNotFoundError()
const accounts = await this.getAccounts()
const chainId = await this.getChainId()
if (!disconnect) {
disconnect = this.onDisconnect.bind(this)
provider.on('disconnect', disconnect)
}
// Remove disconnected shim if it exists
if (shimDisconnect) await config.storage?.removeItem('safe.disconnected')
return { accounts, chainId }
},
async disconnect() {
const provider = await this.getProvider()
if (!provider) throw new ProviderNotFoundError()
if (disconnect) {
provider.removeListener('disconnect', disconnect)
disconnect = undefined
}
// Add shim signalling connector is disconnected
if (shimDisconnect)
await config.storage?.setItem('safe.disconnected', true)
},
async getAccounts() {
const provider = await this.getProvider()
if (!provider) throw new ProviderNotFoundError()
return (await provider.request({ method: 'eth_accounts' })).map(
getAddress,
)
},
async getProvider() {
// Only allowed in iframe context
const isIframe =
typeof window !== 'undefined' && window?.parent !== window
if (!isIframe) return
if (!provider_) {
const { default: SDK } = await import('@safe-global/safe-apps-sdk')
const sdk = new SDK(parameters)
// `getInfo` hangs when not used in Safe App iFrame
// https://github.com/safe-global/safe-apps-sdk/issues/263#issuecomment-1029835840
const safe = await withTimeout(() => sdk.safe.getInfo(), {
timeout: parameters.unstable_getInfoTimeout ?? 10,
})
if (!safe) throw new Error('Could not load Safe information')
// Unwrapping import for Vite compatibility.
// See: https://github.com/vitejs/vite/issues/9703
const SafeAppProvider = await (async () => {
const Provider = await import('@safe-global/safe-apps-provider')
if (
typeof Provider.SafeAppProvider !== 'function' &&
typeof Provider.default.SafeAppProvider === 'function'
)
return Provider.default.SafeAppProvider
return Provider.SafeAppProvider
})()
provider_ = new SafeAppProvider(safe, sdk)
}
return provider_
},
async getChainId() {
const provider = await this.getProvider()
if (!provider) throw new ProviderNotFoundError()
return Number(provider.chainId)
},
async isAuthorized() {
try {
const isDisconnected =
shimDisconnect &&
// If shim exists in storage, connector is disconnected
(await config.storage?.getItem('safe.disconnected'))
if (isDisconnected) return false
const accounts = await this.getAccounts()
return !!accounts.length
} catch {
return false
}
},
onAccountsChanged() {
// Not relevant for Safe because changing account requires app reload.
},
onChainChanged() {
// Not relevant for Safe because Safe smart contract wallets only exist on single chain.
},
onDisconnect() {
config.emitter.emit('disconnect')
},
}))
}