これはElixir Advent Calendar 2017の11日目の記事です。
昨日はTobiasGSmollettさんの「Macro Tips」でした。

Dockerにmonacoindを立てて、ホストからElixirでJSON-RPC APIを叩くコードを試行錯誤しながら書いてみた、
というお話。
以下、Elixir 1.5.2で動作を確認している。

Dockerでmonacoindを起動

まずは、monacoindを起動するDockerfileを書く。
FROM ubuntu:16.04

RUN apt update && \
    apt install -y software-properties-common && \
    add-apt-repository ppa:visvirial/monacoin && \
    apt update && \
    apt install -y monacoind

RUN mkdir /root/.monacoin && \
    touch /root/.monacoin/monacoin.conf && \
    echo 'rpcuser=monacoinuser' >> /root/.monacoin/monacoin.conf && \
    echo 'rpcpassword=pass' >> /root/.monacoin/monacoin.conf && \
    echo 'rpcport=12345' >> /root/.monacoin/monacoin.conf && \
    echo 'server=1' >> /root/.monacoin/monacoin.conf && \
    echo 'rpcallowip=127.0.0.1' >> /root/.monacoin/monacoin.conf && \
    echo 'rpcallowip=192.168.0.0/16' >> /root/.monacoin/monacoin.conf

EXPOSE 12345
CMD ["/usr/bin/monacoind"]
monacoin.confに追記しているrpcallowip=192.168.0.0/16は、ホストからのJSON-RPCリクエストを許可するための設定。
これがないと、403 Forbiddenが返ってきてしまう。
IPアドレスハードコードではなく、Docker コンテナ内からホストの IP アドレスを知るなどを参考にDockerfileにip route | awk 'NR==1 {print "rpcallowip=" $3}'などと書けばいいかと思いきや、
これで得られるIPアドレスではないようで、ruimarinho/bitcoin-coreのdocker-compose.ymlなどを参考にハードコードする事にした。

docker run -p 12345:12345 --rm -it monacoindで起動する。

ホストからAPIを叩く

一旦、curlで実行できるか試してみる。

curl --data-binary '{"method": "getinfo" }' -H 'content-type: text/plain;' http://monacoinuser:pass@192.168.99.100:12345/
{"result":{"version":140200,"protocolversion":70015,"walletversion":130000,"balance":0.00000000,"blocks":621413,"timeoffset":0,"connections":5,"proxy":"","difficulty":248.9643403030258,"testnet":false,"keypoololdest":1512533231,"keypoolsize":100,"paytxfee":0.00000000,"relayfee":0.00100000,"errors":""},"error":null,"id":null}
curl --data-binary '{"method": "getaccountaddress", "params": ["zaneli"] }' -H 'content-type: text/plain;' http://monacoinuser:pass@192.168.99.100:12345/
{"result":"M8iLT3USjLwxTtk8t4aueWp9dKFmRBUMK8","error":null,"id":null}
よさそう。

ElixirでAPIを叩く

上記curlと同じ事をHTTPアクセスすればいいはずだが、せっかくなのでElixirのJSON-RPCライブラリがあれば使ってみたい。
jsonrpc2-elixirがあった。
  • mix.exs
    defmodule Monalixir.Mixfile do
      use Mix.Project
    
      def project do
        [app: :monalixir,
         version: "0.1.0",
         elixir: "~> 1.5",
         build_embedded: Mix.env == :prod,
         start_permanent: Mix.env == :prod,
         deps: deps()]
      end
    
      def application do
        [applications: [:jsonrpc2, :poison, :hackney]]
      end
    
      defp deps do
        [{:jsonrpc2, "~> 1.0"}, {:poison, "~> 3.1"}, {:hackney, "~> 1.7"}]
      end
    end
    
  • config/config.exs
    use Mix.Config
    
      config :monalixir,
        host: "192.168.99.100",
        port: "12345",
        username: "monacoinuser",
        password: "pass"
    
  • lib/monalixir.ex
    defmodule Monalixir do
      alias JSONRPC2.Clients.HTTP
    
      @host Application.get_env(:monalixir, :host)
      @port Application.get_env(:monalixir, :port)
      @username Application.get_env(:monalixir, :username)
      @password Application.get_env(:monalixir, :password)
    
      @url "http://#{@username}:#{@password}@#{@host}:#{@port}/"
    
      def get_info do
        HTTP.call(@url, "getinfo", [])
      end
    
      def get_account_address(account) do
        HTTP.call(@url, "getaccountaddress", [account])
      end
    end
    
iex -S mixで実行してみる。
iex(1)> Monalixir.get_info
{:error,
 {:invalid_response,
  %{"error" => nil, "id" => 0,
    "result" => %{"balance" => 0.0, "blocks" => 208, "connections" => 1,
      "difficulty" => 2.44140625e-4, "errors" => "",
      "keypoololdest" => 1512533231, "keypoolsize" => 100, "paytxfee" => 0.0,
      "protocolversion" => 70015, "proxy" => "", "relayfee" => 0.001,
      "testnet" => false, "timeoffset" => 0, "version" => 140200,
      "walletversion" => 130000}}}}
…あれ?一見正しいレスポンスが返ってきていそうだが、:errorになっている。
調べてみたところ、jsonrpc2-elixirではJSON-RPC 2.0の仕様に従い
レスポンスの形式が {"jsonrpc": "2.0", "id": ..., "result": ...}でないと:invalid_responseとして扱うようだ。
そして、monacoindのAPIはそうではない、つまりJSON-RPC 1.0。

jsonrpc2-elixirのserializerを差し替える

通常ならJSON-RPC 1.0・2.0間の仕様の差異を鑑みて大人しく直接poisonとhackneyでアクセスするのが真っ当だとは思うので、
ここから先は「できそうなのでやってみた」という蛇足になるが…。

jsonrpc2-elixirのREADMEを見たところ、JSONのシリアライズ・デシリアライズ処理を設定で差し替える事ができるようだ。
という事は、無理やりJSON-RPC 2.0の形式にする事もやろうと思えばできるのでは…?
  • config/config.exs
    use Mix.Config
    
       config :jsonrpc2,
         serializer: Monalixir.JsonRPC1
    
      config :monalixir,
        host: "192.168.99.100",
        port: "12345",
        username: "monacoinuser",
        password: "pass"
    
  • lib/serializer.ex
    defmodule Monalixir.JsonRPC1 do
    
      def encode(value) do
        Poison.encode(Map.delete(value, "jsonrpc"))
      end
    
      def decode(iodata) do
        case Poison.decode(iodata) do
          {:ok, value} -> {:ok, Map.put(value, "jsonrpc", "2.0")}
          other -> other
        end
      end
    
    end
    
実行してみる。
iex(1)> Monalixir.get_info
{:ok,
 %{"balance" => 0.0, "blocks" => 646595, "connections" => 5,
   "difficulty" => 554.21183660971, "errors" => "",
   "keypoololdest" => 1512533231, "keypoolsize" => 100, "paytxfee" => 0.0,
   "protocolversion" => 70015, "proxy" => "", "relayfee" => 0.001,
   "testnet" => false, "timeoffset" => 0, "version" => 140200,
   "walletversion" => 130000}}
iex(2)> Monalixir.get_account_address("zaneli")
{:ok, "M8iLT3USjLwxTtk8t4aueWp9dKFmRBUMK8"}
とりあえず、:okが返ってくるのは確認できたのでよしとしよう。

明日はtoku_bassさんの「EExでconfigファイルのテンプレート化」です。

Copyright© 2011-2021 Shunsuke Otani All Right Reserved .