MonapartyのAPIを拡張する
今回は実際にcounterblockのカスタムモジュールを作成してみます。
xchain.ioのAPIの中からBurnsを題材にし、それに対応したJSON RPC APIを実装します。
APIの定義確認
Endpoint
xchain.ioのAPIではアドレスとブロックナンバーで検索出来ますが、今回はアドレスでの検索に絞ります。
| 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
burnedとearnedがStringになっていますが、これは恐らく内部的に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 APIのproxy_to_counterpartydからでは呼べないAPIです。
blockchain.normalize_quantity
burnedとearnedはノーマライズした上で文字列で返したいので、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.pyをcounterblockコンテナ内にコピーします。
一旦母艦から~/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"}]}}
それっぽいレスポンスが返ってきました。