diff --git a/balance-checker/README.md b/balance-checker/README.md new file mode 100644 index 0000000..b59326f --- /dev/null +++ b/balance-checker/README.md @@ -0,0 +1,43 @@ +This is the simple console script to detect unauthorized transfers from the payment addresses. +The script checks the ledger to ensure that the balance matches the expected values for each payment address listed in an input file. + +## Usage + +To run the balance-checker script you must have the following dependencies installed: +- [indy-sdk](https://github.com/hyperledger/indy-sdk) +- [libsovtoken](https://github.com/sovrin-foundation/libsovtoken) +- python3 +- pip3 packages: python3-indy + +Run with this command: + +``` python3 balance-checker.py [--dataFile=/path] [--emailInfoFile=/path]``` + +#### Parameters +* --dataFile - Input .CSV file containing a list of payment addresses with an expected tokens amount. (columns: "Payment Address", "Tokens Amount"). Example: +``` +Payment Address,Tokens Amount +pay:sov:t3vet9QjjSn6SpyqYSqc2ct7aWM4zcXKM5ygUaJktXEkTgL31,100 +pay:sov:2vEjkFFe9LhVr47f8SY6r77ZXbWVMMKpVCaYvaoKwkoukP2stQ,10 +pay:sov:PEMYpH2L8Raob6nysWfCB1KajZygX1AJnaLzHT1eSo8YNxu1d,200 +``` +* --emailInfoFile - Input .JSON file containing information required for email notifications. Example: +``` +{ + "host": "smtp.gmail.com", + "port": 465, + "from": "Sovrin@email.com", + "to": "TargetSovrin@email.com", + "subject": "Balance Checker" +} +``` + +### Run cron job +* on Unix OS with using `crontab` library. + 1) edit the crontab file using the command: `crontab -e` + 2) add a new line `0 0 * * * python3 /path/to/balance-checker.py --dataFile=/path/to/input_data.csv --emailInfoFile=/path/to/email-info.json` - this implies running every day at midnight (* - Minute * - Hour * - Day * - Month * - Day of week). + 3) save the file + + + + diff --git a/balance-checker/balance-checker.py b/balance-checker/balance-checker.py new file mode 100644 index 0000000..9e10005 --- /dev/null +++ b/balance-checker/balance-checker.py @@ -0,0 +1,94 @@ +import argparse +import csv +import logging + +from indy_helpers import * +from utils import * + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +def read_input_data(data_file): + expected_data = {} + + with open(data_file, 'r') as file: + for row in csv.DictReader(file): + expected_data[row["Payment Address"]] = int(row["Tokens Amount"]) + + if len(expected_data) == 0: + raise Exception('There is no a target payment address to check') + + return expected_data + + +def compare(expected_data, actual_data): + failed = {} + for payment_address, expected_amount in expected_data.items(): + actual_amount = actual_data[payment_address] if payment_address in actual_data else 0 + if expected_amount != actual_amount: + failed[payment_address] = {'actual': actual_amount, 'expected': expected_amount} + return failed + + +def run(args): + # Input: + # CSV File + # Payment Address, Tokens Amount + # address 1, 123 + # address 2, 456 + # address 3, 789 + # ....... + # + # Pool Genesis Transactions - interactive input + # + # Email Info File + # { + # "host": "smtp.gmail.com", + # "port": 465, + # "from": "Sovrin@email.com", + # "subject": message subject, + # "body": message content + # } + print("Parsing expected data from CSV file: \"{}\" ...".format(args.dataFile)) + + try: + expected_data = read_input_data(args.dataFile) + except Exception as err: + raise Exception("Can not read input data file: {}".format(err)) + + print("Connecting to Pool...") + + pool_handle = open_pool() + + logging.debug("Load Payment Library") + + load_payment_plugin() + + print("Getting payment sources from the ledger...") + actual_data = get_payment_sources(pool_handle, expected_data.keys()) + + print("Comparing values...") + failed = compare(expected_data, actual_data) + + if len(failed) == 0: + print('Token Balance checker work completed. No differences were found.') + else: + send_email(failed, args.emailInfoFile) + + logging.debug('Closing pool...') + + close_pool(pool_handle) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Script checks the ledger to ensure that the balance matches' + ' the expected values for each payment address listed in an input file') + parser.add_argument('--dataFile', + help='[INPUT] .CSV file containing a list of payment addresses with an expected tokens amount' + '(columns: "Payment Address", "Tokens Amount")') + parser.add_argument('--emailInfoFile', default=None, + help='[INPUT] .JSON file containing information required for email notifications') + args = parser.parse_args() + run(args) diff --git a/balance-checker/constants.py b/balance-checker/constants.py new file mode 100644 index 0000000..3dacc5d --- /dev/null +++ b/balance-checker/constants.py @@ -0,0 +1,4 @@ +PAYMENT_METHOD = 'sov' +POOL_NAME = 'pool1' +LIBRARY = {"darwin": "libsovtoken.dylib", "linux": "libsovtoken.so", "win32": "sovtoken.dll", 'windows': 'sovtoken.dll'} +PROTOCOL_VERSION = 2 diff --git a/balance-checker/email-info.json b/balance-checker/email-info.json new file mode 100644 index 0000000..eea1fb7 --- /dev/null +++ b/balance-checker/email-info.json @@ -0,0 +1,7 @@ +{ + "host": "smtp.gmail.com", + "port": 465, + "from": "Sovrin@email.com", + "to": "TargetSovrin@email.com", + "subject": "Balance Checker" +} \ No newline at end of file diff --git a/balance-checker/indy_helpers.py b/balance-checker/indy_helpers.py new file mode 100644 index 0000000..86119a8 --- /dev/null +++ b/balance-checker/indy_helpers.py @@ -0,0 +1,105 @@ +import json + +from indy.error import IndyError, ErrorCode +from indy import ledger, payment, pool +from constants import PAYMENT_METHOD, POOL_NAME, PROTOCOL_VERSION +from utils import run_coroutine, run_array + + +def open_pool() -> int: + genesis_transactions = input("Enter path to Pool Genesis Transactions file: ") + config = {'genesis_txn': genesis_transactions} + + try: + return run_coroutine(create_and_open_pool(config)) + except IndyError as err: + if err.error_code == ErrorCode.PoolLedgerNotCreatedError: + raise Exception('Pool not found') + if err.error_code == ErrorCode.CommonInvalidParam2: + raise Exception('Invalid Pool name has been provided') + if err.error_code == ErrorCode.PoolLedgerTimeout: + raise Exception('Cannot connect to Pool') + if err.error_code == ErrorCode.CommonIOError: + raise Exception('Genesis Transactions file not found') + raise Exception(err.message) + + +async def create_and_open_pool(config) -> int: + await pool.set_protocol_version(PROTOCOL_VERSION) + + try: + await pool.create_pool_ledger_config(POOL_NAME, json.dumps(config)) + except IndyError as err: + if err.error_code != ErrorCode.PoolLedgerConfigAlreadyExistsError: + raise err + + return await pool.open_pool_ledger(POOL_NAME, None) + + +def close_pool(pool_handle): + try: + run_coroutine(close_and_delete_pool(pool_handle)) + except IndyError as err: + raise Exception(err.message) + + +async def close_and_delete_pool(pool_handle): + await pool.close_pool_ledger(pool_handle) + await pool.delete_pool_ledger_config(POOL_NAME) + + +def get_payment_sources(pool_handle, addresses): + try: + requests = run_array( + [payment.build_get_payment_sources_with_from_request(-1, None, payAddress, -1) for payAddress in addresses]) + + responses = run_array( + [ledger.submit_request(pool_handle, list(request.result())[0]) for request in requests[0]]) + + results = run_array( + [payment.parse_get_payment_sources_with_from_response(PAYMENT_METHOD, response.result()) for response in + responses[0]]) + + res = {} + + for result in results[0]: + sources, next_ = result.result() + sources = json.loads(sources) + + if len(sources) == 0: + continue # TODO! + + address = sources[0]['paymentAddress'] + + if next_ != -1: + get_next_batch_of_payment_sources(sources, pool_handle, address, next_) + + amount = sum(source['amount'] for source in sources) + + res[address] = amount + return res + except IndyError as err: + handle_payment_error(err) + + +def get_next_batch_of_payment_sources(sources, pool_handle, address, next_): + request = run_coroutine(payment.build_get_payment_sources_with_from_request(-1, None, address, next_)) + response = run_coroutine(ledger.submit_request(pool_handle, request)) + batch_sources, next_ = run_coroutine(payment.parse_get_payment_sources_with_from_response(PAYMENT_METHOD, response)) + sources.extend(json.loads(batch_sources)) + if next_ != -1: + get_next_batch_of_payment_sources(sources, pool_handle, address, next_) + else: + return sources + + +def handle_payment_error(err: IndyError): + if err.error_code == ErrorCode.CommonInvalidStructure: + raise Exception('Invalid payment address has been provided') + if err.error_code == ErrorCode.PaymentExtraFundsError: + raise Exception('Extra funds on inputs') + if err.error_code == ErrorCode.PaymentInsufficientFundsError: + raise Exception('Insufficient funds on inputs') + if err.error_code == ErrorCode.PaymentUnknownMethodError: + raise Exception('Payment library not found') + raise Exception(err.message) diff --git a/balance-checker/input_data.csv b/balance-checker/input_data.csv new file mode 100644 index 0000000..5ab301d --- /dev/null +++ b/balance-checker/input_data.csv @@ -0,0 +1,4 @@ +Payment Address,Tokens Amount +pay:sov:t3vet9QjjSn6SpyqYSqc2ct7aWM4zcXKM5ygUaJktXEkTgL31,1000000 +pay:sov:2vEjkFFe9LhVr47f8SY6r77ZXbWVMMKpVCaYvaoKwkoukP2stQ,1 +pay:sov:PEMYpH2L8Raob6nysWfCB1KajZygX1AJnaLzHT1eSo8YNxu1d,0 diff --git a/balance-checker/utils.py b/balance-checker/utils.py new file mode 100644 index 0000000..3321798 --- /dev/null +++ b/balance-checker/utils.py @@ -0,0 +1,91 @@ +import json +import platform +import asyncio +import smtplib +from ctypes import cdll +from getpass import getpass +from pathlib import Path +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +from constants import LIBRARY + + +INITIAL_DIR = Path.home() + +loop = asyncio.get_event_loop() + + +def library(): + your_platform = platform.system().lower() + return LIBRARY[your_platform] if (your_platform in LIBRARY) else 'libsovtoken.so' + + +def load_payment_plugin(): + try: + payment_plugin = cdll.LoadLibrary(library()) + payment_plugin.sovtoken_init() + except Exception as e: + raise Exception(e) + + +def run_coroutine(coroutine): + return loop.run_until_complete(coroutine) + + +def run_array(array: list): + return run_coroutine(asyncio.wait(array)) + + +def read_file(data_file): + with open(data_file, newline='') as data_file: + return data_file.read() + + +def send_email(fails, email_info_file): + try: + email_info = json.loads(read_file(email_info_file)) + except Exception as err: + print("No information for email sending found: {}".format(err)) + return + + password = email_info["password"] if "password" in email_info else getpass( + "Enter Password for Email Account \"{}\": ".format(email_info['from'])) + + lines = ["Payment Address: {}, Expected Tokens: {}, Actual Tokens: {}".format( + address, values['expected'], values['actual']) for address, values in fails.items()] + + body = "Token Balance check failed. The following discrepancies were found: \n {}".format("\n".join(lines)) + + print(body) + print("Sending email notification to {}".format(email_info['to'])) + + try: + server = smtplib.SMTP_SSL(email_info['host'], email_info['port']) + server.ehlo() + server.login(email_info['from'], password) + except Exception as err: + print("Can not connect to email server: {}".format(err)) + return + + message = MIMEMultipart() + + message['From'] = email_info['from'] + message['To'] = email_info['to'] + message['Subject'] = email_info['subject'] + + email_text = """\ +Token Balance check failed +The following discrepancies were found: + +%s + """ % ("\n".join(lines)) + + message.attach(MIMEText(email_text, 'plain')) + + try: + server.sendmail(email_info['from'], email_info['to'], message.as_string()) + print("Mail has been successfully sent to {}".format(email_info['to'])) + except Exception as err: + print("Sending email to {} failed with {}".format(email_info['to'], err)) + server.close() diff --git a/token-distribution/utils.py b/token-distribution/utils.py index c1a9f91..a33c663 100644 --- a/token-distribution/utils.py +++ b/token-distribution/utils.py @@ -6,6 +6,8 @@ import csv import os import zipfile +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from constants import LIBRARY @@ -75,16 +77,20 @@ def send_email(from_, targets, subject_, password): return for target in targets: - email_text = """\n\ - From: %s - To: %s - Subject: %s + message = MIMEMultipart() + message['From'] = from_ + message['To'] = target['to'] + message['Subject'] = subject_ + + email_text = """\ %s - """ % (from_, target['to'], subject_, target['body']) + """ % (target['body']) + + message.attach(MIMEText(email_text, 'plain')) try: - server.sendmail(from_, target['to'], email_text) + server.sendmail(from_, target['to'], message.as_string()) print("Mail has been successfully sent to {}".format(target['to'])) except Exception as err: print("Sending email failed to {} with " + str(err))