bitfeed/server/lib/bitcoin_rpc.ex
Mononaut e845157610 Refactor API/Core bridge & mempool tracking.
API:
- Better RPC handling (cache credentials)
- Process transaction prevouts in dedicated Tasks
- Consume ZMQ sequence msgs
- Track mempool count precisely
- Cache prevouts for mempool transactions
- Send mempool count with every client msg, instead of reconstructing client-side
- Only send block ids over websocket, let clients fetch the full block data via http

Client:
- Simplify transaction queue to avoid setTimeouts
  - improves experience in background tabs
  - no longer need to hold back txs, as duplicates are now handled API-side
- Use API-supplied mempool count, instead of tracking it client-side
- Make mempoolCount a Svelte spring store, so updates transition smoothly
2022-02-18 18:07:58 -06:00

125 lines
3.7 KiB
Elixir

Application.ensure_all_started(:hackney)
defmodule BitcoinStream.RPC do
@moduledoc """
GenServer for bitcoin rpc requests
"""
use GenServer
def start_link(opts) do
{port, opts} = Keyword.pop(opts, :port);
{host, opts} = Keyword.pop(opts, :host);
IO.puts("Starting Bitcoin RPC server on #{host} port #{port}")
GenServer.start_link(__MODULE__, {host, port, nil, nil}, opts)
end
@impl true
def init({host, port, status, _}) do
# start node monitoring loop
creds = rpc_creds();
send(self(), :check_status);
{:ok, {host, port, status, creds}}
end
def handle_info(:check_status, state) do
# Do the desired work here
state = check_status(state)
Process.send_after(self(), :check_status, 60 * 1000)
{:noreply, state}
end
@impl true
def handle_call({:request, method, params}, _from, {host, port, status, creds}) do
case make_request(host, port, creds, method, params) do
{:ok, code, info} ->
{:reply, {:ok, code, info}, {host, port, status, creds}}
{:error, reason} ->
{:reply, {:error, reason}, {host, port, status, creds}}
end
end
@impl true
def handle_call({:get_node_status}, _from, {host, port, status, creds}) do
{:reply, {:ok, status}, {host, port, status, creds}}
end
defp make_request(host, port, creds, method, params) do
with { user, pw } <- creds,
{:ok, rpc_request} <- Jason.encode(%{method: method, params: params}),
{:ok, code, _headers, body_ref} <- :hackney.request(:post, "http://#{host}:#{port}", [{"content-type", "application/json"}], rpc_request, [basic_auth: { user, pw }]),
{:ok, body} <- :hackney.body(body_ref),
{:ok, %{"result" => info}} <- Jason.decode(body) do
{:ok, code, info}
else
{:ok, code, _} ->
IO.puts("RPC request #{method} failed with HTTP code #{code}")
{:error, code}
{:error, reason} ->
IO.puts("RPC request #{method} failed");
IO.inspect(reason)
{:error, reason}
err ->
IO.puts("RPC request #{method} failed: (unknown reason)");
IO.inspect(err);
{:error, err}
end
end
def request(pid, method, params) do
GenServer.call(pid, {:request, method, params}, 60000)
catch
:exit, reason ->
IO.puts("RPC request #{method} failed - probably timed out?")
IO.inspect(reason)
end
def get_node_status(pid) do
GenServer.call(pid, {:get_node_status})
end
def check_status({host, port, status, creds}) do
with {:ok, 200, info} <- make_request(host, port, creds, "getblockchaininfo", []) do
{host, port, info, creds}
else
{:error, reason} ->
IO.puts("node status check failed");
IO.inspect(reason)
{host, port, status, creds}
err ->
IO.puts("node status check failed: (unknown reason)");
IO.inspect(err);
{host, port, status, creds}
end
end
defp rpc_creds() do
cookie_path = System.get_env("BITCOIN_RPC_COOKIE");
rpc_user = System.get_env("BITCOIN_RPC_USER");
rpc_pw = System.get_env("BITCOIN_RPC_PASS");
cond do
(rpc_user != nil && rpc_pw != nil)
-> { rpc_user, rpc_pw }
(cookie_path != nil)
->
with {:ok, cookie} <- File.read(cookie_path),
[ user, pw ] <- String.split(cookie, ":") do
{ user, pw }
else
{:error, reason} ->
IO.puts("Failed to load bitcoin rpc cookie");
IO.inspect(reason)
:error
err ->
IO.puts("Failed to load bitcoin rpc cookie: (unknown reason)");
IO.inspect(err);
:error
end
true ->
IO.puts("Missing bitcoin rpc credentials");
:error
end
end
end