PythonでSubscan APIを使用してステーキング報酬履歴を取得してクリプタクトのカスタムファイル(csv)を作成する

## 背景
以前[こちらの記事](https://qiita.com/7rikaz_h785/items/135fb317844091b21037)でGASでSubscan(DOT)のステーキング履歴(csv)からクリプタクトのカスタムファイルを作成する方法を紹介しましたが、以下の点で使いづらさがあったのでSubscan APIとPythonを使用して取得する方法を検討しました。
 
* csvファイルのDLが必要
* GASの処理に時間がかかる

## 作成したもの
PythonのRequestsモジュールとSubscan APIを使用してステーキング報酬履歴を取得し、クリプタクトのカスタムファイルフォーマットのcsvファイルをローカルに保存するソースコードを作成しました。

## 前提条件
* Subscan APIの[API Keys](https://docs.api.subscan.io/#introduction)を取得済みであること
* Pandas,Requestsモジュールのインストール

## 仕様
* 取得するデータは`reward-slash`を対象する
* 取得件数はコマンドラインから入力する
* 取得するデータはPythonのRequestsモジュールを使用して、[Subscan APIドキュメント](https://docs.api.subscan.io/#introduction)に従い設定したAPI Endpoint情報でHTTPのPOSTメソッドで送信し、Responseオブジェクトの`status_code`が`HTTP Status Codes: 200`の場合に受信したデータをJSONオブジェクトとして保存する
* `HTTP Status Codes: 200`以外の値の場合は`status_code`を表示し、プログラムを終了する
* 取得したデータは[カスタムファイルのフォーマット仕様](https://support.cryptact.com/hc/ja/articles/360002571312-%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90%E6%96%B9%E6%B3%95)で整形し、指定したパスにcsvファイルとして保存する
* ファイルの書き出し方法として新規作成と既存ファイルに追記する2つのケースに対応し、それぞれソースコードを作成する

## 注意事項
* Subscanの仕様やクリプタクトのデータフォーマットは変わることがありますので、利用する際は**自己責任**でお願いします
* データのチェックとして重複は考慮していますが、不足しているデータのチェックまではしていません
* Subscan APIで動作確認したNetworkは**Polkadot**のみ確認しています
* 特に作成したcsvファイルのデータについては**目的のデータを作成できていること**、
  トランザクションデータを参照して、**差異がないこと**や**誤りがないこと**も確認してください

## ソースコード

https://github.com/frdg11ykCgh/getSubscanData-CryptactCustomFormat

使い方はREADME.mdを参照してください。
以下では処理の詳細、利用する上での注意点を記載します。

#### Subscan APIの仕様について
* `reward-slash`から取得する件数は`row`と`page`で指定できます
* Subscan APIを使用して受信データを確認したところ、取得件数は`row`,データの開始要素数（`list`オブジェクト）は`row * page`で決まるようです。[^1]（正直、Pageという名前が分かりづらいです）。
* 今回作成するソースコードではステーキング報酬量を最新のデータ(`list`オブジェクトの要素数:0)から取得するため`row`は**可変**とし、`page`は**0固定**とします（下表参照）
* [2022/06/05追記]
[Subscan APIドキュメント](https://docs.api.subscan.io/#introduction)に記載はありませんが、動作確認したところ1pageで取得できる件数は100件までとなっていました。本コードでは101件以上の値の入力はサポートしておらず、入力する```400 Bad Request```が返るためquit()関数でプログラムは終了します。
101件以上の履歴を取得したい場合は分割処理に対応した[こちらの記事](https://qiita.com/7rikazhexde/items/f2adf603579579b0a0ae)を参考にしてください。

##### 表：row,pageに対する取得件数とデータの開始要素数の関係

|row|page|データの開始要素|取得件数|
|:---:|:---:|:---:|:---:|
|30|0|0|30|
|10|1|10|10|
|10|2|20|10|
|21|1|21|21|

#### API情報について
* RequestsのPOSTメソッド送信するAPI情報は`settings.py`で定義する定数を指定します

```getSubscanStakingRewardsInputNewCreateCsvData.py
# Subscan APIを使用しrequestsモジュールでPOSTリクエストする
# header情報
headers_dict = {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
}
# data-raw情報
# rowの指定のみでlistの情報は取得できるためpageは0固定とする
data_dict = { 'row': 0, 'page': 0, 'address': ADDRESS }
      
# 件数で上書き
data_dict['row'] = input_num
# Staking API / rewards-slash指定
response = requests.post(API_HOST+REQUEST_URL, headers=headers_dict, data=json.dumps(dat
# HTTPステータスコード確認
if response.status_code == 200:
      print(' -- HTTP Status Codes:',response.status_code)
else:
      print(' -- HTTP Status Codes:',response.status_code)
      print(' -- Check Subscan API Documents: ' + SUBSCAN_API_DOC)
      print(' -- quit()')
      quit()
```

#### csvファイル書き出し用データ作成処理
* レスポンスデータはJSONで1件ずつ取得し、`block_timestamp`,`amount`,`event_index`と各固定値を合わせたリストを作成します
* `block_timestamp`はそのままではUNIX時間のため`fromtimestamp()`でローカル時間に変換します
* 日時情報はクリプタクトの指定に合わせるためフォーマットを指定して文字列に変換します
* `amount`はそのままでは実際の報酬量と一致しないため`ADJUST_VALUE`を掛けて調整します

```getSubscanStakingRewardsInputNewCreateCsvData.py
# レスポンスデータ(JSON形式)
response_json = response.json()

# 1件数ずつ処理する
for i in range(input_num):
      # block_timestamp取得(csv:Timestamp)
      block_timestamp = response_json['data']['list'][i]['block_timestamp']
      # UTC(日本時間)以外にする場合はtimedeltaオブジェクトで調整すること。
      dt_block_timestamp = datetime.datetime.fromtimestamp(block_timestamp)
      # フォーマット変更
      dt_block_timestamp_fmt = dt_block_timestamp.strftime('%Y/%m/%d %H:%M:%S')
      # 0埋め表記を有効にするためシングルコーテーションを付ける
      dt_block_timestamp_fmt = "'" + dt_block_timestamp_fmt
      # ステーキング報酬取得(csv:Volume)
      amount = response_json['data']['list'][i]['amount']
      # 報酬量調整
      amount_float = float(amount) * ADJUST_VALUE
      # event_index取得(csv:Comment)
      event_index = response_json['data']['list'][i]['event_index']
      # csvファイル書き用データを作成
      value = [dt_block_timestamp_fmt,ACTION,SOURCE,BASE,amount_float,PRICE,COUNTER,FEE,FEECCY,event_index]
```

#### カスタムファイル用データ整形処理
* カスタムファイル用のデータは`pandas.DataFrame`で作成します（1件ずつ1行分追加）
* ステーキング報酬量は`pandas.DataFrame`のobject型で処理する場合、桁数が変わることがあるためfloat型で数値データに変換して`round()`で桁数を調整します
* Feeは単位が円で数値データとして保存するためint型に変換します

```getSubscanStakingRewardsInputNewCreateCsvData.py
# DataFrame作成
# 行データを追加する
df_add.loc[i, :] = value
# Volumeの表示桁数調整
df_add['Volume'] = df_add['Volume'].astype(float)
df_add = df_add.round({'Volume':10})
# Feeの型変換
df_add['Fee'] = df_add['Fee'].astype(int)
```

#### ファイル書き出し処理(新規作成)
* データはTimestamp列で昇順にソートします
* csvファイルはパスとファイル名+コード実行時の日付で保存します

```getSubscanStakingRewardsInputNewCreateCsvData.py
# ファイル書き出し処理
# Timestamp列で昇順にソート
df_csv = df_new.sort_values('Timestamp')
# プログラム実行時の日付を取得
dt_today = datetime.date.today()
# ファイル名を日付指定で保存
filename = FILE_NAME + '_' + str(dt_today) + '.csv'
# index指定なしでファイル書き出し
df_csv.to_csv(PATH + filename,index = False)
print(' -- Save Location: ['+ PATH +  filename + ']\n')
```

#### ファイル書き出し処理(既存ファイルに追記)
* データは既存ファイルのデータと結合します
* データに重複がある場合は`drop_duplicates()`完全一致の重複行を削除します

```getSubscanStakingRewardsInputAddCsvData.py
# データを結合する
df_cc = pd.concat([df_base,df_add])
# 重複行を削除する
df_cc = df_cc.drop_duplicates()
```

### 最後に
* PythonからSubscan APIを使用してステーキング報酬履歴を取得してクリプタクトのカスタムファイルフォーマットでcsvファイルに保存する方法を紹介しました。
* Subscan APIでは他の情報も取得できるので今後はDOT以外のNetworkや他のRequest情報を取得する方法についても試してみようかと思います。
* 処理についてはGASよりも速いですが、最終的なカスタムファイルは他の履歴も含めるとスプレッドシートで管理できた方が楽なので[こちらの記事](https://dev.classmethod.jp/articles/upload-csv-file-to-google-spreadsheet/)のようにGoogleのAPIを使うことや、GASでSubscan APIを使用して定期実行するなど、改良点はあるので引き続き検討してみようと思います。

[^1]: [Subscan APIドキュメント](https://docs.api.subscan.io/#introduction)には記載がなく正式な情報ではありません。誤りや仕様に関して情報があればコメントしていただけると助かります。