Earlier this month we dove into the brave new world of Bitcoin development by writing an Elixir module that could communicate with a Bitcoin full node. At this end of the day, we had a small handful of Elixir functions that could retrieve some basic information about the Bitcoin blockchain.
Let’s expand on that idea a bit.
In this article we’re going to use the Phoenix framework to build a bare-bones blockchain viewer. Let’s get to it!
Project Scaffolding
First things first, let’s create our new Phoenix project. We’ll be using Phoenix 1.3 and the new phx
generators that shipped with it:
mix phx.new hello_blockchain --no-ecto
Once we’ve gone ahead and set up our new project, let’s add two new routes to our application. One for viewing block headers, and another for viewing full blocks:
mix phx.gen.html Blockchain Header headers --no-schema
mix phx.gen.html Blockchain Blocks blocks --no-schema
Notice that we specified --no-ecto
when generating our new project, and --no-schema
when generating our block and header resources. We don’t be needing Ecto in our blockchain viewer. All of the data we’re rendering lives in our full node!
Our Blockchain Context
When we generated our header and block resources, we also generated a Blockchain
context module. Our context will be our interfacing with our Bitcoin full node.
Sound familiar?
That’s because we implemented the Blockchain
context module in our last article! Let’s remove the auto-generated contents of Blockchain
and copy over the bitcoin_rpc
function.
def bitcoin_rpc(method, params \\ []) do
with url <- Application.get_env(:hello_bitcoin, :bitcoin_url),
command <- %{jsonrpc: "1.0", method: method, params: params},
{:ok, body} <- Poison.encode(command),
{:ok, response} <- HTTPoison.post(url, body),
{:ok, metadata} <- Poison.decode(response.body),
%{"error" => nil, "result" => result} <- metadata do
{:ok, result}
else
%{"error" => reason} -> {:error, reason}
error -> error
end
end
Be sure to add dependencies on :httpoison
and :poison
, and set up your :bitcoin_url
in your configuration file.
Once we’ve finished that, we’ll add four helper functions to our Blockchain
module. we’ll use these functions to fetch the data we want to render with our blockchain viewer:
def getbestblockhash, do: bitcoin_rpc("getbestblockhash")
def getblockhash(height), do: bitcoin_rpc("getblockhash", [height])
def getblock(hash), do: bitcoin_rpc("getblock", [hash])
def getblockheader(hash), do: bitcoin_rpc("getblockheader", [hash])
Now that we have a functional context module, we can start wiring it up to our routes.
Routing
Armed with our newly generated block and header resources, let’s add their new routes to our router:
scope "/", HelloBlockchainWeb do
pipe_through :browser
resources "/", PageController, only: [:index]
resources "/blocks", BlockController, only: [:index, :show]
resources "/headers", HeaderController, only: [:index, :show]
end
For now, we’re only going to be handling the :index
and :show
routes for our blocks and headers.
Now that our routes are established, let’s move on to refactoring our controller modules. We’ll start with the BlockController
.
We’re going to remove all of our controller functions except the index
and show
functions, which we’ll heavily modify.
When we hit the /blocks/
route, we’ll fall into the index
controller function. We want this function to redirect us to the most recent block in the blockchain:
def index(conn, _params) do
with {:ok, hash} <- Blockchain.getbestblockhash() do
redirect(conn, to: block_path(conn, :show, hash))
end
end
We use getbestblockhash
to get the hash of the most recently validated Bitcoin block, and we redirect the user to the :show
route, providing the resulting hash.
Our show
controller function accepts the provided hash
as an argument, fetches more information about the block from our full node, and finally renders the block using the show.html
template:
def show(conn, %{"id" => hash}) do
with {:ok, block} <- Blockchain.getblock(hash) do
render(conn, "show.html", block: block)
end
end
Similarly, the index
function in the HeaderController
redirects the user to the :show
route of the most recently verified block:
def index(conn, _params) do
with {:ok, hash} <- Blockchain.getbestblockhash() do
redirect(conn, to: header_path(conn, :show, hash))
end
end
While the show
function fetches the relevant information using Blockchain.getblockheader
and passes it into its show.html
template:
def show(conn, %{"id" => hash}) do
with {:ok, block} <- Blockchain.getblockheader(hash) do
render(conn, "show.html", block: block)
end
end
There’s one final route we need to implement. When a user hits our application for the first time, they’ll land on the Phoenix landing page. Instead, let’s show them the most recent block header:
def index(conn, _params) do
with {:ok, hash} <- Blockchain.getbestblockhash() do
redirect(conn, to: header_path(conn, :show, hash))
end
end
Once again, we use Blockchain.getbestblockhash
to fetch the most recently verified block hash from our Bitcoin full node. We use that hash to redirect the user to the header’s :show
route.
With our routes properly configured, we can fetch data about any full block or block header just by knowings its hash.
Let’s more on to the final piece of the puzzle: rendering that data.
Templates
Our blockchain viewer now correctly routes the user to the appropriate :show
route for either the block or block header they’re trying to view, and passes all relevant data to the corresponding template to render.
Now all we need to do is build out our templates!
To keep the scope of this article manageable, we’ll keep our user interface as minimal as possible. The more bare-bones way to render our blocks and headers, while still being meaningful to our users, is to render the data received from our controllers as JSON code blocks.
The simplest way to do this would be to dump the output of Poison.encode!
into the DOM:
<code><%= Poison.encode!(@block, pretty: true) %></code>
The pretty: true
option passed into Poison.encode!
ensures that the resulting JSON string is nicely formatted. In our app.css
file, we should set the white-space
rule on <code>
blocks to preserve this formatting:
code {
white-space: pre !important;
}
Beautiful.
A bit minimal, but beautiful none-the-less.
While dumping raw JSON into the DOM is informative, it’s not especially user-friendly. Let’s add an extra layer of interactivity to our blockchain viewer.
The blocks and block headers we receive from our Bitcoin full node come with previousblockhash
field and (usually) a nextblockhash
field. These hashes, as we would expect, point to the previous and next blocks in the blockchain, respectively. Let’s transform these hashes into links so users can easily navigate through the blockchain.
The first thing we’ll do is write a function in the corresponding view file that converts hashes into links. In our HeaderView
module, our hash_link
function would look like this:
defp hash_link(hash), do: "<a href='/headers/#{hash}'>#{hash}</a>"
Using this function, we can write a function that modifies our block header. It replaces the hashes in previousblockhash
and nextblockhash
with links to those blocks, and JSON encodes the resulting object:
def mark_up_block(block) do
block
|> Map.replace("previousblockhash", hash_link(block["previousblockhash"]))
|> Map.replace("nextblockhash", hash_link(block["nextblockhash"]))
|> Poison.encode!(pretty: true)
end
In our HTML template, we can replace the contents of our <code>
block with the result of mark_up_block
:
<code><%= raw(mark_up_block(@block)) %></code>
Notice that we’re wrapping mark_up_block
with raw
. We want to HTML being injected into our JSON to be interpreted as raw HTML, and not encoded as special characters.
After carrying out the same changes in our BlockView
and our block HTMl template, cleaning up our layout template, and adding a few final styling touches, we have our result.
Behold, the most basic of blockchain explorers!
Final Thoughts
Obviously, this is just the tip of the iceberg when it comes to building a Bitcoin blockchain explorer.
I’m super excited about Bitcoin and development projects related to Bitcoin. If you’ve made it this far, I assume you are as well! If you’re looking for a deep dive into Bitcoin development, I recommend you check out the fantastic Mastering Bitcoin (affiliate link) book by Andreas Antonopoulos.
Expect more updates to this blockchain explorer in the future, and more Bitcoin focused projects. In the meantime, check out this project on Github.