MonapartyのAPIを拡張する

今回は実際にcounterblockのカスタムモジュールを作成してみます。

xchain.ioAPIの中からBurnsを題材にし、それに対応したJSON RPC APIを実装します。

APIの定義確認

Endpoint

xchain.ioAPIではアドレスとブロックナンバーで検索出来ますが、今回はアドレスでの検索に絞ります。

Method Endpoint Returns
GET /api/burns/{address} Returns list of 'Burn' transactions
GET /api/burns/{block} Returns list of 'Burn' transactions

Paging

ページングにも対応していて、触ってみたところ最大値は500のようです。

Method Endpoint
GET endpoint/{page}/{limit}

Return Values

burnedearnedStringになっていますが、これは恐らく内部的にsatoshi単位で持っているものをdivisibleに応じて変換して返すためだと思われます。feeなどもすべて文字列でした。

Value Type Description
data Array Broadcasts data
block_index Integer Block number containing the transaction
burned String The amount of Bitcoin (BTC) burned
earned String The amount of Counterparty (XCP) earned
source String Source address where broadcast originated
status String Status of the transaction
timestamp Integer A UNIX timestamp of when the transaction was processed by the network
tx_hash String Transaction Hash
tx_index Integer Transaction Index
total Integer Total number of burns

Example Response

{
    "data": [{
            "block_index": 283809,
            "burned": "1.00000000",
            "earned": "1000.09090909",
            "source": "1EU6VM7zkA9qDw8ReFKHRpSSHJvbuXYNhq",
            "status": "valid",
            "timestamp": 1492254524,
            "tx_hash": "ad6609edbdb3b951627302f65df06636f2535680d69d2ee98f59af05cedf0d94",
            "tx_index": 3069
        }
    ],
    "total": 7
}

データベースの確認

テーブル

sqlite3をインストールして、counterpartyのデータベースに対して.tableコマンドでテーブル一覧を確認してみます。

sudo sqlite3 /var/lib/docker/volumes/federatednode_counterparty-data/_data/monaparty.db
sqlite> .table
addresses                contracts                orders
assets                   credits                  postqueue
balances                 debits                   rps
bet_expirations          destructions             rps_expirations
bet_match_expirations    dividends                rps_match_expirations
bet_match_resolutions    executions               rps_matches
bet_matches              issuances                rpsresolves
bets                     mempool                  sends
blocks                   messages                 storage
broadcasts               nonces                   suicides
btcpays                  order_expirations        transactions
burns                    order_match_expirations  undolog
cancels                  order_matches            undolog_block

今回ターゲットになるテーブルはburnsになるかと思われます。

テーブルの構造

次にburnsテーブルに対して.schemaコマンドで構造を確認します。(一部省略)

sqlite> .schema burns

CREATE TABLE burns(
                      tx_index INTEGER PRIMARY KEY,
                      tx_hash TEXT UNIQUE,
                      block_index INTEGER,
                      source TEXT,
                      burned INTEGER,
                      earned INTEGER,
                      status TEXT);

これだけではtimestampが足りないので、block_indexからtimestampを取ってこれそうなblocksテーブルについても構造を確認します。

sqlite> .schema blocks

CREATE TABLE blocks(
                      block_index INTEGER UNIQUE,
                      block_hash TEXT UNIQUE,
                      block_time INTEGER,
                      previous_block_hash TEXT UNIQUE,
                      difficulty INTEGER,
                      ledger_hash TEXT,
                      txlist_hash TEXT,
                      messages_hash TEXT);

実装

とりあえず最終的なjsonをそのまま返すAPIを作成します。

実際にはcounterblockは直接公開せずにNginxからNode.jsあたりに流してそこから呼ぶような感じになると思いますので、もう少し汎用的なAPIにして呼び出し元で成形するほうが良いのかもしれません。このあたりはThe手探りです。

@API.add_method

関数に@API.add_methodデコレータを付けることで、JSON RPC APIで呼べるようになります。

util.call_jsonrpc_api

モジュール内からCounterparty APIを呼ぶにはutil.call_jsonrpc_apiを使います。

メソッドはsqlを指定し、queryに生のSQLを入れたオブジェクトを渡すと、counterpartyのデータベースに対してSQLを直接実行出来ます。これは、Counterblock APIproxy_to_counterpartydからでは呼べないAPIです。

blockchain.normalize_quantity

burnedearnedノーマライズした上で文字列で返したいので、blockchain.normalize_quantityノーマライズしてから小数点以下8桁付きの文字列に変換します。

コード

my_api.py

from counterblock.lib import util ,blockchain
from counterblock.lib.processor import API

@API.add_method
def get_burns_from_address(address, offset=0, limit=500):
    
    if limit > 500:
        limit = 500
    elif limit < 0:
        limit = 0

    data_sql = "select burns.*, blocks.block_time as timestamp"
    data_sql += " from burns"
    data_sql += " inner join blocks"
    data_sql += " on burns.block_index = blocks.block_index"
    data_sql += " and burns.source = '" + address + "'"
    data_sql += " order by block_index DESC"
    data_sql += " limit " + str(limit) + " offset " + str(offset)

    data_body = util.call_jsonrpc_api("sql", {"query": data_sql}, abort_on_error=True)["result"]

    for x in data_body:
        x["burned"] = "{:.8f}".format(blockchain.normalize_quantity(x["burned"], True))
        x["earned"] = "{:.8f}".format(blockchain.normalize_quantity(x["earned"], True))

    total_sql = "select count(tx_index) as total"
    total_sql += " from burns"
    total_sql += " where source = '" + address + "'"

    total_count = util.call_jsonrpc_api("sql", {"query": total_sql}, abort_on_error=True)["result"][0]["total"]

    return {"data": data_body, "total": total_count}

modules.confの設定

作成したmy_api.pycounterblockコンテナ内にコピーします。 一旦母艦から~/hostdirに放り込んだものを、counterblock/lib配下に作成したcustom_modulesディレクトリにコピーしました。

この場合のcustom_modulesディレクトリの名称、位置や、モジュールのファイル名は、別の場所でも別の名前でも問題ありません。

sudo docker cp hostdir/my_api.py federatednode_counterblock_1:/counterblock/counterblock/lib/custom_modules/my_api.py

modules.confに先ほどコピーしたファイルの位置を追記します。

nano federatednode/config/counterblock/modules.conf

[LoadModule]
lib/modules/assets = True
lib/modules/counterwallet = True
lib/modules/dex = True
lib/modules/transaction_stats = True
lib/modules/betting = True
lib/custom_modules/my_api = True

動作確認

counterblockを再起動して作成したモジュールを有効にします。 これはmodules.confを更新した場合だけではなく、後からmy_api.pyを更新した場合にも再読み込みが必要です。

再起動が終わったらAPIにアクセスしてみます。

fednode restart counterblock

curl -s -X POST --data '{"jsonrpc":"2.0","id":1,"method":"get_burns_from_address","params":{"address":"MCwt89zvuPHaCvHLmY1fvgfoQKot1BApd5","offset":0,"limit":100}}' http://localhost:4100

{"id": 1, "jsonrpc": "2.0", "result": {"total": 1, "data": [{"timestamp": 1511081192, "source": "MCwt89zvuPHaCvHLmY1fvgfoQKot1BApd5", "tx_index": 195, "block_index": 1166003, "status": "valid", "tx_hash": "9f6fd3b04e0f2a54b99d4227aaac660c8dc291df66b74274e87153bfb4394a72", "earned": "1499.88840000", "burned": "1.00000000"}]}}

それっぽいレスポンスが返ってきました。