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: Ledger connector with support for WalletConnect v2 #1549

Merged
6 changes: 3 additions & 3 deletions packages/ledger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@
},
"license": "MIT",
"devDependencies": {
"@walletconnect/types": "^2.7.0",
"typescript": "^4.5.5"
},
"dependencies": {
"@ethersproject/providers": "^5.5.0",
"@ledgerhq/connect-kit-loader": "^1.0.2",
"@walletconnect/client": "^1.7.1",
"@ledgerhq/connect-kit-loader": "^1.1.0",
"@walletconnect/client": "^1.8.0",
"@web3-onboard/common": "^2.3.3",
"rxjs": "^7.5.2"
}
Expand Down
296 changes: 31 additions & 265 deletions packages/ledger/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,279 +1,45 @@
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 = {
walletConnectVersion?: 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 = {
walletConnectVersion: 2
enableDebugLogs?: boolean,
projectId: string
requiredChains?: string[] | number[]
requiredMethods?: string[]
optionalMethods?: string[]
requiredEvents?: string[]
optionalEvents?: 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 ledger(options?: LedgerOptions): WalletInit {
const walletConnectVersion = options?.walletConnectVersion || 1
hlopes-ledger marked this conversation as resolved.
Show resolved Hide resolved
return walletConnectVersion === 1 ?
v1(options as LedgerOptionsWCv1) :
v2(options as LedgerOptionsWCv2)
}

export default ledger
Loading