📜 ⬆️ ⬇️

We work with Bitcoin on Elixir

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!


Where to get the full node


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.


JSON-RPC interface


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!


Elixir application development


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.


Building a 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.


Team Summary


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 .


Conclusion


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