From f43e074f473820b208a6295d7c97f847332f1a1d Mon Sep 17 00:00:00 2001 From: awkweb Date: Mon, 21 Oct 2024 13:38:13 -0400 Subject: [PATCH] feat(connectors): filter mipd by connector rdns (#4343) * 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 --- .changeset/curly-yaks-sin.md | 6 ++ packages/connectors/src/coinbaseWallet.ts | 1 + packages/connectors/src/metaMask.ts | 26 +++--- .../core/src/connectors/createConnector.ts | 1 + packages/core/src/createConfig.test.ts | 84 ++++++++++++++++++- packages/core/src/createConfig.ts | 34 +++++--- packages/core/src/hydrate.ts | 26 ++++-- playgrounds/next/src/app/page.tsx | 2 +- site/dev/creating-connectors.md | 1 + 9 files changed, 148 insertions(+), 33 deletions(-) create mode 100644 .changeset/curly-yaks-sin.md diff --git a/.changeset/curly-yaks-sin.md b/.changeset/curly-yaks-sin.md new file mode 100644 index 0000000000..f320614aab --- /dev/null +++ b/.changeset/curly-yaks-sin.md @@ -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. diff --git a/packages/connectors/src/coinbaseWallet.ts b/packages/connectors/src/coinbaseWallet.ts index 17eb033c31..0987a78318 100644 --- a/packages/connectors/src/coinbaseWallet.ts +++ b/packages/connectors/src/coinbaseWallet.ts @@ -89,6 +89,7 @@ function version4(parameters: Version4Parameters) { return createConnector((config) => ({ id: 'coinbaseWalletSDK', name: 'Coinbase Wallet', + rdns: 'com.coinbase.wallet', supportsSimulation: true, type: coinbaseWallet.type, async connect({ chainId } = {}) { diff --git a/packages/connectors/src/metaMask.ts b/packages/connectors/src/metaMask.ts index 4e44cf4107..16269a4c20 100644 --- a/packages/connectors/src/metaMask.ts +++ b/packages/connectors/src/metaMask.ts @@ -93,12 +93,22 @@ export function metaMask(parameters: MetaMaskParameters = {}) { return createConnector((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 } = {}) { @@ -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 @@ -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() @@ -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 diff --git a/packages/core/src/connectors/createConnector.ts b/packages/core/src/connectors/createConnector.ts index 75a4ae3fad..f5072f1add 100644 --- a/packages/core/src/connectors/createConnector.ts +++ b/packages/core/src/connectors/createConnector.ts @@ -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 diff --git a/packages/core/src/createConfig.test.ts b/packages/core/src/createConfig.test.ts index 5fa3200b39..2e453c82f2 100644 --- a/packages/core/src/createConfig.test.ts +++ b/packages/core/src/createConfig.test.ts @@ -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' @@ -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 { + return { + info: { + icon: 'data:image/svg+xml,', + uuid: crypto.randomUUID(), + ...info, + }, + provider: `` 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 +}) diff --git a/packages/core/src/createConfig.ts b/packages/core/src/createConfig.ts index fc884b1b81..65cf2b7c3d 100644 --- a/packages/core/src/createConfig.ts +++ b/packages/core/src/createConfig.ts @@ -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() + 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(uid()) @@ -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) } diff --git a/packages/core/src/hydrate.ts b/packages/core/src/hydrate.ts index 2fe506592b..3c566ef0b2 100644 --- a/packages/core/src/hydrate.ts +++ b/packages/core/src/hydrate.ts @@ -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() + 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) diff --git a/playgrounds/next/src/app/page.tsx b/playgrounds/next/src/app/page.tsx index f26c08a97f..33c1bf0abb 100644 --- a/playgrounds/next/src/app/page.tsx +++ b/playgrounds/next/src/app/page.tsx @@ -78,7 +78,7 @@ function Account() { status: {account.status} - {account.status !== 'disconnected' && ( + {account.status === 'connected' && ( diff --git a/site/dev/creating-connectors.md b/site/dev/creating-connectors.md index 7081c2e3e3..0b98e834cf 100644 --- a/site/dev/creating-connectors.md +++ b/site/dev/creating-connectors.md @@ -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