Posted By : Amit
Hardware wallets provide safer alternatives to store your private keys so that there are minimal chances of you losing your private key and hence losing the control of your wallet. With a hardware wallet like Ledger Nano S, you are not allowed to transfer your private key out of the device in plain text.
In this blog, we look into the connection set up with the ledger app and providing users a list of wallets from which he can choose.
1) Installing the Ethereum app
2) Libraries for Ledger Integration
The following repository provided from LedgerHQ provides javascript libraries needed for our application - ledgerjs
a) @ledgerhq/hw-transport-u2f - Allows to communicate with Ledger Hardware Wallets
b) @ledgerhq/hw-app-eth - Ledger Hardware Wallet ETH JavaScript bindings.
Install these libraries in your project and you can use them through
import Transport from '@ledgerhq/hw-transport-u2f'
import Eth from '@ledgerhq/hw-app-eth'
3) Start integrating these libraries
We are going to integrate these libraries and will cover the functionality in the following steps
A) Verifying the connection with the user Ledger Wallet
let ledgerPayload = ''
let appEth = ''
const setupLedger = async () => {
let u2fSupported = false
try {
u2fSupported = await Transport.isSupported()
console.info('u2fSupported', u2fSupported)
if (!u2fSupported) {
let unsError = new Error()
unsError.message = 'U2F not supported in this browser. Please try using Google Chrome with a secure (SSL / HTTPS) connection!'
unsError.name = 'U2FNotSupportedError'
throw unsError
}
u2fSupported = true
if (!appEth) {
let transport = await Transport.create()
appEth = await new Eth(transport)
}
const path = localStorage.get('hdDerivationPath')
const result = await appEth.getAddress(
path,
false,
true
)
ledgerPayload = result
} catch (error) {
console.error('unlock ledger error', error)
if (!u2fSupported) {
throw error
} else {
let ledgerTransportError = new Error()
ledgerTransportError.name = 'LedgerTransportError'
ledgerTransportError.message = 'Unable to connect with your ledger device , make sure your device is connected and keep the app open'
throw ledgerTransportError
}
}
}
In the above code, we do the following things
a) We check using the Transport library if the browser user is using is u2f compatible or not, if not we display the error message about u2f incompatible browser message.
b) If the browser is u2f compatible, we pass the transport connection details to the Eth constructor which allows us to perform various functions like sending transactions, generating addresses, and their verification based on parameters.
c) Now we fetch the hdPath used by the ethereum application on the ledger, and fetch the payload details using the getAddress function and save the payload details for address generation for the next step.
d) If there is still any error getting the payload details, which can be due to the reasons like the user has not opened the app, or the ledger device is not unlocked while interacting with our web application, we show him a generic message for the same.
The real error scenarios in the d) case result in TransportError or TransportStatusError, but we have updated the name and message of the errors to show the user a general message.
B) Generating user wallet addresses
In our app, we can generate multiple ethereum addresses and then let the user choose which address he wants to use, to comply with some regulations we can also add a verification step, which is processed using the getAddress call.
const loadMultipleLedgerWallets = async function (offset, limit) {
try {
const payload = ledgerPayload
let balance = 0
let convertedAddress
let wallets = {}
for (let i = offset; i < (offset + limit); i++) {
convertedAddress = createHDWallet(payload, i)
balance = await web3.eth.getBalance(convertedAddress)
wallets[i] = {
address: convertedAddress,
balance: parseFloat(web3.utils.fromWei(balance, 'ether')).toFixed(2)
}
}
ledgerPayload = ''
return wallets
} catch (error) {
let loadWalletInfoError = new Error()
loadWalletInfoError.message = 'Unable to connect with Wethio network, please try again'
loadWalletInfoError.name = 'LoadWalletInfoError'
throw loadWalletInfoError
}
}
a) In the above code, we are using a limit of 5 max addresses whose details can be shown to the user to choose from
b) We fetch the payload details from the last step using the ledgerPayload variable , we call the create wallet function 5 times
const createHDWallet = (payload, index) => {
let derivedKey
const pubKey = payload.publicKey
const chainCode = payload.chainCode
const hdkey = new HDKey()
hdkey.publicKey = Buffer.from(pubKey, 'hex')
hdkey.chainCode = Buffer.from(chainCode, 'hex')
derivedKey = hdkey.derive('m/' + index)
let pubKey = ethUtils.bufferToHex(derivedKey.publicKey)
const buff = ethUtils.publicToAddress(pubKey, true)
return ethUtils.bufferToHex(buff)
}
c) In each iteration of the HDWalletCreate function, we generate a child public key, and through it the address using the index value provided.
For example - the Ethereum app uses the path m/44'/60'/0', and we are generating the public key for the children of this node in HD wallet tree, from child number 0 to 4 in case of the limit being set to 5.
d) Then we fetch the balance of all these addresses using the web3 getBalance api, and return an array of objects with fields, address, and balance and let the user choose one of the addresses.
e) Once the user chooses an address we store the correct hdPath details, initial HD Path along with the wallet index through which the user wants to interact, into the local storage, to retrieve it later for sending transactions.
f) We also update the payload variable to empty string, as we don't need to derive anything from the base public key and base chain code, once we have generated and passed the keys to the user.
December 3, 2024 at 06:11 pm
Your comment is awaiting moderation.