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.
GetInfoNow 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