Skip to content
This repository has been archived by the owner on Sep 6, 2023. It is now read-only.

feat: create MetaMaskSDKConnector using @metamask/sdk #422

Closed
wants to merge 12 commits into from
5 changes: 5 additions & 0 deletions .changeset/honest-dogs-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wagmi/connectors": major
---

MetaMask SDK Connector to switch between mobile and browser wallet
1 change: 1 addition & 0 deletions packages/connectors/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
coinbaseWallet/**
injected/**
ledger/**
metaMaskSDK/**
metaMask/**
mock/**
safe/**
Expand Down
12 changes: 9 additions & 3 deletions packages/connectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
"dependencies": {
"@coinbase/wallet-sdk": "^3.6.6",
"@ledgerhq/connect-kit-loader": "^1.1.0",
"@metamask/sdk": "^0.6.0",
"@safe-global/safe-apps-provider": "^0.17.1",
"@safe-global/safe-apps-sdk": "^8.0.0",
"@walletconnect/ethereum-provider": "2.10.0",
"@walletconnect/utils": "2.10.0",
"@walletconnect/ethereum-provider": "2.9.0",
"@walletconnect/legacy-provider": "^2.0.0",
"@walletconnect/modal": "2.6.1",
"@walletconnect/modal": "2.5.9",
"@walletconnect/utils": "2.9.0",
"abitype": "0.8.7",
"eventemitter3": "^4.0.7"
},
Expand Down Expand Up @@ -56,6 +57,10 @@
"types": "./dist/ledger.d.ts",
"default": "./dist/ledger.js"
},
"./metaMaskSDK": {
"types": "./dist/metaMaskSDK.d.ts",
"default": "./dist/metaMaskSDK.js"
},
"./metaMask": {
"types": "./dist/metaMask.d.ts",
"default": "./dist/metaMask.js"
Expand All @@ -82,6 +87,7 @@
"/coinbaseWallet",
"/injected",
"/ledger",
"/metaMaskSDK",
"/metaMask",
"/mock",
"/safe",
Expand Down
3 changes: 3 additions & 0 deletions packages/connectors/src/metaMask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export class MetaMaskConnector extends InjectedConnector {
chains?: Chain[]
options?: MetaMaskConnectorOptions
} = {}) {
console.warn(
'[DEPRECATED] The MetaMaskConnector is deprecated and will be removed in the next major version. Please use the MetaMaskSDKConnector instead.',
)
const options = {
name: 'MetaMask',
shimDisconnect: true,
Expand Down
5 changes: 5 additions & 0 deletions packages/connectors/src/metaMaskSDK.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { describe, it } from 'vitest'

describe('MetaMaskSDKConnector', () => {
it.todo('inits')
})
190 changes: 190 additions & 0 deletions packages/connectors/src/metaMaskSDK.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { InjectedConnector } from './injected'
import { WindowProvider } from './types'

import { MetaMaskSDK, MetaMaskSDKOptions, SDKProvider } from '@metamask/sdk'
import {
Address,
Chain,
ProviderRpcError,
ResourceUnavailableRpcError,
UserRejectedRequestError,
} from 'viem'

export type MetaMaskSDKConnectorOptions = {
// Keep both sdk and sdkOptions as some users might want to use their own pre-defined sdk instance
sdk?: MetaMaskSDK
sdkOptions?: MetaMaskSDKOptions
}

export class MetaMaskSDKConnector extends InjectedConnector {
readonly id = 'metaMaskSDK'

#sdk: MetaMaskSDK
#provider?: SDKProvider

constructor({
chains,
options: options_,
}: {
chains?: Chain[]
options?: MetaMaskSDKConnectorOptions
} = {}) {
if (!options_?.sdk && !options_?.sdkOptions) {
throw new Error('MetaMaskConnector invalid sdk parameters')
}

let sdk

if (options_?.sdk) {
sdk = options_.sdk
} else {
// force source to 'wagmi' for analytics
if (!options_.sdkOptions)
options_.sdkOptions = {
dappMetadata: { name: 'wagmi' },
}
options_.sdkOptions._source = 'wagmi'
sdk = new MetaMaskSDK(options_.sdkOptions)
}

const sdkProvider = sdk.getProvider()

const options = {
name: 'MetaMask',
shimDisconnect: true,
getProvider() {
// ignore _events from WindowProvider not implemented in SDKProvider
return sdkProvider as unknown as WindowProvider
},
}

super({ chains, options })

this.#sdk = sdk
this.#provider = sdkProvider
}

/**
* Listen to sdk provider events and re-initialize events listeners accordingly
*/
#updateProviderListeners() {
if (this.#provider) {
// Cleanup previous handlers first
this.#provider?.removeListener('accountsChanged', this.onAccountsChanged)
this.#provider?.removeListener('chainChanged', this.onChainChanged)
this.#provider?.removeListener('disconnect', this.onDisconnect)
}

// might need to re-initialize provider if it changed
this.#provider = this.#sdk.getProvider()

this.#provider?.on(
'accountsChanged',
this.onAccountsChanged as (...args: unknown[]) => void,
)
this.#provider?.on(
'chainChanged',
this.onChainChanged as (...args: unknown[]) => void,
)
this.#provider?.on(
'disconnect',
this.onDisconnect as (...args: unknown[]) => void,
)
}

async getProvider() {
if (!this.#sdk.isInitialized()) {
await this.#sdk.init()
}
if (!this.#provider) {
this.#provider = this.#sdk.getProvider()
}
return this.#provider as unknown as WindowProvider
}

async disconnect() {
this.#sdk.terminate()
super.disconnect()
}

async connect({ chainId }: { chainId?: number } = {}): Promise<{
account: Address
chain: {
id: number
unsupported: boolean
}
provider?: SDKProvider
}> {
try {
if (!this.#sdk.isInitialized()) {
await this.#sdk.init()
}

const accounts = (await this.#sdk.connect()) as Address[]

// Get latest provider instance (it may have changed based on user selection)
this.#updateProviderListeners()

// backward compatibility with older wallet (<7.3) version that return accounts before authorization
if (
!this.#sdk.isExtensionActive() &&
!this.#sdk._getConnection()?.isAuthorized()
) {
const waitForAuthorized = () => {
return new Promise((resolve) => {
this.#sdk
._getConnection()
?.getConnector()
.once('authorized', () => {
resolve(true)
})
})
}
await waitForAuthorized()
}

const selectedAccount: Address = accounts?.[0] ?? '0x'

let providerChainId: string | null | undefined = this.#provider?.chainId
if (!providerChainId) {
// request chainId from provider
providerChainId = (await this.#provider?.request({
method: 'eth_chainId',
params: [],
})) as string
}

const chain = {
id: parseInt(providerChainId, 16),
unsupported: false,
}

if (chainId !== undefined && chain.id !== chainId) {
const newChain = await this.switchChain(chainId)
const unsupported = this.isChainUnsupported(newChain.id)
chain.id = newChain.id
chain.unsupported = unsupported
}

if (this.options?.shimDisconnect) {
this.storage?.setItem(this.shimDisconnectKey, true)
}

const connectResponse = {
isConnected: true,
account: selectedAccount,
chain,
provider: this.#provider,
}

return connectResponse
} catch (error) {
if (this.isUserRejectedRequestError(error)) {
throw new UserRejectedRequestError(error as Error)
} else if ((error as ProviderRpcError).code === -32002) {
throw new ResourceUnavailableRpcError(error as ProviderRpcError)
}
throw error
}
}
}
1 change: 1 addition & 0 deletions packages/connectors/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default defineConfig(
'src/coinbaseWallet.ts',
'src/injected.ts',
'src/ledger.ts',
'src/metaMaskSDK.ts',
'src/metaMask.ts',
'src/mock/index.ts',
'src/safe.ts',
Expand Down
Loading
Loading