-
Notifications
You must be signed in to change notification settings - Fork 513
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Ledger connector with support for WalletConnect v2
- Loading branch information
1 parent
dbddeb8
commit b1a0d7a
Showing
6 changed files
with
1,853 additions
and
272 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
return walletConnectVersion === 1 ? | ||
v1(options as LedgerOptionsWCv1) : | ||
v2(options as LedgerOptionsWCv2) | ||
} | ||
|
||
export default ledger |
Oops, something went wrong.