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

Commit

Permalink
feat(connectors): add LedgerConnector (#11)
Browse files Browse the repository at this point in the history
* Add Ledger connector

* Code restructuring and some fixes

- sort connector lists and some properties in alphabetical order
- remove `infuraId` from the connector options
- use the # symbol instead of the private keyword
- move Connect Kit initialization to `#getConnectKit`
  - replace `#connectKitPromise` with `#connectKit`, and instead of
    assigning it in the constructor, make it optional and assign it
    inside `#getConnectKit`
  - optionaly get chainId as a parameter when called by getProvider
    since we need to pass it to connectKit.checkSupport
  - instead of passing the `infuraId` build the rpc from the chain list
- fixes in `connect()`
  - replace the `getAccount` call in connect with a request for
    `eth_requestAccounts`
  - add `chainId` and rename create parameter on `getProvider` for
    consistency with ither connectors
  - now calls `#getConnectKit` passing `chainId` instead of
    just resolving the Connect Kit promise
- replace request for `eth_requestAccounts` with one for
  `eth_accounts` in `getAccount` and call it from
  `this.isAuthorized()`
- add `chainId` argument to `getSigner` and pass it to `getProvider`

* refactor: encapsulate connectkit init inside getProvider

* chore: add stub tests

* chore: add codeowners

* fix: remove "walletconnect" from local storage

* chore: add manual testing instructions

Co-authored-by: Hugo Lopes <hugo.lopes-ext@ledger.fr>
  • Loading branch information
jxom and hlopes-ledger authored Dec 12, 2022
1 parent 803ac4a commit 5db7cba
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-beds-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@wagmi/connectors': minor
---

Added `LedgerConnector`
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tmm @jxom

/packages/connectors/src/ledger @hlopes-ledger
3 changes: 2 additions & 1 deletion packages/connectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ const client = createClient({
## Connectors

- [`InjectedConnector`](/packages/connectors/src/injected.ts)
- [`CoinbaseWalletConnector`](/packages/connectors/src/coinbaseWallet.ts)
- [`InjectedConnector`](/packages/connectors/src/injected.ts)
- [`LedgerConnector`](/packages/connectors/src/ledger.ts)
- [`MetaMaskConnector`](/packages/connectors/src/metaMask.ts)
- [`MockConnector`](/packages/connectors/src/mock.ts)
- [`WalletConnectConnector`](/packages/connectors/src/walletConnect.ts)
Expand Down
4 changes: 4 additions & 0 deletions packages/connectors/ledger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "../dist/ledger.js",
"type": "module"
}
8 changes: 7 additions & 1 deletion packages/connectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@coinbase/wallet-sdk": "^3.5.4",
"@walletconnect/ethereum-provider": "^1.8.0",
"@ledgerhq/connect-kit-loader": "^1.0.0",
"abitype": "^0.1.8",
"eventemitter3": "^4.0.7"
},
Expand All @@ -42,6 +43,10 @@
"types": "./dist/injected.d.ts",
"default": "./dist/injected.js"
},
"./ledger": {
"types": "./dist/ledger.d.ts",
"default": "./dist/ledger.js"
},
"./metaMask": {
"types": "./dist/metaMask.d.ts",
"default": "./dist/metaMask.js"
Expand All @@ -61,7 +66,8 @@
"/injected",
"/metaMask",
"/mock",
"/walletConnect"
"/walletConnect",
"/ledger"
],
"sideEffects": false,
"contributors": [
Expand Down
74 changes: 74 additions & 0 deletions packages/connectors/src/ledger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { testChains } from '@wagmi/core/internal/test'
import { describe, expect, it } from 'vitest'

import { LedgerConnector } from './ledger'

/*
* To manually test the Ledger connector:
*
* - install the Ledger Live app, https://www.ledger.com/ledger-live
* - install the Ledger Connect extension, currently in beta, it should soon be
* distributed with the Ledger Live app for iOS and macOS
* - run the wagmi playground app following the contributing docs on the main
* wagmi repository
* - open the playground app in a web browser
* - press the "Ledger" button
* - see below for the Ledger Connect and Ledger Live flows
* - after the account is selected on the wallet the dapp state should reflect
* the chosen account information
*
* Ledger Connect
*
* - if you are on a platform supported by the Ledger Connect extension
* (currently Safari on iOS and macOS) but don't have it installed or enabled
* you should see a modal explaining how you can do that
* - if you are on a platform supported by the Ledger Connect extension and
* have it installed and enabled it should pop up allowing you to select an
* account
* - when pressing the "Disconnect" button on the dapp, the dapp shows as
* disconnected but you are not actually disconnected until you press
* the "Disconnect" button on Ledger Connect
* - when pressing the "Disconnect" button on Ledger Connect (press the pill
* shaped button with the Ledger logo, then "Disconnect" on the popup), the
* dapp should also show as disconnected
*
* Testing Ledger Live
*
* - if you are on a platform not yet supported by the Connect extension you
* should see a modal allowing you to use the Ledger Live app; pressing
* "Use Ledger Live" or scanning the QR code should open the app and allow
* you to choose an account
* - when pressing the "Disconnect" button on the dapp, Ledger Live should
* also show as disconnected
* - when pressing the "Disconnect" button on Ledger Live, the dapp should
* also show as disconnected
* - when switching accounts on Ledger Live the dapp should reflect those
* changes
*/
describe('LedgerConnector', () => {
it('inits', () => {
const connector = new LedgerConnector({
chains: testChains,
options: {},
})
expect(connector.name).toEqual('Ledger')
})

describe('behavior', () => {
it.todo('connects')

it.todo('disconnects via dapp (wagmi Connector)')

it.todo('disconnects via wallet (Ledger Live)')

it.todo('switch chains via dapp (wagmi Connector)')

it.todo('switch chains via wallet (Ledger Live)')

it.todo('switch accounts via wallet (Ledger Live)')

it.todo('sends a transaction')

it.todo('signs a message')
})
})
189 changes: 189 additions & 0 deletions packages/connectors/src/ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import {
SupportedProviders,
loadConnectKit,
} from '@ledgerhq/connect-kit-loader'
import type { EthereumProvider } from '@ledgerhq/connect-kit-loader'
import {
Chain,
ProviderRpcError,
RpcError,
UserRejectedRequestError,
normalizeChainId,
} from '@wagmi/core'
import { providers } from 'ethers'
import { getAddress } from 'ethers/lib/utils.js'

import type { ConnectorData } from './base'
import { Connector } from './base'

type LedgerConnectorOptions = {
bridge?: string
chainId?: number
enableDebugLogs?: boolean
rpc?: { [chainId: number]: string }
}

type LedgerSigner = providers.JsonRpcSigner

export class LedgerConnector extends Connector<
EthereumProvider,
LedgerConnectorOptions,
LedgerSigner
> {
readonly id = 'ledger'
readonly name = 'Ledger'
readonly ready = true

#provider?: EthereumProvider

constructor({
chains,
options = { enableDebugLogs: false },
}: {
chains?: Chain[]
options?: LedgerConnectorOptions
} = {}) {
super({ chains, options })
}

async connect(): Promise<Required<ConnectorData>> {
try {
const provider = await this.getProvider({ create: true })

if (provider.on) {
provider.on('accountsChanged', this.onAccountsChanged)
provider.on('chainChanged', this.onChainChanged)
provider.on('disconnect', this.onDisconnect)
}

this.emit('message', { type: 'connecting' })

const accounts = (await provider.request({
method: 'eth_requestAccounts',
})) as string[]
const account = getAddress(accounts[0] as string)
const id = await this.getChainId()
const unsupported = this.isChainUnsupported(id)

return {
account,
chain: { id, unsupported },
provider: new providers.Web3Provider(
provider as providers.ExternalProvider,
),
}
} catch (error) {
if ((error as ProviderRpcError).code === 4001) {
throw new UserRejectedRequestError(error)
}
if ((error as RpcError).code === -32002) {
throw error instanceof Error ? error : new Error(String(error))
}

throw error
}
}

async disconnect() {
const provider = await this.getProvider()

if (provider?.disconnect) {
await provider.disconnect()
}

if (provider?.removeListener) {
provider.removeListener('accountsChanged', this.onAccountsChanged)
provider.removeListener('chainChanged', this.onChainChanged)
provider.removeListener('disconnect', this.onDisconnect)
}

typeof localStorage !== 'undefined' &&
localStorage.removeItem('walletconnect')
}

async getAccount() {
const provider = await this.getProvider()
const accounts = (await provider.request({
method: 'eth_accounts',
})) as string[]
const account = getAddress(accounts[0] as string)

return account
}

async getChainId() {
const provider = await this.getProvider()
const chainId = (await provider.request({
method: 'eth_chainId',
})) as number

return normalizeChainId(chainId)
}

async getProvider(
{ chainId, create }: { chainId?: number; create?: boolean } = {
create: false,
},
) {
if (!this.#provider || chainId || create) {
const connectKit = await loadConnectKit()

if (this.options.enableDebugLogs) {
connectKit.enableDebugLogs()
}

const rpc = this.chains.reduce(
(rpc, chain) => ({
...rpc,
[chain.id]: chain.rpcUrls.default.http[0],
}),
{},
)

connectKit.checkSupport({
bridge: this.options.bridge,
providerType: SupportedProviders.Ethereum,
chainId: chainId || this.options.chainId,
rpc: { ...rpc, ...this.options?.rpc },
})

this.#provider = (await connectKit.getProvider()) as EthereumProvider
}
return this.#provider
}

async getSigner({ chainId }: { chainId?: number } = {}) {
const [provider, account] = await Promise.all([
this.getProvider({ chainId }),
this.getAccount(),
])
return new providers.Web3Provider(
provider as providers.ExternalProvider,
chainId,
).getSigner(account)
}

async isAuthorized() {
try {
const account = await this.getAccount()
return !!account
} catch {
return false
}
}

protected onAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) this.emit('disconnect')
else this.emit('change', { account: getAddress(accounts[0] as string) })
}

protected onChainChanged = (chainId: number | string) => {
const id = normalizeChainId(chainId)
const unsupported = this.isChainUnsupported(id)
this.emit('change', { chain: { id, unsupported } })
}

protected onDisconnect = () => {
this.emit('disconnect')
}
}
1 change: 1 addition & 0 deletions packages/connectors/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default defineConfig(
'src/metaMask.ts',
'src/mock/index.ts',
'src/walletConnect.ts',
'src/ledger.ts',
],
platform: 'browser',
}),
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5db7cba

Please sign in to comment.