import { LedgerOptions , WalletModule , HardwareWalletCustomNetwork , Helpers } from '../../../interfaces' ; import ledgerIcon from '../wallet-icons/icon-ledger' ; const LEDGER_LIVE_PATH = `m/44'/60'` ; const ACCOUNTS_TO_GET = 5 ; function ledger ( options ) { const { rpcUrl , LedgerTransport , networkId , preferred , label , iconSrc , svg , customNetwork } = options ; return { name : label || 'Ledger' , svg : svg || ledgerIcon , iconSrc , wallet : async ( helpers ) => { const { BigNumber , networkName , resetWalletState } = helpers ; const provider = await ledgerProvider ( { rpcUrl , networkId , LedgerTransport , BigNumber , networkName , resetWalletState , customNetwork } ) ; return { provider , interface : { name : 'Ledger' , connect : provider . enable , disconnect : provider . disconnect , address : { get : async ( ) => provider . getPrimaryAddress ( ) } , network : { get : async ( ) => networkId } , balance : { get : async ( ) => { const address = provider . getPrimaryAddress ( ) ; return address && provider . getBalance ( address ) ; } } } } ; } , type : 'hardware' , desktop : true , mobile : true , osExclusions : [ 'iOS' ] , preferred } ; } interface LedgerProviderOptions { networkId : number ; rpcUrl : string ; LedgerTransport : any ; BigNumber : any ; customNetwork ? : HardwareWalletCustomNetwork ; networkName : ( id : number ) => string ; resetWalletState : ( options ? : { disconnected : boolean ; walletName : string ; } ) => void ; } async function ledgerProvider ( options ) { const { default : createProvider } = await import ( './providerEngine' ) ; const { generateAddresses , isValidPath } = await import ( './hd-wallet' ) ; const { default : Eth } = await import ( '@ledgerhq/hw-app-eth' ) ; const { TransactionFactory : Transaction , Capability } = await import ( '@ethereumjs/tx' ) ; const { default : Common , Hardfork } = await import ( '@ethereumjs/common' ) ; const ethUtil = await import ( 'ethereumjs-util' ) ; const { TypedDataUtils } = await import ( 'eth-sig-util' ) ; const domainHash = ( message ) => { return TypedDataUtils . hashStruct ( 'EIP712Domain' , message . domain , message . types , true ) ; } ; const messageHash = ( message ) => { return TypedDataUtils . hashStruct ( message . primaryType , message . message , message . types , true ) ; } ; const { networkId , rpcUrl , LedgerTransport , BigNumber , networkName , resetWalletState , customNetwork } = options ; let dPath = '' ; let addressToPath = new Map ( ) ; let enabled = false ; let customPath = false ; let account ; const provider = createProvider ( { getAccounts : ( callback ) => { getAccounts ( ) . then ( ( res ) => callback ( null , res ) ) . catch ( ( err ) => callback ( err , null ) ) ; } , signTransaction : ( transactionData , callback ) => { signTransaction ( transactionData ) . then ( ( res ) => callback ( null , res ) ) . catch ( err => callback ( err , null ) ) ; } , processMessage : ( messageData , callback ) => { signMessage ( messageData ) . then ( ( res ) => callback ( null , res ) ) . catch ( err => callback ( err , null ) ) ; } , processPersonalMessage : ( messageData , callback ) => { signMessage ( messageData ) . then ( ( res ) => callback ( null , res ) ) . catch ( err => callback ( err , null ) ) ; } , signMessage : ( messageData , callback ) => { signMessage ( messageData ) . then ( ( res ) => callback ( null , res ) ) . catch ( err => callback ( err , null ) ) ; } , signPersonalMessage : ( messageData , callback ) => { signMessage ( messageData ) . then ( ( res ) => callback ( null , res ) ) . catch ( err => callback ( err , null ) ) ; } , signTypedMessage : ( messageData , callback ) => { signTypedMessage ( messageData ) . then ( ( res ) => callback ( null , res ) ) . catch ( err => callback ( err , null ) ) ; } , rpcUrl } ) ; provider . setPath = setPath ; provider . dPath = dPath ; provider . enable = enable ; provider . setPrimaryAccount = setPrimaryAccount ; provider . getPrimaryAddress = getPrimaryAddress ; provider . getAccounts = getAccounts ; provider . getMoreAccounts = getMoreAccounts ; provider . getBalance = getBalance ; provider . getBalances = getBalances ; provider . send = provider . sendAsync ; provider . disconnect = disconnect ; provider . isCustomPath = isCustomPath ; let transport ; let transportSubscription ; let eth ; function disconnect ( ) { transport ?. close ( ) ; transportSubscription ?. unsubscribe ( ) ; provider . stop ( ) ; resetWalletState ( { disconnected : true , walletName : 'Ledger' } ) ; } async function setPath ( path , custom ? ) { if ( ! isValidPath ( path ) ) { return false ; } if ( path !== dPath ) { addressToPath = new Map ( ) ; } if ( custom ) { const address = await getAddress ( path ) ; addressToPath . set ( address , path ) ; customPath = true ; return true ; } customPath = false ; dPath = path ; return true ; } function isCustomPath ( ) { return customPath ; } async function createTransport ( ) { try { const observer = { next : ( event ) => { if ( event . type === 'remove' ) { disconnect ( ) ; } } , error : ( ) => { } , complete : ( ) => { } } ; let Transport = LedgerTransport ; if ( ! Transport ) { Transport = ( await supportsWebUSB ( ) ) ? ( await import ( '@ledgerhq/hw-transport-webusb' ) ) . default : ( await import ( '@ledgerhq/hw-transport-u2f' ) ) . default ; } transport = await Transport . create ( ) ; eth = new Eth ( transport ) ; Transport . listen ( observer ) ; } catch ( error ) { console . error ( error ) ; throw new Error ( 'Error connecting to Ledger wallet' ) ; } } function enable ( ) { enabled = true ; return getAccounts ( ) ; } function addresses ( ) { return Array . from ( addressToPath . keys ( ) ) ; } function setPrimaryAccount ( address ) { const accounts = [ ... addressToPath . entries ( ) ] ; const accountIndex = accounts . findIndex ( ( [ accountAddress ] ) => accountAddress === address ) ; accounts . unshift ( accounts . splice ( accountIndex , 1 ) [ 0 ] ) ; addressToPath = new Map ( accounts ) ; } async function getAddress ( path ) { if ( ! eth ) { await createTransport ( ) ; } try { const result = await eth . getAddress ( path ) ; return result . address ; } catch ( error ) { } } async function getPublicKey ( ) { if ( ! dPath ) { throw new Error ( 'a derivation path is needed to get the public key' ) ; } if ( ! eth ) { await createTransport ( ) ; } try { const result = await eth . getAddress ( dPath , false , true ) ; const { publicKey , chainCode } = result ; account = { publicKey , chainCode , path : dPath } ; return account ; } catch ( error ) { console . error ( { error } ) ; throw new Error ( 'There was a problem accessing your Ledger accounts.' ) ; } } function getPrimaryAddress ( ) { return enabled ? addresses ( ) [ 0 ] : undefined ; } async function getMoreAccounts ( ) { const accounts = await getAccounts ( true ) ; return accounts && getBalances ( accounts ) ; } async function getAccounts ( getMore ? ) { if ( ! enabled ) { return [ ] ; } if ( addressToPath . size > 0 && ! getMore ) { return addresses ( ) ; } if ( ! eth ) { await createTransport ( ) ; } if ( dPath === '' ) { dPath = LEDGER_LIVE_PATH ; } if ( dPath === LEDGER_LIVE_PATH ) { const currentAccounts = addressToPath . size ; const paths = [ ] ; for ( let i = currentAccounts ; i < ACCOUNTS_TO_GET + currentAccounts ; i ++ ) { paths . push ( `${ LEDGER_LIVE_PATH }/${ i }'/0/0` ) ; } for ( const path of paths ) { const res = await eth . getAddress ( path ) ; addressToPath . set ( res . address , path ) ; } } else { if ( ! account ) { try { account = await getPublicKey ( ) ; } catch ( error ) { throw error ; } } const addressInfo = generateAddresses ( account , addressToPath . size ) ; addressInfo . forEach ( ( { dPath , address } ) => { addressToPath . set ( address , dPath ) ; } ) ; } return addresses ( ) ; } function getBalances ( addresses ) { return Promise . all ( addresses . map ( address => new Promise ( async ( resolve ) => { const balance = await getBalance ( address ) ; resolve ( { address , balance } ) ; } ) ) ) ; } function getBalance ( address ) { return new Promise ( ( resolve , reject ) => { provider . sendAsync ( { jsonrpc : '2.0' , method : 'eth_getBalance' , params : [ address , 'latest' ] , id : 42 } , ( e , res ) => { e && reject ( e ) ; const result = res && res . result ; if ( result != null ) { resolve ( new BigNumber ( result ) . toString ( 10 ) ) ; } else { resolve ( null ) ; } } ) ; } ) ; } async function signTransaction ( transactionData ) { const path = [ ... addressToPath . values ( ) ] [ 0 ] ; const { rlp } = ethUtil ; const common = new Common ( { chain : customNetwork || networkName ( networkId ) , hardfork : Hardfork . Berlin , eips : [ 1559 ] } ) ; try { transactionData . gasLimit = transactionData . gas ?? transactionData . gasLimit ; const transaction = Transaction . fromTxData ( { ... transactionData } , { common } ) ; let unsignedTx = transaction . getMessageToSign ( false ) ; if ( ! transaction . supports ( Capability . EIP1559FeeMarket ) ) { unsignedTx = rlp . encode ( unsignedTx ) ; } const { v , r , s } = await eth . signTransaction ( path , unsignedTx ) ; const signedTx = Transaction . fromTxData ( { ... transactionData , v : `0x${ v }` , r : `0x${ r }` , s : `0x${ s }` } , { common } ) ; return `0x${ signedTx . serialize ( ) . toString ( 'hex' ) }` ; } catch ( error ) { throw error ; } } async function signMessage ( message ) { if ( addressToPath . size === 0 ) { await enable ( ) ; } const path = [ ... addressToPath . values ( ) ] [ 0 ] ; return eth . signPersonalMessage ( path , ethUtil . stripHexPrefix ( message . data ) ) . then ( ( result ) => { let v = ( result [ 'v' ] - 27 ) . toString ( 16 ) ; if ( v . length < 2 ) { v = '0' + v ; } return `0x${ result [ 'r' ] }${ result [ 's' ] }${ v }` ; } ) ; } async function signTypedMessage ( { data } ) { if ( addressToPath . size === 0 ) { await enable ( ) ; } const path = [ ... addressToPath . values ( ) ] [ 0 ] ; return eth . signEIP712HashedMessage ( path , ethUtil . bufferToHex ( domainHash ( data ) ) , ethUtil . bufferToHex ( messageHash ( data ) ) ) . then ( ( result ) => { let v = ( result [ 'v' ] - 27 ) . toString ( 16 ) ; if ( v . length < 2 ) { v = '0' + v ; } return `0x${ result [ 'r' ] }${ result [ 's' ] }${ v }` ; } ) ; } return provider ; } type Nav = Navigator & { usb : { getDevices ( ) : void ; } ; } ; const supportsWebUSB = ( ) : Promise < boolean > => Promise . resolve ( ! ! navigator && ! ! ( navigator as Nav ) . usb && typeof ( navigator as Nav ) . usb . getDevices === 'function' ) ; export default ledger ;