Skip to content

Commit

Permalink
feat: Ledger connector with support for WalletConnect v2
Browse files Browse the repository at this point in the history
  • Loading branch information
hlopes-ledger committed Feb 24, 2023
1 parent ac060ac commit 911c933
Show file tree
Hide file tree
Showing 5 changed files with 3,276 additions and 269 deletions.
8 changes: 5 additions & 3 deletions packages/ledger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@
},
"license": "MIT",
"devDependencies": {
"typescript": "^4.5.5"
"typescript": "^4.5.5",
"@walletconnect/types": "^2.3.3"
},
"dependencies": {
"@ethersproject/providers": "^5.5.0",
"@ledgerhq/connect-kit-loader": "^1.0.2",
"@walletconnect/client": "^1.7.1",
"@ethersproject/providers": "^5.5.0",
"@walletconnect/ethereum-provider": "2.4.3",
"@walletconnect/client": "^1.8.0",
"@web3-onboard/common": "^2.2.3",
"rxjs": "^7.5.2"
}
Expand Down
301 changes: 35 additions & 266 deletions packages/ledger/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,279 +1,48 @@
import {
Chain,
WalletInit,
GetInterfaceHelpers,
EIP1193Provider,
ProviderAccounts
} from '@web3-onboard/common'
import type { EthereumProvider } from '@ledgerhq/connect-kit-loader'
import type { StaticJsonRpcProvider as StaticJsonRpcProviderType } from '@ethersproject/providers'
import WalletConnect from '@walletconnect/client'
import type { WalletInit } from '@web3-onboard/common'
import v1 from './v1'
import v2 from './v2'

const isHexString = (value: string | number) => {
if (typeof value !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) {
return false
}

return true
}

interface LedgerOptions {
export type LedgerOptionsWCv1 = {
version?: 1
enableDebugLogs?: boolean,
chainId?: number
bridge?: string
infuraId?: string
rpc?: { [chainId: number]: string }
}

function ledger(options?: LedgerOptions): WalletInit {
return () => {
return {
label: 'Ledger',
getIcon: async () => (await import('./icon.js')).default,
getInterface: async ({ chains, EventEmitter }: GetInterfaceHelpers) => {
const {
loadConnectKit,
SupportedProviders,
SupportedProviderImplementations
} = await import('@ledgerhq/connect-kit-loader')

const connectKit = await loadConnectKit()
connectKit.enableDebugLogs()
const checkSupportResult = connectKit.checkSupport({
providerType: SupportedProviders.Ethereum,
chainId: options?.chainId,
infuraId: options?.infuraId,
rpc: options?.rpc
})

// get the Ledger provider instance, it can be either Ledger Connect
// or WalletConnect
const instance = (await connectKit.getProvider()) as EthereumProvider

// return the Ledger Connect provider
if (
checkSupportResult.providerImplementation ===
SupportedProviderImplementations.LedgerConnect
) {
return {
provider: instance as EIP1193Provider
}
}

// fallback to WalletConnect on unsupported platforms
const { StaticJsonRpcProvider } = await import(
'@ethersproject/providers'
)
const { ProviderRpcError, ProviderRpcErrorCode } = await import(
'@web3-onboard/common'
)
const { default: WalletConnect } = await import('@walletconnect/client')
const { Subject, fromEvent } = await import('rxjs')
const { takeUntil, take } = await import('rxjs/operators')
const connector = instance.connector as WalletConnect
const emitter = new EventEmitter()

class EthProvider {
public request: EIP1193Provider['request']
public connector: InstanceType<typeof WalletConnect>
public chains: Chain[]
public disconnect: EIP1193Provider['disconnect']
public emit: typeof EventEmitter['emit']
public on: typeof EventEmitter['on']
public removeListener: typeof EventEmitter['removeListener']

private disconnected$: InstanceType<typeof Subject>
private providers: Record<string, StaticJsonRpcProviderType>

constructor({
connector,
chains
}: {
connector: InstanceType<typeof WalletConnect>
chains: Chain[]
}) {
this.emit = emitter.emit.bind(emitter)
this.on = emitter.on.bind(emitter)
this.removeListener = emitter.removeListener.bind(emitter)
this.connector = connector
this.chains = chains
this.disconnected$ = new Subject()
this.providers = {}

// listen for session updates
fromEvent(this.connector, 'session_update', (error, payload) => {
if (error) {
throw error
}

return payload
})
.pipe(takeUntil(this.disconnected$))
.subscribe({
next: ({ params }) => {
const [{ accounts, chainId }] = params
this.emit('accountsChanged', accounts)
const hexChainId = isHexString(chainId)
? chainId
: `0x${chainId.toString(16)}`
this.emit('chainChanged', hexChainId)
},
error: console.warn
})

// listen for disconnect event
fromEvent(this.connector, 'disconnect', (error, payload) => {
if (error) {
throw error
}

return payload
})
.pipe(takeUntil(this.disconnected$))
.subscribe({
next: () => {
this.emit('accountsChanged', [])
this.disconnected$.next(true)
typeof localStorage !== 'undefined' &&
localStorage.removeItem('walletconnect')
},
error: console.warn
})

this.disconnect = () => this.connector.killSession()

this.request = async ({ method, params }) => {
if (method === 'eth_chainId') {
return isHexString(this.connector.chainId)
? this.connector.chainId
: `0x${this.connector.chainId.toString(16)}`
}

if (method === 'eth_requestAccounts') {
return new Promise<ProviderAccounts>((resolve, reject) => {
// Check if connection is already established
if (!this.connector.connected) {
resolve((instance as any).request({ method }))
} else {
const { accounts, chainId } = this.connector.session

const hexChainId = isHexString(chainId)
? chainId
: `0x${chainId.toString(16)}`

this.emit('chainChanged', hexChainId)
return resolve(accounts)
}

// Subscribe to connection events
fromEvent(this.connector, 'connect', (error, payload) => {
if (error) {
throw error
}

return payload
})
.pipe(take(1))
.subscribe({
next: ({ params }) => {
const [{ accounts, chainId }] = params
this.emit('accountsChanged', accounts)

const hexChainId = isHexString(chainId)
? chainId
: `0x${chainId.toString(16)}`
this.emit('chainChanged', hexChainId)
resolve(accounts)
},
error: reject
})
})
}

if (method === 'eth_selectAccounts') {
throw new ProviderRpcError({
code: ProviderRpcErrorCode.UNSUPPORTED_METHOD,
message: `The Provider does not support the requested method: ${method}`
})
}

if (method == 'wallet_switchEthereumChain') {
throw new ProviderRpcError({
code: ProviderRpcErrorCode.UNSUPPORTED_METHOD,
message: `The Provider does not support the requested method: ${method}`
})
}

// @ts-ignore
if (method === 'eth_sendTransaction') {
// @ts-ignore
return this.connector.sendTransaction(params[0])
}

// @ts-ignore
if (method === 'eth_signTransaction') {
// @ts-ignore
return this.connector.signTransaction(params[0])
}

// @ts-ignore
if (method === 'personal_sign') {
// @ts-ignore
return this.connector.signPersonalMessage(params)
}

// @ts-ignore
if (method === 'eth_sign') {
// @ts-ignore
return this.connector.signMessage(params)
}

// @ts-ignore
if (method.includes('eth_signTypedData')) {
// @ts-ignore
return this.connector.signTypedData(params)
}

if (method === 'eth_accounts') {
return this.connector.sendCustomRequest({
id: 1337,
jsonrpc: '2.0',
method,
params
})
}

const chainId = await this.request({ method: 'eth_chainId' })

if (!this.providers[chainId]) {
const currentChain = chains.find(({ id }) => id === chainId)
export type LedgerOptionsWCv2 = {
version: 2
enableDebugLogs?: boolean,
projectId: string
chains?: number[]
optionalChains?: number[]
methods?: string[]
optionalMethods?: string[]
events?: string[]
optionalEvents?: string[]
rpcMap?: { [chainId: string]: string; }
metadata?: string[]
}

if (!currentChain) {
throw new ProviderRpcError({
code: ProviderRpcErrorCode.CHAIN_NOT_ADDED,
message: `The Provider does not have a rpcUrl to make a request for the requested method: ${method}`
})
}
export type LedgerOptions = (
| LedgerOptionsWCv1
| LedgerOptionsWCv2
)

this.providers[chainId] = new StaticJsonRpcProvider(
currentChain.rpcUrl
)
}
export const isHexString = (value: string | number) => {
if (typeof value !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) {
return false
}

return this.providers[chainId].send(
method,
// @ts-ignore
params
)
}
}
}
return true
}

return {
provider: new EthProvider({ chains, connector })
}
}
}
}
function walletConnect(options?: LedgerOptions): WalletInit {
const version = options?.version || 1
return version === 1 ?
v1(options as LedgerOptionsWCv1) :
v2(options as LedgerOptionsWCv2)
}

export default ledger
export default walletConnect
Loading

0 comments on commit 911c933

Please sign in to comment.