0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-02-28

#目次

#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

##2-1.Lambdaの構築
###2-1-1.IAMロールの作成
AWSサービス間を連携するために新規IAMロールを作成し必要なポリシーをアタッチする
・IAMを起動し、ユースケースLambdaを選択し「次のステップ」をクリック
image.png
・S3バケットへ残高情報を読み書きするためにAmazonS3FullAccess、収益情報をメール通知するためにAmazonSNSFullAccessポリシーをロールにアタッチし「次のステップ」をクリック
image.png
・タグの追加は不要なので何も記入せず「次のステップ」をクリック
・ロール名は適当にNiceHash-Nortificationとして「ロールの作成」をクリック
image.png

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

 関数名:「NiceHash-Nortification-Mail」
 ランタイム:「Python 3.6」#Python3系ならたぶんOK
 アクセス権限:「NiceHash-Nortification」#作成したIAMロール

image.png

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

NiceHash-Nortification-Mail
NiceHash-Nortification-Mail/
├ lambda_function.py
├ nicehash.py
├ marketrate.py
└ s3inout.py

Lambdaで呼び出されるメインプログラム

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で説明)

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で説明)

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で説明)

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 記載のソースをデプロイしただけで実行するとrequestsモジュールが読み込めず以下エラーが発生してしまうため、外部モジュールをLayersへ定義する

{
  "errorMessage": "Unable to import module 'lambda_function': No module named 'requests'",
  "errorType": "Runtime.ImportModuleError"
}

・レイヤーファイルを作成するために、EC2でAmazon Linux AMIから新規インスタンスを作成する
2-1-1の手順で、EC2のロールに対してS3のアクセスポリシーをアタッチ
 ※インターネット環境に接続されたWSLやUbuntu等のUNIXマシンであれば何でもOK
・EC2インスタンスへコンソール接続し、以下CLIコマンドを打鍵してレイヤーファイルを作成する

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バケットへアップロード

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

・LambdaでS3のオブジェクトURLから名前を適当にImportRequestsとしてレイヤーを作成
image.png

・Lambdaで「レイヤーの追加」をクリック
image.png

・カスタムレイヤーから作成したImportRequestsを読み込む
image.png

###2-1-5.タイムアウト値の延長
タイムアウトエラーを回避するためにタイムアウト値を変更する
・Lambdaはデフォルトだと、メモリ:128MB、タイムアウト:3秒になっているため、タイムアウトのみ「3秒5秒」へ変更する
image.png

##2-2.APIによる収益情報取得
外部APIからマイニング収益情報を取得する
・LambdaとNiceHash APIを連携するために、NiceHashへログインしてMySettingsからAPI Keysを発行する
image.png
NiceHash API(nicehash.py)で収益情報を取得するために、lambda_function.pyの対象箇所に発行したAPI Keys、組織IDを入力する

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(marketrate.py)を呼び出して、各仮想通貨市場の日本円相場を取得する。

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

ルール:「新規ルールの作成」
ルール名:DailyTrigger
ルールタイプ:スケジュール式
スケジュール式:cron(0 15 * * ? *) # 毎日0:00に実行するcron

image.png

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

bucket_name/balance_filename.csv
28583

・LambdaとS3のサービス間で連携するために、lambda_function.pyの対象箇所を編集する

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
・「SNSとメール」を連携するために、作成したトピックNiceHashSNSを開き、「サブスクリプションの作成」をクリック
image.png
・プロトコル:Eメール、エンドポイント:通知したいメールアドレスを指定し、「サブスクリプションの作成」をクリック
・サブスクリプションを作成すると、指定したメールアドレスに通知メールが届くので、挿入されたリンクへアクセスしサブスクリプションのアクティベーションを行う
・「LambdaとSNS」を連携するために、lambda_function.pyの対象箇所を編集する

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

#3. 実行結果
・毎日0:00になるとEventBridgeがLambdaをキックして、日次の通知メールが来るようになりました。
IMG_1134.PNG

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

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

  1. 【2018年2月】マイニングするならNicehashの通知設定をしておこう!

  2. Amazon CloudWatch Events で cron 式を使う場合は時差に気をつける

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?