Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(connectors): filter mipd by connector rdns #4343

Merged
merged 10 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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' }),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error spamming SSR (e.g. Next.js) without a URL set

useDeeplink: parameters.useDeeplink ?? true,
})
await sdk.init()
Expand Down Expand Up @@ -440,10 +446,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
Loading