Now let's build the frontend of the app. The frontend will consist of the major functionalities, that is, generating seed, displaying addresses of a seed, and sending ether.
Now let's write the HTML code of the app. Place this code in the index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<br>
<div class="alert alert-info" id="info" role="alert">
Create or use your existing wallet.
</div>
<form>
<div class="form-group">
<label for="seed">Enter 12-word seed</label>
<input type="text" class="form-control" id="seed">
</div>
<button type="button" class="btn btn-primary" onclick="generate_addresses()">Generate Details</button>
<button type="button" class="btn btn-primary" onclick="generate_seed()">Generate New Seed</button>
</form>
<hr>
<h2 class="text-xs-center">Address, Keys and Balances of the seed</h2>
<ol id="list">
</ol>
<hr>
<h2 class="text-xs-center">Send ether</h2>
<form>
<div class="form-group">
<label for="address1">From address</label>
<input type="text" class="form-control" id="address1">
</div>
<div class="form-group">
<label for="address2">To address</label>
<input type="text" class="form-control" id="address2">
</div>
<div class="form-group">
<label for="ether">Ether</label>
<input type="text" class="form-control" id="ether">
</div>
<button type="button" class="btn btn-primary" onclick="send_ether()">Send Ether</button>
</form>
</div>
</div>
</div>
<script src="/js/web3.min.js"></script>
<script src="/js/hooked-web3-provider.min.js"></script>
<script src="/js/lightwallet.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>
Here is how the code works:
- At first, we enqueue a Bootstrap 4 stylesheet.
- Then we display an information box, where we will display various messages to the user.
- And then we have a form with an input box and two buttons. The input box is used to enter the seed, or while generating new seed, the seed is displayed there.
- The Generate Details button is used to display addresses and Generate New Seed is used to generate a new unique seed. When Generate Details is clicked, we call the generate_addresses() method, and when the Generate New Seed button is clicked, we call the generate_seed() method.
- Later, we have an empty ordered list. Here, we will dynamically display the addresses, their balances, and associated private keys of a seed when a user clicks on the Generate Details button.
- Finally, we have another form that takes a from address and a to address and the amount of ether to transfer. The from address must be one of the addresses that's currently displayed in the unordered list.
Now let's write the implementation of each of the functions that the HTML code calls. At first, let's write the code to generate a new seed. Place this code in the main.js file:
function generate_seed()
{
var new_seed = lightwallet.keystore.generateRandomSeed();
document.getElementById("seed").value = new_seed;
generate_addresses(new_seed);
}
The generateRandomSeed() method of the keystore namespace is used to generate a random seed. It takes an optional parameter, which is a string that indicates the extra entropy.
To produce a unique seed, we need really high entropy. LightWallet is already built with methods to produce unique seeds. The algorithm LightWallet uses to produce entropy depends on the environment. But if you feel you can generate better entropy, you can pass the generated entropy to the generateRandomSeed() method, and it will get concatenated with the entropy generated by generateRandomSeed() internally.
After generating a random seed, we call the generate_addresses method. This method takes a seed and displays addresses in it. Before generating addresses, it prompts the user to ask how many addresses they want.
Here is the implementation of the generate_addresses() method. Place this code in the main.js file:
var totalAddresses = 0;
function generate_addresses(seed)
{
if(seed == undefined)
{
seed = document.getElementById("seed").value;
}
if(!lightwallet.keystore.isSeedValid(seed))
{
document.getElementById("info").innerHTML = "Please enter a valid seed";
return;
}
totalAddresses = prompt("How many addresses do you want to generate");
if(!Number.isInteger(parseInt(totalAddresses)))
{
document.getElementById("info").innerHTML = "Please enter valid number of addresses";
return;
}
var password = Math.random().toString();
lightwallet.keystore.createVault({
password: password,
seedPhrase: seed
}, function (err, ks) {
ks.keyFromPassword(password, function (err, pwDerivedKey) {
if(err)
{
document.getElementById("info").innerHTML = err;
}
else
{
ks.generateNewAddress(pwDerivedKey, totalAddresses);
var addresses = ks.getAddresses();
var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
var html = "";
for(var count = 0; count < addresses.length; count++)
{
var address = addresses[count];
var private_key = ks.exportPrivateKey(address, pwDerivedKey);
var balance = web3.eth.getBalance("0x" + address);
html = html + "<li>";
html = html + "<p><b>Address: </b>0x" + address + "</p>";
html = html + "<p><b>Private Key: </b>0x" + private_key + "</p>";
html = html + "<p><b>Balance: </b>" + web3.fromWei(balance, "ether") + " ether</p>";
html = html + "</li>";
}
document.getElementById("list").innerHTML = html;
}
});
});
}
Here is how the code works:
- At first, we have a variable named totalAddresses, which holds a number indicating the total number of addresses the user wants to generate.
- Then we check whether the seed parameter is defined or not. If it's undefined, we fetch the seed from the input field. We are doing this so that the generate_addressess() method can be used to display the information seed while generating a new seed and also if the user clicks on the Generate Details button.
- Then we validate the seed using the isSeedValid() method of the keystore namespace.
- We then ask for the user's input regarding how many addresses they want to generate and display. And then we validate the input.
- The private keys in the keystore namespace are always stored encrypted. While generating keys, we need to encrypt them, and while signing transactions, we need to decrypt the keys. The password for deriving a symmetric encryption key can be taken as input from the user or by supplying a random string as a password. For better user experience, we generate a random string and use it as the password. The symmetric key is not stored inside the keystore namespace; therefore, we need to generate the key from the password whenever we do operations related to the private key, such as generating keys, accessing keys, and so on.
- Then we use the createVault method to create a keystore instance. createVault takes an object and a callback. The object can have four properties: password, seedPharse, salt, and hdPathString. password is compulsory, and everything else is optional. If we don't provide a seedPharse, it will generate and use a random seed. salt is concatenated to the password to increase the security of the symmetric key as the attacker has to also find the salt along with the password. If the salt is not provided, it's randomly generated. The keystore namespace holds the salt unencrypted. hdPathString is used to provide the default derivation path for the keystore namespace, that is, while generating addresses, signing transactions, and so on. If we don't provide a derivation path, then this derivation path is used. If we don't provide hdPathString, then the default value is m/0'/0'/0'. The default purpose of this derivation path is sign. You can create new derivation paths or overwrite the purpose of derivation paths present using the addHdDerivationPath() method of a keystore instance. You can also change the default derivation path using the setDefaultHdDerivationPath() method of a keystore instance. Finally, once the keystore namespace is created, the instance is returned via the callback. So here, we created a keystore using a password and seed only.
- Now we need to generate the number of addresses and their associated keys the user needs. As we can generate millions of addresses from a seed, keystore doesn't generate any address until we want it to because it doesn't know how many addresses we want to generate. After creating the keystore, we generate the symmetric key from the password using the keyFromPassword method. And then we call the generateNewAddress() method to generate addresses and their associated keys.
- generateNewAddress() takes three arguments: password derived key, number of addresses to generate, and derivation path. As we haven't provided a derivation path, it uses the default derivation path of the keystore. If you call generateNewAddress() multiple times, it resumes from the address it created in the last call. For example, if you call this method twice, each time generating two addresses, you will have the first four addresses.
- Then we use getAddresses() to get all the addresses stored in the keystore.
- We decrypt and retrieve private keys of the addresses using the exportPrivateKey method.
- We use web3.eth.getBalance() to get balances of the address.
- And finally, we display all the information inside the unordered list.
Now we know how to generate the address and their private keys from a seed. Now let's write the implementation of the send_ether() method, which is used to send ether from one of the addresses generated from the seed.
Here is the code for this. Place this code in the main.js file:
function send_ether()
{
var seed = document.getElementById("seed").value;
if(!lightwallet.keystore.isSeedValid(seed))
{
document.getElementById("info").innerHTML = "Please enter a valid seed";
return;
}
var password = Math.random().toString();
lightwallet.keystore.createVault({
password: password,
seedPhrase: seed
}, function (err, ks) {
ks.keyFromPassword(password, function (err, pwDerivedKey) {
if(err)
{
document.getElementById("info").innerHTML = err;
}
else
{
ks.generateNewAddress(pwDerivedKey, totalAddresses);
ks.passwordProvider = function (callback) {
callback(null, password);
};
var provider = new HookedWeb3Provider({
host: "http://localhost:8545",
transaction_signer: ks
});
var web3 = new Web3(provider);
var from = document.getElementById("address1").value;
var to = document.getElementById("address2").value;
var value = web3.toWei(document.getElementById("ether").value, "ether");
web3.eth.sendTransaction({
from: from,
to: to,
value: value,
gas: 21000
}, function(error, result){
if(error)
{
document.getElementById("info").innerHTML = error;
}
else
{
document.getElementById("info").innerHTML = "Txn hash: " + result;
}
})
}
});
});
}
Here, the code up and until generating addresses from the seed is self-explanatory. After that, we assign a callback to the passwordProvider property of ks. This callback is invoked during transaction signing to get the password to decrypt the private key. If we don't provide this, LightWallet prompts the user to enter the password. And then, we create a HookedWeb3Provider instance by passing the keystore as the transaction signer. Now when the custom provider wants a transaction to be signed, it calls the hasAddress and signTransactions methods of ks. If the address to be signed is not among the generated addresses, ks will give an error to the custom provider. And finally, we send some ether using the web3.eth.sendTransaction method.