①NiceHashマイニング収益をAWS Lambda×SNSでメール通知する

#目次
- [1.背景](#1-背景)
- [2.構成/構築手順](#2-構成構築手順)
  - [2-1.Lambdaの構築](#2-1lambdaの構築)
     - [2-1-1.IAMロールの作成](#2-1-1iamロールの作成)
     - [2-1-2.Lambda関数の作成](#2-1-2lambda関数の作成)
     - [2-1-3.ソースコードのデプロイ](#2-1-3ソースコードのデプロイ)
     - [2-1-4.レイヤー作成](#2-1-4レイヤー作成)
     - [2-1-5.タイムアウト値の延長](#2-1-5タイムアウト値の延長)
  - [2-2.APIによる収益情報取得](#2-2apiによる収益情報取得)
  - [2-3.EventBridgeによるトリガー定義](#2-3eventbridgeによるトリガー定義)
  - [2-4.S3バケットへの書き出し・読み込み](#2-4s3バケットへの書き出し読み込み)
  - [2-5.Amazon SNSによるメール通知](#2-5amazon-snsによるメール通知)
- [3.実行結果](#3-実行結果)
- [4.終わりに](#4-終わりに)
- [5.更新履歴](#5-更新履歴)



#1. 背景
　日々メインPCとは別にマイニングリグを稼働させて、NiceHashでマイニングをしているのですが、残高確認する際にNiceHashのダッシュボードを都度確認するのは面倒なので日次ジョブとして通知したいと思ってました。
　業務ではコーディングや、AWSに触れる機会が一切ないので、勉強がてらAWS上でスマホへメール通知するシステムを構築してみました。
　最重要なマイニングリグのヘルスチェックエラー通知はMySettingsから設定できる [^1] 仕様なので、今回は収益情報のみをメールの通知対象としました。

#2. 構成/構築手順
システム構成は、AWS Lambdaを中心とした基本的なサーバレスアーキテクチャです。

**処理の流れ**
　1. EventBridge(CroudWatch Event)の日次実行cronがトリガーとなり、Lambda関数をキック
　2. Lambdaでは、外部APIからマイニング収益情報を取得
　3. S3バケットへ残高情報を書き込み、前日の残高情報を取得、残高の増減を算出
　4. Amazon SNSへ値をpublishしてスマホへメールを通知
![Qiita 掲載図①.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/2b92afd7-86a9-a221-1962-f70bfe0d1e7f.png)

##2-1.Lambdaの構築
###2-1-1.IAMロールの作成
**AWSサービス間を連携するために新規IAMロールを作成し必要なポリシーをアタッチする**
・IAMを起動し、ユースケース```Lambda```を選択し「次のステップ」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/2816b8e8-decb-b7b0-2bec-6c71a251332b.png)
・S3バケットへ残高情報を読み書きするために```AmazonS3FullAccess```、収益情報をメール通知するために```AmazonSNSFullAccess```ポリシーをロールにアタッチし「次のステップ」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/6eb06584-1eee-5fa9-8fbc-05fe671b5cbb.png)
・タグの追加は不要なので何も記入せず「次のステップ」をクリック
・ロール名は適当に```NiceHash-Nortification```として「ロールの作成」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/d1055cf0-138e-abcc-e115-f03a17633c3d.png)

###2-1-2.Lambda関数の作成
**呼び出されるLambda関数本体を作成する**
・サービスからLambdaを起動し、以下のように入力し「関数の作成」をクリック

```
　関数名：「NiceHash-Nortification-Mail」
　ランタイム：「Python 3.6」#Python3系ならたぶんＯＫ
　アクセス権限：「NiceHash-Nortification」#作成したIAMロール
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/dfe35802-e408-fadb-246a-040c253937d7.png)

###2-1-3.ソースコードのデプロイ
**Lambdaで実行するプログラムをデプロイする**
・以下4つのpythonファイルを新規に作成してコードをデプロイ

```title:NiceHash-Nortification-Mail
NiceHash-Nortification-Mail/
├ lambda_function.py
├ nicehash.py
├ marketrate.py
└ s3inout.py
```
Lambdaで呼び出されるメインプログラム

```python:lambda_function.py
import json
import datetime
import boto3

import nicehash
import marketrate
import s3inout
#Function kicked by AWS Lambda
def lambda_handler(event, context):
    client = boto3.client('sns')
    #Amazon SNS
    TOPIC_ARN = 'arn:aws:sns:xx-xxxx-x:xxxxxxxxxx:NiceHashSNS' # SNSのARNを指定
    msg = create_message()
    subject = "NiceHash-Mining 日次収益通知"
    #Send a notification message to Amazon SNS
    response = client.publish(
        TopicArn = TOPIC_ARN,
        Message = msg,
        Subject = subject
    )
#Function to get a nortification message
def create_message():
    #NiceHash API    
    host = 'https://api2.nicehash.com'
    organisation_id = 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' # hogehoge
    key = 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' # API Key Code
    secret = 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' # API Secret Key Code
    market='BTC'

    #S3 bucket
    bucket_name = '[bucket_name]'#hogehoge
    key_name = '[balance_filename]'#hogehoge

    #Get mining information from NiceHash API
    PrivateApi = nicehash.private_api(host, organisation_id, key, secret)
    accounts_info = PrivateApi.get_accounts_for_currency(market)
    balance_row = float(accounts_info['totalBalance'])

    #Get currency_to_JPY_rate from CoinGecko API
    TradeTable = marketrate.trade_table(market)
    rate = TradeTable.get_rate()
    balance_jpy = int(balance_row*rate)

    #S3 dealer
    S3dealer = s3inout.s3_dealer(bucket = bucket_name, key = key_name)
    pre_balance = int(S3dealer.read_from_s3_bucket())
    diff = balance_jpy - pre_balance
    S3dealer.write_to_s3_bucket(str(balance_jpy))

    #Nortification message
    time_text = "時刻: " + str(datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))))[:19]
    market_text = "仮想通貨： " + market
    rate_text = "単位仮想通貨価値: " + str(rate) + "円"
    balance_text = "現在の残高: " + str(balance_jpy) + "円"
    pre_balance_text = "昨日の残高: " + str(pre_balance) + "円"
    symbol = "+" if diff > 0 else ""
    diff_txt = "【日次収益: " + str(symbol) + str(diff) + "円】"
    mon_revenue = "推定月次収益: " + str(diff*30) + "円"
    ann_revenue = "推定年次収益: " + str(diff*365) + "円"
    msg = '\n'.join([time_text,market_text,rate_text,balance_text,pre_balance_text,diff_txt,mon_revenue,ann_revenue])
    return msg
```
NiceHash API [（2-2で説明）](##2-2.APIによる収益情報取得)

```python:nicehash.py
from datetime import datetime
from time import mktime
import uuid
import hmac
import requests
import json
from hashlib import sha256
import optparse
import sys
class private_api:
  def __init__(self, host, organisation_id, key, secret, verbose=False):
      self.key = key
      self.secret = secret
      self.organisation_id = organisation_id
      self.host = host
      self.verbose = verbose
  def request(self, method, path, query, body):
      xtime = self.get_epoch_ms_from_now()
      xnonce = str(uuid.uuid4())
      message = bytearray(self.key, 'utf-8')
      message += bytearray('\x00', 'utf-8')
      message += bytearray(str(xtime), 'utf-8')
      message += bytearray('\x00', 'utf-8')
      message += bytearray(xnonce, 'utf-8')
      message += bytearray('\x00', 'utf-8')
      message += bytearray('\x00', 'utf-8')
      message += bytearray(self.organisation_id, 'utf-8')
      message += bytearray('\x00', 'utf-8')
      message += bytearray('\x00', 'utf-8')
      message += bytearray(method, 'utf-8')
      message += bytearray('\x00', 'utf-8')
      message += bytearray(path, 'utf-8')
      message += bytearray('\x00', 'utf-8')
      message += bytearray(query, 'utf-8')
      if body:
          body_json = json.dumps(body)
          message += bytearray('\x00', 'utf-8')
          message += bytearray(body_json, 'utf-8')
      digest = hmac.new(bytearray(self.secret, 'utf-8'), message, sha256).hexdigest()
      xauth = self.key + ":" + digest
      headers = {
          'X-Time': str(xtime),
          'X-Nonce': xnonce,
          'X-Auth': xauth,
          'Content-Type': 'application/json',
          'X-Organization-Id': self.organisation_id,
          'X-Request-Id': str(uuid.uuid4())
      }
      s = requests.Session()
      s.headers = headers
      url = self.host + path
      if query:
          url += '?' + query
      if self.verbose:
          print(method, url)
      if body:
          response = s.request(method, url, data=body_json)
      else:
          response = s.request(method, url)
      if response.status_code == 200:
          return response.json()
      elif response.content:
          raise Exception(str(response.status_code) + ": " + response.reason + ": " + str(response.content))
      else:
          raise Exception(str(response.status_code) + ": " + response.reason)
          
  def get_epoch_ms_from_now(self):
      now = datetime.now()
      now_ec_since_epoch = mktime(now.timetuple()) + now.microsecond / 1000000.0
      return int(now_ec_since_epoch * 1000)
```
CoinGecko API [（2-2で説明）](##2-2.APIによる収益情報取得)

```python:marketrate.py
import requests
import json
class trade_table:
    def __init__(self, market="BTC"):
        #currency-name conversion table
        self.currency_rename_table = {'BTC':'Bitcoin','ETH':'Ethereum','LTC':'Litecoin',
                                      'XRP':'XRP','RVN':'Ravencoin','MATIC':'Polygon',
                                      'BCH':'Bitcoin Cash','XLM':'Stellar','XMR':'Monero','DASH':'Dash'}
        self.market = self.currency_rename_table[market]

    def get_rate(self):
        body = requests.get('https://api.coingecko.com/api/v3/coins/markets?vs_currency=jpy')
        coingecko = json.loads(body.text)
        idx = 0
        while coingecko[idx]['name'] != self.market:
            idx += 1
            #Escape of illegal market_currency name
            if idx > 100:
                return "trade_table_err"
        #market-currency_to_JPY_rate
        else:
            return int(coingecko[idx]['current_price'])
```
S3バケットへの収益情報の読み込み・書き出し[（2-3で説明）](##2-4.S3バケットへの書き出し・読み込み)

```python:s3inout.py
import boto3
class s3_dealer:
  def __init__(self, bucket = 'nice-hash-balance', key = 'balance_latest.txt'):
    self.bucket = bucket
    self.key = key
  
  #Get balance of the previous day
  def read_from_s3_bucket(self):
    S3 = boto3.client('s3')
    res = S3.get_object(Bucket=self.bucket, Key=self.key)
    body = res['Body'].read()
    return body.decode('utf-8')
  
  #Export balance
  def write_to_s3_bucket(self, balance):
    S3 = boto3.resource('s3')
    obj = S3.Object(self.bucket, self.key)
    obj.put(Body=balance)
```

###2-1-4.レイヤー作成
**必要なモジュールをLambdaのレイヤーに取り込む**
・[2-1-3](###2-1-3.ソースコードのデプロイ) 記載のソースをデプロイしただけで実行すると```requests```モジュールが読み込めず以下エラーが発生してしまうため、外部モジュールをLayersへ定義する

```
{
  "errorMessage": "Unable to import module 'lambda_function': No module named 'requests'",
  "errorType": "Runtime.ImportModuleError"
}
```
・レイヤーファイルを作成するために、EC2でAmazon Linux AMIから新規インスタンスを作成する
・[2-1-1](###2-1-1.IAMロールの作成)の手順で、EC2のロールに対してS3のアクセスポリシーをアタッチ
　※インターネット環境に接続されたWSLやUbuntu等のUNIXマシンであれば何でもOK
・EC2インスタンスへコンソール接続し、以下CLIコマンドを打鍵してレイヤーファイルを作成する

```title:ec2-user
[ec2-user@ip-xxx-xx-xx-xxx ~]$ su -
[root@ip-xxx-xx-xx-xxx ~]# mkdir layer/
[root@ip-xxx-xx-xx-xxx ~]# cd layer
[root@ip-xxx-xx-xx-xxx ~]# yum -y install gcc gcc-c++ kernel-devel python-devel libxslt-devel libffi-devel openssl-devel
[root@ip-xxx-xx-xx-xxx ~]# yum -y install python-pip
[root@ip-xxx-xx-xx-xxx ~]# pip install -t ./ requests
[root@ip-xxx-xx-xx-xxx ~]# cd ../
[root@ip-xxx-xx-xx-xxx ~]# zip -r Layer.zip layer/
```

・レイヤーファイルを、S3バケットへアップロード

```title:ec2-user
[root@ip-xxx-xx-xx-xxx ~]# chmod 777 Layer.zip
[root@ip-xxx-xx-xx-xxx ~]# aws s3 cp Layer.zip s3://layerzip-s3
```
・S3でEC2からアップロードしたレイヤーファイルのオブジェクトURLを取得
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/2eaf4090-52e1-7849-adab-b253652a5dde.png)

・LambdaでS3のオブジェクトURLから名前を適当に```ImportRequests```としてレイヤーを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/954fb19a-facc-c57d-4c53-a9c1a7cbb0f5.png)

・Lambdaで「レイヤーの追加」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/5508d6c2-a24e-74db-cf0b-0b82d880f638.png)

・カスタムレイヤーから作成した```ImportRequests```を読み込む
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/bf2f10fb-d51b-ff08-6066-24f8adef10b3.png)


###2-1-5.タイムアウト値の延長
**タイムアウトエラーを回避するためにタイムアウト値を変更する**
・Lambdaはデフォルトだと、メモリ：```128MB```、タイムアウト：```3秒```になっているため、タイムアウトのみ「~~3秒~~ ⇒ **5秒**」へ変更する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/658358f6-1034-530d-b5cb-6fc7da9bbd86.png)



##2-2.APIによる収益情報取得
**外部APIからマイニング収益情報を取得する**
・LambdaとNiceHash APIを連携するために、[NiceHash](https://www.nicehash.com/my/login)へログインしてMySettingsからAPI Keysを発行する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/d9684ff5-eb83-a2f8-1a4a-c89e89e4f8b8.png)
・[NiceHash API](https://www.nicehash.com/docs/)（nicehash.py）で収益情報を取得するために、```lambda_function.py```の対象箇所に発行したAPI Keys、組織IDを入力する

```python:lambda_function.py
#NiceHash API    
host = 'https://api2.nicehash.com'
organisation_id = 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' # hogehoge
key = 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' # API Key Code
secret = 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' # API Secret Key Code
```

・NiceHash APIのみでは、円相場の情報は取得できないため、別の外部API [CoinGecko API](https://www.coingecko.com/ja/api)（marketrate.py）を呼び出して、各仮想通貨市場の日本円相場を取得する。

##2-3.EventBridgeによるトリガー定義
**日次ジョブとしてLambdaをキックするためのトリガーを定義する**
・Lambdaから「トリガーの追加」をクリック
・EventBridgeを選択し、以下のように入力し「追加」をクリック
　※AWSでcronを定義する際は、crontabとの違いや時差を考慮する必要があるため注意する [^2] 

```
ルール：「新規ルールの作成」
ルール名：DailyTrigger
ルールタイプ：スケジュール式
スケジュール式：cron(0 15 * * ? *) # 毎日0:00に実行するcron
```
 [^2]: [Amazon CloudWatch Events で cron 式を使う場合は時差に気をつける](https://qiita.com/niwasawa/items/6e0b191d6aa21213c7ec)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/82cdf638-c562-9eca-5b51-e2388962a34d.png)



##2-4.S3バケットへの書き出し・読み込み
**前日の収益との比較を行うため、S3バケットのファイルに対して書き出し・読み込みを行う**
・S3バケット```bucket_name```を作成して、前日の残高(円)を整数で記載したダミーファイル```balance_filename.csv```を予め格納しておく

```title:bucket_name/balance_filename.csv
28583
```
・LambdaとS3のサービス間で連携するために、```lambda_function.py```の対象箇所を編集する

```python:lambda_function.py
#NiceHash API    
#S3 bucket
bucket_name = '[bucket_name]' # S3バケット名
key_name = '[balance_filename.csv]' # 残高情報が記載されたファイル名
```




##2-5.Amazon SNSによるメール通知
**取得した収益情報をメール通知するために、「SNSとメール」「LambdaとSNS」の連携を行う**
・Amazon SNSを起動し、適当に```NiceHashSNS```としてトピックを作成する
・作成したトピック```NiceHashSNS```のARNをコピーする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/7027681f-6aaf-96ba-8014-62f830f8e8e2.png)
・「SNSとメール」を連携するために、作成したトピック```NiceHashSNS```を開き、「サブスクリプションの作成」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/70274ec9-581b-d60d-33dd-23c2ed6f6337.png)
・プロトコル：```Eメール```、エンドポイント：```通知したいメールアドレス```を指定し、「サブスクリプションの作成」をクリック
・サブスクリプションを作成すると、指定したメールアドレスに通知メールが届くので、挿入されたリンクへアクセスしサブスクリプションのアクティベーションを行う
・「LambdaとSNS」を連携するために、```lambda_function.py```の対象箇所を編集する

```python:lambda_function.py
#Amazon SNS
TOPIC_ARN = 'arn:aws:sns:xx-xxxx-x:xxxxxxxxxx:NiceHashSNS' # コピーしたSNSのARNを記載
```


#3. 実行結果
・毎日0:00になるとEventBridgeがLambdaをキックして、日次の通知メールが来るようになりました。
<img width="300" alt="IMG_1134.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1160324/ede90fd8-a82f-3a9a-2576-db781326e780.png">

#4. 終わりに
・APIで情報取得している割に、通知はメールというのは構成的にビミョかったので、この後にLINE APIを活用してLINE通知する構成に変更しました。次回は、LINE通知する構成について投稿予定です。


#5. 更新履歴
ver. 1.0 初版投稿 2021/02/28

 [^1]: [【2018年2月】マイニングするならNicehashの通知設定をしておこう！](https://ameblo.jp/7preview/entry-12349224321.html)