diff --git a/.gitignore b/.gitignore index 880738d..7950ab2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,9 @@ __pycache__/ config.py .idea/ py3.7/ -py3.7.0/ +py3/ +py3.8/ *.db +*.code-workspace +*.log +py*/ diff --git a/config-example.py b/config-example.py index 10e4408..66b24e0 100644 --- a/config-example.py +++ b/config-example.py @@ -1,6 +1,8 @@ -dbfolder = 'path to FLO Token Scanner data folder' -sse_pubKey = '' -apiUrl = 'flosight api url' -apilayerAccesskey = 'api layer access key' -FLO_DATA_DIR = '/home/username/.flo' -FLO_CLI_PATH = '/usr/local/bin/flo-cli' +dbfolder = '' +debug_status = False +sse_pubKey = '' +apiUrl = 'https://flosight.duckdns.org/api/' + +# your apilayer.net access key +apilayerAccesskey = '' + diff --git a/parsing.py b/parsing.py index de65e03..9395ac3 100644 --- a/parsing.py +++ b/parsing.py @@ -1,170 +1,361 @@ +import pdb import re import arrow -import configparser +import pyflo +import logging +import json -config = configparser.ConfigParser() -config.read('config.ini') +""" +Find make lists of #, *, @ words -marker = None -operation = None -address = None -amount = None +If only 1 hash word and nothing else, then it is token related ( tokencreation or tokentransfer ) -months = { 'jan' : 1, -'feb' : 2, -'mar' : 3, -'apr' : 4, -'may' : 5, -'jun' : 6, -'jul' : 7, -'aug' : 8, -'sep' : 9, -'oct' : 10, -'nov' : 11, -'dec' : 12 } +If @ is present, then we know it is smart contract related + @ (#)pre: - participation , deposit + @ * (#)pre: - one time event creation + @ * (# #)post: - token swap creation + @ - trigger + +Check for 1 @ only +Check for 1 # only +Check for @ (#)pre: +Check for @ * (#)pre: +Check for @ * (# #)post: + +special_character_frequency = { + 'precolon': { + '#':0, + '*':0, + '@':0, + ':':0 +} + +for word in allList: + if word.endswith('#'): + special_character_frequency['#'] = special_character_frequency['#'] + 1 + elif word.endswith('*'): + special_character_frequency['*'] = special_character_frequency['*'] + 1 + elif word.endswith('@'): + special_character_frequency['@'] = special_character_frequency['@'] + 1 + elif word.endswith(':'): + special_character_frequency[':'] = special_character_frequency[':'] + 1 + +""" + +''' +def className(rawstring): + # Create a list that contains @ , # , * and : ; in actual order of occurence with their words. Only : is allowed to exist without a word in front of it. + # Check for 1 @ only followed by :, and the class is trigger + # Check for 1 # only, then the class is tokensystem + # Check for @ in the first position, * in the second position, # in the third position and : in the fourth position, then class is one time event creation + # Check for @ in the first position, * in the second position and : in the third position, then hash is in 4th position, then hash in 5th position | Token swap creation + + allList = findrules(rawstring,['#','*','@',':']) + + pattern_list1 = ['rmt@','rmt*',':',"rmt#","rmt#"] + pattern_list2 = ['rmt#',':',"rmt@"] + pattern_list3 = ['rmt#'] + pattern_list4 = ["rmt@","one-time-event*","floAddress$",':',"rupee#","bioscope#"] + patternmatch = find_first_classification(pattern_list4, search_patterns) + print(f"Patternmatch is {patternmatch}") -def isTransfer(text): - wordlist = ['transfer','send','give'] # keep everything lowercase - textList = text.split(' ') - for word in wordlist: - if word in textList: - return True - return False +rawstring = "test rmt# rmt@ rmt* : rmt# rmt# test" +#className(rawstring) ''' +# Variable configurations +search_patterns = { + 'tokensystem-C':{ + 1:['#'] + }, + 'smart-contract-creation-C':{ + 1:['@','*','#','$',':'], + 2:['@','*','#','$',':','#'] + }, + 'smart-contract-participation-deposit-C':{ + 1:['#','@',':'], + 2:['#','@','$',':'] + }, + 'userchoice-trigger':{ + 1:['@'] + }, + 'smart-contract-participation-ote-ce-C':{ + 1:['#','@'], + 2:['#','@','$'] + }, + 'smart-contract-creation-ce-tokenswap':{ + 1:['@','*','$',':','#','#'] + } +} -def isIncorp(text): - wordlist = ['incorporate','create','start'] # keep everything lowercase - textList = text.split(' ') - for word in wordlist: - if word in textList: - return True - return False +conflict_matrix = { + 'tokensystem-C':{ + # Check for send, check for create, if both are there noise, else conflict resolved + 'tokentransfer', + 'tokencreation' + }, + 'smart-contract-creation-C':{ + # Check contract-conditions for userchoice, if present then userchoice contract, else time based contract + 'creation-one-time-event-userchoice', + 'creation-one-time-event-timebased' + }, + 'smart-contract-participation-deposit-C':{ + # Check *-word, its either one-time-event or a continuos-event + 'participation-one-time-event-userchoice', + 'deposit-continuos-event-tokenswap' + }, + 'smart-contract-participation-ote-ce-C':{ + # Check *-word, its either one-time-event or a continuos-event + 'participation-one-time-event-timebased', + 'participation-continuos-event-tokenswap' + } +} +months = { + 'jan': 1, + 'feb': 2, + 'mar': 3, + 'apr': 4, + 'may': 5, + 'jun': 6, + 'jul': 7, + 'aug': 8, + 'sep': 9, + 'oct': 10, + 'nov': 11, + 'dec': 12 +} -def isSmartContract(text): - textList = text.split(' ') - for word in textList: - if word == '': - continue - if word.endswith('@') and len(word) != 1: - return word - return False +# HELPER FUNCTIONS - -def isSmartContractPay(text): - wordlist = text.split(' ') - if len(wordlist) != 2: +# Find some value or return as noise +def apply_rule1(*argv): + a = argv[0](*argv[1:]) + if a is False: return False - smartContractTrigger = re.findall(r"smartContractTrigger:'.*'", text)[0].split('smartContractTrigger:')[1] - smartContractTrigger = smartContractTrigger[1:-1] - smartContractName = re.findall(r"smartContractName:.*@", text)[0].split('smartContractName:')[1] - smartContractName = smartContractName[:-1] - - if smartContractTrigger and smartContractName: - contractconditions = { 'smartContractTrigger':smartContractTrigger, 'smartContractName':smartContractName } - return contractconditions else: + return a + + +def extract_substing_between(test_str, sub1, sub2): + # getting index of substrings + idx1 = test_str.index(sub1) + idx2 = test_str.index(sub2) + + # length of substring 1 is added to + # get string from next character + res = test_str[idx1 + len(sub1) + 1: idx2] + + # return result + return res + +# StateF functions +def isStateF(text): + try: + statef_string = extract_substing_between(text, 'statef', 'end-statef').strip() + i=iter(statef_string.split(":")) + statef_list = [":".join(x) for x in zip(i,i)] + statef = {} + for keyval in statef_list: + keyval = keyval.split(':') + statef[keyval[0]] = keyval[1] + return statef + except: return False -def extractAmount(text, marker): - count = 0 - returnval = None - splitText = text.split('userchoice')[0].split(' ') - - for word in splitText: - word = word.replace(marker, '') - try: - float(word) - count = count + 1 - returnval = float(word) - except ValueError: - pass - - if count > 1: - return 'Too many' - return returnval +# conflict_list = [['userchoice','payeeaddress'],['userchoice','xxx']] +def resolve_incategory_conflict(input_dictionary , conflict_list): + for conflict_pair in conflict_list: + key0 = conflict_pair[0] + key1 = conflict_pair[1] + dictionary_keys = input_dictionary.keys() + if (key0 in dictionary_keys and key1 in dictionary_keys) or (key0 not in dictionary_keys and key1 not in dictionary_keys): + return False + else: + return True -def extractMarker(text): - textList = text.split(' ') - for word in textList: - if word == '': - continue - if word.endswith('#') and len(word) != 1: - return word - return False - - -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: - value = result * base_units[textList[idx + 1]] - counter = counter + 1 - else: - value = result - counter = counter + 1 - except: - for unit in base_units: - result = word.split(unit) - if len(result) == 2 and result[1]=='' and result[0]!='': - try: - value = float(result[0])*base_units[unit] - counter = counter + 1 - except: - continue - - if counter == 1: - return value +def remove_empty_from_dict(d): + if type(d) is dict: + return dict((k, remove_empty_from_dict(v)) for k, v in d.items() if v and remove_empty_from_dict(v)) + elif type(d) is list: + return [remove_empty_from_dict(v) for v in d if v and remove_empty_from_dict(v)] else: - return None + return d -def extractAddress(text): - textList = text.split(' ') - for word in textList: - if word == '': - continue - if word[-1] == '$' and len(word) != 1: - return word - return None +def outputreturn(*argv): + if argv[0] == 'noise': + parsed_data = {'type': 'noise'} + return parsed_data + elif argv[0] == 'token_incorporation': + parsed_data = { + 'type': 'tokenIncorporation', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #initTokens + 'stateF': argv[4] + } + return parsed_data + elif argv[0] == 'token_transfer': + parsed_data = { + 'type': 'transfer', + 'transferType': 'token', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #amount + 'stateF': argv[4] + } + return parsed_data + elif argv[0] == 'one-time-event-userchoice-smartcontract-incorporation': + parsed_data = { + 'type': 'smartContractIncorporation', + 'contractType': 'one-time-event', + 'tokenIdentification': argv[1], #hashList[0][:-1] + 'contractName': argv[2], #atList[0][:-1] + 'contractAddress': argv[3], #contractaddress[:-1] + 'flodata': argv[4], #string + 'contractConditions': { + 'contractAmount' : argv[5], + 'minimumsubscriptionamount' : argv[6], + 'maximumsubscriptionamount' : argv[7], + 'userchoices' : argv[8], + 'expiryTime' : argv[9] + }, + 'stateF': argv[10] + } + return remove_empty_from_dict(parsed_data) + elif argv[0] == 'one-time-event-userchoice-smartcontract-participation': + parsed_data = { + 'type': 'transfer', + 'transferType': 'smartContract', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'operation': 'transfer', + 'tokenAmount': argv[3], #amount + 'contractName': argv[4], #atList[0][:-1] + 'contractAddress': argv[5], + 'userChoice': argv[6], #userChoice + 'stateF': argv[7] + } + return remove_empty_from_dict(parsed_data) + elif argv[0] == 'one-time-event-userchoice-smartcontract-trigger': + parsed_data = { + 'type': 'smartContractPays', + 'contractName': argv[1], #atList[0][:-1] + 'triggerCondition': argv[2], #triggerCondition.group().strip()[1:-1] + 'stateF': argv[3] + } + return parsed_data + elif argv[0] == 'one-time-event-time-smartcontract-incorporation': + parsed_data = { + 'type': 'smartContractIncorporation', + 'contractType': 'one-time-event', + 'tokenIdentification': argv[1], #hashList[0][:-1] + 'contractName': argv[2], #atList[0][:-1] + 'contractAddress': argv[3], #contractaddress[:-1] + 'flodata': argv[4], #string + 'contractConditions': { + 'contractAmount' : argv[5], + 'minimumsubscriptionamount' : argv[6], + 'maximumsubscriptionamount' : argv[7], + 'payeeAddress' : argv[8], + 'expiryTime' : argv[9] + }, + 'stateF': argv[10] + } + return remove_empty_from_dict(parsed_data) + elif argv[0] == 'continuos-event-token-swap-incorporation': + parsed_data = { + 'type': 'smartContractIncorporation', + 'contractType': 'continuos-event', + 'tokenIdentification': argv[1], #hashList[0][:-1] + 'contractName': argv[2], #atList[0][:-1] + 'contractAddress': argv[3], #contractaddress[:-1] + 'flodata': argv[4], #string + 'contractConditions': { + 'subtype' : argv[5], #tokenswap + 'accepting_token' : argv[6], + 'selling_token' : argv[7], + 'pricetype' : argv[8], + 'price' : argv[9], + }, + 'stateF': argv[10] + } + return parsed_data + elif argv[0] == 'continuos-event-token-swap-deposit': + parsed_data = { + 'type': 'smartContractDeposit', + 'tokenIdentification': argv[1], #hashList[0][:-1] + 'depositAmount': argv[2], #depositAmount + 'contractName': argv[3], #atList[0][:-1] + 'flodata': argv[4], #string + 'depositConditions': { + 'expiryTime' : argv[5] + }, + 'stateF': argv[6] + } + return parsed_data + elif argv[0] == 'smart-contract-one-time-event-continuos-event-participation': + parsed_data = { + 'type': 'transfer', + 'transferType': 'smartContract', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #amount + 'contractName': argv[4], #atList[0][:-1] + 'contractAddress': argv[5], + 'stateF': argv[6] + } + return remove_empty_from_dict(parsed_data) + elif argv[0] == 'nft_create': + parsed_data = { + 'type': 'nftIncorporation', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #initTokens, + 'nftHash': argv[4], #nftHash + 'stateF': argv[5] + } + return parsed_data + elif argv[0] == 'nft_transfer': + parsed_data = { + 'type': 'transfer', + 'transferType': 'nft', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #initTokens, + 'stateF': argv[4] + } + return parsed_data + elif argv[0] == 'infinite_token_create': + parsed_data = { + 'type': 'infiniteTokenIncorporation', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'stateF': argv[3] + } + return parsed_data -def extractContractType(text): - operationList = ['one-time-event*'] # keep everything lowercase - count = 0 - returnval = None - for operation in operationList: - count = count + text.count(operation) - if count > 1: - return 'Too many' - if count == 1 and (returnval is None): - returnval = operation - return returnval +def extract_specialcharacter_words(rawstring, special_characters): + wordList = [] + for word in rawstring.split(' '): + if (len(word) not in [0,1] or word==":") and word[-1] in special_characters: + wordList.append(word) + return wordList -def extractUserchoice(text): - result = re.split('userchoice:\s*', text) - if len(result) != 1 and result[1]!='': - return result[1].strip().strip('"').strip("'") - else: - return None - - -def brackets_toNumber(item): - return float(item[1:-1]) - - -def extractContractConditions(text, contracttype, marker, blocktime): - rulestext = re.split('contract-conditions:\s*', text)[-1] - #rulelist = re.split('\d\.\s*', rulestext) +def extract_contract_conditions(text, contract_type, marker=None, blocktime=None): + try: + rulestext = extract_substing_between(text, 'contract-conditions', 'end-contract-conditions') + except: + return False + if rulestext.strip()[0] == ':': + rulestext = rulestext.strip()[1:].strip() + #rulestext = re.split('contract-conditions:\s*', text)[-1] + # rulelist = re.split('\d\.\s*', rulestext) rulelist = [] numberList = re.findall(r'\(\d\d*\)', rulestext) @@ -174,214 +365,876 @@ def extractContractConditions(text, contracttype, marker, blocktime): numberList = sorted(numberList) for idx, item in enumerate(numberList): if numberList[idx] + 1 != numberList[idx + 1]: - print('Contract condition numbers are not in order') - return None + logger.info('Contract condition numbers are not in order') + return False if idx == len(numberList) - 2: break for i in range(len(numberList)): - rule = rulestext.split('({})'.format(i+1))[1].split('({})'.format(i+2))[0] + rule = rulestext.split('({})'.format(i + 1))[1].split('({})'.format(i + 2))[0] rulelist.append(rule.strip()) - if contracttype == 'one-time-event*': + if contract_type == 'one-time-event': extractedRules = {} - for rule in rulelist: if rule == '': continue elif rule[:10] == 'expirytime': expirytime = re.split('expirytime[\s]*=[\s]*', rule)[1].strip() - try: expirytime_split = expirytime.split(' ') parse_string = '{}/{}/{} {}'.format(expirytime_split[3], months[expirytime_split[1]], expirytime_split[2], expirytime_split[4]) expirytime_object = arrow.get(parse_string, 'YYYY/M/D HH:mm:ss').replace(tzinfo=expirytime_split[5]) blocktime_object = arrow.get(blocktime) if expirytime_object < blocktime_object: - print('Expirytime of the contract is earlier than the block it is incorporated in. This incorporation will be rejected ') - return None + logger.info('Expirytime of the contract is earlier than the block it is incorporated in. This incorporation will be rejected ') + return False extractedRules['expiryTime'] = expirytime except: - print('Error parsing expiry time') - return None + logger.info('Error parsing expiry time') + return False for rule in rulelist: - if rule=='': + if rule == '': continue elif rule[:14] == 'contractamount': - pattern = re.compile('[^contractamount\s*=\s*].*') - searchResult = pattern.search(rule).group(0) + pattern = re.compile('(^contractamount\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) contractamount = searchResult.split(marker)[0] try: extractedRules['contractAmount'] = float(contractamount) except: - print("Contract amount entered is not a decimal") + logger.info("Contract amount entered is not a decimal") elif rule[:11] == 'userchoices': - pattern = re.compile('[^userchoices\s*=\s*].*') - conditions = pattern.search(rule).group(0) + pattern = re.compile('(^userchoices\s*=\s*)(.*)') + conditions = pattern.search(rule).group(2) conditionlist = conditions.split('|') extractedRules['userchoices'] = {} for idx, condition in enumerate(conditionlist): extractedRules['userchoices'][idx] = condition.strip() elif rule[:25] == 'minimumsubscriptionamount': - pattern = re.compile('[^minimumsubscriptionamount\s*=\s*].*') - searchResult = pattern.search(rule).group(0) + pattern = re.compile('(^minimumsubscriptionamount\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) minimumsubscriptionamount = searchResult.split(marker)[0] try: extractedRules['minimumsubscriptionamount'] = float(minimumsubscriptionamount) except: - print("Minimum subscription amount entered is not a decimal") + logger.info("Minimum subscription amount entered is not a decimal") elif rule[:25] == 'maximumsubscriptionamount': - pattern = re.compile('[^maximumsubscriptionamount\s*=\s*].*') - searchResult = pattern.search(rule).group(0) + pattern = re.compile('(^maximumsubscriptionamount\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) maximumsubscriptionamount = searchResult.split(marker)[0] try: extractedRules['maximumsubscriptionamount'] = float(maximumsubscriptionamount) except: - print("Maximum subscription amount entered is not a decimal") + logger.info("Maximum subscription amount entered is not a decimal") elif rule[:12] == 'payeeaddress': - pattern = re.compile('[^payeeAddress\s*=\s*].*') - searchResult = pattern.search(rule).group(0) + pattern = re.compile('(^payeeaddress\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) payeeAddress = searchResult.split(marker)[0] extractedRules['payeeAddress'] = payeeAddress - - if len(extractedRules)>1 and 'expiryTime' in extractedRules: + if len(extractedRules) > 1 and 'expiryTime' in extractedRules: return extractedRules else: - return None - return None + return False + + elif contract_type == 'continuous-event': + extractedRules = {} + for rule in rulelist: + if rule == '': + continue + elif rule[:7] == 'subtype': + # todo : recheck the regular expression for subtype, find an elegant version which covers all permutations and combinations + pattern = re.compile('(^subtype\s*=\s*)(.*)') + subtype = pattern.search(rule).group(2) + extractedRules['subtype'] = subtype + elif rule[:15] == 'accepting_token': + pattern = re.compile('(?<=accepting_token\s=\s)(.*)(? 1: + return extractedRules + else: + return False + return False -def extractTriggerCondition(text): +def extract_tokenswap_contract_conditions(processed_text, contract_type, contract_token): + rulestext = re.split('contract-conditions:\s*', processed_text)[-1] + rulelist = [] + numberList = re.findall(r'\(\d\d*\)', rulestext) + + for idx, item in enumerate(numberList): + numberList[idx] = int(item[1:-1]) + + numberList = sorted(numberList) + for idx, item in enumerate(numberList): + if numberList[idx] + 1 != numberList[idx + 1]: + logger.info('Contract condition numbers are not in order') + return False + if idx == len(numberList) - 2: + break + + for i in range(len(numberList)): + rule = rulestext.split('({})'.format(i + 1))[1].split('({})'.format(i + 2))[0] + rulelist.append(rule.strip()) + + if contract_type == 'continuous-event': + extractedRules = {} + for rule in rulelist: + if rule == '': + continue + elif rule[:7] == 'subtype': + # todo : recheck the regular expression for subtype, find an elegant version which covers all permutations and combinations + pattern = re.compile('(^subtype\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) + subtype = searchResult.split(marker)[0] + #extractedRules['subtype'] = rule.split('=')[1].strip() + extractedRules['subtype'] = subtype + elif rule[:15] == 'accepting_token': + pattern = re.compile('(?<=accepting_token\s=\s).*(? 1: + return extractedRules + else: + return False + + return False + + +def extract_deposit_conditions(text, blocktime=None): + rulestext = re.split('deposit-conditions:\s*', text)[-1] + # rulelist = re.split('\d\.\s*', rulestext) + rulelist = [] + numberList = re.findall(r'\(\d\d*\)', rulestext) + for idx, item in enumerate(numberList): + numberList[idx] = int(item[1:-1]) + + numberList = sorted(numberList) + for idx, item in enumerate(numberList): + if len(numberList) > 1 and numberList[idx] + 1 != numberList[idx + 1]: + logger.info('Deposit condition numbers are not in order') + return False + if idx == len(numberList) - 2: + break + + for i in range(len(numberList)): + rule = rulestext.split('({})'.format(i + 1))[1].split('({})'.format(i + 2))[0] + rulelist.append(rule.strip()) + + # elif contracttype == 'continuous-event*': + extractedRules = {} + for rule in rulelist: + if rule == '': + continue + elif rule[:10] == 'expirytime': + expirytime = re.split('expirytime[\s]*=[\s]*', rule)[1].strip() + try: + expirytime_split = expirytime.split(' ') + parse_string = '{}/{}/{} {}'.format(expirytime_split[3], months[expirytime_split[1]], expirytime_split[2], expirytime_split[4]) + expirytime_object = arrow.get(parse_string, 'YYYY/M/D HH:mm:ss').replace(tzinfo=expirytime_split[5]) + blocktime_object = arrow.get(blocktime) + if expirytime_object < blocktime_object: + logger.info('Expirytime of the contract is earlier than the block it is incorporated in. This incorporation will be rejected ') + return False + extractedRules['expiryTime'] = expirytime + except: + logger.info('Error parsing expiry time') + return False + + """for rule in rulelist: + if rule == '': + continue + elif rule[:7] == 'subtype': + subtype=rule[8:] + #pattern = re.compile('[^subtype\s*=\s*].*') + #searchResult = pattern.search(rule).group(0) + #contractamount = searchResult.split(marker)[0] + extractedRules['subtype'] = subtype """ + + if len(extractedRules) > 0: + return extractedRules + else: + return False + + +def extract_special_character_word(special_character_list, special_character): + for word in special_character_list: + if word.endswith(special_character): + return word[:-1] + return False + + +def extract_NFT_hash(clean_text): + nft_hash = re.search(r"(?:0[xX])?[0-9a-fA-F]{64}",clean_text) + if nft_hash is None: + return False + else: + return nft_hash.group(0) + + +def find_original_case(contract_address, original_text): + dollar_word = extract_specialcharacter_words(original_text,["$"]) + if len(dollar_word)==1 and dollar_word[0][:-1].lower()==contract_address: + return dollar_word[0][:-1] + else: + None + + +def find_word_index_fromstring(originaltext, word): + lowercase_text = originaltext.lower() + result = lowercase_text.find(word) + return originaltext[result:result+len(word)] + + +def find_first_classification(parsed_word_list, search_patterns): + for first_classification in search_patterns.keys(): + counter = 0 + for key in search_patterns[first_classification].keys(): + if checkSearchPattern(parsed_word_list, search_patterns[first_classification][key]): + return {'categorization':f"{first_classification}",'key':f"{key}",'pattern':search_patterns[first_classification][key], 'wordlist':parsed_word_list} + return {'categorization':"noise"} + + +def sort_specialcharacter_wordlist(inputlist): + weight_values = { + '@': 5, + '*': 4, + '#': 3, + '$': 2 + } + + weightlist = [] + for word in inputlist: + if word.endswith("@"): + weightlist.append(5) + elif word.endswith("*"): + weightlist.append(4) + elif word.endswith("#"): + weightlist.append(4) + elif word.endswith("$"): + weightlist.append(4) + + +def firstclassification_rawstring(rawstring): + specialcharacter_wordlist = extract_specialcharacter_words(rawstring,['@','*','$','#',':']) + first_classification = find_first_classification(specialcharacter_wordlist, search_patterns) + return first_classification + + +def checkSearchPattern(parsed_list, searchpattern): + if len(parsed_list)!=len(searchpattern): + return False + else: + for idx,val in enumerate(parsed_list): + if not parsed_list[idx].endswith(searchpattern[idx]): + return False + return True + + +def extractAmount_rule_new(text): + base_units = {'thousand': 10 ** 3, 'k': 10 ** 3, 'million': 10 ** 6, 'm': 10 ** 6, 'billion': 10 ** 9, 'b': 10 ** 9, 'trillion': 10 ** 12, 'lakh':10 ** 5, 'crore':10 ** 7, 'quadrillion':10 ** 15} + amount_tuple = re.findall(r'\b(-?[.\d]+)\s*(thousand|million|billion|trillion|m|b|t|k|lakh|crore|quadrillion)*\b', text) + if len(amount_tuple) > 1 or len(amount_tuple) == 0: + return False + else: + amount_tuple_list = list(amount_tuple[0]) + extracted_amount = float(amount_tuple_list[0]) + extracted_base_unit = amount_tuple_list[1] + if extracted_base_unit in base_units.keys(): + extracted_amount = float(extracted_amount) * base_units[extracted_base_unit] + return extracted_amount + +def extractAmount_rule_new1(text, split_word=None, split_direction=None): + base_units = {'thousand': 10 ** 3, 'k': 10 ** 3, 'million': 10 ** 6, 'm': 10 ** 6, 'billion': 10 ** 9, 'b': 10 ** 9, 'trillion': 10 ** 12, 'lakh':10 ** 5, 'crore':10 ** 7, 'quadrillion':10 ** 15} + if split_word and split_direction: + if split_direction=='pre': + text = text.split(split_word)[0] + if split_direction=='post': + text = text.split(split_word)[1] + + # appending dummy because the regex does not recognize a number at the start of a string + # text = f"dummy {text}" + text = text.replace("'", "") + text = text.replace('"', '') + amount_tuple = re.findall(r'\b(-?[.\d]+)\s*(thousand|million|billion|trillion|m|b|t|k|lakh|crore|quadrillion)*\b', text) + if len(amount_tuple) > 1 or len(amount_tuple) == 0: + return False + else: + amount_tuple_list = list(amount_tuple[0]) + extracted_amount = float(amount_tuple_list[0]) + extracted_base_unit = amount_tuple_list[1] + if extracted_base_unit in base_units.keys(): + extracted_amount = float(extracted_amount) * base_units[extracted_base_unit] + return extracted_amount + + +def extract_userchoice(text): + result = re.split('userchoice:\s*', text) + if len(result) != 1 and result[1] != '': + return result[1].strip().strip('"').strip("'") + else: + return False + + +def findWholeWord(w): + return re.compile(r'\b({0})\b'.format(w), flags=re.IGNORECASE).search + + +def check_flo_address(floaddress, is_testnet): + if pyflo.is_address_valid(floaddress, testnet=is_testnet): + return floaddress + else: + return False + + +def extract_trigger_condition(text): searchResult = re.search('\".*\"', text) if searchResult is None: searchResult = re.search('\'.*\'', text) - return searchResult - return searchResult + + if searchResult is not None: + return searchResult.group().strip()[1:-1] + else: + return False -# Combine test -def parse_flodata(string, blockinfo, netvariable): - - # 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('@') and len(word) != 1: - atList.append(word) - 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) - - # 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]) - if None not in [amount]: - parsed_data = {'type': 'transfer', 'transferType': 'token', 'flodata': string, - 'tokenIdentification': hashList[0][:-1], - 'tokenAmount': amount} - 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) - - # 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, contracttype, marker=hashList[0][:-1], blocktime=blockinfo['time']) - - if config['DEFAULT']['NET'] == 'mainnet' and blockinfo['height'] < 3454510: - if None not in [contracttype, contractconditions]: - parsed_data = {'type': 'smartContractIncorporation', 'contractType': contracttype[:-1], - 'tokenIdentification': hashList[0][:-1], 'contractName': atList[0][:-1], - 'contractAddress': contractaddress[:-1], 'flodata': string, - 'contractConditions': contractconditions} - else: - parsed_data = {'type': 'noise'} - else: - if None not in [contracttype, contractaddress, contractconditions]: - parsed_data = {'type': 'smartContractIncorporation', 'contractType': contracttype[:-1], - 'tokenIdentification': hashList[0][:-1], 'contractName': atList[0][:-1], - 'contractAddress': contractaddress[:-1], '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]) - userChoice = extractUserchoice(cleanstring) - contractaddress = extractAddress(nospacestring) - if None not in [amount, userChoice]: - parsed_data = {'type': 'transfer', 'transferType': 'smartContract', 'flodata': string, - 'tokenIdentification': hashList[0][:-1], - 'operation': 'transfer', 'tokenAmount': amount, 'contractName': atList[0][:-1], - 'userChoice': userChoice} - if contractaddress: - parsed_data['contractAddress'] = contractaddress[:-1] - else: - parsed_data = {'type': 'noise'} +# Regex pattern for Smart Contract and Token name ^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$ +def check_regex(pattern, test_string): + matched = re.match(pattern, test_string) + is_match = bool(matched) + return is_match - # 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): - # 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'} +def check_existence_of_keyword(inputlist, keywordlist): + for word in keywordlist: + if not word in inputlist: + return False + return True + + +def check_word_existence_instring(word, text): + word_exists = re.search(fr"\b{word}\b",text) + if word_exists is None: + return False else: - parsed_data = {'type': 'noise'} + return word_exists.group(0) - return parsed_data +send_category = ['transfer', 'send', 'give'] # keep everything lowercase +create_category = ['incorporate', 'create', 'start'] # keep everything lowercase +deposit_category = ['submit','deposit'] + + +def truefalse_rule2(rawstring, permitted_list, denied_list): + # Find transfer , send , give + foundPermitted = None + foundDenied = None + + for word in permitted_list: + if findWholeWord(word)(rawstring): + foundPermitted = word + break + + for word in denied_list: + if findWholeWord(word)(rawstring): + foundDenied = word + break + + if (foundPermitted is not None) and (foundDenied is None): + return True + else: + return False + + +def selectCategory(rawstring, category1, category2): + foundCategory1 = None + foundCategory2 = None + + for word in category1: + if findWholeWord(word)(rawstring): + foundCategory1 = word + break + + for word in category2: + if findWholeWord(word)(rawstring): + foundCategory2 = word + break + + if ((foundCategory1 is not None) and (foundCategory2 is not None)) or ((foundCategory1 is None) and (foundCategory2 is None)): + return False + elif foundCategory1 is not None: + return 'category1' + elif foundCategory2 is not None: + return 'category2' + + +def select_category_reject(rawstring, category1, category2, reject_list): + foundCategory1 = None + foundCategory2 = None + rejectCategory = None + + for word in category1: + if findWholeWord(word)(rawstring): + foundCategory1 = word + break + + for word in category2: + if findWholeWord(word)(rawstring): + foundCategory2 = word + break + + for word in reject_list: + if findWholeWord(word)(rawstring): + rejectCategory = word + break + + if ((foundCategory1 is not None) and (foundCategory2 is not None)) or ((foundCategory1 is None) and (foundCategory2 is None)) or (rejectCategory is not None): + return False + elif foundCategory1 is not None: + return 'category1' + elif foundCategory2 is not None: + return 'category2' + + +def text_preprocessing(original_text): + # strip white spaces at the beginning and end + processed_text = original_text.strip() + # remove tab spaces + processed_text = re.sub('\t', ' ', processed_text) + # remove new lines/line changes + processed_text = re.sub('\n', ' ', processed_text) + # add a white space after every special character found + processed_text = re.sub("contract-conditions:", "contract-conditions: ", processed_text) + processed_text = re.sub("deposit-conditions:", "deposit-conditions: ", processed_text) + processed_text = re.sub("userchoice:", "userchoice: ", processed_text) + # remove extra whitespaces in between + processed_text = ' '.join(processed_text.split()) + processed_text = re.sub(' +', ' ', processed_text) + clean_text = processed_text + # make everything lowercase + processed_text = processed_text.lower() + + return clean_text,processed_text + + +# TODO - REMOVE SAMPLE TEXT +text_list = [ + "create 500 million rmt#", + + "transfer 200 rmt#", + + "Create Smart Contract with the name India-elections-2019@ of the type one-time-event* using the asset rmt# at the address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1$ with contract-conditions: (1) contractAmount=0.001rmt (2) userChoices=Narendra Modi wins| Narendra Modi loses (3) expiryTime= Wed May 22 2019 21:00:00 GMT+0530", + + "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 with the userchoice:'narendra modi wins'", + + "india-elections-2019@ winning-choice:'narendra modi wins'", + + "Create Smart Contract with the name India-elections-2019@ of the type one-time-event* using the asset rmt# at the address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1$ with contract-conditions: (1) contractAmount=0.001rmt (2) expiryTime= Wed May 22 2019 21:00:00 GMT+0530", + + "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1", + + "Create Smart Contract with the name swap-rupee-bioscope@ of the type continuous-event* at the address oRRCHWouTpMSPuL6yZRwFCuh87ZhuHoL78$ with contract-conditions : (1) subtype = tokenswap (2) accepting_token = rupee# (3) selling_token = bioscope# (4) price = '15' (5) priceType = ‘predetermined’ (6) direction = oneway", + + "Deposit 15 bioscope# to swap-rupee-bioscope@ its FLO address being oRRCHWouTpMSPuL6yZRwFCuh87ZhuHoL78$ with deposit-conditions: (1) expiryTime= Wed Nov 17 2021 21:00:00 GMT+0530 ", + + "Send 15 rupee# to swap-rupee-article@ its FLO address being FJXw6QGVVaZVvqpyF422Aj4FWQ6jm8p2dL$", + + "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 with the userchoice:'narendra modi wins'" +] + +text_list1 = [ + + 'create usd# as infinite-token', + 'transfer 10 usd#', + + 'Create 100 albumname# as NFT with 2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824 as asset hash', + 'Transfer 10 albumname# nft', + + 'Create 400 rmt#', + 'Transfer 20 rmt#' +] + +text_list2 = [ + '''Create Smart Contract with the name swap-rupee-bioscope@ of the type continuous-event* + at the address stateF=bitcoin_price_source:bitpay:usd_inr_exchange_source:bitpay end-stateF oYzeeUBWRpzRuczW6myh2LHGnXPyR2Bc6k$ with contract-conditions : + (1) subtype = tokenswap + (2) accepting_token = rupee# + (3) selling_token = sreeram# + (4) price = "15" + (5) priceType="predetermined" end-contract-conditions''', + + ''' + Create a smart contract of the name simple-crowd-fund@ of the type one-time-event* using asset bioscope# at the FLO address oQkpZCBcAWc945viKqFmJVbVG4aKY4V3Gz$ with contract-conditions:(1) expiryTime= Tue Sep 13 2022 16:10:00 GMT+0530 (2) payeeAddress=oQotdnMBAP1wZ6Kiofx54S2jNjKGiFLYD7 end-contract-conditions + ''', + + ''' + Create a smart contract of the name simple-crowd-fund@ of the type one-time-event* using asset bioscope# at the FLO address oQkpZCBcAWc945viKqFmJVbVG4aKY4V3Gz$ with contract-conditions:(1) expiryTime= Tue Sep 13 2022 16:10:00 GMT+0530 (2) payeeAddress=oU412TvcMe2ah2xzqFpA95vBJ1RoPZY1LR:10:oVq6QTUeNLh8sapQ6J6EjMQMKHxFCt3uAq:20:oLE79kdHPEZ2bxa3PwtysbJeLo9hvPgizU:60:ocdCT9RAzWVsUncMu24r3HXKXFCXD7gTqh:10 end-contract-conditions + ''', + ''' + Create a smart contract of the name simple-crowd-fund@ of the type one-time-event* using asset bioscope# at the FLO address oQkpZCBcAWc945viKqFmJVbVG4aKY4V3Gz$ with contract-conditions:(1) expiryTime= Tue Sep 13 2022 16:10:00 GMT+0530 (2) payeeAddress=oU412TvcMe2ah2xzqFpA95vBJ1RoPZY1LR end-contract-conditions + ''', + ''' + Create a smart contract of the name all-crowd-fund-7@ of the type one-time-event* using asset bioscope# at the FLO address oYX4GvBYtfTBNyUFRCdtYubu7ZS4gchvrb$ with contract-conditions:(1) expiryTime= Sun Nov 15 2022 12:30:00 GMT+0530 (2) payeeAddress=oQotdnMBAP1wZ6Kiofx54S2jNjKGiFLYD7:10:oMunmikKvxsMSTYzShm2X5tGrYDt9EYPij:20:oRpvvGEVKwWiMnzZ528fPhiA2cZA3HgXY5:30:oWpVCjPDGzaiVfEFHs6QVM56V1uY1HyCJJ:40 (3) minimumsubscriptionamount=1 (5) contractAmount=0.6 end-contract-conditions + ''', + ''' + Create a smart contract of the name all-crowd-fund-7@ of the type one-time-event* using asset bioscope# at the FLO address oYX4GvBYtfTBNyUFRCdtYubu7ZS4gchvrb$ with contract-conditions:(1) expiryTime= Sun Nov 15 2022 12:30:00 GMT+0530 (2) payeeAddress=oQotdnMBAP1wZ6Kiofx54S2jNjKGiFLYD7:0:oMunmikKvxsMSTYzShm2X5tGrYDt9EYPij:30:oRpvvGEVKwWiMnzZ528fPhiA2cZA3HgXY5:30:oWpVCjPDGzaiVfEFHs6QVM56V1uY1HyCJJ:40 (3) minimumsubscriptionamount=1 (4) contractAmount=0.6 end-contract-conditions + ''', + '''send 0.02 bioscope# to twitter-survive@ to FLO address oVbebBNuERWbouDg65zLfdataWEMTnsL8r with the userchoice: survives''', + ''' + Create a smart contract of the name twitter-survive@ of the type one-time-event* using asset bioscope# at the FLO address oVbebBNuERWbouDg65zLfdataWEMTnsL8r$ with contract-conditions:(1) expiryTime= Sun Nov 15 2022 14:55:00 GMT+0530 (2) userchoices= survives | dies (3) minimumsubscriptionamount=0.04 (4) maximumsubscriptionamount=1 (5) contractAmount=0.02 end-contract-conditions + ''', + ''' + create 0 teega# + ''' +] + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +formatter = logging.Formatter('%(asctime)s:%(name)s:%(message)s') + +file_handler = logging.FileHandler('tracking.log') +file_handler.setLevel(logging.INFO) +file_handler.setFormatter(formatter) + +stream_handler = logging.StreamHandler() +stream_handler.setFormatter(formatter) + +logger.addHandler(file_handler) +logger.addHandler(stream_handler) + +def parse_flodata(text, blockinfo, net): + if net == 'testnet': + is_testnet = True + else: + is_testnet = False + + if text == '': + return outputreturn('noise') + + clean_text, processed_text = text_preprocessing(text) + # System state + print("Processing stateF") + stateF_mapping = isStateF(processed_text) + first_classification = firstclassification_rawstring(processed_text) + parsed_data = None + + if first_classification['categorization'] == 'tokensystem-C': + # Resolving conflict for 'tokensystem-C' + tokenname = first_classification['wordlist'][0][:-1] + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", tokenname): + return outputreturn('noise') + + isNFT = check_word_existence_instring('nft', processed_text) + + isInfinite = check_word_existence_instring('infinite-token', processed_text) + tokenamount = apply_rule1(extractAmount_rule_new, processed_text) + + ## Cannot be NFT and normal token and infinite token. Find what are the conflicts + # if its an NFT then tokenamount has to be integer and infinite keyword should not be present + # if its a normal token then isNFT and isInfinite should be None/False and token amount has to be present + # if its an infinite token then tokenamount should be None and isNFT should be None/False + # The supply of tokenAmount cannot be 0 + + ################################################## + + if (not tokenamount and not isInfinite) or (isNFT and not tokenamount.is_integer() and not isInfinite) or (isInfinite and tokenamount is not False and isNFT is not False) or tokenamount<=0: + return outputreturn('noise') + operation = apply_rule1(selectCategory, processed_text, send_category, create_category) + if operation == 'category1' and tokenamount is not None: + if isNFT: + return outputreturn('nft_transfer',f"{processed_text}", f"{tokenname}", tokenamount, stateF_mapping) + else: + return outputreturn('token_transfer',f"{processed_text}", f"{tokenname}", tokenamount, stateF_mapping) + elif operation == 'category2': + if isInfinite: + return outputreturn('infinite_token_create',f"{processed_text}", f"{tokenname}", stateF_mapping) + else: + if tokenamount is None: + return outputreturn('noise') + if isNFT: + nft_hash = extract_NFT_hash(clean_text) + if nft_hash is False: + return outputreturn('noise') + return outputreturn('nft_create',f"{processed_text}", f"{tokenname}", tokenamount, f"{nft_hash}", stateF_mapping) + else: + return outputreturn('token_incorporation',f"{processed_text}", f"{first_classification['wordlist'][0][:-1]}", tokenamount, stateF_mapping) + else: + return outputreturn('noise') + + if first_classification['categorization'] == 'smart-contract-creation-C': + # Resolving conflict for 'smart-contract-creation-C' + operation = apply_rule1(selectCategory, processed_text, create_category, send_category+deposit_category) + if not operation: + return outputreturn('noise') + + contract_type = extract_special_character_word(first_classification['wordlist'],'*') + if not check_existence_of_keyword(['one-time-event'],[contract_type]): + return outputreturn('noise') + + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + contract_token = extract_special_character_word(first_classification['wordlist'],'#') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_token): + return outputreturn('noise') + + contract_address = extract_special_character_word(first_classification['wordlist'],'$') + contract_address = find_original_case(contract_address, clean_text) + if not check_flo_address(contract_address, is_testnet): + return outputreturn('noise') + + contract_conditions = extract_contract_conditions(processed_text, contract_type, contract_token, blocktime=blockinfo['time']) + if contract_conditions == False or not resolve_incategory_conflict(contract_conditions,[['userchoices','payeeAddress']]): + return outputreturn('noise') + else: + contractAmount = '' + if 'contractAmount' in contract_conditions.keys(): + contractAmount = contract_conditions['contractAmount'] + try: + if float(contractAmount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + minimum_subscription_amount = '' + if 'minimumsubscriptionamount' in contract_conditions.keys(): + minimum_subscription_amount = contract_conditions['minimumsubscriptionamount'] + try: + if float(minimum_subscription_amount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + maximum_subscription_amount = '' + if 'maximumsubscriptionamount' in contract_conditions.keys(): + maximum_subscription_amount = contract_conditions['maximumsubscriptionamount'] + try: + if float(maximum_subscription_amount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + + if 'userchoices' in contract_conditions.keys(): + return outputreturn('one-time-event-userchoice-smartcontract-incorporation',f"{contract_token}", f"{contract_name}", f"{contract_address}", f"{clean_text}", f"{contractAmount}", f"{minimum_subscription_amount}" , f"{maximum_subscription_amount}", f"{contract_conditions['userchoices']}", f"{contract_conditions['expiryTime']}", stateF_mapping) + elif 'payeeAddress' in contract_conditions.keys(): + contract_conditions['payeeAddress'] = find_word_index_fromstring(clean_text,contract_conditions['payeeAddress']) + # check if colon exists in the payeeAddress string + if ':' in contract_conditions['payeeAddress']: + colon_split = contract_conditions['payeeAddress'].split(':') + if len(colon_split)%2 != 0: + return outputreturn('noise') + split_total = 0 + payeeAddress_split_dictionary = {} + for idx, item in enumerate(colon_split): + if idx%2 == 0: + # check if floid + if not check_flo_address(item, is_testnet): + return outputreturn('noise') + if idx%2 == 1: + # check if number + try: + item = float(item) + if item <= 0: + return outputreturn('noise') + payeeAddress_split_dictionary[colon_split[idx-1]] = item + split_total += item + except: + return outputreturn('noise') + if split_total != 100: + return outputreturn('noise') + else: + contract_conditions['payeeAddress'] = payeeAddress_split_dictionary + return outputreturn('one-time-event-time-smartcontract-incorporation',f"{contract_token}", f"{contract_name}", f"{contract_address}", f"{clean_text}", f"{contractAmount}", f"{minimum_subscription_amount}" , f"{maximum_subscription_amount}", contract_conditions['payeeAddress'], f"{contract_conditions['expiryTime']}", stateF_mapping) + else: + if not check_flo_address(contract_conditions['payeeAddress'], is_testnet): + return outputreturn('noise') + else: + contract_conditions['payeeAddress'] = {f"{contract_conditions['payeeAddress']}":100} + return outputreturn('one-time-event-time-smartcontract-incorporation',f"{contract_token}", f"{contract_name}", f"{contract_address}", f"{clean_text}", f"{contractAmount}", f"{minimum_subscription_amount}" , f"{maximum_subscription_amount}", contract_conditions['payeeAddress'], f"{contract_conditions['expiryTime']}", stateF_mapping) + + if first_classification['categorization'] == 'smart-contract-participation-deposit-C': + # either participation of one-time-event contract or + operation = apply_rule1(select_category_reject, processed_text, send_category, deposit_category, create_category) + if not operation: + return outputreturn('noise') + else: + tokenname = first_classification['wordlist'][0][:-1] + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", tokenname): + return outputreturn('noise') + + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + contract_address = extract_special_character_word(first_classification['wordlist'],'$') + if contract_address is False: + contract_address = '' + else: + contract_address = find_original_case(contract_address, clean_text) + if not check_flo_address(contract_address, is_testnet): + return outputreturn('noise') + + if operation == 'category1': + tokenamount = apply_rule1(extractAmount_rule_new1, processed_text, 'userchoice:', 'pre') + if not tokenamount: + return outputreturn('noise') + try: + if float(tokenamount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + userchoice = extract_userchoice(processed_text) + # todo - do we need more validations for user choice? + if not userchoice: + return outputreturn('noise') + + return outputreturn('one-time-event-userchoice-smartcontract-participation',f"{clean_text}", f"{tokenname}", tokenamount, f"{contract_name}", f"{contract_address}", f"{userchoice}", stateF_mapping) + + elif operation == 'category2': + tokenamount = apply_rule1(extractAmount_rule_new1, processed_text, 'deposit-conditions:', 'pre') + if not tokenamount: + return outputreturn('noise') + try: + if float(tokenamount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + deposit_conditions = extract_deposit_conditions(processed_text, blocktime=blockinfo['time']) + if not deposit_conditions: + return outputreturn("noise") + return outputreturn('continuos-event-token-swap-deposit', f"{tokenname}", tokenamount, f"{contract_name}", f"{clean_text}", f"{deposit_conditions['expiryTime']}", stateF_mapping) + + if first_classification['categorization'] == 'smart-contract-participation-ote-ce-C': + # There is no way to properly differentiate between one-time-event-time-trigger participation and token swap participation + # so we merge them in output return + tokenname = first_classification['wordlist'][0][:-1] + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", tokenname): + return outputreturn('noise') + + tokenamount = apply_rule1(extractAmount_rule_new1, processed_text) + if not tokenamount: + return outputreturn('noise') + try: + if float(tokenamount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + contract_address = extract_special_character_word(first_classification['wordlist'],'$') + if contract_address is False: + contract_address = '' + else: + contract_address = find_original_case(contract_address, clean_text) + if not check_flo_address(contract_address, is_testnet): + return outputreturn('noise') + + return outputreturn('smart-contract-one-time-event-continuos-event-participation', f"{clean_text}", f"{tokenname}", tokenamount, f"{contract_name}", f"{contract_address}", stateF_mapping) + + if first_classification['categorization'] == 'userchoice-trigger': + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + trigger_condition = extract_trigger_condition(processed_text) + if not trigger_condition: + return outputreturn('noise') + return outputreturn('one-time-event-userchoice-smartcontract-trigger', f"{contract_name}", f"{trigger_condition}", stateF_mapping) + + if first_classification['categorization'] == 'smart-contract-creation-ce-tokenswap': + operation = apply_rule1(selectCategory, processed_text, create_category, send_category+deposit_category) + if operation != 'category1': + return outputreturn('noise') + + contract_type = extract_special_character_word(first_classification['wordlist'],'*') + if not check_existence_of_keyword(['continuous-event'],[contract_type]): + return outputreturn('noise') + + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + contract_token = extract_special_character_word(first_classification['wordlist'],'#') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_token): + return outputreturn('noise') + + contract_address = extract_special_character_word(first_classification['wordlist'],'$') + contract_address = find_original_case(contract_address, clean_text) + if not check_flo_address(contract_address, is_testnet): + return outputreturn('noise') + + contract_conditions = extract_contract_conditions(processed_text, contract_type, contract_token, blocktime=blockinfo['time']) + if contract_conditions == False: + return outputreturn('noise') + # todo - Add checks for token swap extract contract conditions + try: + assert contract_conditions['subtype'] == 'tokenswap' + assert check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_conditions['accepting_token']) + assert check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_conditions['selling_token']) + if contract_conditions['priceType']=="'determined'" or contract_conditions['priceType']=='"determined"' or contract_conditions['priceType']=="determined" or contract_conditions['priceType']=="'predetermined'" or contract_conditions['priceType']=='"predetermined"' or contract_conditions['priceType']=="predetermined" or contract_conditions['priceType']=="dynamic": + assert float(contract_conditions['price'])>0 + else: + #assert check_flo_address(find_original_case(contract_conditions['priceType'], clean_text), is_testnet) + assert contract_conditions['priceType'] == 'statef' + except AssertionError: + return outputreturn('noise') + return outputreturn('continuos-event-token-swap-incorporation', f"{contract_token}", f"{contract_name}", f"{contract_address}", f"{clean_text}", f"{contract_conditions['subtype']}", f"{contract_conditions['accepting_token']}", f"{contract_conditions['selling_token']}", f"{contract_conditions['priceType']}", f"{contract_conditions['price']}", stateF_mapping) + + return outputreturn('noise') \ No newline at end of file diff --git a/ranchimallflo_api.py b/ranchimallflo_api.py index 537bbb9..9eb15a4 100644 --- a/ranchimallflo_api.py +++ b/ranchimallflo_api.py @@ -8,35 +8,44 @@ import time from datetime import datetime from quart import jsonify, make_response, Quart, render_template, request, flash, redirect, url_for, send_file from quart_cors import cors -import subprocess - import asyncio from typing import Optional - from config import * import parsing import subprocess - from apscheduler.schedulers.background import BackgroundScheduler import atexit -import pathlib -import io -import zipfile -import tarfile -import pdb - +import pyflo +from operator import itemgetter +import pdb +import ast +import time app = Quart(__name__) app.clients = set() app = cors(app, allow_origin="*") +# Global values and configg +internalTransactionTypes = [ 'tokenswapDepositSettlement', 'tokenswapParticipationSettlement', 'smartContractDepositReturn'] -# helper functions +if net == 'mainnet': + is_testnet = False +elif net == 'testnet': + is_testnet = True + + +# Validation functionss +def check_flo_address(floaddress, is_testnet=False): + return pyflo.is_address_valid(floaddress, testnet=is_testnet) + +def check_integer(value): + return str.isdigit(value) + +# Helper functions def retryRequest(tempserverlist, apicall): if len(tempserverlist) != 0: try: - response = requests.get( - '{}api/{}'.format(tempserverlist[0], apicall)) + response = requests.get('{}api/{}'.format(tempserverlist[0], apicall)) except: tempserverlist.pop(0) return retryRequest(tempserverlist, apicall) @@ -52,10 +61,8 @@ def retryRequest(tempserverlist, apicall): def multiRequest(apicall, net): - testserverlist = ['http://0.0.0.0:9000/', 'https://testnet.flocha.in/', - 'https://testnet-flosight.duckdns.org/'] - mainserverlist = ['http://0.0.0.0:9001/', 'https://livenet.flocha.in/', - 'https://testnet-flosight.duckdns.org/'] + testserverlist = ['http://0.0.0.0:9000/', 'https://testnet.flocha.in/', 'https://testnet-flosight.duckdns.org/'] + mainserverlist = ['http://0.0.0.0:9001/', 'https://livenet.flocha.in/', 'https://testnet-flosight.duckdns.org/'] if net == 'mainnet': return retryRequest(mainserverlist, apicall) elif net == 'testnet': @@ -69,72 +76,600 @@ def blockdetailhelper(blockdetail): else: blockHash = str(blockdetail) blockHeight = None - + # open the latest block database conn = sqlite3.connect(os.path.join(dbfolder, 'latestCache.db')) c = conn.cursor() - if blockHash: - c.execute( - f"select jsonData from latestBlocks where blockHash='{blockHash}'") + c.execute(f"select jsonData from latestBlocks where blockHash='{blockHash}'") elif blockHeight: - c.execute( - f"select jsonData from latestBlocks where blockNumber='{blockHeight}'") - + c.execute(f"select jsonData from latestBlocks where blockNumber='{blockHeight}'") blockJson = c.fetchall() return blockJson - def transactiondetailhelper(transactionHash): # check if legit transaction hash # open the latest block database conn = sqlite3.connect(os.path.join(dbfolder, 'latestCache.db')) c = conn.cursor() - - c.execute(f"select jsonData,parsedFloData from latestTransactions where transactionHash='{transactionHash}'") + c.execute(f"SELECT jsonData, parsedFloData, transactionType, db_reference FROM latestTransactions WHERE transactionHash='{transactionHash}'") transactionJsonData = c.fetchall() - return transactionJsonData - def update_transaction_confirmations(transactionJson): - url = f"{apiUrl}tx/{transactionJson['txid']}" + url = f"{apiUrl}api/v1/tx/{transactionJson['txid']}" response = requests.get(url) if response.status_code == 200: response_data = response.json() transactionJson['confirmations'] = response_data['confirmations'] return transactionJson +def smartcontract_morph_helper(smart_contracts): + contractList = [] + for idx, contract in enumerate(smart_contracts): + contractDict = {} + contractDict['contractName'] = contract[1] + contractDict['contractAddress'] = contract[2] + contractDict['status'] = contract[3] + contractDict['contractType'] = contract[5] + if contractDict['contractType'] in ['continuous-event', 'continuos-event']: + contractDict['contractSubType'] = 'tokenswap' + accepting_selling_tokens = ast.literal_eval(contract[4]) + contractDict['acceptingToken'] = accepting_selling_tokens[0] + contractDict['sellingToken'] = accepting_selling_tokens[1] + contractStructure = fetchContractStructure(contractDict['contractName'], contractDict['contractAddress']) + if contractStructure['pricetype'] == 'dynamic': + # temp fix + if 'oracle_address' in contractStructure.keys(): + contractDict['oracle_address'] = contractStructure['oracle_address'] + contractDict['price'] = fetch_dynamic_swap_price(contractStructure, {'time': datetime.now().timestamp()}) + else: + contractDict['price'] = contractStructure['price'] + elif contractDict['contractType'] == 'one-time-event': + contractDict['tokenIdentification'] = contract[4] + # pull the contract structure + contractStructure = fetchContractStructure(contractDict['contractName'], contractDict['contractAddress']) + # compare + if 'payeeAddress' in contractStructure.keys(): + contractDict['contractSubType'] = 'time-trigger' + else: + choice_list = [] + for obj_key in contractStructure['exitconditions'].keys(): + choice_list.append(contractStructure['exitconditions'][obj_key]) + contractDict['userChoices'] = choice_list + contractDict['contractSubType'] = 'external-trigger' + contractDict['expiryDate'] = contract[9] + contractDict['closeDate'] = contract[10] + + contractDict['transactionHash'] = contract[6] + contractDict['blockNumber'] = contract[7] + contractDict['incorporationDate'] = contract[8] + contractList.append(contractDict) + return contractList + +def smartContractInfo_output(contractName, contractAddress, contractType, subtype): + if contractType == 'continuos-event' and contractType == 'tokenswap': + pass + elif contractType == 'one-time-event' and contractType == 'userchoice': + pass + elif contractType == 'one-time-event' and contractType == 'timetrigger': + pass + +def return_smart_contracts(connection, contractName=None, contractAddress=None): + # find all the contracts details + if contractName and contractAddress: + connection.execute("SELECT * FROM activecontracts WHERE id IN (SELECT max(id) FROM activecontracts GROUP BY contractName, contractAddress) AND contractName=? AND contractAddress=?", (contractName, contractAddress)) + elif contractName and not contractAddress: + connection.execute("SELECT * FROM activecontracts WHERE id IN (SELECT max(id) FROM activecontracts GROUP BY contractName, contractAddress) AND contractName=?", (contractName,)) + elif not contractName and contractAddress: + connection.execute("SELECT * FROM activecontracts WHERE id IN (SELECT max(id) FROM activecontracts GROUP BY contractName, contractAddress) AND contractAddress=?", (contractAddress,)) + else: + connection.execute("SELECT * FROM activecontracts WHERE id IN (SELECT max(id) FROM activecontracts GROUP BY contractName, contractAddress)") + + smart_contracts = connection.fetchall() + return smart_contracts + +def create_database_connection(type, parameters=None): + if type == 'token': + filelocation = os.path.join(dbfolder, 'tokens', parameters['token_name']) + elif type == 'smart_contract': + contractDbName = '{}-{}.db'.format(parameters['contract_name'].strip(), parameters['contract_address'].strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + elif type == 'system_dbs': + filelocation = os.path.join(dbfolder, 'system.db') + elif type == 'latest_cache': + filelocation = os.path.join(dbfolder, 'latestCache.db') + + conn = sqlite3.connect(filelocation) + c = conn.cursor() + return [conn, c] + +def fetchContractStructure(contractName, contractAddress): + # Make connection to contract database + contractDbName = '{}-{}.db'.format(contractName.strip(),contractAddress.strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # fetch from contractStructure + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict, c + conn.close() + + if 'contractAmount' in contractStructure: + contractStructure['contractAmount'] = float(contractStructure['contractAmount']) + if 'payeeAddress' in contractStructure: + contractStructure['payeeAddress'] = json.loads(contractStructure['payeeAddress']) + if 'maximumsubscriptionamount' in contractStructure: + contractStructure['maximumsubscriptionamount'] = float(contractStructure['maximumsubscriptionamount']) + if 'minimumsubscriptionamount' in contractStructure: + contractStructure['minimumsubscriptionamount'] = float(contractStructure['minimumsubscriptionamount']) + if 'price' in contractStructure: + contractStructure['price'] = float(contractStructure['price']) + return contractStructure + else: + return 0 + +def fetchContractStatus(contractName, contractAddress): + conn, c = create_database_connection('system_dbs') + # select status from the last instance of activecontracts where match contractName and contractAddress + c.execute(f'SELECT status FROM activecontracts WHERE contractName="{contractName}" AND contractAddress="{contractAddress}" ORDER BY id DESC LIMIT 1') + status = c.fetchall() + if len(status)==0: + return None + else: + return status[0][0] + +def extract_ip_op_addresses(transactionJson): + sender_address = transactionJson['vin'][0]['addresses'][0] + receiver_address = None + for utxo in transactionJson['vout']: + if utxo['scriptPubKey']['addresses'][0] == sender_address: + continue + receiver_address = utxo['scriptPubKey']['addresses'][0] + return sender_address, receiver_address + +def updatePrices(): + prices = {} + # USD -> INR + response = requests.get(f"https://api.exchangerate-api.com/v4/latest/usd") + price = response.json() + prices['USDINR'] = price['rates']['INR'] + + # Blockchain stuff : BTC,FLO -> USD,INR + # BTC->USD | BTC->INR + response = requests.get(f"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,flo&vs_currencies=usd,inr") + price = response.json() + prices['BTCUSD'] = price['bitcoin']['usd'] + prices['BTCINR'] = price['bitcoin']['inr'] + + # FLO->USD | FLO->INR + response = requests.get(f"https://api.coinlore.net/api/ticker/?id=67") + price = response.json() + prices["FLOUSD"] = float(price[0]['price_usd']) + prices["FLOINR"] = float(prices["FLOUSD"]) * float(prices['USDINR']) + + # 3. update latest price data + print('Prices updated at time: %s' % datetime.now()) + print(prices) + + conn = sqlite3.connect('system.db') + c = conn.cursor() + for pair in list(prices.items()): + pair = list(pair) + c.execute(f"UPDATE ratepairs SET price={pair[1]} WHERE ratepair='{pair[0]}'") + conn.commit() + +def fetch_dynamic_swap_price(contractStructure, blockinfo): + oracle_address = contractStructure['oracle_address'] + # fetch transactions from the blockchain where from address : oracle-address... to address: contract address + # find the first contract transaction which adheres to price change format + # {"price-update":{"contract-name": "", "contract-address": "", "price": 3}} + print(f'oracle address is : {oracle_address}') + response = requests.get(f'{apiUrl}api/v1/address/{oracle_address}?details=txs') + if response.status_code == 200: + response = response.json() + if 'txs' not in response.keys(): # API doesn't return 'transactions' key, if 0 txs present on address + return float(contractStructure['price']) + else: + transactions = response['txs'] + for transaction in transactions: + #transaction_response = requests.get(f'{apiUrl}api/v1/tx/{transaction_hash}') + # if transaction_response.status_code == 200: + floData = transaction['floData'] + # If the blocktime of the transaction is < than the current block time + if transaction['time'] < blockinfo['time']: + # Check if flodata is in the format we are looking for + # ie. {"price-update":{"contract-name": "", "contract-address": "", "price": 3}} + # and receiver address should be contractAddress + try: + sender_address, receiver_address = find_sender_receiver(transaction) + assert receiver_address == contractStructure['contractAddress'] + assert sender_address == oracle_address + floData = json.loads(floData) + # Check if the contract name and address are right + assert floData['price-update']['contract-name'] == contractStructure['contractName'] + assert floData['price-update']['contract-address'] == contractStructure['contractAddress'] + return float(floData['price-update']['price']) + except: + continue + else: + continue + # else: + # print('API error while fetch_dynamic_swap_price') + # return None + return float(contractStructure['price']) + else: + print('API error while fetch_dynamic_swap_price') + return None + +def find_sender_receiver(transaction_data): + # Create vinlist and outputlist + vinlist = [] + querylist = [] + + #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 vin in transaction_data["vin"]: + vinlist.append([vin["addresses"][0], float(vin["value"])]) + + totalinputval = float(transaction_data["valueIn"]) + + # 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(f"System has found more than one address as part of vin. Transaction {transaction_data['txid']} is rejected") + return 0 + + inputlist = [vinlist[0][0], totalinputval] + inputadd = vinlist[0][0] + + # todo Rule 42 - If the number of vout is more than 2, reject the transaction + if len(transaction_data["vout"]) > 2: + print(f"System has found more than 2 address as part of vout. Transaction {transaction_data['txid']} is rejected") + return 0 + + # 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 = [] + addresscounter = 0 + inputcounter = 0 + for obj in transaction_data["vout"]: + if obj["scriptPubKey"]["isAddress"] == True: + addresscounter = addresscounter + 1 + if inputlist[0] == obj["scriptPubKey"]["addresses"][0]: + inputcounter = inputcounter + 1 + continue + outputlist.append([obj["scriptPubKey"]["addresses"][0], obj["value"]]) + + if addresscounter == inputcounter: + outputlist = [inputlist[0]] + elif len(outputlist) != 1: + print(f"Transaction's change is not coming back to the input address. Transaction {transaction_data['txid']} is rejected") + return 0 + else: + outputlist = outputlist[0] + + return inputlist[0], outputlist[0] + +def fetch_contract_status_time_info(contractName, contractAddress): + conn, c = create_database_connection('system_dbs') + c.execute('SELECT status, incorporationDate, expiryDate, closeDate FROM activecontracts WHERE contractName=="{}" AND contractAddress=="{}" ORDER BY id DESC LIMIT 1'.format(contractName, contractAddress)) + contract_status_time_info = c.fetchall() + return contract_status_time_info + +def checkIF_commitee_trigger_tranasaction(transactionDetails): + if transactionDetails[3] == 'trigger': + pass + +def transaction_post_processing(transactionJsonData): + rowarray_list = [] + + for row in transactionJsonData: + transactions_object = {} + parsedFloData = json.loads(row[1]) + transactionDetails = json.loads(row[0]) + + if row[3] in internalTransactionTypes or (row[3]=='trigger' and row[8]!='committee'): + internal_info = {} + internal_info['senderAddress'] = row[4] + internal_info['receiverAddress'] = row[5] + internal_info['tokenAmount'] = row[6] + internal_info['tokenIdentification'] = row[7] + internal_info['contractName'] = parsedFloData['contractName'] + internal_info['transactionTrigger'] = transactionDetails['txid'] + internal_info['time'] = transactionDetails['time'] + internal_info['type'] = row[3] + internal_info['onChain'] = False + transactions_object = internal_info + else: + transactions_object = {**parsedFloData, **transactionDetails} + transactions_object = update_transaction_confirmations(transactions_object) + transactions_object['onChain'] = True + + rowarray_list.append(transactions_object) + + return rowarray_list + +def fetch_token_transactions(token, senderFloAddress=None, destFloAddress=None, limit=None, use_and=False): + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + conn.row_factory = sqlite3.Row + c = conn.cursor() + else: + return jsonify(description="Token doesn't exist"), 404 + + # Build the base SQL query + query = f"SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress, transferAmount, '{token}' AS token, '' AS transactionSubType FROM transactionHistory" + + # Build the WHERE clause based on conditions + conditions = [] + parameters = {} + + if senderFloAddress and not destFloAddress: + conditions.append('sourceFloAddress=:sender_flo_address') + parameters['sender_flo_address'] = senderFloAddress + + elif not senderFloAddress and destFloAddress: + conditions.append('destFloAddress=:dest_flo_address') + parameters['dest_flo_address'] = destFloAddress + + elif senderFloAddress and destFloAddress: + if use_and: + conditions.append('sourceFloAddress=:sender_flo_address AND destFloAddress=:dest_flo_address') + else: + conditions.append('sourceFloAddress=:sender_flo_address OR destFloAddress=:dest_flo_address') + parameters['sender_flo_address'] = senderFloAddress + parameters['dest_flo_address'] = destFloAddress + + # Add the WHERE clause if conditions exist + if conditions: + query += ' WHERE {}'.format(' AND '.join(conditions)) + + # Add the LIMIT clause if specified + if limit is not None: + query += ' LIMIT :limit' + parameters['limit'] = limit + + # Execute the query with parameters + c.execute(query, parameters) + transactionJsonData = c.fetchall() + conn.close() + return transaction_post_processing(transactionJsonData) + +def fetch_contract_transactions(contractName, contractAddress, _from=0, to=100): + sc_file = os.path.join(dbfolder, 'smartContracts', '{}-{}.db'.format(contractName, contractAddress)) + conn = sqlite3.connect(sc_file) + c = conn.cursor() + # Find token db names and attach + contractStructure = fetchContractStructure(contractName, contractAddress) + + if contractStructure['contractType'] == 'continuos-event': + token1 = contractStructure['accepting_token'] + token2 = contractStructure['selling_token'] + token1_file = f"{dbfolder}/tokens/{token1}.db" + token2_file = f"{dbfolder}/tokens/{token2}.db" + conn.execute(f"ATTACH DATABASE '{token1_file}' AS token1db") + conn.execute(f"ATTACH DATABASE '{token2_file}' AS token2db") + + transaction_query = f''' + SELECT t1.jsonData, t1.parsedFloData, t1.time, t1.transactionType, t1.sourceFloAddress, t1.destFloAddress, t1.transferAmount, '{token1}' AS token, s.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token1db.transactionHistory AS t1 + ON t1.transactionHash = s.transactionHash + UNION + SELECT t2.jsonData, t2.parsedFloData, t2.time, t2.transactionType, t2.sourceFloAddress, t2.destFloAddress, t2.transferAmount, '{token2}' AS token, s.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token2db.transactionHistory AS t2 + ON t2.transactionHash = s.transactionHash + WHERE s.id BETWEEN {_from} AND {to} + ''' + + creation_tx_query = ''' + SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress, transferAmount, '' AS token, transactionSubType + FROM contractTransactionHistory + ORDER BY id + LIMIT 1; + ''' + + elif contractStructure['contractType'] == 'one-time-event': + token1 = contractStructure['tokenIdentification'] + token1_file = f"{dbfolder}/tokens/{token1}.db" + conn.execute(f"ATTACH DATABASE '{token1_file}' AS token1db") + + transaction_query = f''' + SELECT t1.jsonData, t1.parsedFloData, t1.time, t1.transactionType, t1.sourceFloAddress, t1.destFloAddress, t1.transferAmount, '{token1}' AS token, s.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token1db.transactionHistory AS t1 + ON t1.transactionHash = s.transactionHash + WHERE s.id BETWEEN {_from} AND {to} + ''' + + creation_tx_query = ''' + SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress, transferAmount, '' AS token, transactionSubType + FROM contractTransactionHistory + ORDER BY id + LIMIT 1; + ''' + + c.execute(transaction_query) + transactionJsonData = c.fetchall() + + c.execute(creation_tx_query) + creation_tx = c.fetchall() + transactionJsonData = creation_tx + transactionJsonData + + return transaction_post_processing(transactionJsonData) + + +def fetch_swap_contract_transactions(contractName, contractAddress, transactionHash=None): + sc_file = os.path.join(dbfolder, 'smartContracts', '{}-{}.db'.format(contractName, contractAddress)) + conn = sqlite3.connect(sc_file) + c = conn.cursor() + # Find token db names and attach + contractStructure = fetchContractStructure(contractName, contractAddress) + token1 = contractStructure['accepting_token'] + token2 = contractStructure['selling_token'] + token1_file = f"{dbfolder}/tokens/{token1}.db" + token2_file = f"{dbfolder}/tokens/{token2}.db" + conn.execute(f"ATTACH DATABASE '{token1_file}' AS token1db") + conn.execute(f"ATTACH DATABASE '{token2_file}' AS token2db") + + # Get data from db + query = f''' + SELECT t1.jsonData, t1.parsedFloData, t1.time, t1.transactionType, t1.sourceFloAddress, t1.destFloAddress, t1.transferAmount, '{token1}' AS token, t1.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token1db.transactionHistory AS t1 + ON t1.transactionHash = s.transactionHash AND s.transactionHash = '{transactionHash}' + UNION + SELECT t2.jsonData, t2.parsedFloData, t2.time, t2.transactionType, t2.sourceFloAddress, t2.destFloAddress, t2.transferAmount, '{token2}' AS token, t2.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token2db.transactionHistory AS t2 + ON t2.transactionHash = s.transactionHash AND s.transactionHash = '{transactionHash}' ''' + + '''if transactionHash: + query += f" WHERE s.transactionHash = '{transactionHash}'"''' + + try: + c.execute(query) + except: + pass + + transactionJsonData = c.fetchall() + return transaction_post_processing(transactionJsonData) + +def sort_transactions(transactionJsonData): + transactionJsonData = sorted(transactionJsonData, key=lambda x: x['time'], reverse=True) + return transactionJsonData + +def process_committee_flodata(flodata): + flo_address_list = [] + try: + contract_committee_actions = flodata['token-tracker']['contract-committee'] + except KeyError: + print('Flodata related to contract committee') + else: + # Adding first and removing later to maintain consistency and not to depend on floData for order of execution + for action in contract_committee_actions.keys(): + if action == 'add': + for floid in contract_committee_actions[f'{action}']: + flo_address_list.append(floid) + + for action in contract_committee_actions.keys(): + if action == 'remove': + for floid in contract_committee_actions[f'{action}']: + flo_address_list.remove(floid) + finally: + return flo_address_list + + +def refresh_committee_list(admin_flo_id, api_url, blocktime): + committee_list = [] + latest_param = 'true' + mempool_param = 'false' + init_id = None + + def process_transaction(transaction_info): + if 'isCoinBase' in transaction_info or transaction_info['vin'][0]['addresses'][0] != admin_flo_id or transaction_info['blocktime'] > blocktime: + return + try: + tx_flodata = json.loads(transaction_info['floData']) + committee_list.extend(process_committee_flodata(tx_flodata)) + except: + pass + + def send_api_request(url): + response = requests.get(url, verify=API_VERIFY) + if response.status_code == 200: + return response.json() + else: + print('Response from the Flosight API failed') + sys.exit(0) + + url = f'{api_url}api/v1/address/{admin_flo_id}?details=txs' + response = send_api_request(url) + for transaction_info in response.get('txs', []): + process_transaction(transaction_info) + + while 'incomplete' in response: + url = f'{api_url}api/v1/address/{admin_flo_id}/txs?latest={latest_param}&mempool={mempool_param}&before={init_id}' + response = send_api_request(url) + for transaction_info in response.get('items', []): + process_transaction(transaction_info) + if 'incomplete' in response: + init_id = response['initItem'] + + return committee_list + @app.route('/') -async def welcome(): - return 'welcome' +async def welcome_msg(): + return jsonify('Welcome to RanchiMall FLO Api v2') + + +@app.route('/api/v1.0/getSystemData', methods=['GET']) +async def systemData(): + # query for the number of flo addresses in tokenAddress mapping + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + tokenAddressCount = c.execute('select count(distinct tokenAddress) from tokenAddressMapping').fetchall()[0][0] + tokenCount = c.execute('select count(distinct token) from tokenAddressMapping').fetchall()[0][0] + contractCount = c.execute('select count(distinct contractName) from contractAddressMapping').fetchall()[0][0] + lastscannedblock = int(c.execute("select value from systemData where attribute=='lastblockscanned'").fetchall()[0][0]) + conn.close() + + # query for total number of validated blocks + conn = sqlite3.connect(os.path.join(dbfolder, 'latestCache.db')) + c = conn.cursor() + validatedBlockCount = c.execute('select count(distinct blockNumber) from latestBlocks').fetchall()[0][0] + validatedTransactionCount = c.execute('select count(distinct transactionHash) from latestTransactions').fetchall()[0][0] + conn.close() + return jsonify(systemAddressCount=tokenAddressCount, systemBlockCount=validatedBlockCount, systemTransactionCount=validatedTransactionCount, systemSmartContractCount=contractCount, systemTokenCount=tokenCount, lastscannedblock=lastscannedblock, result='ok') -# FLO TOKEN APIs @app.route('/api/v1.0/broadcastTx/') async def broadcastTx(raw_transaction_hash): p1 = subprocess.run(['flo-cli',f"-datadir={FLO_DATA_DIR}",'sendrawtransaction',raw_transaction_hash], capture_output=True) return jsonify(args=p1.args,returncode=p1.returncode,stdout=p1.stdout.decode(),stderr=p1.stderr.decode()) +# FLO TOKEN APIs @app.route('/api/v1.0/getTokenList', methods=['GET']) async def getTokenList(): filelist = [] for item in os.listdir(os.path.join(dbfolder, 'tokens')): if os.path.isfile(os.path.join(dbfolder, 'tokens', item)): filelist.append(item[:-3]) - return jsonify(tokens=filelist, result='ok') - + @app.route('/api/v1.0/getTokenInfo', methods=['GET']) async def getTokenInfo(): token = request.args.get('token') - if token is None: return jsonify(result='error', description='token name hasnt been passed') - + dblocation = dbfolder + '/tokens/' + str(token) + '.db' if os.path.exists(dblocation): conn = sqlite3.connect(dblocation) @@ -147,8 +682,7 @@ async def getTokenInfo(): numberOf_distinctAddresses = c.fetchall()[0][0] c.execute('select max(id) from transactionHistory') numberOf_transactions = c.fetchall()[0][0] - c.execute( - 'select contractName, contractAddress, blockNumber, blockHash, transactionHash from tokenContractAssociation') + c.execute('select contractName, contractAddress, blockNumber, blockHash, transactionHash from tokenContractAssociation') associatedContracts = c.fetchall() conn.close() @@ -163,9 +697,7 @@ async def getTokenInfo(): tempdict['transactionHash'] = item[4] associatedContractList.append(tempdict) - return jsonify(result='ok', token=token, incorporationAddress=incorporationRow[1], tokenSupply=incorporationRow[3], - time=incorporationRow[6], blockchainReference=incorporationRow[7], - activeAddress_no=numberOf_distinctAddresses, totalTransactions=numberOf_transactions, associatedSmartContracts=associatedContractList) + return jsonify(result='ok', token=token, incorporationAddress=incorporationRow[1], tokenSupply=incorporationRow[3], time=incorporationRow[6], blockchainReference=incorporationRow[7], activeAddress_no=numberOf_distinctAddresses, totalTransactions=numberOf_transactions, associatedSmartContracts=associatedContractList) @app.route('/api/v1.0/getTokenTransactions', methods=['GET']) @@ -177,7 +709,7 @@ async def getTokenTransactions(): if token is None: return jsonify(result='error', description='token name hasnt been passed') - + dblocation = dbfolder + '/tokens/' + str(token) + '.db' if os.path.exists(dblocation): conn = sqlite3.connect(dblocation) @@ -185,7 +717,7 @@ async def getTokenTransactions(): c = conn.cursor() else: return jsonify(result='error', description='token doesn\'t exist') - + if senderFloAddress and not destFloAddress: if limit is None: c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE sourceFloAddress="{}" ORDER BY id DESC'.format(senderFloAddress)) @@ -224,17 +756,16 @@ async def getTokenBalances(): token = request.args.get('token') if token is None: return jsonify(result='error', description='token name hasnt been passed') - + dblocation = dbfolder + '/tokens/' + str(token) + '.db' if os.path.exists(dblocation): conn = sqlite3.connect(dblocation) c = conn.cursor() else: return jsonify(result='error', description='token doesn\'t exist') - c.execute( - 'SELECT address,SUM(transferBalance) FROM activeTable GROUP BY address') + c.execute('SELECT address,SUM(transferBalance) FROM activeTable GROUP BY address') addressBalances = c.fetchall() - + returnList = {} for address in addressBalances: @@ -243,25 +774,22 @@ async def getTokenBalances(): return jsonify(result='ok', token=token, balances=returnList) -# FLO address APIs - +# FLO Address APIs @app.route('/api/v1.0/getFloAddressInfo', methods=['GET']) async def getFloAddressInfo(): floAddress = request.args.get('floAddress') - if floAddress is None: - return jsonify(result='error', description='floAddress hasn\'t been passed') - + return jsonify(description='floAddress hasn\'t been passed'), 400 + dblocation = dbfolder + '/system.db' if os.path.exists(dblocation): conn = sqlite3.connect(dblocation) c = conn.cursor() c.execute('select token from tokenAddressMapping where tokenAddress="{}"'.format(floAddress)) tokenNames = c.fetchall() - c.execute(f"select contractName, status, tokenIdentification, contractType, transactionHash, blockNumber, blockHash from activecontracts where contractAddress='{floAddress}'") incorporatedContracts = c.fetchall() - + if len(tokenNames) != 0: detailList = {} for token in tokenNames: @@ -276,7 +804,6 @@ async def getFloAddressInfo(): tempdict['balance'] = balance tempdict['token'] = token detailList[token] = tempdict - else: # Address is not associated with any token return jsonify(result='error', description='FLO address is not associated with any tokens') @@ -294,7 +821,7 @@ async def getFloAddressInfo(): tempdict['blockNumber'] = contract[5] tempdict['blockHash'] = contract[6] incorporatedSmartContracts.append(tempdict) - + return jsonify(result='ok', floAddress=floAddress, floAddressBalances=detailList, incorporatedSmartContracts=incorporatedContracts) else: return jsonify(result='ok', floAddress=floAddress, floAddressBalances=detailList, incorporatedSmartContracts=None) @@ -327,8 +854,7 @@ async def getAddressBalance(): tempdict = {} conn = sqlite3.connect(dblocation) c = conn.cursor() - c.execute( - 'SELECT SUM(transferBalance) FROM activeTable WHERE address="{}"'.format(floAddress)) + c.execute('SELECT SUM(transferBalance) FROM activeTable WHERE address="{}"'.format(floAddress)) balance = c.fetchall()[0][0] tempdict['balance'] = balance tempdict['token'] = token @@ -361,13 +887,13 @@ async def getFloAddressTransactions(): if floAddress is None: return jsonify(result='error', description='floAddress has not been passed') - + if token is None: dblocation = dbfolder + '/system.db' if os.path.exists(dblocation): conn = sqlite3.connect(dblocation) c = conn.cursor() - c.execute('select token from tokenAddressMapping where tokenAddress="{}"'.format(floAddress)) + c.execute('SELECT token FROM tokenAddressMapping WHERE tokenAddress="{}"'.format(floAddress)) tokenNames = c.fetchall() else: dblocation = dbfolder + '/tokens/' + str(token) + '.db' @@ -375,10 +901,9 @@ async def getFloAddressTransactions(): tokenNames = [[str(token), ]] else: return jsonify(result='error', description='token doesn\'t exist') - + if len(tokenNames) != 0: allTransactionList = {} - for tokenname in tokenNames: tokenname = tokenname[0] dblocation = dbfolder + '/tokens/' + str(tokenname) + '.db' @@ -409,7 +934,6 @@ async def getFloAddressTransactions(): # SMART CONTRACT APIs - @app.route('/api/v1.0/getSmartContractList', methods=['GET']) async def getContractList(): contractName = request.args.get('contractName') @@ -421,8 +945,7 @@ async def getContractList(): contractList = [] if contractName and contractAddress: - c.execute('select * from activecontracts where contractName="{}" and contractAddress="{}"'.format( - contractName, contractAddress)) + c.execute('select * from activecontracts where contractName="{}" and contractAddress="{}"'.format(contractName, contractAddress)) allcontractsDetailList = c.fetchall() for idx, contract in enumerate(allcontractsDetailList): contractDict = {} @@ -442,8 +965,7 @@ async def getContractList(): contractList.append(contractDict) elif contractName and not contractAddress: - c.execute( - 'select * from activecontracts where contractName="{}"'.format(contractName)) + c.execute('select * from activecontracts where contractName="{}"'.format(contractName)) allcontractsDetailList = c.fetchall() for idx, contract in enumerate(allcontractsDetailList): contractDict = {} @@ -463,8 +985,7 @@ async def getContractList(): contractList.append(contractDict) elif not contractName and contractAddress: - c.execute( - 'select * from activecontracts where contractAddress="{}"'.format(contractAddress)) + c.execute('select * from activecontracts where contractAddress="{}"'.format(contractAddress)) allcontractsDetailList = c.fetchall() for idx, contract in enumerate(allcontractsDetailList): contractDict = {} @@ -502,7 +1023,7 @@ async def getContractList(): contractDict['closeDate'] = contract[10] contractList.append(contractDict) - + return jsonify(smartContracts=contractList, result='ok') @@ -517,16 +1038,14 @@ async def getContractInfo(): if contractAddress is None: return jsonify(result='error', description='Smart Contract\'s address hasn\'t been passed') - contractDbName = '{}-{}.db'.format(contractName.strip(), - contractAddress.strip()) + contractDbName = '{}-{}.db'.format(contractName.strip(),contractAddress.strip()) filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) if os.path.isfile(filelocation): # Make db connection and fetch data conn = sqlite3.connect(filelocation) c = conn.cursor() - c.execute( - 'SELECT attribute,value FROM contractstructure') + c.execute('SELECT attribute,value FROM contractstructure') result = c.fetchall() contractStructure = {} @@ -541,7 +1060,7 @@ async def getContractInfo(): if len(conditionDict) > 0: contractStructure['exitconditions'] = conditionDict del counter, conditionDict - + returnval = contractStructure returnval['userChoice'] = contractStructure['exitconditions'] returnval.pop('exitconditions') @@ -557,8 +1076,7 @@ async def getContractInfo(): conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) c = conn.cursor() - c.execute('select status, incorporationDate, expiryDate, closeDate from activecontracts where contractName=="{}" and contractAddress=="{}"'.format( - contractName.strip(), contractAddress.strip())) + c.execute('select status, incorporationDate, expiryDate, closeDate from activecontracts where contractName=="{}" and contractAddress=="{}"'.format(contractName.strip(), contractAddress.strip())) results = c.fetchall() if len(results) == 1: @@ -591,20 +1109,18 @@ async def getContractInfo(): # this is a normal trigger # find the winning condition - c.execute( - 'select userchoice from contractparticipants where winningAmount is not null limit 1') + c.execute('select userchoice from contractparticipants where winningAmount is not null limit 1') returnval['winningChoice'] = c.fetchall()[0][0] # find the winning participants c.execute( 'select participantAddress, winningAmount from contractparticipants where winningAmount is not null') winnerparticipants = c.fetchall() - returnval['numberOfWinners'] = len( - winnerparticipants) + returnval['numberOfWinners'] = len(winnerparticipants) else: return jsonify(result='error', description='There is more than 1 trigger in the database for the smart contract. Please check your code, this shouldnt happen') - + return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, contractInfo=returnval) else: @@ -622,45 +1138,26 @@ async def getcontractparticipants(): if contractAddress is None: return jsonify(result='error', description='Smart Contract\'s address hasn\'t been passed') - contractDbName = '{}-{}.db'.format(contractName.strip(), - contractAddress.strip()) - filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + contractName = contractName.strip() + contractAddress = contractAddress.strip() + filelocation = os.path.join(dbfolder, 'smartContracts', '{}-{}.db'.format(contractName, contractAddress)) if os.path.isfile(filelocation): # Make db connection and fetch data - conn = sqlite3.connect(filelocation) - c = conn.cursor() - c.execute( - 'SELECT attribute,value FROM contractstructure') - result = c.fetchall() - - contractStructure = {} - conditionDict = {} - counter = 0 - for item in result: - if list(item)[0] == 'exitconditions': - conditionDict[counter] = list(item)[1] - counter = counter + 1 - else: - contractStructure[list(item)[0]] = list(item)[1] - if len(conditionDict) > 0: - contractStructure['exitconditions'] = conditionDict - del counter, conditionDict - + contractStructure = fetchContractStructure(contractName, contractAddress) + conn, c = create_database_connection('smart_contract', {'contract_name': contractName, 'contract_address': contractAddress}) + if 'exitconditions' in contractStructure: # contract is of the type external trigger # check if the contract has been closed - c.execute( - 'select * from contractTransactionHistory where transactionType="trigger"') + c.execute('select * from contractTransactionHistory where transactionType="trigger"') trigger = c.fetchall() if len(trigger) == 1: - c.execute( - 'select value from contractstructure where attribute="tokenIdentification"') + c.execute('select value from contractstructure where attribute="tokenIdentification"') token = c.fetchall() token = token[0][0] - c.execute( - 'SELECT id,participantAddress, tokenAmount, userChoice, transactionHash, winningAmount FROM contractparticipants') + c.execute('SELECT id,participantAddress, tokenAmount, userChoice, transactionHash, winningAmount FROM contractparticipants') result = c.fetchall() conn.close() returnval = {} @@ -668,20 +1165,14 @@ async def getcontractparticipants(): returnval[row[1]] = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], 'transactionHash': row[4], 'winningAmount': row[5], 'tokenIdentification': token} - return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, - participantInfo=returnval) elif len(trigger) == 0: - c.execute( - 'SELECT id,participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants') + c.execute('SELECT id,participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants') result = c.fetchall() conn.close() returnval = {} for row in result: - returnval[row[1]] = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], - 'transactionHash': row[4]} + returnval[row[1]] = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], 'transactionHash': row[4]} - return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, - participantInfo=returnval) else: return jsonify(result='error', description='More than 1 trigger present. This is unusual, please check your code') @@ -696,7 +1187,23 @@ async def getcontractparticipants(): returnval[row[1]] = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], 'transactionHash': row[4]} - return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, participantInfo=returnval) + elif contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap': + c.execute('SELECT * FROM contractparticipants') + contract_participants = c.fetchall() + returnval = {} + for row in contract_participants: + returnval[row[1]] = { + 'participantFloAddress': row[1], + 'participationAmount': row[2], + 'swapPrice': float(row[3]), + 'transactionHash': row[4], + 'blockNumber': row[5], + 'blockHash': row[6], + 'swapAmount': row[7] + } + conn.close() + + return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, participantInfo=returnval) else: return jsonify(result='error', description='Smart Contract with the given name doesn\'t exist') @@ -714,88 +1221,190 @@ async def getParticipantDetails(): if (contractName and contractAddress is None) or (contractName is None and contractAddress): return jsonify(result='error', description='pass both, contractName and contractAddress as url parameters') + #if os.path.isfile(dblocation) and os.path.isfile(contract_db): if os.path.isfile(dblocation): # Make db connection and fetch data conn = sqlite3.connect(dblocation) c = conn.cursor() - # Check if its a participant address - queryString = f"SELECT id, address,contractName, contractAddress, tokenAmount, transactionHash, blockNumber, blockHash FROM contractAddressMapping where address='{floAddress}' and addressType='participant'" - c.execute(queryString) - result = c.fetchall() - if len(result) != 0: - participationDetailsList = [] - for row in result: - detailsDict = {} - detailsDict['contractName'] = row[2] - detailsDict['contractAddress'] = row[3] - detailsDict['tokenAmount'] = row[4] - detailsDict['transactionHash'] = row[5] - - c.execute(f"select status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate from activecontracts where contractName='{detailsDict['contractName']}' and contractAddress='{detailsDict['contractAddress']}'") - temp = c.fetchall() - detailsDict['status'] = temp[0][0] - detailsDict['tokenIdentification'] = temp[0][1] - detailsDict['contractType'] = temp[0][2] - detailsDict['blockNumber'] = temp[0][3] - detailsDict['blockHash'] = temp[0][4] - detailsDict['incorporationDate'] = temp[0][5] - if temp[0][6]: - detailsDict['expiryDate'] = temp[0][6] - if temp[0][7]: - detailsDict['closeDate'] = temp[0][7] - - # check if the contract has been closed - contractDbName = '{}-{}.db'.format(detailsDict['contractName'].strip(), - detailsDict['contractAddress'].strip()) - filelocation = os.path.join( - dbfolder, 'smartContracts', contractDbName) - if os.path.isfile(filelocation): - # Make db connection and fetch data - conn = sqlite3.connect(filelocation) - c = conn.cursor() - c.execute( - 'SELECT attribute,value FROM contractstructure') - result = c.fetchall() - - contractStructure = {} - conditionDict = {} - counter = 0 - for item in result: - if list(item)[0] == 'exitconditions': - conditionDict[counter] = list(item)[1] - counter = counter + 1 - else: - contractStructure[list(item)[0]] = list(item)[1] - if len(conditionDict) > 0: - contractStructure['exitconditions'] = conditionDict - del counter, conditionDict - - if 'exitconditions' in contractStructure: - # contract is of the type external trigger - # check if the contract has been closed - c.execute( - 'select * from contractTransactionHistory where transactionType="trigger"') - trigger = c.fetchall() - - if detailsDict['status'] == 'closed': - c.execute( - f"SELECT userChoice, winningAmount FROM contractparticipants where participantAddress='{floAddress}'") - result = c.fetchall() - conn.close() - detailsDict['userChoice'] = result[0][0] - detailsDict['winningAmount'] = result[0][1] - else: - c.execute( - f"SELECT userChoice FROM contractparticipants where participantAddress='{floAddress}'") - result = c.fetchall() - conn.close() - detailsDict['userChoice'] = result[0][0] - - participationDetailsList.append(detailsDict) - return jsonify(result='ok', floAddress=floAddress, type='participant', participatedContracts=participationDetailsList) + if contractName is not None: + c.execute(f'SELECT * FROM contractAddressMapping WHERE address="{floAddress}" AND addressType="participant" AND contractName="{contractName}" AND contractAddress="{contractAddress}"') else: - return jsonify(result='error', description='Address hasn\'t participanted in any other contract') + c.execute(f'SELECT * FROM contractAddressMapping WHERE address="{floAddress}" AND addressType="participant"') + participant_address_contracts = c.fetchall() + + if len(participant_address_contracts) != 0: + participationDetailsList = [] + for contract in participant_address_contracts: + detailsDict = {} + contract_db = os.path.join(dbfolder, 'smartContracts', f"{contract[3]}-{contract[4]}.db") + # Make db connection and fetch contract structure + conn = sqlite3.connect(contract_db) + c = conn.cursor() + + # Get details of the type of Smart Contract + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + if contractStructure['contractType']=='continuos-event' and contractStructure['subtype']=='tokenswap': + # normal result + swap details + # what is a api detail + c.execute('SELECT * FROM contractparticipants WHERE participantAddress=?',(floAddress,)) + participant_details = c.fetchall() + + if len(participant_details) > 0: + participationList = [] + for participation in participant_details: + c.execute("SELECT value FROM contractstructure WHERE attribute='selling_token'") + structure = c.fetchall() + detailsDict['participationAddress'] = floAddress + detailsDict['participationAmount'] = participation[2] + detailsDict['receivedAmount'] = float(participation[3]) + detailsDict['participationToken'] = contractStructure['accepting_token'] + detailsDict['receivedToken'] = contractStructure['selling_token'] + detailsDict['swapPrice_received_to_participation'] = float(participation[7]) + detailsDict['transactionHash'] = participation[4] + detailsDict['blockNumber'] = participation[5] + detailsDict['blockHash'] = participation[6] + participationList.append(detailsDict) + + participationDetailsList.append(participationList) + + elif contractStructure['contractType']=='one-time-event' and 'payeeAddress' in contractStructure.keys(): + # normal results + conn = sqlite3.connect(dblocation) + c = conn.cursor() + detailsDict = {} + detailsDict['contractName'] = contract[3] + detailsDict['contractAddress'] = contract[4] + detailsDict['tokenAmount'] = contract[5] + detailsDict['transactionHash'] = contract[6] + + c.execute(f"select status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate from activecontracts where contractName='{detailsDict['contractName']}' and contractAddress='{detailsDict['contractAddress']}'") + temp = c.fetchall() + detailsDict['status'] = temp[0][0] + detailsDict['tokenIdentification'] = temp[0][1] + detailsDict['contractType'] = temp[0][2] + detailsDict['blockNumber'] = temp[0][3] + detailsDict['blockHash'] = temp[0][4] + detailsDict['incorporationDate'] = temp[0][5] + if temp[0][6]: + detailsDict['expiryDate'] = temp[0][6] + if temp[0][7]: + detailsDict['closeDate'] = temp[0][7] + + # check if the contract has been closed + contractDbName = '{}-{}.db'.format(detailsDict['contractName'].strip(), detailsDict['contractAddress'].strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # Make db connection and fetch data + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + if 'payeeAddress' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + c.execute(f"SELECT tokenAmount FROM contractparticipants where participantAddress='{floAddress}'") + result = c.fetchall() + conn.close() + detailsDict['tokenAmount'] = result[0][0] + + elif contractStructure['contractType']=='one-time-event' and 'exitconditions' in contractStructure.keys(): + # normal results + winning/losing details + conn = sqlite3.connect(dblocation) + c = conn.cursor() + detailsDict = {} + detailsDict['contractName'] = contract[3] + detailsDict['contractAddress'] = contract[4] + detailsDict['tokenAmount'] = contract[5] + detailsDict['transactionHash'] = contract[6] + + c.execute(f"select status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate from activecontracts where contractName='{detailsDict['contractName']}' and contractAddress='{detailsDict['contractAddress']}'") + temp = c.fetchall() + detailsDict['status'] = temp[0][0] + detailsDict['tokenIdentification'] = temp[0][1] + detailsDict['contractType'] = temp[0][2] + detailsDict['blockNumber'] = temp[0][3] + detailsDict['blockHash'] = temp[0][4] + detailsDict['incorporationDate'] = temp[0][5] + if temp[0][6]: + detailsDict['expiryDate'] = temp[0][6] + if temp[0][7]: + detailsDict['closeDate'] = temp[0][7] + + # check if the contract has been closed + contractDbName = '{}-{}.db'.format(detailsDict['contractName'].strip(), detailsDict['contractAddress'].strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # Make db connection and fetch data + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + if 'exitconditions' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + c.execute('select * from contractTransactionHistory where transactionType="trigger"') + trigger = c.fetchall() + + if detailsDict['status'] == 'closed': + c.execute(f"SELECT userChoice, winningAmount FROM contractparticipants where participantAddress='{floAddress}'") + result = c.fetchall() + conn.close() + detailsDict['userChoice'] = result[0][0] + detailsDict['winningAmount'] = result[0][1] + else: + c.execute(f"SELECT userChoice FROM contractparticipants where participantAddress='{floAddress}'") + result = c.fetchall() + conn.close() + detailsDict['userChoice'] = result[0][0] + + participationDetailsList.append(detailsDict) + + return jsonify(result='ok', floAddress=floAddress, type='participant', participatedContracts=participationDetailsList) + + else: + return jsonify(result='error', description='Address hasn\'t participated in any other contract') else: return jsonify(result='error', description='System error. System db is missing') @@ -811,8 +1420,7 @@ async def getsmartcontracttransactions(): if contractAddress is None: return jsonify(result='error', description='Smart Contract\'s address hasn\'t been passed') - contractDbName = '{}-{}.db'.format(contractName.strip(), - contractAddress.strip()) + contractDbName = '{}-{}.db'.format(contractName.strip(), contractAddress.strip()) filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) if os.path.isfile(filelocation): @@ -839,7 +1447,6 @@ async def getsmartcontracttransactions(): @app.route('/api/v1.0/getBlockDetails/', methods=['GET']) async def getblockdetails(blockdetail): - blockJson = blockdetailhelper(blockdetail) if len(blockJson) != 0: blockJson = json.loads(blockJson[0][0]) @@ -862,7 +1469,6 @@ async def gettransactiondetails(transactionHash): @app.route('/api/v1.0/getLatestTransactionDetails', methods=['GET']) async def getLatestTransactionDetails(): - numberOfLatestBlocks = request.args.get('numberOfLatestBlocks') dblocation = dbfolder + '/latestCache.db' @@ -905,16 +1511,14 @@ async def getLatestTransactionDetails(): @app.route('/api/v1.0/getLatestBlockDetails', methods=['GET']) async def getLatestBlockDetails(): - limit = request.args.get('limit') - dblocation = dbfolder + '/latestCache.db' if os.path.exists(dblocation): conn = sqlite3.connect(dblocation) c = conn.cursor() else: return 'Latest transactions db doesn\'t exist. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api' - + if limit is None: c.execute('''SELECT * FROM ( SELECT * FROM latestBlocks ORDER BY blockNumber DESC LIMIT 4) ORDER BY id ASC;''') else: @@ -951,13 +1555,12 @@ async def getblocktransactions(blockdetail): @app.route('/api/v1.0/categoriseString/') async def categoriseString(urlstring): - # check if the hash is of a transaction - response = requests.get('{}tx/{}'.format(apiUrl, urlstring)) + response = requests.get('{}api/v1/tx/{}'.format(apiUrl, urlstring)) if response.status_code == 200: return jsonify(type='transaction') else: - response = requests.get('{}block/{}'.format(apiUrl, urlstring)) + response = requests.get('{}api/v1/block/{}'.format(apiUrl, urlstring)) if response.status_code == 200: return jsonify(type='block') else: @@ -965,7 +1568,7 @@ async def categoriseString(urlstring): tokenfolder = os.path.join(dbfolder, 'tokens') onlyfiles = [f[:-3] for f in os.listdir(tokenfolder) if os.path.isfile(os.path.join(tokenfolder, f))] - + if urlstring.lower() in onlyfiles: return jsonify(type='token') else: @@ -973,8 +1576,7 @@ async def categoriseString(urlstring): conn = sqlite3.connect(contractfolder) conn.row_factory = lambda cursor, row: row[0] c = conn.cursor() - contractList = c.execute( - 'select contractname from activeContracts').fetchall() + contractList = c.execute('select contractname from activeContracts').fetchall() if urlstring.lower() in contractList: return jsonify(type='smartContract') @@ -993,10 +1595,8 @@ async def getTokenSmartContractList(): # list of smart contracts conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) c = conn.cursor() - contractList = [] - - c.execute('select * from activecontracts') + c.execute('SELECT * FROM activecontracts') allcontractsDetailList = c.fetchall() for idx, contract in enumerate(allcontractsDetailList): contractDict = {} @@ -1013,41 +1613,963 @@ async def getTokenSmartContractList(): contractDict['expiryDate'] = contract[10] if contract[11]: contractDict['closeDate'] = contract[11] - contractList.append(contractDict) return jsonify(tokens=filelist, smartContracts=contractList, result='ok') -@app.route('/api/v1.0/getSystemData', methods=['GET']) -async def systemData(): +################### +### VERSION 2 ### +################### +@app.route('/api/v2/info', methods=['GET']) +async def info(): # query for the number of flo addresses in tokenAddress mapping conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) c = conn.cursor() - tokenAddressCount = c.execute( - 'select count(distinct tokenAddress) from tokenAddressMapping').fetchall()[0][0] - tokenCount = c.execute( - 'select count(distinct token) from tokenAddressMapping').fetchall()[0][0] - contractCount = c.execute( - 'select count(distinct contractName) from contractAddressMapping').fetchall()[0][0] - lastscannedblock = int(c.execute("select value from systemData where attribute=='lastblockscanned'").fetchall()[0][0]) + tokenAddressCount = c.execute('SELECT COUNT(distinct tokenAddress) FROM tokenAddressMapping').fetchall()[0][0] + tokenCount = c.execute('SELECT COUNT(distinct token) FROM tokenAddressMapping').fetchall()[0][0] + contractCount = c.execute('SELECT COUNT(distinct contractName) FROM contractAddressMapping').fetchall()[0][0] + lastscannedblock = int(c.execute("SELECT value FROM systemData WHERE attribute=='lastblockscanned'").fetchall()[0][0]) conn.close() - + # query for total number of validated blocks conn = sqlite3.connect(os.path.join(dbfolder, 'latestCache.db')) c = conn.cursor() - validatedBlockCount = c.execute( - 'select count(distinct blockNumber) from latestBlocks').fetchall()[0][0] - validatedTransactionCount = c.execute( - 'select count(distinct transactionHash) from latestTransactions').fetchall()[0][0] + validatedBlockCount = c.execute('SELECT COUNT(distinct blockNumber) FROM latestBlocks').fetchall()[0][0] + validatedTransactionCount = c.execute('SELECT COUNT(distinct transactionHash) FROM latestTransactions').fetchall()[0][0] + conn.close() + + return jsonify(systemAddressCount=tokenAddressCount, systemBlockCount=validatedBlockCount, systemTransactionCount=validatedTransactionCount, systemSmartContractCount=contractCount, systemTokenCount=tokenCount, lastscannedblock=lastscannedblock), 200 + + +@app.route('/api/v2/broadcastTx/') +async def broadcastTx_v2(raw_transaction_hash): + p1 = subprocess.run(['flo-cli','sendrawtransaction',raw_transaction_hash], capture_output=True) + return jsonify(args=p1.args,returncode=p1.returncode,stdout=p1.stdout.decode(),stderr=p1.stderr.decode()), 200 + + +# FLO TOKEN APIs +@app.route('/api/v2/tokenList', methods=['GET']) +async def tokenList(): + filelist = [] + for item in os.listdir(os.path.join(dbfolder, 'tokens')): + if os.path.isfile(os.path.join(dbfolder, 'tokens', item)): + filelist.append(item[:-3]) + return jsonify(tokens=filelist), 200 + + +@app.route('/api/v2/tokenInfo/', methods=['GET']) +async def tokenInfo(token): + if token is None: + return jsonify(description='Token name hasnt been passed'), 400 + + # todo : input validation + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description="Token doesn't exist"), 404 + c.execute('SELECT * FROM transactionHistory WHERE id=1') + incorporationRow = c.fetchall()[0] + c.execute('SELECT COUNT (DISTINCT address) FROM activeTable') + numberOf_distinctAddresses = c.fetchall()[0][0] + c.execute('SELECT MAX(id) FROM transactionHistory') + numberOf_transactions = c.fetchall()[0][0] + c.execute('SELECT contractName, contractAddress, blockNumber, blockHash, transactionHash FROM tokenContractAssociation') + associatedContracts = c.fetchall() + conn.close() + + associatedContractList = [] + for item in associatedContracts: + tempdict = {} + item = list(item) + tempdict['contractName'] = item[0] + tempdict['contractAddress'] = item[1] + tempdict['blockNumber'] = item[2] + tempdict['blockHash'] = item[3] + tempdict['transactionHash'] = item[4] + associatedContractList.append(tempdict) + + return jsonify(token=token, incorporationAddress=incorporationRow[1], tokenSupply=incorporationRow[3], time=incorporationRow[6], blockchainReference=incorporationRow[7], activeAddress_no=numberOf_distinctAddresses, totalTransactions=numberOf_transactions, associatedSmartContracts=associatedContractList), 200 + + +@app.route('/api/v2/tokenTransactions/', methods=['GET']) +async def tokenTransactions(token): + if token is None: + return jsonify(description='Token name hasnt been passed'), 400 + + # Input validations + senderFloAddress = request.args.get('senderFloAddress') + if senderFloAddress is not None and not check_flo_address(senderFloAddress, is_testnet): + return jsonify(description='senderFloAddress validation failed'), 400 + destFloAddress = request.args.get('destFloAddress') + if destFloAddress is not None and not check_flo_address(destFloAddress, is_testnet): + return jsonify(description='destFloAddress validation failed'), 400 + limit = request.args.get('limit') + if limit is not None and not check_integer(limit): + return jsonify(description='limit validation failed'), 400 + use_AND = request.args.get('use_AND') + if use_AND is not None and use_AND not in [True, False]: + return jsonify(description='use_AND validation failed'), 400 + + _from = int(request.args.get('_from', 1)) # Get page number, default is 1 + to = int(request.args.get('to', 100)) # Get limit, default is 10 + + if _from<1: + return jsonify(description='_from validation failed'), 400 + if to<1: + return jsonify(description='to validation failed'), 400 + + filelocation = os.path.join(dbfolder, 'tokens', f'{token}.db') + + if os.path.isfile(filelocation): + transactionJsonData = fetch_token_transactions(token, senderFloAddress, destFloAddress, limit, use_AND) + sortedFormattedTransactions = sort_transactions(transactionJsonData) + return jsonify(token=token, transactions=sortedFormattedTransactions), 200 + else: + return jsonify(description='Token with the given name doesn\'t exist'), 404 + + +@app.route('/api/v2/tokenBalances/', methods=['GET']) +async def tokenBalances(token): + if token is None: + return jsonify(description='Token name hasnt been passed'), 400 + + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description="Token doesn't exist"), 404 + c.execute('SELECT address,SUM(transferBalance) FROM activeTable GROUP BY address') + addressBalances = c.fetchall() + returnList = {} + for address in addressBalances: + returnList[address[0]] = address[1] + + return jsonify(token=token, balances=returnList), 200 + + +# FLO Address APIs +@app.route('/api/v2/floAddressInfo/', methods=['GET']) +async def floAddressInfo(floAddress): + if floAddress is None: + return jsonify(description='floAddress hasn\'t been passed'), 400 + # input validation + if not check_flo_address(floAddress, is_testnet): + return jsonify(description='floAddress validation failed'), 400 + + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute(f'SELECT token FROM tokenAddressMapping WHERE tokenAddress="{floAddress}"') + tokenNames = c.fetchall() + c.execute(f"SELECT contractName, status, tokenIdentification, contractType, transactionHash, blockNumber, blockHash FROM activecontracts WHERE contractAddress='{floAddress}'") + incorporatedContracts = c.fetchall() + detailList = None + if len(tokenNames) != 0: + detailList = {} + for token in tokenNames: + token = token[0] + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tempdict = {} + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute(f'SELECT SUM(transferBalance) FROM activeTable WHERE address="{floAddress}"') + balance = c.fetchall()[0][0] + tempdict['balance'] = balance + tempdict['token'] = token + detailList[token] = tempdict + #else: + # # Address is not associated with any token + # return jsonify(description='FLO address is not associated with any tokens'), 404 + + incorporatedSmartContracts = None + if len(incorporatedContracts) > 0: + incorporatedSmartContracts = [] + for contract in incorporatedContracts: + tempdict = {} + tempdict['contractName'] = contract[0] + tempdict['contractAddress'] = floAddress + tempdict['status'] = contract[1] + tempdict['tokenIdentification'] = contract[2] + tempdict['contractType'] = contract[3] + tempdict['transactionHash'] = contract[4] + tempdict['blockNumber'] = contract[5] + tempdict['blockHash'] = contract[6] + incorporatedSmartContracts.append(tempdict) + + return jsonify(floAddress=floAddress, floAddressBalances=detailList, incorporatedSmartContracts=incorporatedSmartContracts), 200 + + +@app.route('/api/v2/floAddressBalance/', methods=['GET']) +async def floAddressBalance(floAddress): + if floAddress is None: + return jsonify(description='floAddress hasn\'t been passed'), 400 + # input validation + if not check_flo_address(floAddress, is_testnet): + return jsonify(description='floAddress validation failed'), 400 + + token = request.args.get('token') + if token is None: + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute(f'SELECT token FROM tokenAddressMapping WHERE tokenAddress="{floAddress}"') + tokenNames = c.fetchall() + + if len(tokenNames) != 0: + detailList = {} + for token in tokenNames: + token = token[0] + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tempdict = {} + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute(f'SELECT SUM(transferBalance) FROM activeTable WHERE address="{floAddress}"') + balance = c.fetchall()[0][0] + tempdict['balance'] = balance + tempdict['token'] = token + detailList[token] = tempdict + return jsonify(floAddress=floAddress, floAddressBalances=detailList), 200 + else: + # Address is not associated with any token + return jsonify(floAddress=floAddress, floAddressBalances={}), 200 + else: + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description="Token doesn't exist"), 404 + c.execute(f'SELECT SUM(transferBalance) FROM activeTable WHERE address="{floAddress}"') + balance = c.fetchall()[0][0] + conn.close() + return jsonify(floAddress=floAddress, token=token, balance=balance), 200 + + +@app.route('/api/v2/floAddressTransactions/', methods=['GET']) +async def floAddressTransactions(floAddress): + if floAddress is None: + return jsonify(description='floAddress has not been passed'), 400 + if not check_flo_address(floAddress, is_testnet): + return jsonify(description='floAddress validation failed'), 400 + limit = request.args.get('limit') + if limit is not None and not check_integer(limit): + return jsonify(description='limit validation failed'), 400 + + token = request.args.get('token') + if token is None: + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute('SELECT token FROM tokenAddressMapping WHERE tokenAddress="{}"'.format(floAddress)) + tokenNames = c.fetchall() + else: + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tokenNames = [[str(token), ]] + else: + return jsonify(description="Token doesn't exist"), 404 + + if len(tokenNames) != 0: + allTransactionList = [] + for tokenname in tokenNames: + tokenname = tokenname[0] + transactionJsonData = fetch_token_transactions(tokenname, senderFloAddress=floAddress, destFloAddress=floAddress, limit=limit) + allTransactionList = allTransactionList + transactionJsonData + + sortedFormattedTransactions = sort_transactions(allTransactionList) + if token is None: + return jsonify(floAddress=floAddress, transactions=sortedFormattedTransactions), 200 + else: + return jsonify(floAddress=floAddress, transactions=sortedFormattedTransactions, token=token), 200 + else: + return jsonify(floAddress=floAddress, transactions=[], token=token), 200 + + +# SMART CONTRACT APIs +@app.route('/api/v2/smartContractList', methods=['GET']) +async def getContractList_v2(): + contractName = request.args.get('contractName') + if contractName is not None: + contractName = contractName.strip().lower() + + # todo - Add validation for contractAddress and contractName to prevent SQL injection attacks + contractAddress = request.args.get('contractAddress') + if contractAddress is not None: + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + contractList = [] + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + smart_contracts = return_smart_contracts(c, contractName, contractAddress) + smart_contracts_morphed = smartcontract_morph_helper(smart_contracts) conn.close() - return jsonify(systemAddressCount=tokenAddressCount, systemBlockCount=validatedBlockCount, systemTransactionCount=validatedTransactionCount, systemSmartContractCount=contractCount, systemTokenCount=tokenCount, lastscannedblock=lastscannedblock, result='ok') + committeeAddressList = refresh_committee_list(APP_ADMIN, apiUrl, int(time.time())) + + return jsonify(smartContracts=smart_contracts_morphed, smartContractCommittee=committeeAddressList), 200 + + +@app.route('/api/v2/smartContractInfo', methods=['GET']) +async def getContractInfo_v2(): + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + if contractName is None: + return jsonify(description='Smart Contract\'s name hasn\'t been passed'), 400 + contractName = contractName.strip().lower() + + if contractAddress is None: + return jsonify(description='Smart Contract\'s address hasn\'t been passed'), 400 + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + contractStructure = fetchContractStructure(contractName, contractAddress) + if contractStructure: + returnval = contractStructure + # Categorize into what type of contract it is right now + if contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap': + conn, c = create_database_connection('smart_contract', {'contract_name': contractName, 'contract_address': contractAddress}) + + c.execute('SELECT COUNT(participantAddress), SUM(tokenAmount), SUM(winningAmount) FROM contractparticipants') + participation_details = c.fetchall() + + c.execute('SELECT depositAmount FROM contractdeposits') + deposit_details = c.fetchall() + + returnval['numberOfParticipants'] = participation_details[0][0] + returnval['totalParticipationAmount'] = participation_details[0][1] + returnval['totalHonorAmount'] = participation_details[0][2] + c.execute('SELECT COUNT(DISTINCT transactionHash) FROM contractdeposits') + returnval['numberOfDeposits'] = c.fetchall()[0][0] + c.execute('SELECT SUM(depositBalance) AS totalDepositBalance FROM contractdeposits c1 WHERE id = ( SELECT MAX(id) FROM contractdeposits c2 WHERE c1.transactionHash = c2.transactionHash);') + returnval['currentDepositBalance'] = c.fetchall()[0][0] + # todo - add code to token tracker to save continuos event subtype KEY as contractSubtype as part of contractStructure and remove the following line + returnval['contractSubtype'] = 'tokenswap' + returnval['priceType'] = returnval['pricetype'] + if returnval['pricetype'] not in ['predetermined']: + returnval['price'] = fetch_dynamic_swap_price(contractStructure, {'time': datetime.now().timestamp()}) + returnval['acceptingToken'] = returnval['accepting_token'] + returnval['sellingToken'] = returnval['selling_token'] + + elif contractStructure['contractType'] == 'one-time-event' and 'exitconditions' in contractStructure.keys(): + choice_list = [] + for obj_key in contractStructure['exitconditions'].keys(): + choice_list.append(contractStructure['exitconditions'][obj_key]) + returnval['userChoices'] = choice_list + returnval.pop('exitconditions') + + contract_status_time_info = fetch_contract_status_time_info(contractName, contractAddress) + if len(contract_status_time_info) == 1: + for status_time_info in contract_status_time_info: + returnval['status'] = status_time_info[0] + returnval['incorporationDate'] = status_time_info[1] + if status_time_info[2]: + returnval['expiryDate'] = status_time_info[2] + if status_time_info[3]: + returnval['closeDate'] = status_time_info[3] + # todo - add code to token tracker to save one-time-event subtype as part of contractStructure and remove the following line + returnval['contractSubtype'] = 'external-trigger' + elif contractStructure['contractType'] == 'one-time-event' and 'payeeAddress' in contractStructure.keys(): + contract_status_time_info = fetch_contract_status_time_info(contractName, contractAddress) + if len(contract_status_time_info) == 1: + for status_time_info in contract_status_time_info: + returnval['status'] = status_time_info[0] + returnval['incorporationDate'] = status_time_info[1] + if status_time_info[2]: + returnval['expiryDate'] = status_time_info[2] + if status_time_info[3]: + returnval['closeDate'] = status_time_info[3] + returnval['contractSubtype'] = 'time-trigger' + + return jsonify(contractName=contractName, contractAddress=contractAddress, contractInfo=returnval), 200 + else: + return jsonify(details="Smart Contract with the given name doesn't exist"), 404 + + +@app.route('/api/v2/smartContractParticipants', methods=['GET']) +async def getcontractparticipants_v2(): + contractName = request.args.get('contractName') + if contractName is None: + return jsonify(description='Smart Contract\'s name hasn\'t been passed'), 400 + contractName = contractName.strip().lower() + + contractAddress = request.args.get('contractAddress') + if contractAddress is None: + return jsonify(description='Smart Contract\'s address hasn\'t been passed'), 400 + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + filelocation = os.path.join(dbfolder, 'smartContracts', '{}-{}.db'.format(contractName, contractAddress)) + if os.path.isfile(filelocation): + # Make db connection and fetch data + contractStructure = fetchContractStructure(contractName, contractAddress) + contractStatus = fetchContractStatus(contractName, contractAddress) + conn, c = create_database_connection('smart_contract', {'contract_name': contractName, 'contract_address': contractAddress}) + if 'exitconditions' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + if contractStatus == 'closed': + token = contractStructure['tokenIdentification'] + c.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash, winningAmount FROM contractparticipants') + result = c.fetchall() + returnval = [] + for row in result: + # Check value of winning amount + c.execute(f'SELECT winningAmount FROM contractwinners WHERE referenceTxHash="{row[4]}"') + participant_winningAmount = c.fetchall() + if participant_winningAmount != []: + participant_winningAmount = participant_winningAmount[0][0] + else: + participant_winningAmount = 0 + participation = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], 'transactionHash': row[4], 'winningAmount': participant_winningAmount, 'tokenIdentification': token} + returnval.append(participation) + else: + c.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants') + result = c.fetchall() + conn.close() + returnval = [] + for row in result: + participation = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], 'transactionHash': row[4]} + returnval.append(participation) + return jsonify(contractName=contractName, contractAddress=contractAddress, contractType=contractStructure['contractType'], contractSubtype='external-trigger', participantInfo=returnval), 200 + elif 'payeeAddress' in contractStructure: + # contract is of the type internal trigger + c.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants') + result = c.fetchall() + conn.close() + returnval = [] + for row in result: + participation = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'transactionHash': row[4]} + returnval.append(participation) + return jsonify(contractName=contractName, contractAddress=contractAddress, contractType=contractStructure['contractType'], contractSubtype='time-trigger', participantInfo=returnval), 200 + elif contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap': + c.execute('SELECT * FROM contractparticipants') + contract_participants = c.fetchall() + returnval = [] + for row in contract_participants: + participation = { + 'participantFloAddress': row[1], + 'participationAmount': row[2], + 'swapPrice': float(row[3]), + 'transactionHash': row[4], + 'blockNumber': row[5], + 'blockHash': row[6], + 'swapAmount': row[7] + } + returnval.append(participation) + conn.close() + return jsonify(contractName=contractName, contractAddress=contractAddress, contractType=contractStructure['contractType'], contractSubtype=contractStructure['subtype'], participantInfo=returnval), 200 + else: + return jsonify(description='Smart Contract with the given name doesn\'t exist'), 404 + + +@app.route('/api/v2/participantDetails/', methods=['GET']) +async def participantDetails(floAddress): + if floAddress is None: + return jsonify(description='FLO address hasn\'t been passed'), 400 + if not check_flo_address(floAddress, is_testnet): + return jsonify(description='floAddress validation failed'), 400 + + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + # Url param checking + if contractName is None and contractAddress is None: + return jsonify(description='Pass both, contractName and contractAddress as url parameters'), 400 + elif contractName is None: + return jsonify(description='Pass contractName as url parameter'), 400 + else: + return jsonify(description='Pass contractAddress as url parameter'), 400 + + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + contractName = contractName.strip().lower() + contractAddress = contractAddress.strip() + + systemdb_location = os.path.join(dbfolder, 'system.db') + if os.path.isfile(systemdb_location): + # Make db connection and fetch data + systemdb_conn = sqlite3.connect(systemdb_location) + c = systemdb_conn.cursor() + if contractName is not None: + c.execute(f'SELECT * FROM contractAddressMapping WHERE address="{floAddress}" AND addressType="participant" AND contractName="{contractName}" AND contractAddress="{contractAddress}"') + else: + c.execute(f'SELECT * FROM contractAddressMapping WHERE address="{floAddress}" AND addressType="participant"') + participant_address_contracts = c.fetchall() + + if len(participant_address_contracts) != 0: + participationDetailsList = [] + for contract in participant_address_contracts: + detailsDict = {} + contract_db = os.path.join(dbfolder, 'smartContracts', f"{contract[3]}-{contract[4]}.db") + # Make db connection and fetch contract structure + contractdb_conn = sqlite3.connect(contract_db) + contract_c = contractdb_conn.cursor() + # Get details of the type of Smart Contract + contract_c.execute('SELECT attribute,value FROM contractstructure') + result = contract_c.fetchall() + + contractStructure = fetchContractStructure(contract[3], contract[4]) + contractDbName = '{}-{}.db'.format(contract[3], contract[4]) + + if contractStructure['contractType']=='continuos-event' and contractStructure['subtype']=='tokenswap': + # normal result + swap details + # what is a api detail + contract_c.execute('SELECT * FROM contractparticipants WHERE participantAddress=?',(floAddress,)) + participant_details = contract_c.fetchall() + if len(participant_details) > 0: + participationList = [] + for participation in participant_details: + detailsDict['participationAddress'] = floAddress + detailsDict['participationAmount'] = participation[2] + detailsDict['receivedAmount'] = float(participation[3]) + detailsDict['participationToken'] = contractStructure['accepting_token'] + detailsDict['receivedToken'] = contractStructure['selling_token'] + detailsDict['swapPrice_received_to_participation'] = float(participation[7]) + detailsDict['transactionHash'] = participation[4] + detailsDict['blockNumber'] = participation[5] + detailsDict['blockHash'] = participation[6] + participationList.append(detailsDict) + participationDetailsList.append(participationList) + + elif contractStructure['contractType']=='one-time-event' and 'payeeAddress' in contractStructure.keys(): + # normal results + detailsDict = {} + detailsDict['contractName'] = contract[3] + detailsDict['contractAddress'] = contract[4] + detailsDict['tokenAmount'] = contract[5] + detailsDict['transactionHash'] = contract[6] + + c.execute(f"SELECT status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate FROM activecontracts WHERE contractName='{detailsDict['contractName']}' AND contractAddress='{detailsDict['contractAddress']}'") + temp = c.fetchall() + detailsDict['status'] = temp[0][0] + detailsDict['tokenIdentification'] = temp[0][1] + detailsDict['contractType'] = temp[0][2] + detailsDict['blockNumber'] = temp[0][3] + detailsDict['blockHash'] = temp[0][4] + detailsDict['incorporationDate'] = temp[0][5] + if temp[0][6]: + detailsDict['expiryDate'] = temp[0][6] + if temp[0][7]: + detailsDict['closeDate'] = temp[0][7] + + # check if the contract has been closed + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + if 'payeeAddress' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + contract_c.execute(f"SELECT tokenAmount FROM contractparticipants WHERE participantAddress='{floAddress}'") + result = contract_c.fetchall() + detailsDict['tokenAmount'] = result[0][0] + + elif contractStructure['contractType']=='one-time-event' and 'exitconditions' in contractStructure.keys(): + # normal results + winning/losing details + detailsDict = {} + detailsDict['contractName'] = contract[3] + detailsDict['contractAddress'] = contract[4] + detailsDict['tokenAmount'] = contract[5] + detailsDict['transactionHash'] = contract[6] + + c.execute(f"SELECT status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate FROM activecontracts WHERE contractName='{detailsDict['contractName']}' AND contractAddress='{detailsDict['contractAddress']}'") + temp = c.fetchall() + detailsDict['status'] = temp[0][0] + detailsDict['tokenIdentification'] = temp[0][1] + detailsDict['contractType'] = temp[0][2] + detailsDict['blockNumber'] = temp[0][3] + detailsDict['blockHash'] = temp[0][4] + detailsDict['incorporationDate'] = temp[0][5] + if temp[0][6]: + detailsDict['expiryDate'] = temp[0][6] + if temp[0][7]: + detailsDict['closeDate'] = temp[0][7] + + # check if the contract has been closed + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # Make db connection and fetch data + contract_c.execute('SELECT attribute,value FROM contractstructure') + result = contract_c.fetchall() + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + if 'exitconditions' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + if detailsDict['status'] == 'closed': + contract_c.execute(f"SELECT userChoice, winningAmount FROM contractparticipants WHERE participantAddress='{floAddress}'") + result = contract_c.fetchall() + detailsDict['userChoice'] = result[0][0] + detailsDict['winningAmount'] = result[0][1] + else: + contract_c.execute(f"SELECT userChoice FROM contractparticipants WHERE participantAddress='{floAddress}'") + result = contract_c.fetchall() + detailsDict['userChoice'] = result[0][0] + participationDetailsList.append(detailsDict) + + return jsonify(floAddress=floAddress, type='participant', participatedContracts=participationDetailsList), 200 + else: + return jsonify(description="Address hasn't participated in any other contract"), 404 + else: + return jsonify(description='System error. System.db is missing. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api'), 500 + + +@app.route('/api/v2/smartContractTransactions', methods=['GET']) +async def smartcontracttransactions(): + contractName = request.args.get('contractName') + if contractName is None: + return jsonify(description='Smart Contract\'s name hasn\'t been passed'), 400 + contractName = contractName.strip().lower() + contractAddress = request.args.get('contractAddress') + if contractAddress is None: + return jsonify(description='Smart Contract\'s address hasn\'t been passed'), 400 + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + _from = int(request.args.get('_from', 1)) # Get page number, default is 1 + to = int(request.args.get('to', 100)) # Get limit, default is 10 + + if _from<1: + return jsonify(description='_from validation failed'), 400 + if to<1: + return jsonify(description='to validation failed'), 400 + + contractDbName = '{}-{}.db'.format(contractName, contractAddress) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + + if os.path.isfile(filelocation): + # Make db connection and fetch data + transactionJsonData = fetch_contract_transactions(contractName, contractAddress, _from, to) + transactionJsonData = sort_transactions(transactionJsonData) + return jsonify(contractName=contractName, contractAddress=contractAddress, contractTransactions=transactionJsonData), 200 + else: + return jsonify(description='Smart Contract with the given name doesn\'t exist'), 404 + + +# todo - add options to only ask for active/consumed/returned deposits +@app.route('/api/v2/smartContractDeposits', methods=['GET']) +async def smartcontractdeposits(): + # todo - put validation for transactionHash + contractName = request.args.get('contractName') + if contractName is None: + return jsonify(description='Smart Contract\'s name hasn\'t been passed'), 400 + contractName = contractName.strip().lower() + + contractAddress = request.args.get('contractAddress') + if contractAddress is None: + return jsonify(description='Smart Contract\'s address hasn\'t been passed'), 400 + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + contractDbName = '{}-{}.db'.format(contractName, contractAddress) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # active deposits + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('''SELECT depositorAddress, transactionHash, status, depositBalance FROM contractdeposits + WHERE (transactionHash, id) IN (SELECT transactionHash, MAX(id) FROM contractdeposits GROUP BY transactionHash) + ORDER BY id DESC; ''') + + distinct_deposits = c.fetchall() + deposit_info = [] + for a_deposit in distinct_deposits: + #c.execute(f"SELECT depositBalance FROM contractdeposits WHERE (transactionHash, id) IN (SELECT transactionHash, MIN(id) FROM contractdeposits GROUP BY transactionHash );") + c.execute(f"SELECT depositBalance, unix_expiryTime FROM contractdeposits WHERE transactionHash=='{a_deposit[1]}' ORDER BY id LIMIT 1") + original_deposit_balance = c.fetchall() + obj = { + 'depositorAddress': a_deposit[0], + 'transactionHash': a_deposit[1], + 'status': a_deposit[2], + 'originalBalance': original_deposit_balance[0][0], + 'currentBalance': a_deposit[3], + 'time': original_deposit_balance[0][1] + } + deposit_info.append(obj) + c.execute('SELECT SUM(depositBalance) AS totalDepositBalance FROM contractdeposits c1 WHERE id = ( SELECT MAX(id) FROM contractdeposits c2 WHERE c1.transactionHash = c2.transactionHash);') + currentDepositBalance = c.fetchall()[0][0] + return jsonify(currentDepositBalance=currentDepositBalance, depositInfo=deposit_info), 200 + else: + return jsonify(description='Smart Contract with the given name doesn\'t exist'), 404 + + +@app.route('/api/v2/blockDetails/', methods=['GET']) +async def blockdetails(blockHash): + # todo - validate blockHash + blockJson = blockdetailhelper(blockHash) + if len(blockJson) != 0: + blockJson = json.loads(blockJson[0][0]) + return jsonify(blockDetails=blockJson), 200 + else: + return jsonify(description='Block doesn\'t exist in database'), 404 + + +@app.route('/api/v2/transactionDetails/', methods=['GET']) +async def transactiondetails1(transactionHash): + # todo - validate transactionHash + transactionJsonData = transactiondetailhelper(transactionHash) + + if len(transactionJsonData) != 0: + transactionJson = json.loads(transactionJsonData[0][0]) + transactionJson = update_transaction_confirmations(transactionJson) + parseResult = json.loads(transactionJsonData[0][1]) + operation = transactionJsonData[0][2] + db_reference = transactionJsonData[0][3] + sender_address, receiver_address = extract_ip_op_addresses(transactionJson) + + mergeTx = {**parseResult, **transactionJson} + # TODO (CRITICAL): Write conditions to include and filter on chain and offchain transactions + mergeTx['onChain'] = True + + operationDetails = {} + if operation == 'smartContractDeposit': + # open the db reference and check if there is a deposit return + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + c.execute("SELECT depositAmount, blockNumber FROM contractdeposits WHERE status='deposit-return' AND transactionHash=?",(transactionJson['txid'],)) + returned_deposit_tx = c.fetchall() + if len(returned_deposit_tx) == 1: + operationDetails['returned_depositAmount'] = returned_deposit_tx[0][0] + operationDetails['returned_blockNumber'] = returned_deposit_tx[0][1] + c.execute("SELECT depositAmount, blockNumber FROM contractdeposits WHERE status='deposit-honor' AND transactionHash=?",(transactionJson['txid'],)) + deposit_honors = c.fetchall() + operationDetails['depositHonors'] = {} + operationDetails['depositHonors']['list'] = [] + operationDetails['depositHonors']['count'] = len(deposit_honors) + for deposit_honor in deposit_honors: + operationDetails['depositHonors']['list'].append({'honor_amount':deposit_honor[0],'blockNumber':deposit_honor[1]}) + + c.execute("SELECT depositBalance FROM contractdeposits WHERE id=(SELECT max(id) FROM contractdeposits WHERE transactionHash=?)",(transactionJson['txid'],)) + depositBalance = c.fetchall() + operationDetails['depositBalance'] = depositBalance[0][0] + operationDetails['consumedAmount'] = parseResult['depositAmount'] - operationDetails['depositBalance'] + + elif operation == 'tokenswap-participation': + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + c.execute('SELECT tokenAmount, winningAmount, userChoice FROM contractparticipants WHERE transactionHash=?',(transactionJson['txid'],)) + swap_amounts = c.fetchall() + c.execute("SELECT value FROM contractstructure WHERE attribute='selling_token'") + structure = c.fetchall() + operationDetails['participationAmount'] = swap_amounts[0][0] + operationDetails['receivedAmount'] = swap_amounts[0][1] + operationDetails['participationToken'] = parseResult['tokenIdentification'] + operationDetails['receivedToken'] = structure[0][0] + operationDetails['swapPrice_received_to_participation'] = float(swap_amounts[0][2]) + + elif operation == 'smartContractPays': + # Find what happened because of the trigger + # Find who + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + c.execute('SELECT participantAddress, tokenAmount, userChoice, winningAmount FROM contractparticipants WHERE winningAmount IS NOT NULL') + winner_participants = c.fetchall() + if len(winner_participants) != 0: + operationDetails['total_winners'] = len(winner_participants) + operationDetails['winning_choice'] = winner_participants[0][2] + operationDetails['winner_list'] = [] + for participant in winner_participants: + winner_details = {} + winner_details['participantAddress'] = participant[0] + winner_details['participationAmount'] = participant[1] + winner_details['winningAmount'] = participant[3] + operationDetails['winner_list'].append(winner_details) + + elif operation == 'ote-externaltrigger-participation': + # Find if this guy has won + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + c.execute('SELECT winningAmount FROM contractparticipants WHERE transactionHash=?',(transactionHash,)) + winningAmount = c.fetchall() + if winningAmount[0][0] is not None: + operationDetails['winningAmount'] = winningAmount[0][0] + + elif operation == 'tokenswapParticipation': + contractName, contractAddress = db_reference.rsplit('-',1) + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + txhash_txs = fetch_swap_contract_transactions(contractName, contractAddress, transactionHash) + mergeTx['subTransactions'] = [] + for transaction in txhash_txs: + if transaction['onChain'] == False: + mergeTx['subTransactions'].append(transaction) + + mergeTx['operation'] = operation + mergeTx['operationDetails'] = operationDetails + return jsonify(mergeTx), 200 + else: + return jsonify(description='Transaction doesn\'t exist in database'), 404 + + +@app.route('/api/v2/latestTransactionDetails', methods=['GET']) +async def latestTransactionDetails(): + limit = request.args.get('limit') + if limit is not None and not check_integer(limit): + return jsonify(description='limit validation failed'), 400 + + dblocation = dbfolder + '/latestCache.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description='Latest transactions db doesn\'t exist. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api'), 500 + + if limit is not None: + c.execute('SELECT * FROM latestTransactions WHERE blockNumber IN (SELECT DISTINCT blockNumber FROM latestTransactions ORDER BY blockNumber DESC LIMIT {}) ORDER BY id DESC;'.format(int(limit))) + else: + c.execute('''SELECT * FROM latestTransactions WHERE blockNumber IN (SELECT DISTINCT blockNumber FROM latestTransactions ORDER BY blockNumber DESC) ORDER BY id DESC;''') + latestTransactions = c.fetchall() + c.close() + tx_list = [] + for idx, item in enumerate(latestTransactions): + item = list(item) + tx_parsed_details = {} + tx_parsed_details['transactionDetails'] = json.loads(item[3]) + tx_parsed_details['transactionDetails'] = update_transaction_confirmations(tx_parsed_details['transactionDetails']) + tx_parsed_details['parsedFloData'] = json.loads(item[5]) + tx_parsed_details['parsedFloData']['transactionType'] = item[4] + tx_parsed_details['transactionDetails']['blockheight'] = int(item[2]) + tx_parsed_details = {**tx_parsed_details['transactionDetails'], **tx_parsed_details['parsedFloData']} + # TODO (CRITICAL): Write conditions to include and filter on chain and offchain transactions + tx_parsed_details['onChain'] = True + tx_list.append(tx_parsed_details) + return jsonify(latestTransactions=tx_list), 200 + + +@app.route('/api/v2/latestBlockDetails', methods=['GET']) +async def latestBlockDetails(): + limit = request.args.get('limit') + if limit is not None and not check_integer(limit): + return jsonify(description='limit validation failed'), 400 + + dblocation = dbfolder + '/latestCache.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description='Latest transactions db doesn\'t exist. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api'), 404 + + if limit is None: + c.execute('''SELECT jsonData FROM ( SELECT * FROM latestBlocks ORDER BY blockNumber DESC LIMIT 4) ORDER BY id DESC;''') + else: + c.execute(f'SELECT jsonData FROM ( SELECT * FROM latestBlocks ORDER BY blockNumber DESC LIMIT {limit}) ORDER BY id DESC;') + latestBlocks = c.fetchall() + c.close() + + templst = [] + for idx, item in enumerate(latestBlocks): + templst.append(json.loads(item[0])) + + return jsonify(latestBlocks=templst), 200 + + +@app.route('/api/v2/blockTransactions/', methods=['GET']) +async def blocktransactions(blockHash): + blockJson = blockdetailhelper(blockHash) + if len(blockJson) != 0: + blockJson = json.loads(blockJson[0][0]) + blocktxlist = blockJson['txs'] + blocktxs = [] + for i in range(len(blocktxlist)): + temptx = transactiondetailhelper(blocktxlist[i]['txid']) + transactionJson = json.loads(temptx[0][0]) + parseResult = json.loads(temptx[0][1]) + blocktxs.append({**parseResult , **transactionJson}) + + # TODO (CRITICAL): Write conditions to include and filter on chain and offchain transactions + #blocktxs['onChain'] = True + return jsonify(transactions=blocktxs, blockKeyword=blockHash), 200 + else: + return jsonify(description='Block doesn\'t exist in database'), 404 + + +@app.route('/api/v2/categoriseString/') +async def categoriseString_v2(urlstring): + # check if the hash is of a transaction + response = requests.get('{}api/v1/tx/{}'.format(apiUrl, urlstring)) + if response.status_code == 200: + return jsonify(type='transaction'), 200 + else: + response = requests.get('{}api/v1/block/{}'.format(apiUrl, urlstring)) + if response.status_code == 200: + return jsonify(type='block'), 200 + else: + # check urlstring is a token name + tokenfolder = os.path.join(dbfolder, 'tokens') + onlyfiles = [f[:-3] + for f in os.listdir(tokenfolder) if os.path.isfile(os.path.join(tokenfolder, f))] + if urlstring.lower() in onlyfiles: + return jsonify(type='token'), 200 + else: + systemdb = os.path.join(dbfolder, 'system.db') + conn = sqlite3.connect(systemdb) + conn.row_factory = lambda cursor, row: row[0] + c = conn.cursor() + contractList = c.execute('select contractname from activeContracts').fetchall() + + if urlstring.lower() in contractList: + return jsonify(type='smartContract'), 200 + else: + return jsonify(type='noise'), 200 + + +@app.route('/api/v2/tokenSmartContractList', methods=['GET']) +async def tokenSmartContractList(): + # list of tokens + filelist = [] + for item in os.listdir(os.path.join(dbfolder, 'tokens')): + if os.path.isfile(os.path.join(dbfolder, 'tokens', item)): + filelist.append(item[:-3]) + + # list of smart contracts + contractName = request.args.get('contractName') + if contractName is not None: + contractName = contractName.strip().lower() + + # todo - Add validation for contractAddress and contractName to prevent SQL injection attacks + contractAddress = request.args.get('contractAddress') + if contractAddress is not None: + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + smart_contracts = return_smart_contracts(c, contractName, contractAddress) + smart_contracts_morphed = smartcontract_morph_helper(smart_contracts) + conn.close() + + committeeAddressList = refresh_committee_list(APP_ADMIN, apiUrl, int(time.time())) + return jsonify(tokens=filelist, smartContracts=smart_contracts_morphed, smartContractCommittee=committeeAddressList), 200 class ServerSentEvent: - def __init__( self, data: str, @@ -1097,65 +2619,22 @@ async def sse(): response.timeout = None return response -@app.route('/api/v2.0/flocoreHeight') -async def flocoreHeight_func(): - testnet = request.args.get('testnet') - if testnet == None: - blockchaininfo = subprocess.check_output(f"{FLO_CLI_PATH} --datadir={FLO_DATA_DIR} getblockchaininfo", shell=True) - else: - blockchaininfo = subprocess.check_output([f"{FLO_CLI_PATH} --testnet --datadir={FLO_DATA_DIR} getblockchaininfo"], shell=True) - blockchaininfo = json.loads(blockchaininfo) - return jsonify(chain=blockchaininfo["chain"], blocks=blockchaininfo["blocks"],headers=blockchaininfo["headers"],bestblockhash=blockchaininfo["bestblockhash"],difficulty=blockchaininfo["difficulty"],mediantime=blockchaininfo["mediantime"]) - - -@app.route('/api/v1.0/getPrices', methods=['GET']) -async def getPriceData(): +@app.route('/api/v2/prices', methods=['GET']) +async def priceData(): # read system.db for price data conn = sqlite3.connect('system.db') c = conn.cursor() ratepairs = c.execute('select ratepair, price from ratepairs') ratepairs = ratepairs.fetchall() prices = {} - for ratepair in ratepairs: ratepair = list(ratepair) prices[ratepair[0]] = ratepair[1] - - return jsonify(prices=prices, result='ok') + return jsonify(prices=prices), 200 -''' Stuff required for getPrices endpoint ''' - -def updatePrices(): - prices = {} - # USD -> INR - response = requests.get(f"https://api.exchangerate-api.com/v4/latest/usd") - price = response.json() - prices['USDINR'] = price['rates']['INR'] - - # Blockchain stuff : BTC,FLO -> USD,INR - # BTC->USD | BTC->INR - response = requests.get(f"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,flo&vs_currencies=usd,inr") - price = response.json() - prices['BTCUSD'] = price['bitcoin']['usd'] - prices['BTCINR'] = price['bitcoin']['inr'] - - # FLO->USD | FLO->INR - response = requests.get(f"https://api.coinlore.net/api/ticker/?id=67") - price = response.json() - prices["FLOUSD"] = price[0]['price_usd'] - prices["FLOINR"] = float(prices["FLOUSD"]) * float(prices['USDINR']) - - # 3. update latest price data - print('Prices updated at time: %s' % datetime.now()) - print(prices) - - conn = sqlite3.connect('system.db') - c = conn.cursor() - for pair in list(prices.items()): - pair = list(pair) - c.execute(f"UPDATE ratepairs SET price={pair[1]} WHERE ratepair='{pair[0]}'") - conn.commit() +####################### +####################### # if system.db isn't present, initialize it if not os.path.isfile(f"system.db"): @@ -1177,10 +2656,10 @@ if not os.path.isfile(f"system.db"): # assign a scheduler for updating prices in the background scheduler = BackgroundScheduler() -scheduler.add_job(func=updatePrices, trigger="interval", seconds=600) +scheduler.add_job(func=updatePrices, trigger="interval", seconds=7200) scheduler.start() # Shut down the scheduler when exiting the app atexit.register(lambda: scheduler.shutdown()) if __name__ == "__main__": - app.run(debug=False, host='0.0.0.0', port=5010) + app.run(debug=debug_status, host=HOST, port=PORT) diff --git a/requirements.txt b/requirements.txt index 00cf4dd..af06d01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aiofiles==0.4.0 +aiofiles APScheduler==3.9.1 arrow==0.15.2 blinker==1.4 @@ -17,7 +17,7 @@ Jinja2==2.11.3 MarkupSafe==1.1.1 multidict==4.5.2 priority==1.3.0 -pybtc==2.0.9 +pyflo-lib==2.0.9 pycparser==2.19 python-dateutil==2.8.0 Quart==0.10.0