From e88ec4f727f06bc6a87fb93d4f84b88a9d8666d4 Mon Sep 17 00:00:00 2001 From: Vivek Teega Date: Mon, 8 Apr 2019 10:39:28 +0000 Subject: [PATCH] Complete Token tracking system --- config.ini | 4 +- models.py | 59 +++-- parsing.py | 94 +++++-- system.db | Bin 0 -> 12288 bytes tokens/rmt.db | Bin 0 -> 16384 bytes tracktokens-smartcontract.py | 369 -------------------------- tracktokens-smartcontracts.py | 470 ++++++++++++++++++++++++++++++++++ 7 files changed, 567 insertions(+), 429 deletions(-) create mode 100644 system.db create mode 100644 tokens/rmt.db delete mode 100644 tracktokens-smartcontract.py create mode 100644 tracktokens-smartcontracts.py diff --git a/config.ini b/config.ini index aff3442..a533fcd 100644 --- a/config.ini +++ b/config.ini @@ -1,6 +1,4 @@ [DEFAULT] NET = testnet FLO_CLI_PATH = /usr/local/bin/flo-cli -START_BLOCK = 498770 - - +START_BLOCK = 525300 diff --git a/models.py b/models.py index bbe505a..80e590f 100644 --- a/models.py +++ b/models.py @@ -5,44 +5,36 @@ Base = declarative_base() ContractBase = declarative_base() SystemBase = declarative_base() -class Extra(Base): - __tablename__ = "extra" - - id = Column('id', Integer, primary_key=True) - lastblockscanned = Column('lastblockscanned', Integer) - -class TransactionHistory(Base): - __tablename__ = "transactionhistory" - - id = Column('id', Integer, primary_key=True) - blockno = Column('blockno', Integer) - fromAddress = Column('fromAddress', String) - toAddress = Column('toAddress', String) - amount = Column('amount', Float) - blockchainReference = Column('blockchainReference', String) - -class TransactionTable(Base): - __tablename__ = "transactiontable" +class ActiveTable(Base): + __tablename__ = "activeTable" id = Column('id', Integer, primary_key=True) address = Column('address', String) parentid = Column('parentid', Integer) + consumedpid = Column('consumedpid', String) + transferBalance = Column('transferBalance', Float) + +class ConsumedTable(Base): + __tablename__ = "consumedTable" + + primaryKey = Column('primaryKey', Integer, primary_key=True) + id = Column('id', Integer) + address = Column('address', String) + parentid = Column('parentid', Integer) + consumedpid = Column('consumedpid', String) transferBalance = Column('transferBalance', Float) class TransferLogs(Base): __tablename__ = "transferlogs" - id = Column('id', Integer, primary_key=True) - primaryIDReference = Column('primaryIDReference', Integer) - transferDescription = Column('transferDescription', String) - transferIDConsumed = Column('transferIDConsumed', Integer) - blockchainReference = Column('blockchainReference', String) - -class Webtable(Base): - __tablename__ = "webtable" - - id = Column('id', Integer, primary_key=True) - transferDescription = Column('transferDescription', String) + primary_key = Column('id', Integer, primary_key=True) + sourceFloAddress = Column('sourceFloAddress', String) + destFloAddress = Column('destFloAddress', String) + transferAmount = Column('transferAmount', Float) + sourceId = Column('sourceId', Integer) + destinationId = Column('destinationId', Integer) + blockNumber = Column('blockNumber', Integer) + time = Column('time', Integer) blockchainReference = Column('blockchainReference', String) class ContractStructure(ContractBase): @@ -59,7 +51,7 @@ class ContractParticipants(ContractBase): id = Column('id', Integer, primary_key=True) participantAddress = Column('participantAddress', String) tokenAmount = Column('tokenAmount', Float) - contractCondition = Column('contractCondition', String) + userPreference = Column('userPreference', String) class ActiveContracts(SystemBase): __tablename__ = "activecontracts" @@ -68,3 +60,10 @@ class ActiveContracts(SystemBase): contractName = Column('contractName', String) contractAddress = Column('contractAddress', String) +class SystemData(SystemBase): + __tablename__ = "systemData" + + id = Column('id', Integer, primary_key=True) + attribute = Column('attribute', String) + value = Column('value', String) + diff --git a/parsing.py b/parsing.py index 222ed12..f4eaaa7 100644 --- a/parsing.py +++ b/parsing.py @@ -29,7 +29,7 @@ def isSmartContract(text): for word in textList: if word == '': continue - if word.endswith('@'): + if word.endswith('@') and len(word) != 1: return word return False @@ -74,7 +74,7 @@ def extractMarker(text): for word in textList: if word == '': continue - if word.endswith('#'): + if word.endswith('#') and len(word) != 1: return word return False @@ -82,15 +82,27 @@ def extractMarker(text): def extractInitTokens(text): base_units = {'thousand':10**3 , 'million':10**6 ,'billion':10**9, 'trillion':10**12} textList = text.split(' ') + counter = 0 + value = None for idx,word in enumerate(textList): - try: - result = float(word) - if textList[idx+1] in base_units: - return result*base_units[textList[idx+1]] - return result - except: - continue - return None + for unit in base_units: + result = word.split(unit) + if len(result) == 1: + try: + result = float(word) + if textList[idx+1] in base_units: + value = result*base_units[textList[idx+1]] + counter = counter + 1 + except: + continue + elif len(result) == 2 and result[1]=='': + value = float(result[0])*base_units[unit] + counter = counter + 1 + + if counter == 1: + return value + else: + return None def extractAddress(text): @@ -98,7 +110,7 @@ def extractAddress(text): for word in textList: if word == '': continue - if word[-1] == '$': + if word[-1] == '$' and len(word) != 1: return word return None @@ -145,6 +157,10 @@ def extractContractConditions(text, contracttype, marker): extractedRules['smartcontractpays'][idx] = condition.strip() else: print("something is wrong with smartcontractpays conditions") + elif rule[:10]=='expirytime': + pattern = re.compile('[^expirytime="].*[^"]') + searchResult = pattern.search(rule).group(0) + extractedRules['expirytime'] = searchResult if 'userassetcommitment' in extractedRules and 'smartcontractpays' in extractedRules: return extractedRules @@ -164,37 +180,55 @@ def extractTriggerCondition(text): # Combine test def parse_flodata(string): + # todo Rule 20 - remove 'text:' from the start of flodata if it exists if string[0:5] == 'text:': string = string.split('text:')[1] + # todo Rule 21 - Collapse multiple spaces into a single space in the whole of flodata + # todo Rule 22 - convert flodata to lowercase to make the system case insensitive nospacestring = re.sub(' +', ' ', string) cleanstring = nospacestring.lower() + # todo Rule 23 - Count number of words ending with @ and # atList = [] hashList = [] for word in cleanstring.split(' '): - if word.endswith('@'): + if word.endswith('@') and len(word) != 1: atList.append(word) - if word.endswith('#'): + if word.endswith('#') and len(word) != 1: hashList.append(word) + # todo Rule 24 - Reject the following conditions - a. number of # & number of @ is equal to 0 then reject + # todo Rule 25 - If number of # or number of @ is greater than 1, reject + # todo Rule 25.a - If a transaction is rejected, it means parsed_data type is noise # Filter noise first - check if the words end with either @ or # if (len(atList)==0 and len(hashList)==0) or len(atList)>1 or len(hashList)>1: parsed_data = {'type': 'noise'} + # todo Rule 26 - if number of # is 1 and number of @ is 0, then check if its token creation or token transfer transaction + elif len(hashList)==1 and len(atList)==0: # Passing the above check means token creation or transfer incorporation = isIncorp(cleanstring) transfer = isTransfer(cleanstring) - if incorporation and not transfer: + # todo Rule 27 - if (neither token incorporation and token transfer) OR both token incorporation and token transfer, reject + if (not incorporation and not transfer) or (incorporation and transfer): + parsed_data = {'type': 'noise'} + + # todo Rule 28 - if token creation and not token transfer then it is confirmed that is it a token creation transaction + # todo Rule 29 - Extract total number of tokens issued, if its not mentioned then reject + elif incorporation and not transfer: initTokens = extractInitTokens(cleanstring) if initTokens is not None: parsed_data = {'type': 'tokenIncorporation', 'flodata': string, 'tokenIdentification': hashList[0][:-1], 'tokenAmount': initTokens} else: parsed_data = {'type': 'noise'} + + # todo Rule 30 - if not token creation and is token transfer then then process it for token transfer rules + # todo Rule 31 - Extract number of tokens to be sent and the address to which to be sent, both data is mandatory elif not incorporation and transfer: amount = extractAmount(cleanstring, hashList[0][:-1]) address = extractAddress(nospacestring) @@ -204,15 +238,19 @@ def parse_flodata(string): 'tokenAmount': amount, 'address': address[:-1]} else: parsed_data = {'type': 'noise'} - else: - parsed_data = {'type': 'noise'} + # todo Rule 32 - if number of # is 1 and number of @ is 1, then process for smart contract transfer or creation elif len(hashList)==1 and len(atList)==1: # Passing the above check means Smart Contract creation or transfer incorporation = isIncorp(cleanstring) transfer = isTransfer(cleanstring) - if incorporation and not transfer: + # todo Rule 33 - if a confusing smart contract command is given, like creating and sending at the same time, or no + if (not incorporation and not transfer) or (incorporation and transfer): + parsed_data = {'type': 'noise'} + + # todo Rule 34 - if incorporation and not transfer, then extract type of contract, address of the contract and conditions of the contract. Reject if any of those is not present + elif incorporation and not transfer: contracttype = extractContractType(cleanstring) contractaddress = extractAddress(nospacestring) contractconditions = extractContractConditions(cleanstring, 'betting*', marker) @@ -224,29 +262,31 @@ def parse_flodata(string): 'contractConditions': contractconditions} else: parsed_data = {'type': 'noise'} + + # todo Rule 35 - if it is not incorporation and it is transfer, then extract smart contract amount to be locked and userPreference. If any of them is missing, then reject elif not incorporation and transfer: + # We are at the send/transfer of smart contract amount = extractAmount(cleanstring, hashList[0][:-1]) - contractcondition = extractContractCondition(cleanstring) - if None not in [amount, contractcondition]: + userPreference = extractContractCondition(cleanstring) + if None not in [amount, userPreference]: parsed_data = {'type': 'transfer', 'transferType': 'smartContract', 'flodata': string, 'tokenIdentification': hashList[0][:-1], 'operation': 'transfer', 'tokenAmount': amount, 'contractName': atList[0][:-1], - 'contractCondition': contractcondition} + 'userPreference': userPreference} else: parsed_data = {'type': 'noise'} - else: - parsed_data = {'type': 'noise'} - elif len(hashList)==0 and len(atList)==1: + # todo Rule 36 - Check for only a single @ and the substring "smart contract system says" in flodata, else reject + elif (len(hashList)==0 and len(atList)==1) and 'smart contract system says' in cleanstring: # Passing the above check means Smart Contract pays | exitcondition triggered from the committee + # todo Rule 37 - Extract the trigger condition given by the committee. If its missing, reject triggerCondition = extractTriggerCondition(cleanstring) if triggerCondition is not None: parsed_data = {'type': 'smartContractPays', 'contractName': atList[0][:-1], 'triggerCondition': triggerCondition.group().strip()[1:-1]} else: parsed_data = {'type':'noise'} + else: + parsed_data = {'type': 'noise'} - return parsed_data - -result = parse_flodata("for contractname@ Smart Contract System says 'NAMO=WIN'") -print(result) \ No newline at end of file + return parsed_data \ No newline at end of file diff --git a/system.db b/system.db new file mode 100644 index 0000000000000000000000000000000000000000..adfac0e2fa3eac7c163939ba631d20adcba35f9b GIT binary patch literal 12288 zcmeI#&r8EF6bJC68;lL+ybZ-e*3-sRaI(AIEIO=JV-T z0RIp(VeW_M!JG1ZB+W}6l*i}xZg0F;OZG5MheESsvP~!@CyWt7W@hKiUW8?m{NiFx zx{`mF&dA~3OW9Pdtyfav=7WF$1Rwwb2tWV=5P$##AOHaf{7N7zTZL+^Ml-3!Ad%`x zX*oOfo7GFjIAT3NaL-)M{HuWZcb>P;N=2b{8V@FBIKFpy z_uS$0$EiprYY*4Fcj@pxyKwug9!HH*BWuz^wb`W4<&Sp=4R+eKmY;|fB*y_009U<00Izz00bcLj|JxQ zKdrnGvk(w~00bZa0SG_<0uX=z1Rwwb2>fAzU79BcJBd(wkc`7e6^hYFM(tL+ebjmf D+(L7? literal 0 HcmV?d00001 diff --git a/tokens/rmt.db b/tokens/rmt.db new file mode 100644 index 0000000000000000000000000000000000000000..49d5091ed3608d9eceff40a89caf05e803b34649 GIT binary patch literal 16384 zcmeI%!B5jr90%~0H8LH@zqj|^_p?*eKa*?I#!m--=>_h|<1Y*Ggu+6JD{Vb!#~m63AOHafKmY;|fB*y_0D;>e zaIsaGU0hz~KPnM5hAaxG7uqZs`me(5SlH1UYPUhU>UO(9vK6vAKj&CvPwO_GH*}&M zbcxn$x7W%1oEZA!z+}y#uUb~X!cZh9s=l+U>S@(tVf3fkbO+V-$6h3oX8S~duq!Z28)bVr{6EM$YxdR{SdmUBplUAco zRvl|?e(iiPGrPF5!hd*_;+eh|j$LMT6Up3{LXJ<&)x<898rQnl&?= z$tTTJ(wsNv%cc7_dD8zZxi{xaU%B{&1_1~_00Izz00bZa0SG_<0uX?}e)d u$(u?|*{Ir*Ld&(fEm?A1G7W`EvcXunEY~)eq*Q8_S&{6rMJ1zFHGTrbVjgt> literal 0 HcmV?d00001 diff --git a/tracktokens-smartcontract.py b/tracktokens-smartcontract.py deleted file mode 100644 index 86e769a..0000000 --- a/tracktokens-smartcontract.py +++ /dev/null @@ -1,369 +0,0 @@ -import requests -import json -import sqlite3 -import argparse -import configparser -import subprocess -import sys -import parsing -import os -import shutil -from sqlalchemy.orm import sessionmaker, relationship -from sqlalchemy import create_engine, func, desc -from models import Extra, TransactionHistory, TransactionTable, TransferLogs, Webtable, Base, ContractStructure, ContractBase, ContractParticipants, SystemBase, ActiveContracts - - -committeeAddressList = ['oUc4dVvxwK7w5MHUHtev8UawN3eDjiZnNx'] - -def transferToken(tokenIdentification, tokenAmount, inputAddress, outputAddress): - engine = create_engine('sqlite:///tokens/{}.db'.format(tokenIdentification), echo=True) - Base.metadata.create_all(bind=engine) - session = sessionmaker(bind=engine)() - availableTokens = session.query(func.sum(TransactionTable.transferBalance)).filter_by(address=inputAddress).all()[0][0] - commentTransferAmount = tokenAmount - if availableTokens is None: - print("The input address dosen't exist in our database ") - session.close() - return None - - elif availableTokens < commentTransferAmount: - print( - "\nThe transfer amount passed in the comments is more than the user owns\nThis transaction will be discarded\n") - session.close() - return None - - elif availableTokens >= commentTransferAmount: - table = session.query(TransactionTable).filter(TransactionTable.address==inputAddress, TransactionTable.transferBalance>0).all() - pidlst = [] - checksum = 0 - for row in table: - if checksum >= commentTransferAmount: - break - pidlst.append(row.id) - checksum = checksum + row.transferBalance - - balance = commentTransferAmount - opbalance = session.query(func.sum(TransactionTable.transferBalance)).filter_by(address=outputAddress).all()[0][0] - - if opbalance is None: - opbalance = 0 - - ipbalance = availableTokens - - for pid in pidlst: - temp = session.query(TransactionTable.transferBalance).filter_by(id=pid).all()[0][0] - - if balance <= temp: - - session.add(TransactionTable(address=outputAddress, parentid=pid, transferBalance=balance)) - entry = session.query(TransactionTable).filter(TransactionTable.id == pid).all() - entry[0].transferBalance = temp - balance - session.commit() - - ## transaction logs section ## - result = session.query(TransactionTable.id).order_by(desc(TransactionTable.id)).all() - lastid = result[-1].id - transferDescription = str(balance) + " tokens transferred to " + str( - outputAddress) + " from " + str(inputAddress) - blockchainReference = '{}tx/{}'.format(neturl, str(transaction)) - session.add(TransferLogs(primaryIDReference=lastid, transferDescription=transferDescription, - transferIDConsumed=pid, blockchainReference=blockchainReference)) - transferDescription = str(inputAddress) + " balance UPDATED from " + str( - temp) + " to " + str(temp - balance) - blockchainReference = '{}tx/{}'.format(neturl, str(transaction)) - session.add(TransferLogs(primaryIDReference=pid, transferDescription=transferDescription, - blockchainReference=blockchainReference)) - - ## transaction history table ## - session.add(TransactionHistory(blockno=blockindex, fromAddress=inputAddress, - toAddress=outputAddress, amount=str(balance), - blockchainReference=blockchainReference)) - - ##webpage table section ## - transferDescription = str(commentTransferAmount) + " tokens transferred from " + str( - inputAddress) + " to " + str(outputAddress) - session.add( - Webtable(transferDescription=transferDescription, blockchainReference=blockchainReference)) - - transferDescription = "UPDATE " + str(outputAddress) + " balance from " + str( - opbalance) + " to " + str(opbalance + commentTransferAmount) - session.add( - Webtable(transferDescription=transferDescription, blockchainReference=blockchainReference)) - - transferDescription = "UPDATE " + str(inputAddress) + " balance from " + str( - ipbalance) + " to " + str(ipbalance - commentTransferAmount) - session.add( - Webtable(transferDescription=transferDescription, blockchainReference=blockchainReference)) - - balance = 0 - session.commit() - - elif balance > temp: - session.add(TransactionTable(address=outputAddress, parentid=pid, transferBalance=temp)) - entry = session.query(TransactionTable).filter(TransactionTable.id == pid).all() - entry[0].transferBalance = 0 - session.commit() - - ##transaction logs section ## - result = session.query(TransactionTable.id).order_by(desc(TransactionTable.id)).all() - lastid = result[-1].id - transferDescription = str(temp) + " tokens transferred to " + str(outputAddress) + " from " + str(inputAddress) - blockchainReference = '{}tx/{}'.format(neturl, str(transaction)) - session.add(TransferLogs(primaryIDReference=lastid, transferDescription=transferDescription, - transferIDConsumed=pid, blockchainReference=blockchainReference)) - - transferDescription = str() + " balance UPDATED from " + str(temp) + " to " + str(0) - blockchainReference = '{}tx/{}'.format(neturl, str(transaction)) - session.add(TransferLogs(primaryIDReference=pid, transferDescription=transferDescription, - blockchainReference=blockchainReference)) - - ## transaction history table ## - session.add(TransactionHistory(blockno=blockindex, fromAddress=inputAddress, - toAddress=outputAddress, amount=str(balance), - blockchainReference=blockchainReference)) - balance = balance - temp - session.commit() - session.close() - return 1 - - -def startWorking(transaction_data, parsed_data): - - # Do the necessary checks for the inputs and outputs - - # Create inputlist and outputlist - inputlist = [] - querylist = [] - - for obj in transaction_data["vin"]: - querylist.append([obj["txid"], obj["vout"]]) - - if len(querylist) > 1: - print("Program has detected more than one input address ") - print("This transaction will be discarded") - return - - inputval = 0 - inputadd = '' - - for query in querylist: - string = "{} getrawtransaction {} 1".format(localapi, str(query[0])) - response = subprocess.check_output(string, shell=True) - content = json.loads(response.decode("utf-8")) - - for objec in content["vout"]: - if objec["n"] == query[1]: - inputadd = objec["scriptPubKey"]["addresses"][0] - inputval = inputval + objec["value"] - - inputlist = [[inputadd, inputval]] - - if len(transaction_data["vout"]) > 2: - print("Program has detected more than one output address ") - print("This transaction will be discarded") - return - - outputlist = [] - for obj in transaction_data["vout"]: - if obj["scriptPubKey"]["type"] == "pubkeyhash": - if inputlist[0][0] == obj["scriptPubKey"]["addresses"][0]: - continue - - outputlist.append([obj["scriptPubKey"]["addresses"][0], obj["value"]]) - - print("\n\nInput List") - print(inputlist) - print("\nOutput List") - print(outputlist) - - - # Do operations as per the type of transaction - if parsed_data['type'] == 'transfer': - print('Found a transaction of the type transfer') - - if parsed_data['transferType'] == 'token': - - returnval = transferToken(parsed_data['tokenIdentification'], parsed_data['tokenAmount'], inputlist[0][0], outputlist[0][0]) - if returnval is None: - print("Something went wrong in the token transfer method") - - elif parsed_data['transferType'] == 'smartContract': - - # Check if the tokenAmount being transferred exists in the address & do the token transfer - returnval = transferToken(parsed_data['tokenIdentification'], parsed_data['tokenAmount'], inputlist[0][0], outputlist[0][0]) - if returnval is not None: - # Store participant details in the smart contract's db - engine = create_engine('sqlite:///smartContracts/{}.db'.format(parsed_data['contractName']), echo=True) - Base.metadata.create_all(bind=engine) - session = sessionmaker(bind=engine)() - session.add(ContractParticipants(participantAddress=inputadd, tokenAmount=parsed_data['tokenAmount'], - contractCondition=parsed_data['contractCondition'])) - session.commit() - session.close() - - - else: - print("Something went wrong in the smartcontract token transfer method") - - - elif parsed_data['type'] == 'tokenIncorporation': - if not os.path.isfile('./tokens/{}.db'.format(parsed_data['tokenIdentification'])): - engine = create_engine('sqlite:///tokens/{}.db'.format(parsed_data['tokenIdentification']), echo=True) - Base.metadata.create_all(bind=engine) - session = sessionmaker(bind=engine)() - session.add(TransactionTable(address=outputlist[0][0], parentid=0, transferBalance=parsed_data['tokenAmount'])) - session.commit() - session.close() - else: - print('Transaction rejected as the token with same name has already been incorporated') - - elif parsed_data['type'] == 'smartContractIncorporation': - if not os.path.isfile('./smartContracts/{}.db'.format(parsed_data['contractName'])): - if parsed_data['contractType'] == 'betting': - print("Smart contract is of the type betting") - if parsed_data['contractName'] is not None and parsed_data['contractAddress'] == outputlist[0][0]: - print("Hey I have passed the first test for smart contract") - engine = create_engine('sqlite:///smartContracts/{}.db'.format(parsed_data['contractName']), echo=True) - ContractBase.metadata.create_all(bind=engine) - session = sessionmaker(bind=engine)() - session.add(ContractStructure(attribute='contractType', index=0, value=parsed_data['contractType'])) - session.add(ContractStructure(attribute='contractName', index=0, value=parsed_data['contractName'])) - session.add( - ContractStructure(attribute='tokenIdentification', index=0, value=parsed_data['tokenIdentification'])) - session.add( - ContractStructure(attribute='contractAddress', index=0, value=parsed_data['contractAddress'])) - session.add( - ContractStructure(attribute='flodata', index=0, - value=parsed_data['flodata'])) - session.add( - ContractStructure(attribute='userassetcommitment', index=0, - value=parsed_data['contractConditions']['userassetcommitment'].split(parsed_data['tokenIdentification'][:-1])[0])) - for key, value in parsed_data['contractConditions']['smartcontractpays'].items(): - session.add(ContractStructure(attribute='exitconditions', index=key, value=value)) - session.commit() - session.close() - - # Store smart contract address in system's db, to be ignored during future transfers - engine = create_engine('sqlite:///system.db', - echo=True) - SystemBase.metadata.create_all(bind=engine) - session = sessionmaker(bind=engine)() - session.add( - ActiveContracts(contractName=parsed_data['contractName'], contractAddress=parsed_data['contractAddress'])) - session.commit() - session.close() - else: - print('Transaction rejected as a smartcontract with same name has already been incorporated') - elif parsed_data['type'] == 'smartContractPays': - print('Found a transaction of the type smartContractPays') - - # Check if input address is a committee address - if inputlist[0][0] in committeeAddressList: - - # Check if the output address is an active Smart contract address - engine = create_engine('sqlite:///system.db', echo=True) - connection = engine.connect() - activeContracts = connection.execute('select * from activecontracts').fetchall() - connection.close() - - # Change columns into rows - https://stackoverflow.com/questions/44360162/how-to-access-a-column-in-a-list-of-lists-in-python - activeContracts = list(zip(*activeContracts)) - if outputlist[0][0] in activeContracts[2] and parsed_data['contractName'] in activeContracts[1]: - - engine = create_engine('sqlite:///smartContracts/{}.db'.format(parsed_data['contractName']), echo=True) - connection = engine.connect() - contractWinners = connection.execute('select * from contractparticipants where contractCondition="{}"'.format(parsed_data['triggerCondition'])).fetchall() - tokenSum = connection.execute('select sum(tokenAmount) from contractparticipants').fetchall()[0][0] - tokenIdentification = connection.execute('select value from contractstructure where attribute="tokenIdentification"').fetchall()[0][0] - connection.close() - - contractWinners = list(zip(*contractWinners)) - for address in contractWinners[1]: - transferToken(tokenIdentification, tokenSum/len(contractWinners[1]), outputlist[0][0], address) - - else: - print('This trigger doesn\'t apply to an active contract. It will be discarded') - - else: - print('Input address is not part of the committee address list. This trigger is rejected') - - - -# Read configuration -config = configparser.ConfigParser() -config.read('config.ini') - -# Read command line arguments -parser = argparse.ArgumentParser(description='Script tracks RMT using FLO data on the FLO blockchain - https://flo.cash') -parser.add_argument('-r', '--reset', nargs='?', const=1, type=int, help='Purge existing db and rebuild it') -args = parser.parse_args() - -# Assignment the flo-cli command -if config['DEFAULT']['NET'] == 'mainnet': - neturl = 'https://florincoin.info/' - localapi = config['DEFAULT']['FLO_CLI_PATH'] -elif config['DEFAULT']['NET'] == 'testnet': - neturl = 'https://testnet.florincoin.info/' - localapi = '{} --testnet'.format(config['DEFAULT']['FLO_CLI_PATH']) -else: - print("NET parameter is wrong\nThe script will exit now ") - -apppath = os.path.dirname(os.path.realpath(__file__)) -dirpath = os.path.join(apppath, 'tokens') -if not os.path.isdir(dirpath): - os.mkdir(dirpath) -dirpath = os.path.join(apppath, 'smartContracts') -if not os.path.isdir(dirpath): - os.mkdir(dirpath) - -# Delete database and smartcontract directory if reset is set to 1 -if args.reset == 1: - apppath = os.path.dirname(os.path.realpath(__file__)) - dirpath = os.path.join(apppath, 'tokens') - shutil.rmtree(dirpath) - os.mkdir(dirpath) - dirpath = os.path.join(apppath, 'smartContracts') - shutil.rmtree(dirpath) - os.mkdir(dirpath) - dirpath = os.path.join(apppath, 'system.db') - if os.path.exists(dirpath): - os.remove(dirpath) - - -# Read start block no -startblock = int(config['DEFAULT']['START_BLOCK']) - -# Find current block height -string = "{} getblockcount".format(localapi) -response = subprocess.check_output(string, shell=True) -current_index = json.loads(response.decode("utf-8")) -print("current_block_height : " + str(current_index)) - - -for blockindex in range( startblock, current_index ): - print(blockindex) - - # Scan every block - string = "{} getblockhash {}".format(localapi, str(blockindex)) - response = subprocess.check_output(string, shell=True) - blockhash = response.decode("utf-8") - - string = "{} getblock {}".format(localapi, str(blockhash)) - response = subprocess.check_output(string, shell=True) - blockinfo = json.loads(response.decode("utf-8")) - - # Scan every transaction - for transaction in blockinfo["tx"]: - string = "{} getrawtransaction {} 1".format(localapi, str(transaction)) - response = subprocess.check_output(string, shell=True) - transaction_data = json.loads(response.decode("utf-8")) - text = transaction_data["floData"] - - if blockindex == 498385: - print('debug point') - - parsed_data = parsing.parse_flodata(text) - if parsed_data['type'] != 'noise': - print(blockindex) - print(parsed_data['type']) - startWorking(transaction_data, parsed_data) \ No newline at end of file diff --git a/tracktokens-smartcontracts.py b/tracktokens-smartcontracts.py new file mode 100644 index 0000000..da86d5e --- /dev/null +++ b/tracktokens-smartcontracts.py @@ -0,0 +1,470 @@ +import requests +import json +import sqlite3 +import argparse +import configparser +import subprocess +import sys +import parsing +import time +import os +import shutil +from sqlalchemy.orm import sessionmaker, relationship +from sqlalchemy import create_engine, func, desc +from models import SystemData, ActiveTable, ConsumedTable, TransferLogs, Base, ContractStructure, ContractBase, ContractParticipants, SystemBase, ActiveContracts + + +committeeAddressList = ['oUc4dVvxwK7w5MHUHtev8UawN3eDjiZnNx'] + +def transferToken(tokenIdentification, tokenAmount, inputAddress, outputAddress): + engine = create_engine('sqlite:///tokens/{}.db'.format(tokenIdentification), echo=True) + Base.metadata.create_all(bind=engine) + session = sessionmaker(bind=engine)() + availableTokens = session.query(func.sum(ActiveTable.transferBalance)).filter_by(address=inputAddress).all()[0][0] + commentTransferAmount = tokenAmount + if availableTokens is None: + print("The input address dosen't exist in our database ") + session.close() + return None + + elif availableTokens < commentTransferAmount: + print("\nThe transfer amount passed in the comments is more than the user owns\nThis transaction will be discarded\n") + session.close() + return None + + elif availableTokens >= commentTransferAmount: + table = session.query(ActiveTable).filter(ActiveTable.address==inputAddress).all() + string = "{} getblock {}".format(localapi, transaction_data['blockhash']) + response = subprocess.check_output(string, shell=True) + block_data = json.loads(response.decode("utf-8")) + + pidlst = [] + checksum = 0 + for row in table: + if checksum >= commentTransferAmount: + break + pidlst.append([row.id, row.transferBalance]) + checksum = checksum + row.transferBalance + + if checksum == commentTransferAmount: + consumedpid_string = '' + + # Update all pids in pidlist's transferBalance to 0 + lastid = session.query(ActiveTable)[-1].id + for piditem in pidlst: + entry = session.query(ActiveTable).filter(ActiveTable.id == piditem[0]).all() + consumedpid_string = consumedpid_string + '{},'.format(piditem[0]) + session.add(TransferLogs(sourceFloAddress=inputAddress, destFloAddress=outputAddress, + transferAmount=entry[0].transferBalance, sourceId=piditem[0], destinationId=lastid+1, + blockNumber=block_data['height'], time=block_data['time'], + blockchainReference=transaction_data['txid'])) + entry[0].transferBalance = 0 + + if len(consumedpid_string)>1: + consumedpid_string = consumedpid_string[:-1] + + # Make new entry + session.add(ActiveTable(address=outputAddress, consumedpid=consumedpid_string, + transferBalance=commentTransferAmount)) + + # Migration + # shift pid of used utxos from active to consumed + for piditem in pidlst: + # move the parentids consumed to consumedpid column in both activeTable and consumedTable + entries = session.query(ActiveTable).filter(ActiveTable.parentid == piditem[0]).all() + for entry in entries: + entry.consumedpid = entry.consumedpid + ',{}'.format(piditem[0]) + entry.parentid = None + + entries = session.query(ConsumedTable).filter(ConsumedTable.parentid == piditem[0]).all() + for entry in entries: + entry.consumedpid = entry.consumedpid + ',{}'.format(piditem[0]) + entry.parentid = None + + # move the pids consumed in the transaction to consumedTable and delete them from activeTable + session.execute( + 'INSERT INTO consumedTable (id, address, parentid, consumedpid, transferBalance) SELECT id, address, parentid, consumedpid, transferBalance FROM activeTable WHERE id={}'.format( + piditem[0])) + session.execute('DELETE FROM activeTable WHERE id={}'.format(piditem[0])) + session.commit() + session.commit() + + if checksum > commentTransferAmount: + consumedpid_string = '' + # Update all pids in pidlist's transferBalance + lastid = session.query(ActiveTable)[-1].id + for idx, piditem in enumerate(pidlst): + entry = session.query(ActiveTable).filter(ActiveTable.id == piditem[0]).all() + if idx != len(pidlst) - 1: + session.add(TransferLogs(sourceFloAddress=inputAddress, destFloAddress=outputAddress, + transferAmount=entry[0].transferBalance, sourceId=piditem[0], + destinationId=lastid + 1, + blockNumber=block_data['height'], time=block_data['time'], + blockchainReference=transaction_data['txid'])) + entry[0].transferBalance = 0 + consumedpid_string = consumedpid_string + '{},'.format(piditem[0]) + else: + session.add(TransferLogs(sourceFloAddress=inputAddress, destFloAddress=outputAddress, + transferAmount=piditem[1]-(checksum - commentTransferAmount), sourceId=piditem[0], + destinationId=lastid + 1, + blockNumber=block_data['height'], time=block_data['time'], + blockchainReference=transaction_data['txid'])) + entry[0].transferBalance = checksum - commentTransferAmount + + + if len(consumedpid_string) > 1: + consumedpid_string = consumedpid_string[:-1] + + # Make new entry + session.add(ActiveTable(address=outputAddress, parentid= pidlst[-1][0], consumedpid=consumedpid_string, + transferBalance=commentTransferAmount)) + + # Migration + # shift pid of used utxos from active to consumed + for piditem in pidlst[:-1]: + # move the parentids consumed to consumedpid column in both activeTable and consumedTable + entries = session.query(ActiveTable).filter(ActiveTable.parentid == piditem[0]).all() + for entry in entries: + entry.consumedpid = entry.consumedpid + ',{}'.format(piditem[0]) + entry.parentid = None + + entries = session.query(ConsumedTable).filter(ConsumedTable.parentid == piditem[0]).all() + for entry in entries: + entry.consumedpid = entry.consumedpid + ',{}'.format(piditem[0]) + entry.parentid = None + + # move the pids consumed in the transaction to consumedTable and delete them from activeTable + session.execute( + 'INSERT INTO consumedTable (id, address, parentid, consumedpid, transferBalance) SELECT id, address, parentid, consumedpid, transferBalance FROM activeTable WHERE id={}'.format( + piditem[0])) + session.execute('DELETE FROM activeTable WHERE id={}'.format(piditem[0])) + session.commit() + session.commit() + session.close() + return 1 + + +def startWorking(transaction_data, parsed_data): + + # Do the necessary checks for the inputs and outputs + + # todo Rule 38 - Here we are doing FLO processing. We attach asset amounts to a FLO address, so every FLO address + # will have multiple feed ins of the asset. Each of those feedins will be an input to the address. + # an address can also spend the asset. Each of those spends is an output of that address feeding the asset into some + # other address an as input + + # Rule 38 reframe - For checking any asset transfer on the flo blockchain it is possible that some transactions may use more than one + # vins. However in any single transaction the system considers valid, they can be only one source address from which the flodata is + # originting. To ensure consistency, we will have to check that even if there are more than one vins in a transaction, there should be + # excatly one FLO address on the originating side and that FLO address should be the owner of the asset tokens being transferred + + # Create vinlist and outputlist + vinlist = [] + querylist = [] + + # todo Rule 39 - Create a list of vins for a given transaction id + for obj in transaction_data["vin"]: + querylist.append([obj["txid"], obj["vout"]]) + + + totalinputval = 0 + inputadd = '' + + # todo Rule 40 - For each vin, find the feeding address and the fed value. Make an inputlist containing [inputaddress, n value] + for query in querylist: + string = "{} getrawtransaction {} 1".format(localapi, str(query[0])) + response = subprocess.check_output(string, shell=True) + content = json.loads(response.decode("utf-8")) + + for objec in content["vout"]: + if objec["n"] == query[1]: + inputadd = objec["scriptPubKey"]["addresses"][0] + totalinputval = totalinputval + objec["value"] + vinlist.append([inputadd, objec["value"]]) + + # todo Rule 41 - Check if all the addresses in a transaction on the input side are the same + for idx, item in enumerate(vinlist): + if idx == 0: + temp = item[0] + continue + if item[0] != temp: + print('System has found more than one address as part of vin') + return + + inputlist = [vinlist[0][0], totalinputval] + + + # todo Rule 42 - If the number of vout is more than 2, reject the transaction + if len(transaction_data["vout"]) > 2: + print("Program has detected more than 2 vouts ") + print("This transaction will be discarded") + return + + # todo Rule 43 - A transaction accepted by the system has two vouts, 1. The FLO address of the receiver + # 2. Flo address of the sender as change address. If the vout address is change address, then the other adddress + # is the recevier address + + outputlist = [] + for obj in transaction_data["vout"]: + if obj["scriptPubKey"]["type"] == "pubkeyhash": + if inputlist[0] == obj["scriptPubKey"]["addresses"][0]: + continue + outputlist.append([obj["scriptPubKey"]["addresses"][0], obj["value"]]) + + if len(outputlist) != 1: + print("The transaction change is not coming back to the input address") + return + + outputlist = outputlist[0] + + print("\n\nInput Address") + print(inputlist) + print("\nOutput Address") + print(outputlist) + + # All FLO checks completed at this point. + # Semantic rules for parsed data begins + + + # todo Rule 44 - Process as per the type of transaction + if parsed_data['type'] == 'transfer': + print('Found a transaction of the type transfer') + + # todo Rule 45 - If the transfer type is token, then call the function transferToken to adjust the balances + if parsed_data['transferType'] == 'token': + returnval = transferToken(parsed_data['tokenIdentification'], parsed_data['tokenAmount'], inputlist[0], outputlist[0]) + if returnval is None: + print("Something went wrong in the token transfer method") + + # todo Rule 46 - If the transfer type is smart contract, then call the function transferToken to do sanity checks & lock the balance + elif parsed_data['transferType'] == 'smartContract': + # Check if the contract has expired + if parsed_data['expiryTime'] and time.time()>parsed_data['expiryTime']: + print('Contract has expired and will not accept any user participation') + return + # Check if the tokenAmount being transferred exists in the address & do the token transfer + returnval = transferToken(parsed_data['tokenIdentification'], parsed_data['tokenAmount'], inputlist[0], outputlist[0]) + if returnval is not None: + # Store participant details in the smart contract's db + engine = create_engine('sqlite:///smartContracts/{}.db'.format(parsed_data['contractName']), echo=True) + Base.metadata.create_all(bind=engine) + session = sessionmaker(bind=engine)() + session.add(ContractParticipants(participantAddress=inputadd, tokenAmount=parsed_data['tokenAmount'], userPreference=parsed_data['userPreference'])) + session.commit() + session.close() + else: + print("Something went wrong in the smartcontract token transfer method") + + # todo Rule 47 - If the parsed data type is token incorporation, then check if the name hasn't been taken already + # if it has been taken then reject the incorporation. Else incorporate it + elif parsed_data['type'] == 'tokenIncorporation': + if not os.path.isfile('./tokens/{}.db'.format(parsed_data['tokenIdentification'])): + engine = create_engine('sqlite:///tokens/{}.db'.format(parsed_data['tokenIdentification']), echo=True) + Base.metadata.create_all(bind=engine) + session = sessionmaker(bind=engine)() + session.add(ActiveTable(address=outputlist[0], parentid=0, transferBalance=parsed_data['tokenAmount'])) + string = "{} getblock {}".format(localapi, transaction_data['blockhash']) + response = subprocess.check_output(string, shell=True) + block_data = json.loads(response.decode("utf-8")) + session.add(TransferLogs(sourceFloAddress=inputadd, destFloAddress=outputlist[0][0], transferAmount=parsed_data['tokenAmount'], sourceId=0, destinationId=1, blockNumber=block_data['height'], time=block_data['time'], blockchainReference=transaction_data['txid'])) + session.commit() + session.close() + else: + print('Transaction rejected as the token with same name has already been incorporated') + + # todo Rule 48 - If the parsed data type if smart contract incorporation, then check if the name hasn't been taken already + # if it has been taken then reject the incorporation. + elif parsed_data['type'] == 'smartContractIncorporation': + if not os.path.isfile('./smartContracts/{}.db'.format(parsed_data['contractName'])): + # todo Rule 49 - If the contract name hasn't been taken before, check if the contract type is an authorized type by the system + if parsed_data['contractType'] == 'betting': + print("Smart contract is of the type betting") + # todo Rule 50 - Contract address mentioned in flodata field should be same as the receiver FLO address on the output side + # henceforth we will not consider any flo private key initiated comment as valid from this address + # Unlocking can only be done through smart contract system address + if parsed_data['contractAddress'] == outputlist[0]: + print("Hey I have passed the first test for smart contract") + engine = create_engine('sqlite:///smartContracts/{}.db'.format(parsed_data['contractName']), echo=True) + ContractBase.metadata.create_all(bind=engine) + session = sessionmaker(bind=engine)() + session.add(ContractStructure(attribute='contractType', index=0, value=parsed_data['contractType'])) + session.add(ContractStructure(attribute='contractName', index=0, value=parsed_data['contractName'])) + session.add( + ContractStructure(attribute='tokenIdentification', index=0, value=parsed_data['tokenIdentification'])) + session.add( + ContractStructure(attribute='contractAddress', index=0, value=parsed_data['contractAddress'])) + session.add( + ContractStructure(attribute='flodata', index=0, + value=parsed_data['flodata'])) + session.add( + ContractStructure(attribute='userassetcommitment', index=0, + value=parsed_data['contractConditions']['userassetcommitment'].split(parsed_data['tokenIdentification'][:-1])[0])) + for key, value in parsed_data['contractConditions']['smartcontractpays'].items(): + session.add(ContractStructure(attribute='exitconditions', index=key, value=value)) + session.commit() + session.close() + + # Store smart contract address in system's db, to be ignored during future transfers + engine = create_engine('sqlite:///system.db', + echo=True) + SystemBase.metadata.create_all(bind=engine) + session = sessionmaker(bind=engine)() + session.add( + ActiveContracts(contractName=parsed_data['contractName'], contractAddress=parsed_data['contractAddress'])) + session.commit() + session.close() + else: + print('Transaction rejected as a smartcontract with same name has already been incorporated') + elif parsed_data['type'] == 'smartContractPays': + print('Found a transaction of the type smartContractPays') + + # Check if input address is a committee address + if inputlist[0] in committeeAddressList: + + # Check if the output address is an active Smart contract address + engine = create_engine('sqlite:///system.db', echo=True) + connection = engine.connect() + activeContracts = connection.execute('select * from activecontracts').fetchall() + connection.close() + + # Change columns into rows - https://stackoverflow.com/questions/44360162/how-to-access-a-column-in-a-list-of-lists-in-python + activeContracts = list(zip(*activeContracts)) + if outputlist[0] in activeContracts[2] and parsed_data['contractName'] in activeContracts[1]: + + engine = create_engine('sqlite:///smartContracts/{}.db'.format(parsed_data['contractName']), echo=True) + connection = engine.connect() + contractWinners = connection.execute('select * from contractparticipants where userPreference="{}"'.format(parsed_data['triggerCondition'])).fetchall() + tokenSum = connection.execute('select sum(tokenAmount) from contractparticipants').fetchall()[0][0] + tokenIdentification = connection.execute('select value from contractstructure where attribute="tokenIdentification"').fetchall()[0][0] + connection.close() + + contractWinners = list(zip(*contractWinners)) + for address in contractWinners[1]: + transferToken(tokenIdentification, tokenSum/len(contractWinners[1]), outputlist[0], address) + + else: + print('This trigger doesn\'t apply to an active contract. It will be discarded') + + else: + print('Input address is not part of the committee address list. This trigger is rejected') + + +# todo Rule 1 - Read command line arguments to reset the databases as blank +# Rule 2 - Read config to set testnet/mainnet +# Rule 3 - Set flo blockexplorer location depending on testnet or mainnet +# Rule 4 - Set the local flo-cli path depending on testnet or mainnet +# Rule 5 - Set the block number to scan from + + + +# Read command line arguments +parser = argparse.ArgumentParser(description='Script tracks RMT using FLO data on the FLO blockchain - https://flo.cash') +parser.add_argument('-r', '--reset', nargs='?', const=1, type=int, help='Purge existing db and rebuild it') +args = parser.parse_args() + +apppath = os.path.dirname(os.path.realpath(__file__)) +dirpath = os.path.join(apppath, 'tokens') +if not os.path.isdir(dirpath): + os.mkdir(dirpath) +dirpath = os.path.join(apppath, 'smartContracts') +if not os.path.isdir(dirpath): + os.mkdir(dirpath) + +# Read configuration +config = configparser.ConfigParser() +config.read('config.ini') + +# Assignment the flo-cli command +if config['DEFAULT']['NET'] == 'mainnet': + neturl = 'https://florincoin.info/' + localapi = config['DEFAULT']['FLO_CLI_PATH'] +elif config['DEFAULT']['NET'] == 'testnet': + neturl = 'https://testnet.florincoin.info/' + localapi = '{} --testnet'.format(config['DEFAULT']['FLO_CLI_PATH']) +else: + print("NET parameter is wrong\nThe script will exit now ") + + +# Delete database and smartcontract directory if reset is set to 1 +if args.reset == 1: + apppath = os.path.dirname(os.path.realpath(__file__)) + dirpath = os.path.join(apppath, 'tokens') + shutil.rmtree(dirpath) + os.mkdir(dirpath) + dirpath = os.path.join(apppath, 'smartContracts') + shutil.rmtree(dirpath) + os.mkdir(dirpath) + dirpath = os.path.join(apppath, 'system.db') + if os.path.exists(dirpath): + os.remove(dirpath) + + # Read start block no + startblock = int(config['DEFAULT']['START_BLOCK']) + engine = create_engine('sqlite:///system.db', echo=True) + SystemBase.metadata.create_all(bind=engine) + session = sessionmaker(bind=engine)() + session.add( SystemData(attribute='lastblockscanned', value=startblock-1)) + session.commit() + session.close() + + +# Read start block no +engine = create_engine('sqlite:///system.db', echo=True) +SystemBase.metadata.create_all(bind=engine) +session = sessionmaker(bind=engine)() +startblock = int(session.query(SystemData).filter_by(attribute='lastblockscanned').all()[0].value) +session.commit() +session.close() + + + +# todo Rule 6 - Find current block height +# Rule 7 - Start analysing the block contents from starting block to current height + +# Find current block height +string = "{} getblockcount".format(localapi) +response = subprocess.check_output(string, shell=True) +current_index = json.loads(response.decode("utf-8")) +print("current_block_height : " + str(current_index)) + + +for blockindex in range( startblock, current_index ): + print(blockindex) + + # Scan every block + string = "{} getblockhash {}".format(localapi, str(blockindex)) + response = subprocess.check_output(string, shell=True) + blockhash = response.decode("utf-8") + + string = "{} getblock {}".format(localapi, str(blockhash)) + response = subprocess.check_output(string, shell=True) + blockinfo = json.loads(response.decode("utf-8")) + + # todo Rule 8 - read every transaction from every block to find and parse flodata + + # Scan every transaction + for transaction in blockinfo["tx"]: + string = "{} getrawtransaction {} 1".format(localapi, str(transaction)) + response = subprocess.check_output(string, shell=True) + transaction_data = json.loads(response.decode("utf-8")) + text = transaction_data["floData"] + + if blockindex == 525362: + print('debug point') + + # todo Rule 9 - Reject all noise transactions. Further rules are in parsing.py + + parsed_data = parsing.parse_flodata(text) + if parsed_data['type'] != 'noise': + print(blockindex) + print(parsed_data['type']) + startWorking(transaction_data, parsed_data) + + engine = create_engine('sqlite:///system.db') + SystemBase.metadata.create_all(bind=engine) + session = sessionmaker(bind=engine)() + entry = session.query(SystemData).filter(SystemData.attribute == 'lastblockscanned').all()[0] + entry.value = str(blockindex) + session.commit() + session.close() +