Building web applications with the bcoin library
bcoin is an awesome full node implementation, built in such a way that each of its modules can be pulled out and reassembled in any configuration, or run totally independently from any kind of node, wallet, or network. The developers are also committed to maintaining browser compatibility in every module, even adding alternative scripts for components like databases and cryptography so every bcoin function will run properly in the browser. In a previous guide, we illustrated how to run a full node in the web browser. While that is an amusing novelty and intriguing proof-of-concept, its actual utility is limited by the security and efficiency of the platform. But because bcoin was developed with a modular architecture, we can build very useful web-based applications for Bitcoin using small bits of the code base, and only importing what we need.
Let's build a Bitcoin web-app
For this guide, we're going to build a simple website that turns "xpub" Extended Public Keys into hierarchical-deterministic wallet accounts, and outputs both legacy and SegWit addresses.
You can play with the finished app at https://bcoin.io/apps/address.
Install the bcoin library
Nothing special here, this is how all bcoin projects start. If you already have bcoin installed somewhere, you don't have to do this again.
git clone https://github.com/bcoin-org/bcoin
cd bcoin
npm install
Install the browser-bundling tools.
There are a few popular tools out there that convert nodejs-style JavaScript for
browser compatibility. You might have already heard of browserify
or webpack but for this guide we are going
to use bpkg. bpkg
was created by the bcoin
developers in order to get the minimum functionality we need WITH ZERO DEPENDENCIES.
This a monumental boon for security, especially when developing applications for cryptocurrency.
Let's install it globally so we can just run it from the command line in any directory:
npm install -g bpkg
There is one other package we can install as an option and that is uglify-es.
This package minifies
the code after it has been converted, to save an immense amount of space in the
final JavaScript files. For this project, uglify-es
will save us about 40% of
the final file size! Keep in mind, this is a compromise of security for convenience.
Until the bcoin development team re-implements uglify-es
, we have to trust that
it won't inject vulnerabilities into our code. For the purposes of this guide, we
are going to accept that risk ;-)
npm install -g uglify-es
Compile a bcoin module for the browser
In bcoin, mnemonic seeds, HD derivation, and private keys are all handled by the hd module. Here's the commands to compile the hd module for our web-app:
mkdir <new dir for your app>
cd <wherever your bcoin repo is installed>
bpkg --browser --standalone --plugin [ uglify-es --toplevel ] --name HD \
--output <your new app dir>/HD.js lib/hd/index.js
Here's a rundown of those bpkg
command options:
browser
Sets the environment for the browser instead of nodejs.
standalone
Enforces universal compatibility, allowing us to access the module
from the global scope.
plugin
Runs our code through the minifier, explained above.
name
This is the name of the global object created by our output.
output
Destination for final output file.
lib/hd/index.js
Finally, we add the target entry-point for the process.
Initialize our webpage
Next we'll create the simplest-possible html file and import the module we just compiled.
Create this file in the same directory that HD.js
was exported to.
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>bcoin webapp</title>
</head>
<body>
<!-- Input elements and text will go here -->
<script type="text/javascript" src="HD.js"></script>
<script type="text/javascript">
// Additional JavaScript code will go here
</script>
</body>
</html>
If you open this file in a web browser, you'll see blank page, but the JavaScript
module is loaded in there! Open up the developer console and just start typing HD.
In browsers like Chrome, the console will reveal to you all the methods and properties
of the HD
object. Right away, we can see a list of awesome things we can do with
this module!
Create an HD object from a user's xpub
Let's add a text-input field to the webpage for the user to type in a xpub string:
<label for='xpub'>Extended public key: </label>
<input id='xpub' oninput='parseXpub()'>
<div id='xpub-check'></div>
You can add some CSS here too but the really important bit is oninput=parseXpub()
.
This is telling the page to call a JavaScript function every time anything is typed or
changed in the text field. We'll write that function next and insert it after the <script>
tag in the HTML page. The first thing we want to do is parse the user's input and
return an error if the key isn't valid -- bcoin will take care of all the hard work!
Creating a bcoin HD
object from a base58-encoded xpub is simple, we'll just wrap it
in a decent user experience:
function parseXpub() {
const string = document.getElementById('xpub').value;
let xpub;
try {
// attempt to create an HD object from the input string
xpub = HD.fromBase58(string);
} catch (e) {
// if the string is malformed, an error will be thrown
document.getElementById('xpub-check').innerHTML = `Bad xpub: ${e.message}`;
return false;
}
document.getElementById('xpub-check').innerHTML = `xpub OK`;
}
At this point you can already paste an xpub string into the text field, and it will display an error if the key is not complete and valid. You can try changing one character and see the error detection. For testing purposes, you can use this example xpub from the BIP32 spec:
xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5
Extract the metadata encoded by the key
Extended Public Keys are packed with details about how they were derived. We'll
pull some of that information out and display it to the user. We can tell right
away by the string's prefix what network it is for. In bcoin, key prefixes are defined
by each network in the
protocol/networks.js
file. The xpub also tells us how far down the derivation tree it is, and at what index.
In BIP44 paths, the index is a "hardened" key often referred to as the "account index".
Simply by instantiating an HD
object, bcoin has already extracted those properties.
Let's print out those data to a new div
in the webpage.
Add some more lines to the parseXpub()
function started already:
function parseXpub() {
...
// derive network from first letter of string
const names = {
x: 'main',
t: 'testnet',
r: 'regtest',
s: 'simnet'
};
const network = names[string[0]];
// get all other metadata imported by bcoin
const depth = xpub.depth;
const childIndex = xpub.childIndex;
const hard = childIndex >= HD.common.HARDENED;
const account = hard ? (childIndex - HD.common.HARDENED) : childIndex;
// compose output and insert into html
let explain = '';
explain += `Network: ${network}<br>`;
explain += `Depth: ${depth}<br>`;
explain += `Child Index: ${account + (hard ? "'" : '')}<br>`;
document.getElementById('explain').innerHTML = explain;
}
Then somewhere in the body of the html document, add a target for the output:
<div id='explain'></div>
Derive child keys from the BIP32 path
Now that we have a master public key, we can generate an (almost) infinite number of Bitcoin addresses. The bcoin wallet is designed to follow BIP44 which specifies a series of derivations and a function for each level. It's a standard path that many Bitcoin wallets follow with a hardened account index, a "soft" branch index to specify receive or change, and finally an incremented index for addresses. For the purposes of this guide, we'll assume only BIP44 xpubs are being entered, and allow the user to derive these typical addresses.
For testing this, let's use a BIP44-based xpub example:
tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba
We'll get a user-input path with defaults set to receive
address (as opposed to change
)
and address index 0
. Notice again how we call the whole chain of derivation functions
any time a value is changed with the attribute onchange='parseXpub()'
.
<div>
Derivation path:
<input type='number' onchange='parseXpub()' id='branch' min='0' value='0'>/
<input type='number' onchange='parseXpub()' id='index' min='0' value='0'>
</div>
In bcoin, traversing the HD path of keys is a recursive process, so once we get the
user input, it's a pretty simple chain to get the key we want. The second parameter
we're passing here to each derive()
call is a boolean that represents hardened
derivation. Learn more about that
here
and here.
Continue the parseXpub()
function as follows:
function parseXpub() {
...
// gather the value of all the input fields
const branch = parseInt(document.getElementById('branch').value);
const index = parseInt(document.getElementById('index').value);
// derive a key from a key from the master :-)
const key = xpub.derive(branch, false).derive(index, false);
}
Derive address from key
Now that we can import a master public key and generate any child key the user wants, we
need to derive from that key a usable Bitcoin address. This is actually a function
the bcoin HD
module can not do. So we'll need to import just one more tiny bit
of the bcoin library: KeyRing
.
With bpkg
, exporting modules from bcoin is a cinch:
cd <wherever your bcoin repo is installed>
bpkg --browser --standalone --plugin [ uglify-es --toplevel ] --name KeyRing \
--output <your app dir>/KeyRing.js lib/primitives/keyring.js
Add the new keyring
module to your webapp:
<script type='text/javascript' src='KeyRing.js'></script>
Now we can access the KeyRing
module, create KeyRing
objects from public keys,
and get the addresses. We'll actually make two KeyRing
's so we can derive both legacy
and SegWit addresses. First, add a <div>
for the output to fill in:
<div id='address'></div>
Then add this code to the end of the parseXpub()
function:
function parseXpub() {
...
// create a KeyRing object from the derived public key
const ringLegacy = KeyRing.fromPublic(key.publicKey);
// set witness to false for legacy address
ringLegacy.witness = false;
// get the address in base58 format for this network
const legAddr = ringLegacy.getAddress('base58', network);
// do it all again but this time with witness enabled
const ringWitness = KeyRing.fromPublic(key.publicKey);
ringWitness.witness = true;
const witAddr = ringWitness.getAddress('string', network);
// print the output in to the HTML elements
let addrInfo = '';
addrInfo += `Legacy address: ${legAddr}<br>`;
addrInfo += `SegWit address: ${witAddr}`;
document.getElementById('address').innerHTML = addrInfo;
}
I added some bells and whistles to the final version. You can review the source code here.
A word about security
Web browsers are inherently dangerous environments. Browser plugins can modify any content or JavaScript function on the page, and web sites are easy vectors for phishing attacks. Tools like ours are nice because they can work on almost any platform, or be imported into an Electron app or Cordova app, where the environment can be better secured. Verify whatever source code you can, only run trusted software, and use offline machines whenever possible for these types of calculations!
What's next
bcoin has a JavaScript module for every Bitcoin function you can think of: keys, transactions, blocks, wallets, output scripts...! There's a lot you can do without running any kind of node. The simplicity of bcoin means you can create stand-alone web applications and run them online or offline. Sign transactions on an air-gapped computer, or use the script parser to test complicated smart contracts.
You can even connect to an actual running full or SPV node!
bcoin has an HTTP API which you could link to from your
webapp. You could even use bpkg
to bundle a
complete node and wallet client and connect
to your bcoin node via websockets!
Whatever you build, be sure to let us know! We want to hear from you on Twitter, GitHub or Telegram.