Skip to content

Commit

Permalink
feat(connectors): filter mipd by connector rdns (#4343)
Browse files Browse the repository at this point in the history
* feat(connectors): filter mipd by connector rdns

* feat: add connect listeners

* test: boost coverage

* test(core): eip 6963 announce

* test: tweaks

* chore: format

* chore: changeset

* feat: works with ssr flag

* chore: changeset

* chore: tweaks

---------

Co-authored-by: tmm <[email protected]>
  • Loading branch information
tmm and tmm authored Oct 21, 2024
1 parent 2595ec1 commit f43e074
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 33 deletions.
6 changes: 6 additions & 0 deletions .changeset/curly-yaks-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@wagmi/connectors": minor
"@wagmi/core": minor
---

Added `rdns` property to connector interface. This is used to filter out duplicate [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) injected providers when [`createConfig#multiInjectedProviderDiscovery`](https://wagmi.sh/core/api/createConfig#multiinjectedproviderdiscovery) is enabled and `createConfig#connectors` already matches EIP-6963 providers' `rdns` property.
1 change: 1 addition & 0 deletions packages/connectors/src/coinbaseWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function version4(parameters: Version4Parameters) {
return createConnector<Provider>((config) => ({
id: 'coinbaseWalletSDK',
name: 'Coinbase Wallet',
rdns: 'com.coinbase.wallet',
supportsSimulation: true,
type: coinbaseWallet.type,
async connect({ chainId } = {}) {
Expand Down
26 changes: 14 additions & 12 deletions packages/connectors/src/metaMask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,22 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
return createConnector<Provider, Properties>((config) => ({
id: 'metaMaskSDK',
name: 'MetaMask',
rdns: 'io.metamask',
type: metaMask.type,
async setup() {
const provider = await this.getProvider()
if (provider && !connect) {
connect = this.onConnect.bind(this)
provider.on('connect', connect as Listener)
if (provider?.on) {
if (!connect) {
connect = this.onConnect.bind(this)
provider.on('connect', connect as Listener)
}

// 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 as Listener)
}
}
},
async connect({ chainId, isReconnecting } = {}) {
Expand Down Expand Up @@ -193,10 +203,6 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
const provider = await this.getProvider()

// Manage EIP-1193 event listeners
if (accountsChanged) {
provider.removeListener('accountsChanged', accountsChanged)
accountsChanged = undefined
}
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged)
chainChanged = undefined
Expand Down Expand Up @@ -257,7 +263,7 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
parameters.dappMetadata ??
(typeof window !== 'undefined'
? { url: window.location.origin }
: { name: 'wagmi' }),
: { name: 'wagmi', url: 'https://wagmi.sh' }),
useDeeplink: parameters.useDeeplink ?? true,
})
await sdk.init()
Expand Down Expand Up @@ -445,10 +451,6 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
config.emitter.emit('disconnect')

// Manage EIP-1193 event listeners
if (!accountsChanged) {
accountsChanged = this.onAccountsChanged.bind(this)
provider.on('accountsChanged', accountsChanged as Listener)
}
if (chainChanged) {
provider.removeListener('chainChanged', chainChanged)
chainChanged = undefined
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/connectors/createConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type CreateConnectorFn<
readonly icon?: string | undefined
readonly id: string
readonly name: string
readonly rdns?: string | undefined
readonly supportsSimulation?: boolean | undefined
readonly type: string

Expand Down
84 changes: 83 additions & 1 deletion packages/core/src/createConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { accounts, chain, wait } from '@wagmi/test'
import {
type EIP1193Provider,
type EIP6963ProviderDetail,
announceProvider,
} from 'mipd'
import { http } from 'viem'
import { expect, test, vi } from 'vitest'

import { connect } from './actions/connect.js'
import { disconnect } from './actions/disconnect.js'
import { switchChain } from './actions/switchChain.js'
import { createConnector } from './connectors/createConnector.js'
import { mock } from './connectors/mock.js'
import { createConfig } from './createConfig.js'
import { createStorage } from './createStorage.js'
Expand Down Expand Up @@ -346,10 +352,86 @@ test('behavior: setup connector', async () => {

await connect(config, {
chainId: mainnet.id,
connector: config.connectors[0]!,
connector: config.connectors.find((x) => x.uid === connector.uid)!,
})

expect(config.state.current).toBe(connector.uid)

await disconnect(config)
})

test('behavior: eip 6963 providers', async () => {
const detail_1 = getProviderDetail({ name: 'Foo Wallet', rdns: 'com.foo' })
const detail_2 = getProviderDetail({ name: 'Bar Wallet', rdns: 'com.bar' })
const detail_3 = getProviderDetail({ name: 'Mock', rdns: 'com.mock' })

const config = createConfig({
chains: [mainnet],
connectors: [
createConnector((c) => {
return {
...mock({ accounts })(c),
rdns: 'com.mock',
}
}),
],
transports: {
[mainnet.id]: http(),
},
})

await wait(100)
announceProvider(detail_1)()
await wait(100)
announceProvider(detail_1)()
await wait(100)
announceProvider(detail_2)()
await wait(100)
announceProvider(detail_3)()
await wait(100)

expect(config.connectors.map((x) => x.rdns ?? x.id)).toMatchInlineSnapshot(`
[
"com.mock",
"com.example",
"com.foo",
"com.bar",
]
`)
})

function getProviderDetail(
info: Pick<EIP6963ProviderDetail['info'], 'name' | 'rdns'>,
): EIP6963ProviderDetail {
return {
info: {
icon: 'data:image/svg+xml,<svg width="32px" height="32px" viewBox="0 0 32 32"/>',
uuid: crypto.randomUUID(),
...info,
},
provider: `<EIP1193Provider_${info.rdns}>` as unknown as EIP1193Provider,
}
}

vi.mock(import('mipd'), async (importOriginal) => {
const mod = await importOriginal()

let _cache: typeof mod | undefined
if (!_cache)
_cache = {
...mod,
createStore() {
const store = mod.createStore()
return {
...store,
getProviders() {
return [
getProviderDetail({ name: 'Example', rdns: 'com.example' }),
getProviderDetail({ name: 'Mock', rdns: 'com.mock' }),
]
},
}
},
}
return _cache
})
34 changes: 23 additions & 11 deletions packages/core/src/createConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,23 @@ export function createConfig<
: undefined

const chains = createStore(() => rest.chains)
const connectors = createStore(() =>
[
...(rest.connectors ?? []),
...(!ssr
? (mipd?.getProviders().map(providerDetailToConnector) ?? [])
: []),
].map(setup),
)
const connectors = createStore(() => {
const collection = []
const rdnsSet = new Set<string>()
for (const connectorFns of rest.connectors ?? []) {
const connector = setup(connectorFns)
collection.push(connector)
if (!ssr && connector.rdns) rdnsSet.add(connector.rdns)
}
if (!ssr && mipd) {
const providers = mipd.getProviders()
for (const provider of providers) {
if (rdnsSet.has(provider.info.rdns)) continue
collection.push(setup(providerDetailToConnector(provider)))
}
}
return collection
})
function setup(connectorFn: CreateConnectorFn): Connector {
// Set up emitter with uid and add to connector so they are "linked" together.
const emitter = createEmitter<ConnectorEventMap>(uid())
Expand Down Expand Up @@ -313,15 +322,18 @@ export function createConfig<

// EIP-6963 subscribe for new wallet providers
mipd?.subscribe((providerDetails) => {
const currentConnectorIds = new Map()
const connectorIdSet = new Set()
const connectorRdnsSet = new Set()
for (const connector of connectors.getState()) {
currentConnectorIds.set(connector.id, true)
connectorIdSet.add(connector.id)
if (connector.rdns) connectorRdnsSet.add(connector.rdns)
}

const newConnectors: Connector[] = []
for (const providerDetail of providerDetails) {
if (connectorRdnsSet.has(providerDetail.info.rdns)) continue
const connector = setup(providerDetailToConnector(providerDetail))
if (currentConnectorIds.has(connector.id)) continue
if (connectorIdSet.has(connector.id)) continue
newConnectors.push(connector)
}

Expand Down
26 changes: 18 additions & 8 deletions packages/core/src/hydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,24 @@ export function hydrate(config: Config, parameters: HydrateParameters) {
async onMount() {
if (config._internal.ssr) {
await config._internal.store.persist.rehydrate()
const mipdConnectors = config._internal.mipd
?.getProviders()
.map(config._internal.connectors.providerDetailToConnector)
.map(config._internal.connectors.setup)
config._internal.connectors.setState((connectors) => [
...connectors,
...(mipdConnectors ?? []),
])
if (config._internal.mipd) {
config._internal.connectors.setState((connectors) => {
const rdnsSet = new Set<string>()
for (const connector of connectors ?? []) {
if (connector.rdns) rdnsSet.add(connector.rdns)
}
const mipdConnectors = []
const providers = config._internal.mipd?.getProviders() ?? []
for (const provider of providers) {
if (rdnsSet.has(provider.info.rdns)) continue
const connectorFn =
config._internal.connectors.providerDetailToConnector(provider)
const connector = config._internal.connectors.setup(connectorFn)
mipdConnectors.push(connector)
}
return [...connectors, ...mipdConnectors]
})
}
}

if (reconnectOnMount) reconnect(config)
Expand Down
2 changes: 1 addition & 1 deletion playgrounds/next/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function Account() {
status: {account.status}
</div>

{account.status !== 'disconnected' && (
{account.status === 'connected' && (
<button type="button" onClick={() => disconnect()}>
Disconnect
</button>
Expand Down
1 change: 1 addition & 0 deletions site/dev/creating-connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The type error tells you what properties are missing from `createConnector`'s re
- `icon`: Optional icon URL for the connector.
- `id`: The ID for the connector. This should be camel-cased and as short as possible. Example: `fooBarBaz`.
- `name`: Human-readable name for the connector. Example: `'Foo Bar Baz'`.
- `rdns`: Optional reverse DNS for the connector. This is used to filter out duplicate [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) injected providers when `createConfig#multiInjectedProviderDiscovery` is enabled.
- `supportsSimulation`: Whether the connector supports contract simulation. This should be disabled if a connector's wallet cannot accurately simulate contract writes or display contract revert messages. Defaults to `false`.

#### Methods
Expand Down

0 comments on commit f43e074

Please sign in to comment.