Recently, I was captured with the magic world of Bitcoin . There was no limit to the thirst for knowledge, and the wonderful book “Mastering Bitcoin” by Andreas Antonopoulos and complete immersion in Bitcoin development helped quench it. The book covers in detail the technical fundamentals of Bitcoin, but nothing helps in studying a new business like practice.
A simple application on the Elixir for managing a full Bitcoin node and communicating with it via the JSON-RPC interface , in my opinion, is an excellent “Hello, World!”. Go!
To establish a connection with a full Bitcoin Core node, you must first get somewhere somewhere. It is enough just to run your node locally , because public nodes with an open JSON-RPC interface can be counted on fingers.
bitcoind
and configure it in the bitcoin.config
file:
rpcuser=<username> rpcpassword=<password>
Defined values <username>
and <password>
will be used for authentication when sending requests to a bitcoin node.
After the setup is complete, it's time to start the full node:
bitcoind -conf=<path to bitcoin.config> -daemon
Once started, the full node daemon will start connecting to peers, download and verify transactions in blocks.
Make sure that everything works as it should:
bitcoin-cli getinfo
This command will return basic information about the node, including its version and the number of blocks received and verified. Downloading and verifying the entire blockchain can take several days, but for now, we will continue to work on our project.
Bitcoin node works through the JSON-RPC interface, which can be used to extract information about the blockchain and interact with the node.
Curiously, the bitcoin-cli
that we used earlier to get information about the node works on top of the JSON-RPC API. A list of all possible RPC commands of the node can be seen by calling the bitcoin-cli help
or by bitcoin-cli help
Bitcoin Wiki .
The JSON-RPC protocol receives incoming commands via an HTTP server, which means you can do without bitcoin-cli
and register these RPC commands yourself.
For example, getinfo
curl` :
curl --data-binary '{"jsonrpc":"1.0","method":"getinfo","params":[]}' \ http://<user>:<pass>@localhost:8332/
Similarly, you can execute such commands in any programming environment with an HTTP client, for example, in Elixir!
Having thought out the strategy of interaction with a full Bitcoin node, let's do an Elixir application.
Let's create a new project and update mix.exs
to add the poison
library to dependencies, which we will need to encrypt and decrypt JSON objects, and httpoison
is one of the best HTTP clients for Elixir.
defp deps do [ {:httpoison, "~> 0.13"}, {:poison, "~> 3.1"} ] end
Now that we’ve finished with the part responsible for code generation, let's move on to the implementation of interaction with a bitcoin node.
Let's start working with the HelloBitcoin
module and first of all put the stub for the getinfo
function:
defmodule HelloBitcoin do def getinfo do raise "TODO: Implement getinfo" end end
For simplicity, we will interact with this module via iex -S mix
. Before proceeding to the next step, let's make sure that everything works correctly.
Calling the HelloBitcoin.getinfo
stub should result in a runtime exception:
iex(1)> HelloBitcoin.getinfo HelloBitcoin.getinfo ** (RuntimeError) TODO: Implement getinfo (hello_bitcoin) lib/hello_bitcoin.ex:4: HelloBitcoin.getinfo/0
Fine. Mistake. As it should be.
GetInfo
Now fill the getinfo function getinfo
content.
I repeat: we need to send an HTTP request using the POST
method to the HTTP server of a Bitcoin node (usually listening to http://localhost:8332
) and transmit a JSON object containing the GetInfo
command and the necessary parameters.
It turned out that httpoison
copes with this task in two accounts:
def getinfo do with url <- Application.get_env(:hello_bitcoin, :bitcoin_url), command <- %{jsonrpc: "1.0", method: "getinfo", params: []}, body <- Poison.encode!(command), headers <- [{"Content-Type", "application/json"}] do HTTPoison.post!(url, body, headers) end end
First we get the url
from the bitcoin_url
key in the application configuration. The address must be in the config/config.exs
and point to the local node:
config :hello_bitcoin, bitcoin_url: "http://<user>:<password>@localhost:8332"
Next, create a dictionary representing our JSON-RPC command. In this case, we write "getinfo"
in the method
field, and leave the params
field empty. And Poison.encode!
request body by converting the command to JSON format using Poison.encode!
.
The HelloBitcoin.getinfo
call should return a successful response from a Bitcoin node with a status code of 200
, as well as the result of the getinfo
in JSON format:
%HTTPoison.Response{ body: "{\"result\":{\"version\":140200,\"protocolversion\":70015,\"walletversion\":130000,\"balance\":0.00000000,\"blocks\":482864,\"timeoffset\":-1,\"connections\":8,\"proxy\":\"\",\"difficulty\":888171856257.3206,\"testnet\":false,\"keypoololdest\":1503512537,\"keypoolsize\":100,\"paytxfee\":0.00000000,\"relayfee\":0.00001000,\"errors\":\"\"},\"error\":null,\"id\":null}\n", headers: [{"Content-Type", "application/json"}, {"Date", "Thu, 31 Aug 2017 21:27:02 GMT"}, {"Content-Length", "328"}], request_url: "http://localhost:8332", status_code: 200 }
Perfectly.
Decipher the received JSON text in body
and get the result:
HTTPoison.post!(url, body) |> Map.get(:body) |> Poison.decode!
Now the results of the HelloBitcoin.getinfo
call, received from bitcoind
, will be presented in a more convenient form:
%{"error" => nil, "id" => nil, "result" => %{"balance" => 0.0, "blocks" => 483001, "connections" => 8, "difficulty" => 888171856257.3206, "errors" => "", "keypoololdest" => 1503512537, "keypoolsize" => 100, "paytxfee" => 0.0, "protocolversion" => 70015, "proxy" => "", "relayfee" => 1.0e-5, "testnet" => false, "timeoffset" => -1, "version" => 140200, "walletversion" => 130000}}
Note that the data we need ( "result"
) is wrapped in a dictionary containing metadata about the query itself. This metadata contains a string with a possible error and a request identifier.
getinfo
function so that it includes error handling and returns actual data in the case of an error-free query:
with url <- Application.get_env(:hello_bitcoin, :bitcoin_url), command <- %{jsonrpc: "1.0", method: "getinfo", params: []}, {:ok, body} <- Poison.encode(command), {:ok, response} <- HTTPoison.post(url, body), {:ok, metadata} <- Poison.decode(response.body), %{"error" => nil, "result" => result} <- metadata do result else %{"error" => reason} -> {:error, reason} error -> error end
Now, if there are no errors, the getinfo
function will return a {:ok, result}
tuple containing the result of the RPC call, and in the opposite case we will get the {:error, reason}
tuple with a description of the error.
In a similar manner, you can implement other blockchain RPC commands, for example, getblockhash
:
def getblockhash(index) do with url <- Application.get_env(:hello_bitcoin, :bitcoin_url), command <- %{jsonrpc: "1.0", method: "getblockhash", params: [index]}, {: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
getblockhash
calling getblockhash
with a zero index, we get the first block of the chain .
HelloBitcoin.getblockhash(0) {:ok, "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"}
The getblockhash
function works correctly, and it is almost identical to the getinfo
function.
To avoid duplication of code, we will select the general functional part in the new auxiliary function bitcoin_rpc
:
defp 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
Now we will override the getinfo
and getblockhash
in accordance with the bitcoin_rpc
function:
def getinfo, do: bitcoin_rpc("getinfo") def getblockhash(index), do: bitcoin_rpc("getblockhash", [index])
You can see that bitcoin_rpc
is a full-fledged RPC-interface for Bitcoin, allowing you to easily execute any RPC commands.
If you are interested in trying to implement all of the above on your machine, then the source of the project can be found on GitHub .
Well, the rather long article came to an end explaining a relatively simple idea. A full Bitcoin node provides a JSON-RPC interface, which can be accessed using any language (for example, Elixir) or a stack. Bitcoin development is a surprisingly entertaining thing, in which it is interesting to go even deeper.
The next part of a series of articles on working with Bitcoin on Elixir is available here .
Source: https://habr.com/ru/post/341072/
All Articles