lib/electrum_client.ex

defmodule ElectrumClient do
  use GenServer

  alias ElectrumClient.{Address, Endpoint}

  alias ElectrumClient.Calls.Blockchain.ScriptHash.{
    GetBalance,
    GetHistory,
    ListUnspent,
    Subscribe
  }

  alias ElectrumClient.Calls.Blockchain.Transaction.{
    GetTransaction,
    Broadcast
  }

  @callback get_transaction(txid :: String.t()) :: map()
  @callback get_address_history(address :: String.t()) :: list()

  def start_link(electrum_ip, electrum_port) do
    GenServer.start_link(__MODULE__, %{electrum_ip: electrum_ip, electrum_port: electrum_port},
      name: __MODULE__
    )
  end

  @impl true
  def init(%{electrum_ip: electrum_ip, electrum_port: electrum_port} = state) do
    {:ok, socket} =
      :gen_tcp.connect(to_charlist(electrum_ip), electrum_port,
        mode: :binary,
        packet: 0,
        keepalive: true,
        active: false,
        reuseaddr: true,
        send_timeout: 5000,
        send_timeout_close: true
      )

    state =
      state
      |> Map.put(:socket, socket)

    {:ok, state}
  end

  def get_balance(address) do
    GenServer.call(__MODULE__, {:get_balance, address})
  end

  def get_address_history(address) do
    GenServer.call(__MODULE__, {:get_address_history, address})
  end

  def list_unspent(address) do
    GenServer.call(__MODULE__, {:list_unspent, address})
  end

  def subscribe_address(address) do
    GenServer.cast(__MODULE__, {:subscribe_address, address})
  end

  def get_transaction(transaction_id) do
    GenServer.call(__MODULE__, {:get_transaction, transaction_id})
  end

  def get_transaction_output(transaction_id, vout) do
    GenServer.call(__MODULE__, {:get_transaction_output, transaction_id, vout})
  end

  def broadcast_transaction(transaction) do
    GenServer.call(__MODULE__, {:broadcast_transaction, transaction})
  end

  @impl true
  def handle_call({:list_unspent, address}, _from, %{socket: socket} = state) do
    result =
      address
      |> Address.to_script_hash()
      |> ListUnspent.encode_params()
      |> Endpoint.request(socket)
      |> ListUnspent.translate()

    {:reply, result, state}
  end

  @impl true
  def handle_call({:get_balance, address}, _from, %{socket: socket} = state) do
    result = GetBalance.call(socket, address)

    {:reply, result, state}
  end

  @impl true
  def handle_call({:get_address_history, address}, _from, %{socket: socket} = state) do
    result =
      GetHistory.encode_params(address)
      |> Endpoint.request(socket)
      |> GetHistory.translate()

    {:reply, result, state}
  end

  @impl true
  def handle_call(
        {:get_transaction, transaction_id},
        _from,
        %{socket: socket} = state
      ) do
    result =
      GetTransaction.encode_params(transaction_id)
      |> Endpoint.request(socket)
      |> GetTransaction.translate()

    {:reply, result, state}
  end

  @impl true
  def handle_call(
        {:get_transaction_output, transaction_id, vout},
        _from,
        %{socket: socket} = state
      ) do
    output =
      GetTransaction.encode_params(transaction_id)
      |> Endpoint.request(socket)
      |> GetTransaction.translate()
      |> Map.get(:transaction)
      |> Map.get(:outputs)
      |> Enum.at(vout)

    {:reply, output, state}
  end

  @impl true
  def handle_call({:broadcast_transaction, transaction}, _from, %{socket: socket} = state) do
    result =
      Broadcast.encode_params(transaction)
      |> Endpoint.request(socket)
      |> Broadcast.translate()

    {:reply, result, state}
  end

  @impl true
  def handle_cast({:subscribe_address, address}, %{socket: socket} = state) do
    response = Subscribe.call(socket, address)

    IO.inspect(response, label: "subscribe response")

    {:noreply, state}
  end

  @impl true
  def handle_info({:tcp, _port, message}, state) do
    %{
      "jsonrpc" => "2.0",
      "method" => "blockchain.scripthash.subscribe",
      "params" => subscribe_params
    } = Jason.decode!(message)

    result = Subscribe.interpret_event(subscribe_params)

    IO.inspect("Something moved: #{result}")

    {:noreply, state}
  end
end