Skip to content

ST-620 Balance checker initial version #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions balance-checker/README.md
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"to": "[email protected]",
"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




94 changes: 94 additions & 0 deletions balance-checker/balance-checker.py
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
# "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)
4 changes: 4 additions & 0 deletions balance-checker/constants.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions balance-checker/email-info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"host": "smtp.gmail.com",
"port": 465,
"from": "[email protected]",
"to": "[email protected]",
"subject": "Balance Checker"
}
105 changes: 105 additions & 0 deletions balance-checker/indy_helpers.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions balance-checker/input_data.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Payment Address,Tokens Amount
pay:sov:t3vet9QjjSn6SpyqYSqc2ct7aWM4zcXKM5ygUaJktXEkTgL31,1000000
pay:sov:2vEjkFFe9LhVr47f8SY6r77ZXbWVMMKpVCaYvaoKwkoukP2stQ,1
pay:sov:PEMYpH2L8Raob6nysWfCB1KajZygX1AJnaLzHT1eSo8YNxu1d,0
91 changes: 91 additions & 0 deletions balance-checker/utils.py
Original file line number Diff line number Diff line change
@@ -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()
18 changes: 12 additions & 6 deletions token-distribution/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down