1- import { Wallet } from '@ant-design/web3-common' ;
1+ import type { Account , Wallet } from '@ant-design/web3-common' ;
22import { LedgerFilled } from '@ant-design/web3-icons' ;
33import type { DeviceSessionId } from '@ledgerhq/device-management-kit' ;
44import { DeviceStatus as DeviceStatusType } from '@ledgerhq/device-management-kit' ;
55
6- import { LedgerAccount } from '../types' ;
6+ import type { LedgerAccount } from '../types' ;
77import AppCommand from './AppCommand' ;
88import AvailableDevices from './AvailableDevices' ;
99import Connect from './Connect' ;
@@ -17,6 +17,12 @@ export class Ledger {
1717 icon : < LedgerFilled /> ,
1818 group : 'Hardware' ,
1919 remark : 'Ledger Hardware Wallet' ,
20+ app : {
21+ link : 'https://www.ledger.com/' ,
22+ } ,
23+ universalProtocol : {
24+ link : 'https://www.ledger.com/' ,
25+ } ,
2026 hasWalletReady : async ( ) => true ,
2127 } ;
2228
@@ -31,6 +37,10 @@ export class Ledger {
3137 public sessionId : DeviceSessionId | null = null ;
3238 public accounts : LedgerAccount [ ] = [ ] ;
3339
40+ private _getWalletConnectProvider ?: ( ) => Promise < any > ;
41+ private _walletConnectAccount ?: Account ;
42+ private _sessionDeleteHandler ?: ( ) => void ;
43+
3444 constructor ( name ?: string , derivationPath ?: string ) {
3545 this . wallet . name = name || 'Ledger' ;
3646 this . derivationPath = derivationPath || "44'/60'/0'/0/0" ;
@@ -132,6 +142,12 @@ export class Ledger {
132142 } ;
133143
134144 public signMessage = async ( message : string ) => {
145+ // Check if using WalletConnect
146+ if ( this . _walletConnectAccount ) {
147+ return this . _signMessageWithWalletConnect ( message ) ;
148+ }
149+
150+ // Use hardware wallet signing
135151 if ( ! this . sessionId ) {
136152 throw new LedgerError (
137153 'NO_SESSION' ,
@@ -146,6 +162,12 @@ export class Ledger {
146162 } ;
147163
148164 public signTypedData = async ( typedData : any ) => {
165+ // Check if using WalletConnect
166+ if ( this . _walletConnectAccount ) {
167+ return this . _signTypedDataWithWalletConnect ( typedData ) ;
168+ }
169+
170+ // Use hardware wallet signing
149171 if ( ! this . sessionId ) {
150172 throw new LedgerError (
151173 'NO_SESSION' ,
@@ -162,4 +184,287 @@ export class Ledger {
162184 throw new LedgerError ( 'SIGN_TYPED_DATA_FAILED' , 'Failed to sign typed data' ) ;
163185 }
164186 } ;
187+
188+ public setWalletConnectProviderGetter = ( providerGetter : ( ) => Promise < any > ) => {
189+ this . _getWalletConnectProvider = providerGetter ;
190+ } ;
191+
192+ public connectWalletConnect = async ( ) => {
193+ if ( ! this . _getWalletConnectProvider ) {
194+ throw new LedgerError ( 'WALLETCONNECT_NOT_CONFIGURED' , 'WalletConnect is not configured' ) ;
195+ }
196+
197+ const provider = await this . _getWalletConnectProvider ( ) ;
198+ if ( ! provider ) {
199+ throw new LedgerError (
200+ 'WALLETCONNECT_PROVIDER_NOT_AVAILABLE' ,
201+ 'WalletConnect provider not available' ,
202+ ) ;
203+ }
204+
205+ try {
206+ // Check if there's an existing pairing (from getQrCode or previous connection)
207+ const hasPairing = provider . client ?. session ?. map ?. size > 0 ;
208+
209+ // Connect to WalletConnect
210+ // If hasPairing is true, skipPairing will make connect() return immediately with existing session
211+ // If hasPairing is false, connect() will wait for mobile wallet to confirm
212+ await provider . connect ( {
213+ namespaces : {
214+ eip155 : {
215+ methods : [
216+ 'eth_sendTransaction' ,
217+ 'eth_signTransaction' ,
218+ 'eth_sign' ,
219+ 'eth_signTypedData' ,
220+ 'personal_sign' ,
221+ ] ,
222+ chains : [ 'eip155:1' ] , // Mainnet, can be extended
223+ events : [ 'chainChanged' , 'accountsChanged' ] ,
224+ } ,
225+ } ,
226+ skipPairing : hasPairing ,
227+ } ) ;
228+
229+ // Get session after connection (mobile wallet confirmed or existing session used)
230+ const session = provider . session ;
231+ if ( ! session ) {
232+ throw new LedgerError (
233+ 'WALLETCONNECT_SESSION_NOT_AVAILABLE' ,
234+ 'WalletConnect session not available' ,
235+ ) ;
236+ }
237+
238+ // Listen for session_delete event to handle remote disconnection
239+ this . _sessionDeleteHandler = ( ) => {
240+ this . _walletConnectAccount = undefined ;
241+ } ;
242+ provider . on ( 'session_delete' , this . _sessionDeleteHandler ) ;
243+
244+ const accounts = session . namespaces . eip155 ?. accounts || [ ] ;
245+ if ( accounts . length === 0 ) {
246+ throw new LedgerError (
247+ 'WALLETCONNECT_NO_ACCOUNTS' ,
248+ 'No accounts found in WalletConnect session' ,
249+ ) ;
250+ }
251+
252+ // Extract address from account string (format: eip155:1:0x...)
253+ const address = accounts [ 0 ] . split ( ':' ) [ 2 ] ;
254+ if ( ! address ) {
255+ throw new LedgerError (
256+ 'WALLETCONNECT_INVALID_ACCOUNT' ,
257+ 'Invalid account format from WalletConnect' ,
258+ ) ;
259+ }
260+
261+ this . _walletConnectAccount = {
262+ address,
263+ } ;
264+
265+ return this . _walletConnectAccount ;
266+ } catch ( error : any ) {
267+ if ( error instanceof LedgerError ) {
268+ throw error ;
269+ }
270+ throw new LedgerError (
271+ 'WALLETCONNECT_CONNECTION_FAILED' ,
272+ error ?. message || 'Failed to connect via WalletConnect' ,
273+ ) ;
274+ }
275+ } ;
276+
277+ public disconnectWalletConnect = async ( ) => {
278+ if ( this . _getWalletConnectProvider ) {
279+ try {
280+ const provider = await this . _getWalletConnectProvider ( ) ;
281+ if ( provider ) {
282+ // Remove session_delete listener
283+ if ( this . _sessionDeleteHandler ) {
284+ provider . off ( 'session_delete' , this . _sessionDeleteHandler ) ;
285+ this . _sessionDeleteHandler = undefined ;
286+ }
287+
288+ if ( provider . session ) {
289+ await provider . disconnect ( ) ;
290+ }
291+ }
292+ } catch {
293+ // Ignore disconnect errors
294+ }
295+ }
296+ this . _walletConnectAccount = undefined ;
297+ } ;
298+
299+ public getWalletConnectAccount = ( ) : Account | undefined => {
300+ return this . _walletConnectAccount ;
301+ } ;
302+
303+ /**
304+ * Sign message using WalletConnect
305+ * @private
306+ */
307+ private _signMessageWithWalletConnect = async ( message : string ) : Promise < any > => {
308+ if ( ! this . _getWalletConnectProvider ) {
309+ throw new LedgerError ( 'WALLETCONNECT_NOT_CONFIGURED' , 'WalletConnect is not configured' ) ;
310+ }
311+
312+ if ( ! this . _walletConnectAccount ?. address ) {
313+ throw new LedgerError ( 'WALLETCONNECT_NO_ACCOUNTS' , 'No WalletConnect account available' ) ;
314+ }
315+
316+ try {
317+ const provider = await this . _getWalletConnectProvider ( ) ;
318+ if ( ! provider ) {
319+ throw new LedgerError (
320+ 'WALLETCONNECT_PROVIDER_NOT_AVAILABLE' ,
321+ 'WalletConnect provider not available' ,
322+ ) ;
323+ }
324+
325+ const session = provider . session ;
326+ if ( ! session ) {
327+ throw new LedgerError (
328+ 'WALLETCONNECT_SESSION_NOT_AVAILABLE' ,
329+ 'WalletConnect session not available' ,
330+ ) ;
331+ }
332+
333+ // Get chain ID from session (format: eip155:1)
334+ const accounts = session . namespaces . eip155 ?. accounts || [ ] ;
335+ if ( accounts . length === 0 ) {
336+ throw new LedgerError (
337+ 'WALLETCONNECT_NO_ACCOUNTS' ,
338+ 'No accounts found in WalletConnect session' ,
339+ ) ;
340+ }
341+
342+ // Extract chain ID from account string (format: eip155:1:0x...)
343+ const chainId = accounts [ 0 ] . split ( ':' ) [ 1 ] ;
344+ const chain = chainId ? `eip155:${ chainId } ` : undefined ;
345+
346+ // Convert message to hex string if it's not already
347+ let messageHex : string ;
348+ if ( message . startsWith ( '0x' ) ) {
349+ messageHex = message ;
350+ } else {
351+ // Convert string to hex using TextEncoder for browser compatibility
352+ const encoder = new TextEncoder ( ) ;
353+ const bytes = encoder . encode ( message ) ;
354+ messageHex = `0x${ Array . from ( bytes )
355+ . map ( ( byte ) => byte . toString ( 16 ) . padStart ( 2 , '0' ) )
356+ . join ( '' ) } `;
357+ }
358+
359+ // Call personal_sign via WalletConnect
360+ const signature = ( await provider . request (
361+ {
362+ method : 'personal_sign' ,
363+ params : [ messageHex , this . _walletConnectAccount . address ] ,
364+ } ,
365+ chain ,
366+ ) ) as string ;
367+
368+ return signature ;
369+ } catch ( error : any ) {
370+ if ( error instanceof LedgerError ) {
371+ throw error ;
372+ }
373+ throw new LedgerError (
374+ 'SIGN_MESSAGE_FAILED' ,
375+ error ?. message || 'Failed to sign message via WalletConnect' ,
376+ ) ;
377+ }
378+ } ;
379+
380+ /**
381+ * Sign typed data (EIP-712) using WalletConnect
382+ * @private
383+ */
384+ private _signTypedDataWithWalletConnect = async ( typedData : any ) : Promise < any > => {
385+ if ( ! this . _getWalletConnectProvider ) {
386+ throw new LedgerError ( 'WALLETCONNECT_NOT_CONFIGURED' , 'WalletConnect is not configured' ) ;
387+ }
388+
389+ if ( ! this . _walletConnectAccount ?. address ) {
390+ throw new LedgerError ( 'WALLETCONNECT_NO_ACCOUNTS' , 'No WalletConnect account available' ) ;
391+ }
392+
393+ try {
394+ const provider = await this . _getWalletConnectProvider ( ) ;
395+ if ( ! provider ) {
396+ throw new LedgerError (
397+ 'WALLETCONNECT_PROVIDER_NOT_AVAILABLE' ,
398+ 'WalletConnect provider not available' ,
399+ ) ;
400+ }
401+
402+ const session = provider . session ;
403+ if ( ! session ) {
404+ throw new LedgerError (
405+ 'WALLETCONNECT_SESSION_NOT_AVAILABLE' ,
406+ 'WalletConnect session not available' ,
407+ ) ;
408+ }
409+
410+ // Get chain ID from session (format: eip155:1)
411+ const accounts = session . namespaces . eip155 ?. accounts || [ ] ;
412+ if ( accounts . length === 0 ) {
413+ throw new LedgerError (
414+ 'WALLETCONNECT_NO_ACCOUNTS' ,
415+ 'No accounts found in WalletConnect session' ,
416+ ) ;
417+ }
418+
419+ // Extract chain ID from account string (format: eip155:1:0x...)
420+ const chainId = accounts [ 0 ] . split ( ':' ) [ 1 ] ;
421+ const chain = chainId ? `eip155:${ chainId } ` : undefined ;
422+
423+ // Ensure chainId in domain is a number (not string) for proper serialization
424+ const normalizedTypedData = {
425+ ...typedData ,
426+ domain : {
427+ ...typedData . domain ,
428+ chainId :
429+ typeof typedData . domain ?. chainId === 'string'
430+ ? parseInt ( typedData . domain . chainId , 10 )
431+ : typedData . domain ?. chainId ,
432+ } ,
433+ } ;
434+
435+ // Call eth_signTypedData via WalletConnect
436+ // Note: WalletConnect v2 expects the second parameter to be a JSON string
437+ // Some wallets may also support eth_signTypedData_v4
438+ let signature : string ;
439+ try {
440+ // Try eth_signTypedData_v4 first (preferred for WalletConnect v2)
441+ signature = ( await provider . request (
442+ {
443+ method : 'eth_signTypedData_v4' ,
444+ params : [ this . _walletConnectAccount . address , JSON . stringify ( normalizedTypedData ) ] ,
445+ } ,
446+ chain ,
447+ ) ) as string ;
448+ } catch ( error : any ) {
449+ // Fallback to eth_signTypedData if v4 is not supported
450+ signature = ( await provider . request (
451+ {
452+ method : 'eth_signTypedData' ,
453+ params : [ this . _walletConnectAccount . address , JSON . stringify ( normalizedTypedData ) ] ,
454+ } ,
455+ chain ,
456+ ) ) as string ;
457+ }
458+
459+ return signature ;
460+ } catch ( error : any ) {
461+ if ( error instanceof LedgerError ) {
462+ throw error ;
463+ }
464+ throw new LedgerError (
465+ 'SIGN_TYPED_DATA_FAILED' ,
466+ error ?. message || 'Failed to sign typed data via WalletConnect' ,
467+ ) ;
468+ }
469+ } ;
165470}
0 commit comments