flo-token-tracking/ranchimallflo_api.py
tripathyr a6364d3208
Update ranchimallflo_api.py
Added testnet address checking for smartcontracts
2024-12-01 12:29:51 +05:30

4459 lines
185 KiB
Python

from collections import defaultdict
import json
import os
import requests
import sys
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 asyncio
from typing import Optional
from config import *
import parsing
import subprocess
import pyflo
from operator import itemgetter
import pdb
import ast
import time
#MYSQL ENHANCEMENTS START
import configparser
import aiohttp
import aiomysql
import asyncio
import atexit
import pymysql
from dbutils.pooled_db import PooledDB
from apscheduler.schedulers.background import BackgroundScheduler
import logging
# Configuration Setup
config = configparser.ConfigParser()
config.read('config.ini')
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Quart(__name__)
app.clients = set()
app = cors(app, allow_origin="*")
API_TIMEOUT = int(config['API']['API_TIMEOUT']) # 1 second
RETRY_TIMEOUT_LONG = int(config['API']['RETRY_TIMEOUT_LONG']) # 30 minutes
RETRY_TIMEOUT_SHORT = int(config['API']['RETRY_TIMEOUT_SHORT']) # 1 minute
DB_RETRY_TIMEOUT = int(config['API']['DB_RETRY_TIMEOUT']) # 1 minute
# Global connection pools
sync_connection_pool = None
async_connection_pool = None
class MySQLConfig:
def __init__(self):
self.username = config['MYSQL'].get('USERNAME', 'default_user')
self.password = config['MYSQL'].get('PASSWORD', 'default_password')
self.host = config['MYSQL'].get('HOST', 'localhost')
self.database_prefix = config['MYSQL'].get('DATABASE_PREFIX', 'rm')
mysql_config = MySQLConfig()
net = config['DEFAULT'].get('NET', 'mainnet') # Default to 'mainnet' if not set
# Initialize connection pools
async def initialize_connection_pools():
global sync_connection_pool, async_connection_pool
try:
# Sync connection pool
sync_connection_pool = PooledDB(
creator=pymysql,
maxconnections=10,
mincached=2,
maxcached=5,
blocking=True,
host=mysql_config.host,
user=mysql_config.username,
password=mysql_config.password,
charset="utf8mb4",
)
# Async connection pool
async_connection_pool = await aiomysql.create_pool(
host=mysql_config.host,
user=mysql_config.username,
password=mysql_config.password,
db=None,
charset="utf8mb4",
maxsize=10,
minsize=2,
loop=asyncio.get_event_loop(),
)
print("Connection pools initialized successfully.")
except Exception as e:
print(f"Error initializing connection pools: {e}")
raise RuntimeError("Failed to initialize database connection pools")
@app.before_serving
async def before_serving():
await initialize_connection_pools()
print("Connection pools initialized before serving requests.")
@app.after_serving
async def after_serving():
if async_connection_pool:
async_connection_pool.close()
await async_connection_pool.wait_closed()
print("Async connection pool closed.")
async def get_mysql_connection(db_name, no_standardize=False, USE_ASYNC=False):
logger.info("ABC: Entering get_mysql_connection function")
conn = None # Initialize conn to None to avoid "referenced before assignment"
logger.info("ABC: Initialized conn to None")
db_name = standardize_db_name(db_name) if not no_standardize else db_name
logger.info(f"ABC: Database name standardized to: {db_name}")
try:
logger.info("ABC: Checking USE_ASYNC flag")
if USE_ASYNC:
logger.info("ABC: Async mode enabled")
if not async_connection_pool:
logger.error("ABC: Async connection pool not initialized")
raise RuntimeError("Async connection pool not initialized")
logger.info("ABC: Acquiring async connection")
conn = await async_connection_pool.acquire() # Acquire connection
if conn is None:
logger.error("ABC: Failed to acquire a valid connection")
raise RuntimeError("Failed to acquire a valid async connection")
logger.info("ABC: Async connection acquired successfully")
async with conn.cursor() as cursor:
logger.info("ABC: Creating async cursor and selecting database")
await cursor.execute(f"USE `{db_name}`") # Use escaped database name
logger.info(f"ABC: Database `{db_name}` selected successfully in async mode")
return conn # Return connection for further use
else:
logger.info("ABC: Sync mode enabled")
if not sync_connection_pool:
logger.error("ABC: Sync connection pool not initialized")
raise RuntimeError("Sync connection pool not initialized")
logger.info("ABC: Acquiring sync connection")
conn = sync_connection_pool.connection() # Acquire sync connection
if conn is None:
logger.error("ABC: Failed to acquire a valid connection")
raise RuntimeError("Failed to acquire a valid sync connection")
logger.info("ABC: Sync connection acquired successfully")
conn.select_db(db_name) # Select database
logger.info(f"ABC: Database `{db_name}` selected successfully in sync mode")
return conn # Return connection for further use
except Exception as e:
logger.error(f"ABC: Error in database connection: {e}")
raise
def is_backend_ready():
"""
Dummy function that always indicates the backend is ready.
"""
return True
#MYSQL ENHANCEMENTS END
INTERNAL_ERROR = "Unable to process request, try again later"
BACKEND_NOT_READY_ERROR = "Server is still syncing, try again later!"
BACKEND_NOT_READY_WARNING = "Server is still syncing, data may not be final"
# Global values and configg
internalTransactionTypes = [ 'tokenswapDepositSettlement', 'tokenswapParticipationSettlement', 'smartContractDepositReturn']
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)
""" ??? NOT USED???
# Helper functions
def retryRequest(tempserverlist, apicall):
if len(tempserverlist) != 0:
try:
response = requests.get('{}api/{}'.format(tempserverlist[0], apicall))
except:
tempserverlist.pop(0)
return retryRequest(tempserverlist, apicall)
else:
if response.status_code == 200:
return json.loads(response.content)
else:
tempserverlist.pop(0)
return retryRequest(tempserverlist, apicall)
else:
print("None of the APIs are responding for the call {}".format(apicall))
sys.exit(0)
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/']
if net == 'mainnet':
return retryRequest(mainserverlist, apicall)
elif net == 'testnet':
return retryRequest(testserverlist, apicall)
"""
async def blockdetailhelper(blockdetail):
# Determine whether the input is blockHash or blockHeight
if blockdetail.isdigit():
blockHash = None
blockHeight = int(blockdetail)
else:
blockHash = str(blockdetail)
blockHeight = None
# Get the database connection for the "latestCache" DB asynchronously
conn = await get_mysql_connection("latestCache", USE_ASYNC=True) # Use the async connection pool
try:
async with conn.cursor() as cursor:
# Query the database based on blockHash or blockHeight
if blockHash:
query = "SELECT jsonData FROM latestBlocks WHERE blockHash = %s"
await cursor.execute(query, (blockHash,))
elif blockHeight:
query = "SELECT jsonData FROM latestBlocks WHERE blockNumber = %s"
await cursor.execute(query, (blockHeight,))
else:
raise ValueError("Invalid blockdetail input. Must be blockHash or blockHeight.")
# Fetch the result asynchronously
result = await cursor.fetchall()
except aiomysql.MySQLError as e:
print(f"Error querying database: {e}")
result = []
finally:
# Release the connection back to the pool
await conn.commit()
return result
async def transactiondetailhelper(transactionHash):
# Get the database connection for the "latestCache" DB asynchronously
conn = await get_mysql_connection("latestCache", USE_ASYNC=True) # Use the async connection pool
try:
async with conn.cursor() as cursor:
# Query the database for the transaction hash
query = """
SELECT jsonData, parsedFloData, transactionType, db_reference
FROM latestTransactions
WHERE transactionHash = %s
"""
await cursor.execute(query, (transactionHash,))
# Fetch the result asynchronously
transactionJsonData = await cursor.fetchall()
except aiomysql.MySQLError as e:
print(f"Error querying database: {e}")
transactionJsonData = []
finally:
# Release the connection back to the pool
await conn.commit()
return transactionJsonData
#ATTEMPT 1
# async def update_transaction_confirmations(transactionJson):
# url = f"{apiUrl}api/v1/tx/{transactionJson['txid']}"
# try:
# async with aiohttp.ClientSession() as session:
# async with session.get(url) as response:
# if response.status == 200:
# response_data = await response.json()
# transactionJson['confirmations'] = response_data['confirmations']
# except Exception as e:
# print(f"Error fetching transaction confirmation: {e}")
# return transactionJson
# ATTEMPT 2
# async def update_transaction_confirmations(transactionJson):
# try:
# # Simulate the response without making an actual API call as it is slowing down
# transactionJson['confirmations'] = transactionJson['confirmations']
# logger.info(f"Mock confirmation set for transaction {transactionJson['txid']}: {transactionJson['confirmations']} confirmations")
# except Exception as e:
# print(f"Error updating transaction confirmation: {e}")
# return transactionJson
#ATTEMPT 3
# async def update_transaction_confirmations(transactionJson):
# url = f"{apiUrl}api/v1/tx/{transactionJson['txid']}"
# try:
# timeout = aiohttp.ClientTimeout(total=API_TIMEOUT)
# async with aiohttp.ClientSession(timeout=timeout) as session:
# async with session.get(url) as response:
# if response.status == 200:
# response_data = await response.json()
# transactionJson['confirmations'] = response_data['confirmations']
# else:
# print(f"API error: {response.status}")
# except asyncio.TimeoutError:
# print(f"Request timed out after {API_TIMEOUT} seconds")
# except Exception as e:
# print(f"Error fetching transaction confirmation: {e}")
# return transactionJson
async def update_transaction_confirmations(transactionJson):
if transactionJson.get('confirmations', 0) >= 100:
return transactionJson
url = f"{apiUrl}api/v1/tx/{transactionJson['txid']}"
try:
timeout = aiohttp.ClientTimeout(total=API_TIMEOUT)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url) as response:
if response.status == 200:
response_data = await response.json()
transactionJson['confirmations'] = response_data['confirmations']
else:
print(f"API error: {response.status}")
except asyncio.TimeoutError:
print(f"Request timed out after {API_TIMEOUT} seconds")
except Exception as e:
print(f"Error fetching transaction confirmation: {e}")
return transactionJson
async 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]
# Awaiting async calls
contractStructure = await 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'] = await fetch_dynamic_swap_price(contractStructure, {'time': datetime.now().timestamp()})
else:
contractDict['price'] = contractStructure['price']
elif contractDict['contractType'] == 'one-time-event':
contractDict['tokenIdentification'] = contract[4]
# Awaiting async call
contractStructure = await fetchContractStructure(contractDict['contractName'], contractDict['contractAddress'])
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 return_smart_contracts(connection, contractName=None, contractAddress=None):
cursor = connection.cursor()
query = """
SELECT * FROM activecontracts
WHERE id IN (
SELECT MAX(id) FROM activecontracts GROUP BY contractName, contractAddress
)
"""
conditions = []
params = []
if contractName:
conditions.append("contractName = %s")
params.append(contractName)
if contractAddress:
conditions.append("contractAddress = %s")
params.append(contractAddress)
if conditions:
query += " AND " + " AND ".join(conditions)
try:
cursor.execute(query, params)
smart_contracts = cursor.fetchall()
except Exception as e:
print(f"Error fetching smart contracts: {e}")
smart_contracts = []
finally:
cursor.close()
return smart_contracts
async def fetchContractStructure(contractName, contractAddress):
"""
Fetches the structure of a smart contract from the MySQL database.
Args:
contractName (str): The name of the contract.
contractAddress (str): The address of the contract.
Returns:
dict: The contract structure if found, or 0 if the database does not exist.
"""
# Construct the database name dynamically
db_name = f"{contractName.strip()}_{contractAddress.strip()}"
# Get the database connection asynchronously
conn = await get_mysql_connection(db_name, USE_ASYNC=True) # Use async connection pool
try:
# Use async cursor
async with conn.cursor() as cursor:
# Fetch contract structure from the database asynchronously
await cursor.execute('SELECT attribute, value FROM contractstructure')
result = await cursor.fetchall()
contractStructure = {}
conditionDict = {}
counter = 0
for item in result:
attribute, value = item
if attribute == 'exitconditions':
conditionDict[counter] = value
counter += 1
else:
contractStructure[attribute] = value
if conditionDict:
contractStructure['exitconditions'] = conditionDict
# Convert specific fields to appropriate data types
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
except aiomysql.MySQLError as e:
print(f"Database error while fetching contract structure: {e}")
return 0
except Exception as e:
print(f"Unexpected error: {e}")
return 0
finally:
# Release the connection back to the pool (commit if necessary)
await conn.commit()
async def fetchContractStatus(contractName, contractAddress):
try:
# Get the system database connection asynchronously
conn = await get_mysql_connection('system', USE_ASYNC=True) # Using async connection pool
async with conn.cursor() as cursor:
# Query to fetch the contract status
query = """
SELECT status
FROM activecontracts
WHERE contractName = %s AND contractAddress = %s
ORDER BY id DESC
LIMIT 1
"""
await cursor.execute(query, (contractName, contractAddress))
status = await cursor.fetchone()
# Return the status if found, otherwise None
if status:
return status[0]
else:
return None
except aiomysql.MySQLError as e:
print(f"Database error while fetching contract status: {e}")
return None
except Exception as e:
print(f"Unexpected error: {e}")
return None
finally:
# Ensure the connection is released back to the pool
await conn.commit() # Commit if needed (to handle connection lifecycle)
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
async def updatePrices():
"""
Updates the latest price data for various currency pairs in the MySQL database asynchronously.
"""
prices = {}
# USD -> INR
try:
async with aiohttp.ClientSession() as session:
async with session.get("https://api.exchangerate-api.com/v4/latest/usd", timeout=10) as response:
if response.status == 200:
price = await response.json()
prices['USDINR'] = price['rates']['INR']
except Exception as e:
print(f"Error fetching USD to INR exchange rate: {e}")
# Blockchain stuff: BTC, FLO -> USD, INR
# BTC -> USD | BTC -> INR
try:
async with aiohttp.ClientSession() as session:
async with session.get("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,flo&vs_currencies=usd,inr", timeout=10) as response:
if response.status == 200:
price = await response.json()
prices['BTCUSD'] = price['bitcoin']['usd']
prices['BTCINR'] = price['bitcoin']['inr']
except Exception as e:
print(f"Error fetching BTC prices: {e}")
# FLO -> USD | FLO -> INR
try:
async with aiohttp.ClientSession() as session:
async with session.get("https://api.coinlore.net/api/ticker/?id=67", timeout=10) as response:
if response.status == 200:
price = await response.json()
prices["FLOUSD"] = float(price[0]['price_usd'])
if 'USDINR' in prices:
prices["FLOINR"] = float(prices["FLOUSD"]) * float(prices['USDINR'])
except Exception as e:
print(f"Error fetching FLO prices: {e}")
# Log the updated prices
print('Prices updated at time: %s' % datetime.now())
print(prices)
# Update prices in the database asynchronously
try:
# Use async MySQL connection
conn = await get_mysql_connection('system', USE_ASYNC=True)
async with conn.cursor() as cursor:
# Update each rate pair in the database asynchronously
for pair, price in prices.items():
await cursor.execute(
"UPDATE ratepairs SET price = %s WHERE ratepair = %s",
(price, pair)
)
await conn.commit()
print("Prices successfully updated in the database.")
except aiomysql.MySQLError as e:
print(f"Database error while updating prices: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
async def fetch_dynamic_swap_price(contractStructure, blockinfo):
oracle_address = contractStructure['oracle_address']
print(f'Oracle address is: {oracle_address}')
async def send_api_request(url):
timeout = aiohttp.ClientTimeout(total=API_TIMEOUT)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url) as response:
if response.status == 200:
return await response.json()
else:
print(f'API error: {response.status}')
return None
except asyncio.TimeoutError:
print(f"Request timed out after {API_TIMEOUT} seconds")
return None
except Exception as e:
print(f"Error during API request: {e}")
return None
try:
# Fetch transactions associated with the oracle address
url = f'{apiUrl}api/v1/addr/{oracle_address}'
response_data = await send_api_request(url)
if response_data is None:
return None
if 'transactions' not in response_data:
return float(contractStructure['price'])
transactions = response_data['transactions']
for transaction_hash in transactions:
transaction_url = f'{apiUrl}api/v1/tx/{transaction_hash}'
transaction_response = await send_api_request(transaction_url)
if transaction_response is None:
continue
transaction = transaction_response
floData = transaction.get('floData', None)
if not floData or transaction['time'] >= blockinfo['time']:
continue
try:
sender_address, receiver_address = find_sender_receiver(transaction)
assert receiver_address == contractStructure['contractAddress']
assert sender_address == oracle_address
floData = json.loads(floData)
assert floData['price-update']['contract-name'] == contractStructure['contractName']
assert floData['price-update']['contract-address'] == contractStructure['contractAddress']
return float(floData['price-update']['price'])
except Exception as e:
print(f"Error processing transaction: {e}")
continue
# If no matching transaction is found, return the default price
return float(contractStructure['price'])
except Exception as e:
print(f"Error in fetch_dynamic_swap_price: {e}")
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["addr"], 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"]["type"] == "pubkeyhash":
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]
async def fetch_contract_status_time_info(contractName, contractAddress):
try:
# Connect to the system database asynchronously
conn = await get_mysql_connection('system', USE_ASYNC=True) # Use async connection pool
async with conn.cursor() as cursor:
# Query to fetch contract status and time info
query = """
SELECT status, incorporationDate, expiryDate, closeDate
FROM activecontracts
WHERE contractName = %s AND contractAddress = %s
ORDER BY id DESC
LIMIT 1
"""
await cursor.execute(query, (contractName, contractAddress))
contract_status_time_info = await cursor.fetchone()
# Return the result or an empty list if no data is found
return contract_status_time_info if contract_status_time_info else []
except aiomysql.MySQLError as e:
print(f"Database error while fetching contract status and time info: {e}")
return []
except Exception as e:
print(f"Unexpected error: {e}")
return []
finally:
# Ensure that the connection is properly released
await conn.commit()
def checkIF_commitee_trigger_tranasaction(transactionDetails):
if transactionDetails[3] == 'trigger':
pass
async 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 = {
'senderAddress': row[4],
'receiverAddress': row[5],
'tokenAmount': row[6],
'tokenIdentification': row[7],
'contractName': parsedFloData['contractName'],
'transactionTrigger': transactionDetails['txid'],
'time': transactionDetails['time'],
'type': row[3],
'onChain': False
}
transactions_object = internal_info
else:
transactions_object = {**parsedFloData, **transactionDetails}
# Awaiting the asynchronous update_transaction_confirmations function
transactions_object = await update_transaction_confirmations(transactions_object)
transactions_object['onChain'] = True
rowarray_list.append(transactions_object)
return rowarray_list
def standardize_db_name(db_name):
"""
Ensures the database name has the proper prefix and suffix.
Args:
db_name (str): The logical database name.
Returns:
str: The standardized database name.
"""
if not (db_name.startswith(f"{mysql_config.database_prefix}_") and db_name.endswith("_db")):
db_name = f"{mysql_config.database_prefix}_{db_name}_db"
return db_name
async def fetch_transactions_from_token(token_name, floAddress, limit=None):
"""
Fetch transactions for a specific token and FLO address.
Args:
token_name (str): The name of the token.
floAddress (str): The FLO address.
limit (int, optional): Maximum number of transactions to fetch.
Returns:
list: List of transactions for the token.
"""
token_db_name = standardize_db_name(token_name)
try:
# Acquire a connection using `async with` to manage cleanup
async with await get_mysql_connection(token_db_name, USE_ASYNC=True) as conn:
# Create a cursor using `async with` for cleanup
async with conn.cursor(dictionary=True) as cursor:
# Query to fetch transactions
query = """
SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress,
transferAmount, %s AS token, '' AS transactionSubType
FROM transactionHistory
WHERE sourceFloAddress = %s OR destFloAddress = %s
"""
parameters = [token_name, floAddress, floAddress]
# Add LIMIT clause if provided
if limit is not None:
query += " LIMIT %s"
parameters.append(limit)
# Execute the query and fetch all results
await cursor.execute(query, parameters)
return await cursor.fetchall()
except Exception as e:
# Log the error and re-raise it
logger.error(f"Error in fetch_transactions_from_token: {e}", exc_info=True)
raise
async def fetch_token_transactions(tokens, senderFloAddress=None, destFloAddress=None, limit=None, use_and=False):
"""
Fetch transactions for multiple tokens (or a single token).
Args:
tokens (list or str): List of token names or a single token name.
senderFloAddress (str, optional): Sender FLO address.
destFloAddress (str, optional): Destination FLO address.
limit (int, optional): Maximum number of transactions.
use_and (bool, optional): Use AND or OR for filtering.
Returns:
list: Combined list of transactions for all tokens.
"""
# Automatically convert a single token name into a list
if isinstance(tokens, str):
tokens = [tokens]
if not tokens or not isinstance(tokens, list):
return jsonify(description="Invalid or missing tokens"), 400
try:
# Fetch transactions in parallel for all tokens
tasks = [
fetch_transactions_from_token(
token,
senderFloAddress or destFloAddress, # Use either sender or destination address
limit
)
for token in tokens
]
results = await asyncio.gather(*tasks)
# Combine and process results
all_transactions = []
for result in results:
all_transactions.extend(result)
# Post-process and return transactions
return await transaction_post_processing(all_transactions)
except Exception as e:
print(f"Unexpected error while fetching token transactions: {e}")
return jsonify(description="Unexpected error occurred"), 500
async def fetch_contract_transactions(contractName, contractAddress, _from=0, to=100, USE_ASYNC=False):
"""
Fetches transactions related to a smart contract and associated tokens asynchronously.
Args:
contractName (str): Name of the smart contract.
contractAddress (str): Address of the smart contract.
_from (int, optional): Starting index for transactions. Defaults to 0.
to (int, optional): Ending index for transactions. Defaults to 100.
USE_ASYNC (bool, optional): Flag to use async connection.
Returns:
list: Processed transactions.
"""
try:
# Standardize smart contract database name
sc_db_name = standardize_db_name(f"{contractName}_{contractAddress}")
# Fetch contract structure asynchronously
contractStructure = await fetchContractStructure(contractName, contractAddress)
if not contractStructure:
return jsonify(description="Invalid contract structure"), 404
transactionJsonData = []
creation_tx_query = """
SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress,
transferAmount, '' AS token, transactionSubType
FROM contractTransactionHistory
ORDER BY id
LIMIT 1;
"""
# Open the connection asynchronously (using the appropriate connection pool)
conn_sc = await get_mysql_connection(sc_db_name, USE_ASYNC=USE_ASYNC)
async with conn_sc.cursor() as cursor_sc:
# Fetch creation transaction asynchronously
await cursor_sc.execute(creation_tx_query)
creation_tx = await cursor_sc.fetchall()
transactionJsonData = creation_tx
# Fetch token transactions concurrently for continuous or one-time event contracts
if contractStructure['contractType'] == 'continuos-event':
token1 = contractStructure['accepting_token']
token2 = contractStructure['selling_token']
# Fetch transactions for token1 and token2 concurrently
transaction_results_token1, transaction_results_token2 = await asyncio.gather(
fetch_token_transactions_for_contract(token1, sc_db_name, _from, to, USE_ASYNC),
fetch_token_transactions_for_contract(token2, sc_db_name, _from, to, USE_ASYNC)
)
# Combine results for token1 and token2
transactionJsonData += transaction_results_token1 + transaction_results_token2
elif contractStructure['contractType'] == 'one-time-event':
token1 = contractStructure['tokenIdentification']
# Fetch transactions for one-time event contract
result = await fetch_token_transactions_for_contract(token1, sc_db_name, _from, to, USE_ASYNC)
transactionJsonData += result
# Post-process and return transactions
return await transaction_post_processing(transactionJsonData)
except aiomysql.MySQLError as e:
print(f"Database error while fetching contract transactions: {e}")
return jsonify(description="Database error"), 500
except Exception as e:
print(f"Unexpected error: {e}")
return jsonify(description="Unexpected error occurred"), 500
async def fetch_token_transactions_for_contract(token_name, sc_db_name, _from, to, USE_ASYNC=False):
"""
Fetches transactions for a specific token and smart contract database asynchronously.
Args:
token_name (str): The name of the token.
sc_db_name (str): The name of the smart contract database.
_from (int): Starting index for transactions.
to (int): Ending index for transactions.
USE_ASYNC (bool, optional): Flag to use async connection.
Returns:
list: List of transactions for the token.
"""
token_db_name = standardize_db_name(token_name)
try:
# Open separate connections for sc_db_name and token_db_name
conn_token = await get_mysql_connection(token_db_name, USE_ASYNC=USE_ASYNC)
conn_sc = await get_mysql_connection(sc_db_name, USE_ASYNC=USE_ASYNC)
# Fetch data from both connections and perform the join in-memory
async with conn_sc.cursor(dictionary=True) as cursor_sc, conn_token.cursor(dictionary=True) as cursor_token:
# Fetch data from contractTransactionHistory (sc_db_name)
query_sc = """
SELECT transactionHash, transactionSubType, id
FROM contractTransactionHistory
WHERE id BETWEEN %s AND %s
"""
await cursor_sc.execute(query_sc, (_from, to))
sc_transactions = await cursor_sc.fetchall()
# Fetch data from transactionHistory (token_db_name)
transaction_hashes = tuple(tx['transactionHash'] for tx in sc_transactions)
query_token = f"""
SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress,
transferAmount, '{token_name}' AS token
FROM transactionHistory
WHERE transactionHash IN %s
"""
await cursor_token.execute(query_token, (transaction_hashes,))
token_transactions = await cursor_token.fetchall()
# Combine results from both queries
combined_transactions = []
for sc_tx in sc_transactions:
matching_tx = next((tx for tx in token_transactions if tx['transactionHash'] == sc_tx['transactionHash']), None)
if matching_tx:
combined_tx = {**matching_tx, 'transactionSubType': sc_tx['transactionSubType']}
combined_transactions.append(combined_tx)
return combined_transactions
except aiomysql.MySQLError as e:
print(f"Error fetching transactions for token {token_name}: {e}")
return []
async def fetch_swap_contract_transactions(contractName, contractAddress, transactionHash=None, USE_ASYNC=False):
"""
Fetches swap contract transactions involving two tokens asynchronously.
Args:
contractName (str): Name of the swap contract.
contractAddress (str): Address of the swap contract.
transactionHash (str, optional): Specific transaction hash to filter transactions.
USE_ASYNC (bool, optional): Whether to use async connection for database interactions.
Returns:
list: Processed transactions.
"""
try:
# Standardize smart contract database name
sc_db_name = standardize_db_name(f"{contractName}_{contractAddress}")
# Fetch contract structure
contractStructure = await fetchContractStructure(contractName, contractAddress)
if not contractStructure:
return jsonify(description="Invalid contract structure"), 404
# Get token names
token1 = contractStructure['accepting_token']
token2 = contractStructure['selling_token']
# Fetch contract transactions from contractTransactionHistory
contract_transactions_query = """
SELECT transactionHash, transactionSubType
FROM contractTransactionHistory
"""
if transactionHash:
contract_transactions_query += " WHERE transactionHash = %s"
# Open connection to smart contract database asynchronously
async with await get_mysql_connection(sc_db_name, USE_ASYNC=USE_ASYNC) as conn_sc:
async with conn_sc.cursor(dictionary=True) as cursor_sc:
# Execute contract transactions query
if transactionHash:
await cursor_sc.execute(contract_transactions_query, (transactionHash,))
else:
await cursor_sc.execute(contract_transactions_query)
contract_transactions = await cursor_sc.fetchall()
transaction_hashes = [tx["transactionHash"] for tx in contract_transactions]
# Open connections to token databases concurrently
async with await get_mysql_connection(standardize_db_name(token1), USE_ASYNC=USE_ASYNC) as conn_token1, \
await get_mysql_connection(standardize_db_name(token2), USE_ASYNC=USE_ASYNC) as conn_token2:
# Fetch transactions for token1 and token2 concurrently
token1_query = f"""
SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress,
transferAmount, '{token1}' AS token
FROM transactionHistory
WHERE transactionHash IN ({','.join(['%s'] * len(transaction_hashes))})
"""
token2_query = f"""
SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress,
transferAmount, '{token2}' AS token
FROM transactionHistory
WHERE transactionHash IN ({','.join(['%s'] * len(transaction_hashes))})
"""
async with conn_token1.cursor(dictionary=True) as cursor_token1, \
conn_token2.cursor(dictionary=True) as cursor_token2:
await asyncio.gather(
cursor_token1.execute(token1_query, transaction_hashes),
cursor_token2.execute(token2_query, transaction_hashes)
)
token1_transactions, token2_transactions = await asyncio.gather(
cursor_token1.fetchall(),
cursor_token2.fetchall()
)
# Combine and post-process transactions
all_transactions = token1_transactions + token2_transactions
for tx in all_transactions:
for contract_tx in contract_transactions:
if tx["transactionHash"] == contract_tx["transactionHash"]:
tx["transactionSubType"] = contract_tx["transactionSubType"]
break
# Return the processed transactions
return await transaction_post_processing(all_transactions)
except aiomysql.MySQLError as e:
logger.error(f"Database error while fetching swap contract transactions: {e}", exc_info=True)
return jsonify(description="Database error"), 500
except Exception as e:
logger.error(f"Unexpected error: {e}", exc_info=True)
return jsonify(description="Unexpected error occurred"), 500
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
async def refresh_committee_list(admin_flo_id, api_url, blocktime):
committee_list = []
latest_param = 'true'
mempool_param = 'false'
init_id = None
async 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 Exception as e:
print(f"Error processing transaction: {e}")
async def send_api_request(url):
timeout = aiohttp.ClientTimeout(total=API_TIMEOUT)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, ssl=API_VERIFY) as response:
if response.status == 200:
return await response.json()
else:
print('Response from the Blockbook API failed')
raise RuntimeError(f"API request failed with status {response.status}")
except asyncio.TimeoutError:
print(f"Request timed out after {API_TIMEOUT} seconds")
return ["timeout"]
except Exception as e:
print(f"Error during API request: {e}")
return None
url = f'{api_url}api/v1/address/{admin_flo_id}?details=txs'
response = await send_api_request(url)
if response == ["timeout"]:
return ["timeout"]
if response is None:
return []
for transaction_info in response.get('txs', []):
await 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 = await send_api_request(url)
if response == ["timeout"]:
return ["timeout"]
if response is None:
return []
for transaction_info in response.get('items', []):
await process_transaction(transaction_info)
if 'incomplete' in response:
init_id = response['initItem']
return committee_list
@app.route('/')
async def welcome_msg():
return jsonify('Welcome to RanchiMall FLO Api v2')
@app.route('/api/v1.0/getSystemData', methods=['GET'])
async def systemData():
try:
# Standardized database names
system_db_name = standardize_db_name("system")
latest_cache_db_name = standardize_db_name("latestCache")
# Query for the number of FLO addresses in tokenAddress mapping
async with await get_mysql_connection(system_db_name, USE_ASYNC=True) as conn_system:
async with conn_system.cursor() as cursor_system:
await cursor_system.execute('SELECT COUNT(DISTINCT tokenAddress) FROM tokenAddressMapping')
tokenAddressCount = (await cursor_system.fetchone())[0]
await cursor_system.execute('SELECT COUNT(DISTINCT token) FROM tokenAddressMapping')
tokenCount = (await cursor_system.fetchone())[0]
await cursor_system.execute('SELECT COUNT(DISTINCT contractName) FROM contractAddressMapping')
contractCount = (await cursor_system.fetchone())[0]
await cursor_system.execute("SELECT value FROM systemData WHERE attribute='lastblockscanned'")
lastscannedblock = int((await cursor_system.fetchone())[0])
# Query for total number of validated blocks
async with await get_mysql_connection(latest_cache_db_name, USE_ASYNC=True) as conn_cache:
async with conn_cache.cursor() as cursor_cache:
await cursor_cache.execute('SELECT COUNT(DISTINCT blockNumber) FROM latestBlocks')
validatedBlockCount = (await cursor_cache.fetchone())[0]
await cursor_cache.execute('SELECT COUNT(DISTINCT transactionHash) FROM latestTransactions')
validatedTransactionCount = (await cursor_cache.fetchone())[0]
# Return the system data as JSON
return jsonify(
systemAddressCount=tokenAddressCount,
systemBlockCount=validatedBlockCount,
systemTransactionCount=validatedTransactionCount,
systemSmartContractCount=contractCount,
systemTokenCount=tokenCount,
lastscannedblock=lastscannedblock,
result='ok'
)
except Exception as e:
logger.error(f"Error in systemData function: {e}", exc_info=True)
return jsonify(result='error', description=INTERNAL_ERROR)
@app.route('/api/v1.0/broadcastTx/<raw_transaction_hash>')
async def broadcastTx(raw_transaction_hash):
try:
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())
except Exception as e:
print("broadcastTx:", e)
return jsonify(result='error', description=INTERNAL_ERROR)
@app.route('/api/v1.0/getTokenList', methods=['GET'])
async def getTokenList():
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR)
try:
# Prefix and suffix for databases
database_prefix = f"{mysql_config.database_prefix}_"
database_suffix = "_db"
# Initialize token list
token_list = []
# Use `async with` to handle connection cleanup automatically
async with await get_mysql_connection("information_schema", no_standardize=True, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Query to get all databases matching the prefix and suffix
query = f"SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME LIKE '{database_prefix}%' AND SCHEMA_NAME LIKE '%{database_suffix}'"
await cursor.execute(query)
all_databases = [row[0] for row in await cursor.fetchall()]
# Separate token databases from smart contract databases
for db_name in all_databases:
# Remove the prefix and suffix for parsing
stripped_name = db_name[len(database_prefix):-len(database_suffix)]
# Exclude "latestCache" and "system" databases
if stripped_name in ["latestCache", "system"]:
continue
# Token databases will not contain an address-like string
parts = stripped_name.split('_')
if len(parts) == 1: # Token databases have a single part (e.g., usd, inr)
token_list.append(stripped_name)
elif len(parts) == 2 and len(parts[1]) == 34 and (parts[1].startswith('F') or parts[1].startswith('o')):
# Smart contracts are excluded
continue
# Return the token list in the original format
return jsonify(tokens=token_list, result='ok')
except Exception as e:
logger.error(f"getTokenList: {e}", exc_info=True)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getTokenInfo', methods=['GET'])
async def getTokenInfo():
try:
token = request.args.get('token')
if token is None:
return jsonify(result='error', description='token name hasn\'t been passed'), 400
# Standardize token database name
db_name = standardize_db_name(token)
# Connect to the token database asynchronously
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch incorporation details
query = "SELECT * FROM transactionHistory WHERE id = 1"
await cursor.execute(query)
incorporationRow = await cursor.fetchone()
if not incorporationRow:
return jsonify(result='error', description='Incorporation details not found'), 404
# Fetch the number of distinct addresses
query = "SELECT COUNT(DISTINCT address) FROM activeTable"
await cursor.execute(query)
numberOf_distinctAddresses = (await cursor.fetchone())[0]
# Fetch the total number of transactions
query = "SELECT MAX(id) FROM transactionHistory"
await cursor.execute(query)
numberOf_transactions = (await cursor.fetchone())[0]
# Fetch associated contracts
query = """
SELECT contractName, contractAddress, blockNumber, blockHash, transactionHash
FROM tokenContractAssociation
"""
await cursor.execute(query)
associatedContracts = await cursor.fetchall()
# Prepare associated contract list
associatedContractList = [
{
'contractName': item[0],
'contractAddress': item[1],
'blockNumber': item[2],
'blockHash': item[3],
'transactionHash': item[4],
}
for item in associatedContracts
]
# Prepare the response
response = {
'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,
}
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
else:
return jsonify(response), 200
except Exception as e:
logger.error(f"getTokenInfo: {e}", exc_info=True)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getTokenTransactions', methods=['GET'])
async def getTokenTransactions():
try:
token = request.args.get('token')
senderFloAddress = request.args.get('senderFloAddress')
destFloAddress = request.args.get('destFloAddress')
limit = request.args.get('limit')
if token is None:
return jsonify(result='error', description='token name hasn\'t been passed'), 400
# Standardize token database name
db_name = standardize_db_name(token)
# Connect to the token database asynchronously
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Build the base query
query = "SELECT jsonData, parsedFloData FROM transactionHistory"
conditions = []
params = []
# Add filters based on sender and destination addresses
if senderFloAddress and not destFloAddress:
conditions.append("sourceFloAddress = %s")
params.append(senderFloAddress)
elif not senderFloAddress and destFloAddress:
conditions.append("destFloAddress = %s")
params.append(destFloAddress)
elif senderFloAddress and destFloAddress:
conditions.append("sourceFloAddress = %s AND destFloAddress = %s")
params.extend([senderFloAddress, destFloAddress])
# Add conditions to the query
if conditions:
query += " WHERE " + " AND ".join(conditions)
# Add ordering and limit
query += " ORDER BY id DESC"
if limit is not None:
if not limit.isdigit():
return jsonify(result='error', description='limit validation failed'), 400
query += " LIMIT %s"
params.append(int(limit))
# Execute the query asynchronously
await cursor.execute(query, params)
transactionJsonData = await cursor.fetchall()
# Process the results
rowarray_list = {}
for row in transactionJsonData:
transactions_object = {}
transactions_object['transactionDetails'] = json.loads(row[0])
transactions_object['transactionDetails'] = await update_transaction_confirmations(transactions_object['transactionDetails'])
transactions_object['parsedFloData'] = json.loads(row[1])
rowarray_list[transactions_object['transactionDetails']['txid']] = transactions_object
# Return the response
if not is_backend_ready():
return jsonify(result='ok', token=token, transactions=rowarray_list, warning=BACKEND_NOT_READY_WARNING), 206
else:
return jsonify(result='ok', token=token, transactions=rowarray_list), 200
except Exception as e:
logger.error(f"getTokenTransactions: {e}", exc_info=True)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getTokenBalances', methods=['GET'])
async def getTokenBalances():
try:
token = request.args.get('token')
if token is None:
return jsonify(result='error', description='token name hasn\'t been passed'), 400
# Standardize token database name
db_name = standardize_db_name(token)
# Connect to the token database asynchronously
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch address balances grouped by address asynchronously
query = "SELECT address, SUM(transferBalance) FROM activeTable GROUP BY address"
await cursor.execute(query)
addressBalances = await cursor.fetchall()
# Prepare the response
returnList = {address[0]: address[1] for address in addressBalances}
if not is_backend_ready():
return jsonify(result='ok', token=token, balances=returnList, warning=BACKEND_NOT_READY_WARNING), 206
else:
return jsonify(result='ok', token=token, balances=returnList), 200
except Exception as e:
logger.error(f"getTokenBalances: {e}", exc_info=True)
return jsonify(result='error', description=INTERNAL_ERROR), 500
# FLO Address APIs
@app.route('/api/v1.0/getFloAddressInfo', methods=['GET'])
async def getFloAddressInfo():
try:
floAddress = request.args.get('floAddress')
if floAddress is None:
return jsonify(description='floAddress hasn\'t been passed'), 400
# Connect to the system database asynchronously
db_name = standardize_db_name("system")
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch associated tokens asynchronously
query = "SELECT token FROM tokenAddressMapping WHERE tokenAddress = %s"
await cursor.execute(query, (floAddress,))
tokenNames = await cursor.fetchall()
# Fetch incorporated contracts asynchronously
query = """
SELECT contractName, status, tokenIdentification, contractType, transactionHash, blockNumber, blockHash
FROM activecontracts
WHERE contractAddress = %s
"""
await cursor.execute(query, (floAddress,))
incorporatedContracts = await cursor.fetchall()
# Prepare token details by querying each token database separately asynchronously
detailList = {}
if tokenNames:
tasks = []
for token in tokenNames:
token_name = token[0]
token_db_name = standardize_db_name(token_name)
tasks.append(fetch_token_balance(token_name, token_db_name, floAddress))
# Run all tasks concurrently
token_balances = await asyncio.gather(*tasks)
# Add token balances to the details list
for balance in token_balances:
detailList.update(balance)
else:
# Address is not associated with any token
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(result='error', description='FLO address is not associated with any tokens'), 404
# Prepare contract details asynchronously
incorporatedSmartContracts = []
if incorporatedContracts:
for contract in incorporatedContracts:
tempdict = {
'contractName': contract[0],
'contractAddress': floAddress,
'status': contract[1],
'tokenIdentification': contract[2],
'contractType': contract[3],
'transactionHash': contract[4],
'blockNumber': contract[5],
'blockHash': contract[6],
}
incorporatedSmartContracts.append(tempdict)
else:
incorporatedSmartContracts = None
# Return the response
response = {
'result': 'ok',
'floAddress': floAddress,
'floAddressBalances': detailList,
'incorporatedSmartContracts': incorporatedSmartContracts,
}
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
else:
return jsonify(response), 200
except Exception as e:
logger.error(f"getFloAddressInfo: {e}", exc_info=True)
return jsonify(result='error', description=INTERNAL_ERROR), 500
async def fetch_token_balance(token_name, floAddress):
"""
Fetches the balance for a specific token in the provided token database.
Args:
token_name (str): The name of the token.
floAddress (str): The FLO address to fetch the balance for.
Returns:
dict: A dictionary with the token balance.
"""
try:
token_db_name = standardize_db_name(token_name)
# Use `async with` for resource management
async with await get_mysql_connection(token_db_name, USE_ASYNC=True) as conn_token:
async with conn_token.cursor() as cursor_token:
# Fetch balance for the token asynchronously
query = "SELECT SUM(transferBalance) FROM activeTable WHERE address = %s"
await cursor_token.execute(query, (floAddress,))
balance = (await cursor_token.fetchone())[0] or 0
return {token_name: {'balance': balance, 'token': token_name}}
except Exception as e:
print(f"Error fetching balance for token {token_name}: {e}")
return {token_name: {'balance': 0, 'token': token_name}}
@app.route('/api/v1.0/getFloAddressBalance', methods=['GET'])
async def getAddressBalance():
try:
floAddress = request.args.get('floAddress')
token = request.args.get('token')
if floAddress is None:
return jsonify(result='error', description='floAddress hasn\'t been passed'), 400
if token is None:
# If no specific token is provided, get balances for all associated tokens
db_name = standardize_db_name("system")
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch associated tokens asynchronously
query = "SELECT token FROM tokenAddressMapping WHERE tokenAddress = %s"
await cursor.execute(query, (floAddress,))
tokenNames = [row[0] for row in await cursor.fetchall()]
if tokenNames:
# Use asyncio to fetch balances in parallel
tasks = [fetch_token_balance(token_name, floAddress) for token_name in tokenNames]
results = await asyncio.gather(*tasks)
# Combine results into detailList
detailList = {k: v for result in results for k, v in result.items()}
if not is_backend_ready():
return jsonify(result='ok', warning=BACKEND_NOT_READY_WARNING, floAddress=floAddress, floAddressBalances=detailList), 206
else:
return jsonify(result='ok', floAddress=floAddress, floAddressBalances=detailList), 200
else:
# Address is not associated with any token
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(result='error', description='FLO address is not associated with any tokens'), 404
else:
# If a specific token is provided, get the balance for that token
token_db_name = standardize_db_name(token)
async with await get_mysql_connection(token_db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch balance for the address asynchronously
query = "SELECT SUM(transferBalance) FROM activeTable WHERE address = %s"
await cursor.execute(query, (floAddress,))
balance = (await cursor.fetchone())[0] or 0
if not is_backend_ready():
return jsonify(result='ok', warning=BACKEND_NOT_READY_WARNING, token=token, floAddress=floAddress, balance=balance), 206
else:
return jsonify(result='ok', token=token, floAddress=floAddress, balance=balance), 200
except Exception as e:
print("getAddressBalance:", e)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getFloAddressTransactions', methods=['GET'])
async def getFloAddressTransactions():
try:
floAddress = request.args.get('floAddress')
token = request.args.get('token')
limit = request.args.get('limit')
if floAddress is None:
return jsonify(result='error', description='floAddress has not been passed'), 400
# Handle token-specific or all-token scenarios
if token is None:
# Fetch all tokens associated with the floAddress
db_name = standardize_db_name("system")
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
query = "SELECT token FROM tokenAddressMapping WHERE tokenAddress = %s"
await cursor.execute(query, (floAddress,))
tokenNames = await cursor.fetchall()
else:
# Check if the token database exists
token_db_name = standardize_db_name(token)
try:
async with await get_mysql_connection(token_db_name, USE_ASYNC=True):
tokenNames = [(token,)]
except Exception:
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(result='error', description='Token does not exist'), 404
# Process transactions for the tokens
if tokenNames:
allTransactionList = {}
tasks = [] # List to hold async tasks for fetching transactions
for token_row in tokenNames:
tokenname = token_row[0]
token_db_name = standardize_db_name(tokenname)
tasks.append(fetch_transactions_for_token(token_db_name, floAddress, limit))
# Run all tasks concurrently
transaction_data = await asyncio.gather(*tasks)
# Process fetched transactions
for data in transaction_data:
for row in data:
transactions_object = {}
transactions_object['transactionDetails'] = json.loads(row[0])
transactions_object['transactionDetails'] = await update_transaction_confirmations(
transactions_object['transactionDetails']
)
transactions_object['parsedFloData'] = json.loads(row[1])
allTransactionList[transactions_object['transactionDetails']['txid']] = transactions_object
# Construct response based on token presence
response = {
'result': 'ok',
'floAddress': floAddress,
'transactions': allTransactionList
}
if token is not None:
response['token'] = token
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
else:
return jsonify(response), 200
else:
# No token transactions associated with this address
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(result='error', description='No token transactions present on this address'), 404
except Exception as e:
print("getFloAddressTransactions:", e)
return jsonify(result='error', description=INTERNAL_ERROR), 500
async def fetch_transactions_for_token(token_db_name, floAddress, limit):
"""
Fetches transactions for a specific token database.
Args:
token_db_name (str): The token database name.
floAddress (str): The FLO address to filter transactions.
limit (int): The limit for the number of transactions.
Returns:
list: List of transactions for the token.
"""
try:
async with await get_mysql_connection(token_db_name, USE_ASYNC=True) as conn_token:
async with conn_token.cursor() as cursor_token:
# Build query to fetch transactions
query = """
SELECT jsonData, parsedFloData
FROM transactionHistory
WHERE sourceFloAddress = %s OR destFloAddress = %s
ORDER BY id DESC
"""
params = (floAddress, floAddress)
if limit:
query += " LIMIT %s"
params = (floAddress, floAddress, int(limit))
await cursor_token.execute(query, params)
return await cursor_token.fetchall()
except Exception as e:
print(f"Error fetching transactions for token {token_db_name}: {e}")
return []
# SMART CONTRACT APIs
@app.route('/api/v1.0/getSmartContractList', methods=['GET'])
async def getContractList():
try:
# Get query parameters
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
# Standardize system database name
system_db_name = standardize_db_name("system")
# Initialize contract list
contractList = []
# Connect to the system database asynchronously and manage it with async context
async with await get_mysql_connection(system_db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Build the query dynamically based on input parameters
query = "SELECT * FROM activecontracts"
conditions = []
params = []
if contractName:
conditions.append("contractName=%s")
params.append(contractName)
if contractAddress:
conditions.append("contractAddress=%s")
params.append(contractAddress)
if conditions:
query += " WHERE " + " AND ".join(conditions)
# Execute the query asynchronously
await cursor.execute(query, tuple(params))
allcontractsDetailList = await cursor.fetchall()
# Process the results asynchronously
for contract in allcontractsDetailList:
contractDict = {
'contractName': contract[1],
'contractAddress': contract[2],
'status': contract[3],
'tokenIdentification': contract[4],
'contractType': contract[5],
'transactionHash': contract[6],
'blockNumber': contract[7],
'incorporationDate': contract[8],
}
if contract[9]:
contractDict['expiryDate'] = contract[9]
if contract[10]:
contractDict['closeDate'] = contract[10]
contractList.append(contractDict)
# Check backend readiness and return response
if not is_backend_ready():
return jsonify(smartContracts=contractList, result='ok', warning=BACKEND_NOT_READY_WARNING), 206
else:
return jsonify(smartContracts=contractList, result='ok'), 200
except Exception as e:
print("getContractList:", e)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getSmartContractInfo', methods=['GET'], endpoint='getSmartContractInfoV1')
async def getContractInfo():
logger.info("Entering getContractInfo function")
try:
# Get query parameters
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
if not contractName:
logger.error("Contract name is missing")
return jsonify(result='error', description="Smart Contract's name hasn't been passed"), 400
if not contractAddress:
logger.error("Contract address is missing")
return jsonify(result='error', description="Smart Contract's address hasn't been passed"), 400
# Standardize the smart contract database name
contract_db_name = standardize_db_name(f"{contractName}_{contractAddress}")
logger.info(f"Standardized contract database name: {contract_db_name}")
# Initialize response dictionary
returnval = {}
# Fetch contract structure
try:
async with await get_mysql_connection(contract_db_name, USE_ASYNC=True) as contract_conn:
async with contract_conn.cursor() as cursor:
await cursor.execute("SELECT attribute, value FROM contractstructure")
result = await cursor.fetchall()
if not result:
logger.error("No contract structure found")
return jsonify(result='error', description="No contract structure found for the specified smart contract"), 404
# Process contract structure data
contractStructure = {}
conditionDict = {}
for item in result:
if item[0] == 'exitconditions':
conditionDict[len(conditionDict)] = item[1]
else:
contractStructure[item[0]] = item[1]
if conditionDict:
contractStructure['exitconditions'] = conditionDict
# Update the response dictionary
returnval.update(contractStructure)
if 'exitconditions' in returnval:
returnval['userChoice'] = returnval.pop('exitconditions')
except Exception as e:
logger.error(f"Error fetching contract structure: {e}", exc_info=True)
return jsonify(result='error', description="Failed to fetch contract structure"), 500
# Return the final response
logger.info("Returning final response with contract information")
return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, contractInfo=returnval), 200
except Exception as e:
logger.error(f"Unhandled error in getContractInfo: {e}", exc_info=True)
return jsonify(result='error', description="Internal Server Error"), 500
@app.route('/api/v1.0/getSmartContractParticipants', methods=['GET'])
async def getcontractparticipants():
try:
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
if not contractName:
return jsonify(result='error', description="Smart Contract's name hasn't been passed"), 400
if not contractAddress:
return jsonify(result='error', description="Smart Contract's address hasn't been passed"), 400
contractName = contractName.strip().lower()
contractAddress = contractAddress.strip()
# Standardize smart contract database name
contract_db_name = standardize_db_name(f"{contractName}_{contractAddress}")
# Fetch contract structure asynchronously
contractStructure = await fetchContractStructure(contractName, contractAddress)
# Initialize response
returnval = {}
# Get an async connection to the contract database
async with await get_mysql_connection(contract_db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Handle external trigger contracts
if 'exitconditions' in contractStructure:
await cursor.execute('SELECT * FROM contractTransactionHistory WHERE transactionType="trigger"')
triggers = await cursor.fetchall()
if len(triggers) == 1:
await cursor.execute('SELECT value FROM contractstructure WHERE attribute="tokenIdentification"')
token = (await cursor.fetchone())[0]
await cursor.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash, winningAmount FROM contractparticipants')
result = await cursor.fetchall()
for row in result:
returnval[row[1]] = {
'participantFloAddress': row[1],
'tokenAmount': row[2],
'userChoice': row[3],
'transactionHash': row[4],
'winningAmount': row[5],
'tokenIdentification': token
}
elif len(triggers) == 0:
await cursor.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants')
result = await cursor.fetchall()
for row in result:
returnval[row[1]] = {
'participantFloAddress': row[1],
'tokenAmount': row[2],
'userChoice': row[3],
'transactionHash': row[4]
}
else:
return jsonify(result='error', description='More than 1 trigger present. This is unusual, please check your code'), 500
# Handle internal trigger contracts
elif 'payeeAddress' in contractStructure:
await cursor.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants')
result = await cursor.fetchall()
for row in result:
returnval[row[1]] = {
'participantFloAddress': row[1],
'tokenAmount': row[2],
'userChoice': row[3],
'transactionHash': row[4]
}
# Handle continuous-event contracts with token swaps
elif contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap':
await cursor.execute('SELECT * FROM contractparticipants')
contract_participants = await cursor.fetchall()
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]
}
# Final response
if not is_backend_ready():
return jsonify(result='ok', warning=BACKEND_NOT_READY_WARNING, contractName=contractName, contractAddress=contractAddress, participantInfo=returnval), 206
else:
return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, participantInfo=returnval), 200
except Exception as e:
print("getcontractparticipants:", e)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getParticipantDetails', methods=['GET'], endpoint='getParticipantDetailsV1')
async def getParticipantDetails():
try:
floAddress = request.args.get('floAddress')
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
if not floAddress:
return jsonify(result='error', description="FLO address hasn't been passed"), 400
if (contractName and not contractAddress) or (not contractName and contractAddress):
return jsonify(result='error', description='Pass both contractName and contractAddress as URL parameters'), 400
floAddress = floAddress.strip()
if contractName:
contractName = contractName.strip().lower()
if contractAddress:
contractAddress = contractAddress.strip()
# Standardize the contract database name
system_db_name = standardize_db_name("system")
contract_db_name = standardize_db_name(f"{contractName}_{contractAddress}") if contractName and contractAddress else None
# Connect to the system database asynchronously
async with await get_mysql_connection(system_db_name, USE_ASYNC=True) as conn_system:
async with conn_system.cursor() as cursor_system:
# Query contract participation details for the FLO address
query = """
SELECT *
FROM contractAddressMapping
WHERE address = %s AND addressType = 'participant'
"""
params = [floAddress]
if contractName and contractAddress:
query += " AND contractName = %s AND contractAddress = %s"
params.extend([contractName, contractAddress])
await cursor_system.execute(query, tuple(params))
participant_address_contracts = await cursor_system.fetchall()
if not participant_address_contracts:
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(result='error', description="Address hasn't participated in any contract"), 404
participationDetailsList = []
for contract in participant_address_contracts:
detailsDict = {}
contract_name = contract[3]
contract_address = contract[4]
db_name = standardize_db_name(f"{contract_name}_{contract_address}")
# Fetch participation details from the contract database
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn_contract:
async with conn_contract.cursor() as cursor_contract:
# Fetch contract structure asynchronously
contractStructure = await fetchContractStructure(contract_name, contract_address)
if contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap':
await cursor_contract.execute("SELECT * FROM contractparticipants WHERE participantAddress = %s", (floAddress,))
participant_details = await cursor_contract.fetchall()
participationList = []
for participation in participant_details:
detailsDict = {
'participationAddress': floAddress,
'participationAmount': participation[2],
'receivedAmount': float(participation[3]),
'participationToken': contractStructure['accepting_token'],
'receivedToken': contractStructure['selling_token'],
'swapPrice_received_to_participation': float(participation[7]),
'transactionHash': participation[4],
'blockNumber': participation[5],
'blockHash': participation[6],
}
participationList.append(detailsDict)
participationDetailsList.append(participationList)
elif contractStructure['contractType'] == 'one-time-event' and 'payeeAddress' in contractStructure:
detailsDict['contractName'] = contract_name
detailsDict['contractAddress'] = contract_address
await cursor_system.execute('''
SELECT status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate
FROM activecontracts
WHERE contractName = %s AND contractAddress = %s
''', (contract_name, contract_address))
temp = await cursor_system.fetchone()
detailsDict.update({
'status': temp[0],
'tokenIdentification': temp[1],
'contractType': temp[2],
'blockNumber': temp[3],
'blockHash': temp[4],
'incorporationDate': temp[5],
'expiryDate': temp[6],
'closeDate': temp[7]
})
await cursor_contract.execute("SELECT tokenAmount FROM contractparticipants WHERE participantAddress = %s", (floAddress,))
result = await cursor_contract.fetchone()
detailsDict['tokenAmount'] = result[0]
participationDetailsList.append(detailsDict)
elif contractStructure['contractType'] == 'one-time-event' and 'exitconditions' in contractStructure:
detailsDict['contractName'] = contract_name
detailsDict['contractAddress'] = contract_address
await cursor_system.execute('''
SELECT status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate
FROM activecontracts
WHERE contractName = %s AND contractAddress = %s
''', (contract_name, contract_address))
temp = await cursor_system.fetchone()
detailsDict.update({
'status': temp[0],
'tokenIdentification': temp[1],
'contractType': temp[2],
'blockNumber': temp[3],
'blockHash': temp[4],
'incorporationDate': temp[5],
'expiryDate': temp[6],
'closeDate': temp[7]
})
await cursor_contract.execute("""
SELECT userChoice, winningAmount
FROM contractparticipants
WHERE participantAddress = %s
""", (floAddress,))
result = await cursor_contract.fetchone()
detailsDict['userChoice'] = result[0]
detailsDict['winningAmount'] = result[1]
participationDetailsList.append(detailsDict)
# Final response based on backend readiness
if not is_backend_ready():
return jsonify(
warning=BACKEND_NOT_READY_WARNING,
floAddress=floAddress,
type='participant',
participatedContracts=participationDetailsList
), 206
else:
return jsonify(
floAddress=floAddress,
type='participant',
participatedContracts=participationDetailsList
), 200
except Exception as e:
print("getParticipantDetails:", e)
return jsonify(description="Unexpected error occurred"), 500
@app.route('/api/v1.0/getSmartContractTransactions', methods=['GET'])
async def getsmartcontracttransactions():
try:
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
if not contractName:
return jsonify(result='error', description="Smart Contract's name hasn't been passed"), 400
if not contractAddress:
return jsonify(result='error', description="Smart Contract's address hasn't been passed"), 400
# Standardize the contract database name
contract_db_name = standardize_db_name(f"{contractName.strip()}_{contractAddress.strip()}")
# Establish asynchronous connection to the contract database
async with await get_mysql_connection(contract_db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch contract transaction data asynchronously
query = '''
SELECT jsonData, parsedFloData
FROM contractTransactionHistory
'''
await cursor.execute(query)
result = await cursor.fetchall()
# Process the fetched transaction data
returnval = {}
for item in result:
transactions_object = {}
transactions_object['transactionDetails'] = json.loads(item[0])
transactions_object['transactionDetails'] = await update_transaction_confirmations(transactions_object['transactionDetails'])
transactions_object['parsedFloData'] = json.loads(item[1])
returnval[transactions_object['transactionDetails']['txid']] = transactions_object
# Check backend readiness
if not is_backend_ready():
return jsonify(
result='ok',
warning=BACKEND_NOT_READY_WARNING,
contractName=contractName,
contractAddress=contractAddress,
contractTransactions=returnval
), 206
else:
return jsonify(
result='ok',
contractName=contractName,
contractAddress=contractAddress,
contractTransactions=returnval
), 200
except aiomysql.MySQLError as e:
print(f"MySQL error while fetching transactions: {e}")
return jsonify(result='error', description='Database error occurred'), 500
except Exception as e:
print("getSmartContractTransactions:", e)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getBlockDetails/<blockdetail>', methods=['GET'])
async def getblockdetails(blockdetail):
try:
blockJson = await blockdetailhelper(blockdetail)
if len(blockJson) != 0:
blockJson = json.loads(blockJson[0][0])
return jsonify(result='ok', blockDetails=blockJson)
else:
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR)
else:
return jsonify(result='error', description='Block doesn\'t exist in database')
except Exception as e:
print("getblockdetails:", e)
return jsonify(result='error', description=INTERNAL_ERROR)
@app.route('/api/v1.0/getTransactionDetails/<transactionHash>', methods=['GET'])
async def gettransactiondetails(transactionHash):
try:
transactionJsonData = await transactiondetailhelper(transactionHash)
if len(transactionJsonData) != 0:
transactionJson = json.loads(transactionJsonData[0][0])
transactionJson = await update_transaction_confirmations(transactionJson)
parseResult = json.loads(transactionJsonData[0][1])
return jsonify(parsedFloData=parseResult, transactionDetails=transactionJson, transactionHash=transactionHash, result='ok')
else:
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR)
else:
return jsonify(result='error', description='Transaction doesn\'t exist in database')
except Exception as e:
print("gettransactiondetails:", e)
return jsonify(result='error', description=INTERNAL_ERROR)
@app.route('/api/v1.0/getLatestTransactionDetails', methods=['GET'])
async def getLatestTransactionDetails():
try:
numberOfLatestBlocks = request.args.get('numberOfLatestBlocks')
# Standardize the database name and connect to MySQL asynchronously
db_name = standardize_db_name('latestCache')
# Use `async with` for resource management
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
tempdict = {}
if numberOfLatestBlocks is not None:
# Fetch transactions from the latest blocks based on the specified limit
query = '''
SELECT *
FROM latestTransactions
WHERE blockNumber IN (
SELECT DISTINCT blockNumber
FROM latestTransactions
ORDER BY blockNumber DESC
LIMIT %s
)
ORDER BY id ASC;
'''
await cursor.execute(query, (int(numberOfLatestBlocks),))
else:
# Fetch transactions from all distinct latest blocks
query = '''
SELECT *
FROM latestTransactions
WHERE blockNumber IN (
SELECT DISTINCT blockNumber
FROM latestTransactions
ORDER BY blockNumber DESC
)
ORDER BY id ASC;
'''
await cursor.execute(query)
# Fetch the transactions asynchronously
latestTransactions = await cursor.fetchall()
# Process each transaction
tempdict = {}
for item in latestTransactions:
item = list(item)
tx_parsed_details = {}
# Parse transaction details
tx_parsed_details['transactionDetails'] = json.loads(item[3])
tx_parsed_details['transactionDetails'] = await 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])
# Merge parsed details and transaction details
tx_parsed_details = {**tx_parsed_details['transactionDetails'], **tx_parsed_details['parsedFloData']}
# Add on-chain flag
tx_parsed_details['onChain'] = True
# Add transaction to the dictionary using txid as the key
tempdict[json.loads(item[3])['txid']] = tx_parsed_details
# Respond based on backend readiness
if not is_backend_ready():
return jsonify(
result='ok',
warning=BACKEND_NOT_READY_WARNING,
latestTransactions=tempdict
), 206
else:
return jsonify(result='ok', latestTransactions=tempdict), 200
except Exception as e:
print("getLatestTransactionDetails:", e)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getLatestBlockDetails', methods=['GET'])
async def getLatestBlockDetails():
try:
limit = request.args.get('limit')
# Standardize the database name
db_name = standardize_db_name('latestCache')
# Use `async with` for resource management
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Default query for the latest 4 blocks if no limit is provided
query = '''
SELECT *
FROM (
SELECT *
FROM latestBlocks
ORDER BY blockNumber DESC
LIMIT %s
) AS subquery
ORDER BY id ASC;
'''
# Set the limit for the query
limit = int(limit) if limit is not None else 4
# Execute the query
await cursor.execute(query, (limit,))
latestBlocks = await cursor.fetchall()
# Parse the block details
tempdict = {}
for item in latestBlocks:
tempdict[json.loads(item[3])['hash']] = json.loads(item[3])
# Respond based on backend readiness
if not is_backend_ready():
return jsonify(result='ok', warning=BACKEND_NOT_READY_WARNING, latestBlocks=tempdict), 206
else:
return jsonify(result='ok', latestBlocks=tempdict), 200
except Exception as e:
print("getLatestBlockDetails:", e)
return jsonify(result='error', description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getBlockTransactions/<blockdetail>', methods=['GET'])
async def getblocktransactions(blockdetail):
try:
blockJson = await blockdetailhelper(blockdetail)
if len(blockJson) != 0:
blockJson = json.loads(blockJson[0][0])
blocktxlist = blockJson['tx']
blocktxs = {}
for i in range(len(blocktxlist)):
temptx = await transactiondetailhelper(blocktxlist[i])
transactionJson = json.loads(temptx[0][0])
transactionJson = await update_transaction_confirmations(transactionJson)
parseResult = json.loads(temptx[0][1])
blocktxs[blocktxlist[i]] = {
"parsedFloData" : parseResult,
"transactionDetails" : transactionJson
}
return jsonify(result='ok', transactions=blocktxs, blockKeyword=blockdetail)
else:
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR)
else:
return jsonify(result='error', description='Block doesn\'t exist in database')
except Exception as e:
print("getblocktransactions:", e)
return jsonify(result='error', description=INTERNAL_ERROR)
@app.route('/api/v1.0/categoriseString/<urlstring>', methods=['GET'])
async def categoriseString(urlstring):
try:
# Check if the hash is of a transaction asynchronously
async with aiohttp.ClientSession() as session:
async with session.get(f"{apiUrl}api/v1/tx/{urlstring}") as response:
if response.status == 200:
return jsonify(type="transaction"), 200
# Check if the hash is of a block asynchronously
async with session.get(f"{apiUrl}api/v1/block/{urlstring}") as response:
if response.status == 200:
return jsonify(type="block"), 200
# Initialize variables for database handling
database_prefix = f"{mysql_config.database_prefix}_"
database_suffix = "_db"
token_names = set()
contract_list = []
# Connect to MySQL information_schema using async `get_mysql_connection`
async with await get_mysql_connection("information_schema", no_standardize=True, USE_ASYNC=True) as conn_info:
async with conn_info.cursor() as cursor_info:
# Query to fetch all databases matching the prefix and suffix
await cursor_info.execute(
f"SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME LIKE '{database_prefix}%' AND SCHEMA_NAME LIKE '%{database_suffix}'"
)
databases = await cursor_info.fetchall()
# Separate token databases and smart contract databases
for db in databases:
stripped_name = db[0][len(database_prefix):-len(database_suffix)]
# Exclude "latestCache" and "system" databases
if stripped_name in ["latestCache", "system"]:
continue
parts = stripped_name.split('_')
if len(parts) == 1: # Token database
token_names.add(stripped_name.lower())
elif len(parts) == 2 and len(parts[1]) == 34 and (parts[1].startswith('F') or parts[1].startswith('o')): # Smart contract database
contract_list.append({
"contractName": parts[0],
"contractAddress": parts[1]
})
# Check if the string matches a token name
if urlstring.lower() in token_names:
return jsonify(type="token"), 200
# Connect to the system database asynchronously to check for smart contracts
async with await get_mysql_connection("system", USE_ASYNC=True) as conn_system:
async with conn_system.cursor() as cursor_system:
await cursor_system.execute("SELECT DISTINCT contractName FROM activeContracts")
smart_contract_names = {row[0].lower() for row in await cursor_system.fetchall()}
if urlstring.lower() in smart_contract_names:
return jsonify(type="smartContract"), 200
# If no match, classify as noise
return jsonify(type="noise"), 200
except Exception as e:
print("categoriseString:", e)
return jsonify(result="error", description=INTERNAL_ERROR), 500
@app.route('/api/v1.0/getTokenSmartContractList', methods=['GET'])
async def getTokenSmartContractList():
try:
# Prefix and suffix for databases
database_prefix = f"{mysql_config.database_prefix}_"
database_suffix = "_db"
# Initialize lists
token_list = []
contract_list = []
# Connect to MySQL information schema using `get_mysql_connection` with `no_standardize`
async with await get_mysql_connection("information_schema", no_standardize=True, USE_ASYNC=True) as conn_info:
async with conn_info.cursor() as cursor_info:
# Query all databases matching the prefix and suffix
await cursor_info.execute(
f"SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME LIKE '{database_prefix}%' AND SCHEMA_NAME LIKE '%{database_suffix}'"
)
all_databases = [row[0] for row in await cursor_info.fetchall()]
# Separate token and smart contract databases
for db_name in all_databases:
stripped_name = db_name[len(database_prefix):-len(database_suffix)]
# Exclude "latestCache" and "system" databases
if stripped_name in ["latestCache", "system"]:
continue
parts = stripped_name.split('_')
if len(parts) == 1: # Token database
token_list.append(stripped_name)
elif len(parts) == 2 and len(parts[1]) == 34 and (parts[1].startswith('F') or parts[1].startswith('o')): # Smart contract database
contract_list.append({
"contractName": parts[0],
"contractAddress": parts[1]
})
# Populate smart contract details
async with await get_mysql_connection("system", USE_ASYNC=True) as conn_system:
async with conn_system.cursor() as cursor_system:
await cursor_system.execute("SELECT * FROM activeContracts")
all_contracts_detail_list = await cursor_system.fetchall()
for contract in all_contracts_detail_list:
for item in contract_list:
if item["contractName"] == contract[1] and item["contractAddress"] == contract[2]:
item.update({
"status": contract[3],
"tokenIdentification": contract[4],
"contractType": contract[5],
"transactionHash": contract[6],
"blockNumber": contract[7],
"blockHash": contract[8],
"incorporationDate": contract[9],
"expiryDate": contract[10] if contract[10] else None,
"closeDate": contract[11] if contract[11] else None,
})
# Return response
if not is_backend_ready():
return jsonify(
tokens=token_list,
warning=BACKEND_NOT_READY_WARNING,
smartContracts=contract_list,
result="ok"
), 206
else:
return jsonify(
tokens=token_list,
smartContracts=contract_list,
result="ok"
), 200
except aiomysql.MySQLError as e:
print("Database error in getTokenSmartContractList:", e)
return jsonify(result="error", description="Database error occurred"), 500
except Exception as e:
print("getTokenSmartContractList:", e)
return jsonify(result="error", description=INTERNAL_ERROR), 500
###################
### VERSION 2 ###
###################
@app.route('/api/v2/info', methods=['GET'])
async def info():
try:
# Standardize database names
system_db_name = standardize_db_name("system")
latest_cache_db_name = standardize_db_name("latestCache")
# Initialize data variables
tokenAddressCount = tokenCount = contractCount = lastscannedblock = 0
validatedBlockCount = validatedTransactionCount = 0
# Use async with for resource management
async with await get_mysql_connection(system_db_name, USE_ASYNC=True) as conn_system:
async with conn_system.cursor() as cursor_system:
# Query for the number of FLO addresses in tokenAddress mapping
await cursor_system.execute("SELECT COUNT(DISTINCT tokenAddress) FROM tokenAddressMapping")
tokenAddressCount = (await cursor_system.fetchone())[0]
await cursor_system.execute("SELECT COUNT(DISTINCT token) FROM tokenAddressMapping")
tokenCount = (await cursor_system.fetchone())[0]
await cursor_system.execute("SELECT COUNT(DISTINCT contractName) FROM contractAddressMapping")
contractCount = (await cursor_system.fetchone())[0]
await cursor_system.execute("SELECT value FROM systemData WHERE attribute='lastblockscanned'")
lastscannedblock = int((await cursor_system.fetchone())[0])
async with await get_mysql_connection(latest_cache_db_name, USE_ASYNC=True) as conn_latest_cache:
async with conn_latest_cache.cursor() as cursor_latest_cache:
# Query for total number of validated blocks
await cursor_latest_cache.execute("SELECT COUNT(DISTINCT blockNumber) FROM latestBlocks")
validatedBlockCount = (await cursor_latest_cache.fetchone())[0]
await cursor_latest_cache.execute("SELECT COUNT(DISTINCT transactionHash) FROM latestTransactions")
validatedTransactionCount = (await cursor_latest_cache.fetchone())[0]
# Construct response based on backend readiness
response_data = {
"systemAddressCount": tokenAddressCount,
"systemBlockCount": validatedBlockCount,
"systemTransactionCount": validatedTransactionCount,
"systemSmartContractCount": contractCount,
"systemTokenCount": tokenCount,
"lastscannedblock": lastscannedblock
}
if not is_backend_ready():
response_data["warning"] = BACKEND_NOT_READY_WARNING
return jsonify(response_data), 206
else:
return jsonify(response_data), 200
except Exception as e:
print("info:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/broadcastTx/<raw_transaction_hash>')
async def broadcastTx_v2(raw_transaction_hash):
try:
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()), 200
except Exception as e:
print("broadcastTx_v2:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/tokenList', methods=['GET'])
async def tokenList():
try:
# Initialize token list
token_list = []
# Prefix and suffix for token databases
database_prefix = f"{mysql_config.database_prefix}_"
database_suffix = "_db"
# Connect to the MySQL information schema asynchronously
async with await get_mysql_connection("information_schema", no_standardize=True, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
try:
# Query to fetch all databases matching the prefix and suffix
await cursor.execute(
f"SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME LIKE '{database_prefix}%' AND SCHEMA_NAME LIKE '%{database_suffix}'"
)
all_databases = [row[0] for row in await cursor.fetchall()]
# Filter token databases and exclude smart contract databases
for db_name in all_databases:
stripped_name = db_name[len(database_prefix):-len(database_suffix)]
# Exclude "latestCache" and "system" databases
if stripped_name in ["latestCache", "system"]:
continue
parts = stripped_name.split('_')
# Include token databases and exclude smart contract databases
if len(parts) == 1: # Token databases have a single part (e.g., usd, inr)
token_list.append(stripped_name)
elif len(parts) == 2 and len(parts[1]) == 34 and (parts[1].startswith('F') or parts[1].startswith('o')):
# Smart contract databases are excluded
continue
except Exception as e:
print(f"Error querying databases: {e}")
return jsonify(description="Error querying databases"), 500
# Return response with backend readiness check
if not is_backend_ready():
return jsonify(warning=BACKEND_NOT_READY_WARNING, tokens=token_list), 206
else:
return jsonify(tokens=token_list), 200
except Exception as e:
print(f"tokenList:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/tokenInfo/<token>', methods=['GET'])
async def tokenInfo(token):
try:
if token is None:
return jsonify(description='Token name has not been passed'), 400
# Standardize database name for the token
token_db_name = standardize_db_name(token)
try:
# Connect to the token database asynchronously
async with await get_mysql_connection(token_db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch incorporation data
await cursor.execute('SELECT * FROM transactionHistory WHERE id=1')
incorporationRow = await cursor.fetchone()
# Fetch distinct active addresses count
await cursor.execute('SELECT COUNT(DISTINCT address) FROM activeTable')
numberOf_distinctAddresses = (await cursor.fetchone())[0]
# Fetch total number of transactions
await cursor.execute('SELECT MAX(id) FROM transactionHistory')
numberOf_transactions = (await cursor.fetchone())[0]
# Fetch associated contracts
await cursor.execute('''
SELECT contractName, contractAddress, blockNumber, blockHash, transactionHash
FROM tokenContractAssociation
''')
associatedContracts = await cursor.fetchall()
# Process associated contracts
associatedContractList = [
{
'contractName': item[0],
'contractAddress': item[1],
'blockNumber': item[2],
'blockHash': item[3],
'transactionHash': item[4],
}
for item in associatedContracts
]
# Prepare the response
response = {
'token': token,
'incorporationAddress': incorporationRow[1],
'tokenSupply': incorporationRow[3],
'time': incorporationRow[6],
'blockchainReference': incorporationRow[7],
'activeAddress_no': numberOf_distinctAddresses,
'totalTransactions': numberOf_transactions,
'associatedSmartContracts': associatedContractList,
}
if not is_backend_ready():
return jsonify(warning=BACKEND_NOT_READY_WARNING, **response), 206
else:
return jsonify(**response), 200
except aiomysql.MySQLError:
if not is_backend_ready():
return jsonify(description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(description="Token database doesn't exist"), 404
except Exception as e:
print("tokenInfo:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/tokenTransactions/<token>', methods=['GET'])
async def tokenTransactions(token):
try:
if token is None:
return jsonify(description='Token name has not 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)) # Page number, default is 1
to = int(request.args.get('to', 100)) # Number of items per page, default is 100
if _from < 1:
return jsonify(description='_from validation failed'), 400
if to < 1:
return jsonify(description='to validation failed'), 400
# Construct token database name
token_db_name = standardize_db_name(token)
# Connect to the token database and execute the query asynchronously
async with await get_mysql_connection(token_db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Build the query dynamically
query = "SELECT jsonData, parsedFloData FROM transactionHistory WHERE 1=1"
params = []
if senderFloAddress:
query += " AND sourceFloAddress = %s"
params.append(senderFloAddress)
if destFloAddress:
query += " AND destFloAddress = %s"
params.append(destFloAddress)
if use_AND == 'True' and senderFloAddress and destFloAddress:
query += " AND sourceFloAddress = %s AND destFloAddress = %s"
params.extend([senderFloAddress, destFloAddress])
query += " ORDER BY id DESC LIMIT %s OFFSET %s"
params.extend([to, (_from - 1) * to])
# Execute the query asynchronously
await cursor.execute(query, tuple(params))
transactionJsonData = await cursor.fetchall()
# Process and format transactions asynchronously
sortedFormattedTransactions = []
for row in transactionJsonData:
transactions_object = {}
transactions_object['transactionDetails'] = json.loads(row[0])
transactions_object['transactionDetails'] = await update_transaction_confirmations(transactions_object['transactionDetails'])
transactions_object['parsedFloData'] = json.loads(row[1])
sortedFormattedTransactions.append(transactions_object)
# Prepare response
if not is_backend_ready():
return jsonify(warning=BACKEND_NOT_READY_WARNING, token=token, transactions=sortedFormattedTransactions), 206
else:
return jsonify(token=token, transactions=sortedFormattedTransactions), 200
except aiomysql.MySQLError as e:
print(f"Database error in tokenTransactions: {e}")
return jsonify(description="Database error occurred"), 500
except Exception as e:
print("tokenTransactions:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/tokenBalances/<token>', methods=['GET'])
async def tokenBalances(token):
try:
# Validate the token parameter
if not token:
return jsonify(description="Token name has not been passed"), 400
# Standardize the token database name
token_db_name = standardize_db_name(token)
try:
# Establish async connection to the token database
async with await get_mysql_connection(token_db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch balances grouped by address
query = "SELECT address, SUM(transferBalance) FROM activeTable GROUP BY address"
await cursor.execute(query)
addressBalances = await cursor.fetchall()
# Create the return dictionary
returnList = {address: balance for address, balance in addressBalances}
except aiomysql.MySQLError as e:
print(f"Database error while fetching balances for token {token}: {e}")
if not is_backend_ready():
return jsonify(description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(description="Token database doesn't exist"), 404
# Prepare and return the response
if not is_backend_ready():
return jsonify(
warning=BACKEND_NOT_READY_WARNING,
token=token,
balances=returnList
), 206
else:
return jsonify(
token=token,
balances=returnList
), 200
except Exception as e:
print("tokenBalances:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/floAddressInfo/<floAddress>', methods=['GET'])
async def floAddressInfo(floAddress):
try:
if not floAddress:
return jsonify(description="floAddress hasn't been passed"), 400
# Validate the FLO address
if not check_flo_address(floAddress, is_testnet):
return jsonify(description="floAddress validation failed"), 400
detail_list = {}
incorporated_smart_contracts = []
# Connect to the system database to fetch associated tokens and contracts
async with await get_mysql_connection(standardize_db_name("system"), USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch tokens associated with the FLO address
await cursor.execute("SELECT DISTINCT token FROM tokenAddressMapping WHERE tokenAddress = %s", (floAddress,))
token_names = [row[0] for row in await cursor.fetchall()]
# Fetch incorporated smart contracts
smart_contract_query = """
SELECT contractName, status, tokenIdentification, contractType, transactionHash, blockNumber, blockHash
FROM activeContracts
WHERE contractAddress = %s
"""
await cursor.execute(smart_contract_query, (floAddress,))
incorporated_contracts = await cursor.fetchall()
# Process token balances concurrently
if token_names:
tasks = [fetch_token_balance(token, floAddress) for token in token_names]
balances = await asyncio.gather(*tasks, return_exceptions=True)
for token, balance_result in zip(token_names, balances):
if isinstance(balance_result, Exception):
print(f"Error fetching balance for token {token}: {balance_result}")
else:
detail_list.update(balance_result)
# Process incorporated contracts
for contract in incorporated_contracts:
incorporated_smart_contracts.append({
'contractName': contract[0],
'contractAddress': floAddress,
'status': contract[1],
'tokenIdentification': contract[2],
'contractType': contract[3],
'transactionHash': contract[4],
'blockNumber': contract[5],
'blockHash': contract[6],
})
# Prepare the response
response = {
'floAddress': floAddress,
'floAddressBalances': detail_list or None,
'incorporatedSmartContracts': incorporated_smart_contracts or None
}
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
else:
return jsonify(response), 200
except Exception as e:
print(f"floAddressInfo: {e}")
return jsonify(description="Unexpected error occurred"), 500
@app.route('/api/v2/floAddressBalance/<floAddress>', methods=['GET'])
async def floAddressBalance(floAddress):
try:
if not floAddress:
return jsonify(description="floAddress hasn't been passed"), 400
# Validate the FLO address
if not check_flo_address(floAddress, is_testnet):
return jsonify(description="floAddress validation failed"), 400
token = request.args.get('token')
# Case 1: Fetch balances for all associated tokens
if not token:
async with await get_mysql_connection(standardize_db_name("system"), USE_ASYNC=True) as system_conn:
async with system_conn.cursor() as cursor:
# Fetch tokens associated with the FLO address
query = "SELECT DISTINCT token FROM tokenAddressMapping WHERE tokenAddress = %s"
await cursor.execute(query, (floAddress,))
token_names = [row[0] for row in await cursor.fetchall()]
if not token_names:
return jsonify(description="No tokens associated with the FLO address"), 404
# Fetch balances for each token concurrently
tasks = [fetch_token_balance(token_name, floAddress) for token_name in token_names]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Aggregate balances
detail_list = {}
for token_name, result in zip(token_names, results):
if isinstance(result, Exception):
print(f"Error fetching balance for token {token_name}: {result}")
else:
detail_list.update(result)
# Prepare the response
response = {
'floAddress': floAddress,
'floAddressBalances': detail_list
}
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
return jsonify(response), 200
# Case 2: Fetch balance for a specific token
else:
result = await fetch_token_balance(token, floAddress)
balance = result.get(token, {}).get('balance', 0)
response = {
'floAddress': floAddress,
'token': token,
'balance': balance
}
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
return jsonify(response), 200
except aiomysql.MySQLError as e:
print(f"Database error in floAddressBalance: {e}")
return jsonify(description="Database error occurred"), 500
except Exception as e:
print(f"floAddressBalance: {e}")
return jsonify(description="Unexpected error occurred"), 500
@app.route('/api/v2/floAddressTransactions/<floAddress>', methods=['GET'])
async def floAddressTransactions(floAddress):
try:
# Validate input
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
# Validate limit
limit = request.args.get('limit')
if limit is not None and not check_integer(limit):
return jsonify(description='limit validation failed'), 400
# Get optional token filter
token = request.args.get('token')
all_transaction_list = []
if token is None:
try:
# Fetch associated tokens for the FLO address
async with await get_mysql_connection(standardize_db_name("system"), USE_ASYNC=True) as system_conn:
async with system_conn.cursor() as cursor:
query = "SELECT DISTINCT token FROM tokenAddressMapping WHERE tokenAddress = %s"
await cursor.execute(query, (floAddress,))
token_names = [row[0] for row in await cursor.fetchall()]
# If no tokens found, return 404
if not token_names:
return jsonify(description="No tokens associated with the FLO address"), 404
# Fetch transactions for all associated tokens concurrently
tasks = [
fetch_token_transactions(token_name, floAddress, floAddress, limit)
for token_name in token_names
]
transaction_data = await asyncio.gather(*tasks)
# Aggregate transactions from all tokens
for data in transaction_data:
all_transaction_list.extend(data)
except aiomysql.MySQLError as e:
print(f"Database error while fetching tokens: {e}")
return jsonify(description="Database error occurred"), 500
else:
try:
# Fetch transactions for the specified token
all_transaction_list = await fetch_token_transactions(
token, floAddress, floAddress, limit
)
except aiomysql.MySQLError as e:
print(f"Error accessing token database {token}: {e}")
return jsonify(description="Token database error occurred"), 500
# Sort and format transactions
sorted_formatted_transactions = sort_transactions(all_transaction_list)
# Prepare response
response = {
"floAddress": floAddress,
"transactions": sorted_formatted_transactions
}
if token:
response["token"] = token
if not is_backend_ready():
response["warning"] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
return jsonify(response), 200
except Exception as e:
print("floAddressTransactions:", e)
return jsonify(description="Unexpected error occurred"), 500
# SMART CONTRACT APIs
@app.route('/api/v2/smartContractList', methods=['GET'])
async def getContractList_v2():
try:
# Retrieve and validate query parameters
contractName = request.args.get('contractName')
if contractName:
contractName = contractName.strip().lower()
contractAddress = request.args.get('contractAddress')
if contractAddress:
contractAddress = contractAddress.strip()
if not check_flo_address(contractAddress, is_testnet):
return jsonify(description="contractAddress validation failed"), 400
# Standardize the system database name
system_db_name = standardize_db_name("system")
# Initialize contract list
smart_contracts_morphed = []
# Fetch contracts from the database asynchronously
async with await get_mysql_connection(system_db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Build query dynamically
query = "SELECT * FROM activecontracts"
conditions = []
params = []
if contractName:
conditions.append("contractName=%s")
params.append(contractName)
if contractAddress:
conditions.append("contractAddress=%s")
params.append(contractAddress)
if conditions:
query += " WHERE " + " AND ".join(conditions)
# Execute query and fetch results
await cursor.execute(query, tuple(params))
smart_contracts = await cursor.fetchall()
# Morph the smart contract data
smart_contracts_morphed = await smartcontract_morph_helper(smart_contracts)
# Fetch the committee address list asynchronously
committeeAddressList = await refresh_committee_list(APP_ADMIN, apiUrl, int(time.time()))
# Prepare the response
response = {
"smartContracts": smart_contracts_morphed,
"smartContractCommittee": committeeAddressList,
}
if not is_backend_ready():
response["warning"] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
return jsonify(response), 200
except Exception as e:
print("getContractList_v2:", e)
return jsonify(description="Unexpected error occurred"), 500
@app.route('/api/v2/getSmartContractInfo', methods=['GET'], endpoint='getSmartContractInfoV2')
async def getContractInfo():
try:
# Validate query parameters
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
if not contractName:
return jsonify(result='error', description="Smart Contract's name hasn't been passed"), 400
if not contractAddress:
return jsonify(result='error', description="Smart Contract's address hasn't been passed"), 400
# Standardize the database names
contract_db_name = standardize_db_name(f"{contractName}_{contractAddress}")
system_db_name = standardize_db_name("system")
# Initialize the response structure
contract_info = {}
# Fetch contract structure and participants/token details
async with await get_mysql_connection(contract_db_name, USE_ASYNC=True) as conn_contract:
async with conn_contract.cursor() as cursor:
# Fetch contract structure
await cursor.execute("SELECT attribute, value FROM contractstructure")
results = await cursor.fetchall()
exit_conditions = {}
for idx, (attribute, value) in enumerate(results):
if attribute == 'exitconditions':
exit_conditions[idx] = value
else:
contract_info[attribute] = value
if exit_conditions:
contract_info['userChoice'] = exit_conditions
# Fetch participant details
await cursor.execute("SELECT COUNT(participantAddress) FROM contractparticipants")
contract_info['numberOfParticipants'] = (await cursor.fetchone())[0]
await cursor.execute("SELECT SUM(tokenAmount) FROM contractparticipants")
contract_info['tokenAmountDeposited'] = (await cursor.fetchone())[0]
# Fetch system-level contract details
async with await get_mysql_connection(system_db_name, USE_ASYNC=True) as conn_system:
async with conn_system.cursor() as cursor:
query = """
SELECT status, incorporationDate, expiryDate, closeDate
FROM activecontracts
WHERE contractName = %s AND contractAddress = %s
"""
await cursor.execute(query, (contractName, contractAddress))
system_results = await cursor.fetchone()
if system_results:
contract_info.update({
'status': system_results[0],
'incorporationDate': system_results[1],
'expiryDate': system_results[2],
'closeDate': system_results[3]
})
# Handle additional logic for closed contracts
if contract_info.get('status') == 'closed' and contract_info.get('contractType') == 'one-time-event':
async with await get_mysql_connection(contract_db_name, USE_ASYNC=True) as conn_contract:
async with conn_contract.cursor() as cursor:
await cursor.execute("""
SELECT transactionType, transactionSubType
FROM contractTransactionHistory
WHERE transactionType = 'trigger'
""")
triggers = await cursor.fetchall()
if len(triggers) == 1:
trigger_type = triggers[0][1]
contract_info['triggerType'] = trigger_type
# Fetch winning details if user choices exist
if 'userChoice' in contract_info:
if trigger_type is None:
await cursor.execute("""
SELECT userChoice
FROM contractparticipants
WHERE winningAmount IS NOT NULL
LIMIT 1
""")
contract_info['winningChoice'] = (await cursor.fetchone())[0]
await cursor.execute("""
SELECT participantAddress, winningAmount
FROM contractparticipants
WHERE winningAmount IS NOT NULL
""")
winners = await cursor.fetchall()
contract_info['numberOfWinners'] = len(winners)
else:
return jsonify(result='error', description="Data integrity issue: multiple triggers found"), 500
# Final response
response = {
'result': 'ok',
'contractName': contractName,
'contractAddress': contractAddress,
'contractInfo': contract_info
}
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
return jsonify(response), 200
except Exception as e:
print("getContractInfo:", e)
return jsonify(result='error', description="Unexpected error occurred"), 500
@app.route('/api/v2/smartContractParticipants', methods=['GET'])
async def getcontractparticipants_v2():
try:
# Validate query parameters
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
if not contractName:
return jsonify(description="Smart Contract's name hasn't been passed"), 400
if not contractAddress:
return jsonify(description="Smart Contract's address hasn't been passed"), 400
if not check_flo_address(contractAddress, is_testnet):
return jsonify(description="contractAddress validation failed"), 400
# Standardize database name
contractName = contractName.strip().lower()
contractAddress = contractAddress.strip()
db_name = standardize_db_name(f"{contractName}_{contractAddress}")
# Fetch contract structure and status
contractStructure = await fetchContractStructure(contractName, contractAddress)
contractStatus = await fetchContractStatus(contractName, contractAddress)
# Initialize the response
participantInfo = []
# Async connection to the contract database
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Handle external-trigger contracts
if 'exitconditions' in contractStructure:
token = contractStructure.get('tokenIdentification', None)
if contractStatus == 'closed':
query = '''
SELECT id, participantAddress, tokenAmount, userChoice, transactionHash
FROM contractparticipants
'''
await cursor.execute(query)
participants = await cursor.fetchall()
for row in participants:
await cursor.execute(
'SELECT winningAmount FROM contractwinners WHERE referenceTxHash = %s',
(row[4],)
)
winningAmount = (await cursor.fetchone() or [0])[0]
participantInfo.append({
'participantFloAddress': row[1],
'tokenAmount': row[2],
'userChoice': row[3],
'transactionHash': row[4],
'winningAmount': winningAmount,
'tokenIdentification': token
})
else:
query = '''
SELECT id, participantAddress, tokenAmount, userChoice, transactionHash
FROM contractparticipants
'''
await cursor.execute(query)
participants = await cursor.fetchall()
for row in participants:
participantInfo.append({
'participantFloAddress': row[1],
'tokenAmount': row[2],
'userChoice': row[3],
'transactionHash': row[4]
})
contractSubtype = 'external-trigger'
# Handle time-trigger contracts
elif 'payeeAddress' in contractStructure:
query = '''
SELECT id, participantAddress, tokenAmount, transactionHash
FROM contractparticipants
'''
await cursor.execute(query)
participants = await cursor.fetchall()
for row in participants:
participantInfo.append({
'participantFloAddress': row[1],
'tokenAmount': row[2],
'transactionHash': row[3]
})
contractSubtype = 'time-trigger'
# Handle tokenswap contracts
elif contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap':
query = '''
SELECT id, participantAddress, participationAmount, swapPrice, transactionHash, blockNumber, blockHash, swapAmount
FROM contractparticipants
'''
await cursor.execute(query)
participants = await cursor.fetchall()
for row in participants:
participantInfo.append({
'participantFloAddress': row[1],
'participationAmount': row[2],
'swapPrice': float(row[3]),
'transactionHash': row[4],
'blockNumber': row[5],
'blockHash': row[6],
'swapAmount': row[7]
})
contractSubtype = 'tokenswap'
else:
return jsonify(description="Unsupported contract type"), 400
# Prepare the response
response = {
'contractName': contractName,
'contractAddress': contractAddress,
'contractType': contractStructure['contractType'],
'contractSubtype': contractSubtype,
'participantInfo': participantInfo
}
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
else:
return jsonify(response), 200
except Exception as e:
print("getcontractparticipants_v2:", e)
return jsonify(description="Unexpected error occurred"), 500
@app.route('/api/v2/getParticipantDetails', methods=['GET'], endpoint='getParticipantDetailsV2')
async def getParticipantDetails():
try:
floAddress = request.args.get('floAddress')
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
# Validate input parameters
if not floAddress:
return jsonify(result='error', description='FLO address hasn\'t been passed'), 400
if (contractName and not contractAddress) or (contractAddress and not contractName):
return jsonify(result='error', description='Pass both, contractName and contractAddress as URL parameters'), 400
floAddress = floAddress.strip()
if contractName:
contractName = contractName.strip().lower()
if contractAddress:
contractAddress = contractAddress.strip()
# Standardize database names
system_db_name = standardize_db_name("system")
contract_db_name = standardize_db_name(f"{contractName}_{contractAddress}") if contractName and contractAddress else None
participationDetailsList = []
# Connect to the system database asynchronously
async with await get_mysql_connection(system_db_name, USE_ASYNC=True) as conn_system:
async with conn_system.cursor() as cursor_system:
# Build the query dynamically
if contractName and contractAddress:
query = '''
SELECT *
FROM contractAddressMapping
WHERE address = %s AND addressType = "participant" AND contractName = %s AND contractAddress = %s
'''
params = (floAddress, contractName, contractAddress)
else:
query = '''
SELECT *
FROM contractAddressMapping
WHERE address = %s AND addressType = "participant"
'''
params = (floAddress,)
# Execute the query asynchronously
await cursor_system.execute(query, params)
participant_address_contracts = await cursor_system.fetchall()
if not participant_address_contracts:
if not is_backend_ready():
return jsonify(result='error', description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(result='error', description='Address hasn\'t participated in any contract'), 404
# Process each contract the address has participated in
for contract in participant_address_contracts:
detailsDict = {}
contract_name = contract[3]
contract_address = contract[4]
contract_db_name = standardize_db_name(f"{contract_name}_{contract_address}")
# Fetch contract structure asynchronously
contractStructure = await fetchContractStructure(contract_name, contract_address)
# Async connection to the contract database
async with await get_mysql_connection(contract_db_name, USE_ASYNC=True) as conn_contract:
async with conn_contract.cursor() as cursor_contract:
if contractStructure['contractType'] == 'tokenswap':
# Fetch participant details for tokenswap contracts
query = '''
SELECT participantAddress, participationAmount, receivedAmount, transactionHash, blockNumber, blockHash
FROM contractparticipants
WHERE participantAddress = %s
'''
await cursor_contract.execute(query, (floAddress,))
participation_details = await cursor_contract.fetchall()
participationList = []
for row in participation_details:
participationList.append({
'participationAddress': floAddress,
'participationAmount': row[1],
'receivedAmount': row[2],
'transactionHash': row[3],
'blockNumber': row[4],
'blockHash': row[5]
})
detailsDict['contractName'] = contract_name
detailsDict['contractAddress'] = contract_address
detailsDict['participationDetails'] = participationList
elif contractStructure['contractType'] == 'one-time-event' and 'payeeAddress' in contractStructure:
# Fetch participant details for one-time-event contracts
query = '''
SELECT tokenAmount, transactionHash
FROM contractparticipants
WHERE participantAddress = %s
'''
await cursor_contract.execute(query, (floAddress,))
result = await cursor_contract.fetchone()
detailsDict['contractName'] = contract_name
detailsDict['contractAddress'] = contract_address
detailsDict['tokenAmount'] = result[0]
detailsDict['transactionHash'] = result[1]
# Add more contract type logic here if needed
participationDetailsList.append(detailsDict)
# Finalize the response
if not is_backend_ready():
return jsonify(
result='ok',
warning=BACKEND_NOT_READY_WARNING,
floAddress=floAddress,
type='participant',
participatedContracts=participationDetailsList
), 206
else:
return jsonify(
result='ok',
floAddress=floAddress,
type='participant',
participatedContracts=participationDetailsList
), 200
except aiomysql.MySQLError as db_error:
print(f"Database error: {db_error}")
return jsonify(result='error', description="Database error occurred"), 500
except Exception as e:
print(f"getParticipantDetails: {e}")
return jsonify(result='error', description="Unexpected error occurred"), 500
@app.route('/api/v2/smartContractTransactions', methods=['GET'])
async def smartcontracttransactions():
try:
# Get and validate query parameters
contractName = request.args.get('contractName')
if not contractName:
return jsonify(description="Smart Contract's name hasn't been passed"), 400
contractName = contractName.strip().lower()
contractAddress = request.args.get('contractAddress')
if not contractAddress:
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)) # Page number, default is 1
to = int(request.args.get('to', 100)) # Limit per page, default is 100
if _from < 1:
return jsonify(description="_from validation failed"), 400
if to < 1:
return jsonify(description="to validation failed"), 400
# Standardize the contract database name
contractDbName = standardize_db_name(f"{contractName}_{contractAddress}")
# Check if the smart contract database exists
async with await get_mysql_connection("information_schema", no_standardize=True, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
query = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = %s"
await cursor.execute(query, (contractDbName,))
db_exists = await cursor.fetchone()
if not db_exists:
# Handle missing smart contract database
if not is_backend_ready():
return jsonify(description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(description="Smart Contract with the given name doesn't exist"), 404
# Fetch transaction data for the smart contract
transactionJsonData = await fetch_contract_transactions(contractName, contractAddress, _from, to)
transactionJsonData = sort_transactions(transactionJsonData)
# Construct response
response_data = {
"contractName": contractName,
"contractAddress": contractAddress,
"contractTransactions": transactionJsonData
}
if not is_backend_ready():
response_data["warning"] = BACKEND_NOT_READY_WARNING
return jsonify(response_data), 206
else:
return jsonify(response_data), 200
except ValueError as ve:
# Handle invalid input for _from or to
print(f"Value error in smartcontracttransactions: {ve}")
return jsonify(description="Invalid input for _from or to"), 400
except Exception as e:
# General error handling
print(f"smartcontracttransactions: {e}")
return jsonify(description=INTERNAL_ERROR), 500
# todo - add options to only ask for active/consumed/returned deposits
@app.route('/api/v2/smartContractDeposits', methods=['GET'])
async def smartcontractdeposits():
try:
# Validate input parameters
contractName = request.args.get('contractName')
if not contractName:
return jsonify(description="Smart Contract's name hasn't been passed"), 400
contractName = contractName.strip().lower()
contractAddress = request.args.get('contractAddress')
if not contractAddress:
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
# Standardize the database name
db_name = standardize_db_name(f"{contractName}_{contractAddress}")
# Connect to the smart contract database asynchronously
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Fetch distinct deposits with the latest balances
distinct_deposits_query = """
SELECT depositorAddress, transactionHash, status, depositBalance
FROM contractdeposits
WHERE (transactionHash, id) IN (
SELECT transactionHash, MAX(id)
FROM contractdeposits
GROUP BY transactionHash
)
ORDER BY id DESC;
"""
await cursor.execute(distinct_deposits_query)
distinct_deposits = await cursor.fetchall()
deposit_info = []
for deposit in distinct_deposits:
depositor_address, transaction_hash, status, current_balance = deposit
# Fetch the original deposit balance and expiry time asynchronously
original_deposit_query = """
SELECT depositBalance, unix_expiryTime
FROM contractdeposits
WHERE transactionHash = %s
ORDER BY id
LIMIT 1;
"""
await cursor.execute(original_deposit_query, (transaction_hash,))
original_deposit = await cursor.fetchone()
if original_deposit:
original_balance, expiry_time = original_deposit
deposit_info.append({
'depositorAddress': depositor_address,
'transactionHash': transaction_hash,
'status': status,
'originalBalance': original_balance,
'currentBalance': current_balance,
'time': expiry_time
})
# Fetch the current total deposit balance
total_deposit_balance_query = """
SELECT SUM(depositBalance) AS totalDepositBalance
FROM contractdeposits c1
WHERE id = (
SELECT MAX(id)
FROM contractdeposits c2
WHERE c1.transactionHash = c2.transactionHash
);
"""
await cursor.execute(total_deposit_balance_query)
current_deposit_balance = await cursor.fetchone()
# Prepare the response
response = {
'currentDepositBalance': current_deposit_balance[0] if current_deposit_balance else 0,
'depositInfo': deposit_info
}
# Return response based on backend readiness
if not is_backend_ready():
response['warning'] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
else:
return jsonify(response), 200
except aiomysql.MySQLError as db_error:
print(f"Database error in smartcontractdeposits: {db_error}")
return jsonify(description="Database error occurred"), 500
except Exception as e:
print(f"smartcontractdeposits: {e}")
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/blockDetails/<blockHash>', methods=['GET'])
async def blockdetails(blockHash):
try:
# todo - validate blockHash
blockJson = await blockdetailhelper(blockHash)
if len(blockJson) != 0:
blockJson = json.loads(blockJson[0][0])
return jsonify(blockDetails=blockJson), 200
else:
if not is_backend_ready():
return jsonify(description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(description='Block doesn\'t exist in database'), 404
except Exception as e:
print("blockdetails:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/transactionDetails/<transactionHash>', methods=['GET'])
async def transactiondetails1(transactionHash):
try:
# Fetch transaction details using the helper
transactionJsonData = await transactiondetailhelper(transactionHash)
if transactionJsonData:
transactionJson = json.loads(transactionJsonData[0][0])
transactionJson = await 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}
mergeTx['onChain'] = True
operationDetails = {}
# Standardize database name
db_name = standardize_db_name(db_reference)
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
if operation == 'smartContractDeposit':
# Handle smart contract deposits
await cursor.execute("""
SELECT depositAmount, blockNumber
FROM contractdeposits
WHERE status = 'deposit-return' AND transactionHash = %s
""", (transactionJson['txid'],))
returned_deposit_tx = await cursor.fetchall()
if returned_deposit_tx:
operationDetails['returned_depositAmount'] = returned_deposit_tx[0][0]
operationDetails['returned_blockNumber'] = returned_deposit_tx[0][1]
await cursor.execute("""
SELECT depositAmount, blockNumber
FROM contractdeposits
WHERE status = 'deposit-honor' AND transactionHash = %s
""", (transactionJson['txid'],))
deposit_honors = await cursor.fetchall()
operationDetails['depositHonors'] = {
'list': [{'honor_amount': honor[0], 'blockNumber': honor[1]} for honor in deposit_honors],
'count': len(deposit_honors)
}
await cursor.execute("""
SELECT depositBalance
FROM contractdeposits
WHERE id = (SELECT MAX(id) FROM contractdeposits WHERE transactionHash = %s)
""", (transactionJson['txid'],))
depositBalance = await cursor.fetchone()
operationDetails['depositBalance'] = depositBalance[0]
operationDetails['consumedAmount'] = parseResult['depositAmount'] - operationDetails['depositBalance']
elif operation == 'tokenswap-participation':
# Handle token swap participation
await cursor.execute("""
SELECT tokenAmount, winningAmount, userChoice
FROM contractparticipants
WHERE transactionHash = %s
""", (transactionJson['txid'],))
swap_amounts = await cursor.fetchone()
await cursor.execute("SELECT value FROM contractstructure WHERE attribute = 'selling_token'")
structure = await cursor.fetchone()
operationDetails = {
'participationAmount': swap_amounts[0],
'receivedAmount': swap_amounts[1],
'participationToken': parseResult['tokenIdentification'],
'receivedToken': structure[0],
'swapPrice_received_to_participation': float(swap_amounts[2])
}
elif operation == 'smartContractPays':
# Handle smart contract payouts
await cursor.execute("""
SELECT participantAddress, tokenAmount, userChoice, winningAmount
FROM contractparticipants
WHERE winningAmount IS NOT NULL
""")
winner_participants = await cursor.fetchall()
operationDetails = {
'total_winners': len(winner_participants),
'winning_choice': winner_participants[0][2] if winner_participants else None,
'winner_list': [
{
'participantAddress': participant[0],
'participationAmount': participant[1],
'winningAmount': participant[3]
} for participant in winner_participants
]
}
elif operation == 'ote-externaltrigger-participation':
# Handle external-trigger participation
await cursor.execute("""
SELECT winningAmount
FROM contractparticipants
WHERE transactionHash = %s
""", (transactionHash,))
winningAmount = await cursor.fetchone()
if winningAmount and winningAmount[0] is not None:
operationDetails['winningAmount'] = winningAmount[0]
elif operation == 'tokenswapParticipation':
# Handle token swap participation
contractName, contractAddress = db_reference.rsplit('_', 1)
txhash_txs = await fetch_swap_contract_transactions(contractName, contractAddress, transactionHash)
mergeTx['subTransactions'] = [
tx for tx in txhash_txs if not tx.get('onChain', True)
]
mergeTx['operation'] = operation
mergeTx['operationDetails'] = operationDetails
return jsonify(mergeTx), 200
else:
# Handle no transaction found
if not is_backend_ready():
return jsonify(description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(description="Transaction doesn't exist in database"), 404
except Exception as e:
print(f"transactiondetails1: {e}")
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/latestTransactionDetails', methods=['GET'])
async def latestTransactionDetails():
try:
# Validate the 'limit' parameter
limit = request.args.get('limit')
if limit is not None and not check_integer(limit):
return jsonify(description='limit validation failed'), 400
# Standardize the database name
db_name = standardize_db_name("latestCache")
# Connect to the database asynchronously
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Build and execute the query
if limit is not None:
query = """
SELECT * FROM latestTransactions
WHERE blockNumber IN (
SELECT DISTINCT blockNumber
FROM latestTransactions
ORDER BY blockNumber DESC
LIMIT %s
)
ORDER BY id DESC;
"""
await cursor.execute(query, (int(limit),))
else:
query = """
SELECT * FROM latestTransactions
WHERE blockNumber IN (
SELECT DISTINCT blockNumber
FROM latestTransactions
ORDER BY blockNumber DESC
)
ORDER BY id DESC;
"""
await cursor.execute(query)
# Fetch and process transactions
latestTransactions = await cursor.fetchall()
tx_list = []
for item in latestTransactions:
item = list(item)
tx_parsed_details = {}
# Parse transaction details
tx_parsed_details['transactionDetails'] = json.loads(item[3])
tx_parsed_details['transactionDetails'] = await 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])
# Merge parsed details
merged_tx = {**tx_parsed_details['transactionDetails'], **tx_parsed_details['parsedFloData']}
merged_tx['onChain'] = True
# Append to the transaction list
tx_list.append(merged_tx)
# Return response based on backend readiness
if not is_backend_ready():
return jsonify(warning=BACKEND_NOT_READY_WARNING, latestTransactions=tx_list), 206
else:
return jsonify(latestTransactions=tx_list), 200
except Exception as e:
print("latestTransactionDetails:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/latestBlockDetails', methods=['GET'])
async def latestBlockDetails():
try:
# Validate the 'limit' parameter
limit = request.args.get('limit')
if limit is not None and not check_integer(limit):
return jsonify(description='limit validation failed'), 400
# Standardize the database name
db_name = standardize_db_name("latestCache")
# Connect to the database asynchronously
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Build the SQL query
if limit is None:
query = """
SELECT jsonData
FROM (
SELECT *
FROM latestBlocks
ORDER BY blockNumber DESC
LIMIT 4
) subquery
ORDER BY id DESC;
"""
await cursor.execute(query)
else:
query = """
SELECT jsonData
FROM (
SELECT *
FROM latestBlocks
ORDER BY blockNumber DESC
LIMIT %s
) subquery
ORDER BY id DESC;
"""
await cursor.execute(query, (int(limit),))
# Fetch and parse the blocks
latestBlocks = await cursor.fetchall()
templst = [json.loads(item[0]) for item in latestBlocks]
# Return response based on backend readiness
if not is_backend_ready():
return jsonify(warning=BACKEND_NOT_READY_WARNING, latestBlocks=templst), 206
else:
return jsonify(latestBlocks=templst), 200
except Exception as e:
print("latestBlockDetails:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/blockTransactions/<blockHash>', methods=['GET'])
async def blocktransactions(blockHash):
try:
blockJson = await blockdetailhelper(blockHash)
if len(blockJson) != 0:
blockJson = json.loads(blockJson[0][0])
blocktxlist = blockJson['txs']
blocktxs = []
for i in range(len(blocktxlist)):
temptx = await 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:
if not is_backend_ready():
return jsonify(description=BACKEND_NOT_READY_ERROR), 503
else:
return jsonify(description='Block doesn\'t exist in database'), 404
except Exception as e:
print("blocktransactions:", e)
return jsonify(description=INTERNAL_ERROR), 500
@app.route('/api/v2/categoriseString/<urlstring>', methods=['GET'])
async def categoriseString_v2(urlstring):
try:
# Check if the string is a transaction or block hash
async with aiohttp.ClientSession() as session:
# Check if it's a transaction hash
async with session.get(f"{apiUrl}api/v1/tx/{urlstring}") as response:
if response.status == 200:
return jsonify(type='transaction'), 200
# Check if it's a block hash
async with session.get(f"{apiUrl}api/v1/block/{urlstring}") as response:
if response.status == 200:
return jsonify(type='block'), 200
# Check if the string is a token name or a smart contract name
db_name = standardize_db_name("system")
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Check if it's a token
query = """
SELECT DISTINCT tokenName
FROM tokenInfo
WHERE LOWER(tokenName) = %s;
"""
await cursor.execute(query, (urlstring.lower(),))
token_result = await cursor.fetchone()
if token_result:
return jsonify(type='token'), 200
# Check if it's a smart contract name
query = """
SELECT DISTINCT contractName
FROM activeContracts
WHERE LOWER(contractName) = %s;
"""
await cursor.execute(query, (urlstring.lower(),))
contract_result = await cursor.fetchone()
if contract_result:
return jsonify(type='smartContract'), 200
# If no match, classify as noise
return jsonify(type='noise'), 200
except Exception as e:
print("categoriseString_v2:", e)
return jsonify(description="Internal Error"), 500
# Assuming `get_mysql_connection` has been updated to work with aiomysql and async
@app.route('/api/v2/tokenSmartContractList', methods=['GET'])
async def tokenSmartContractList():
try:
# Prefix for databases
database_prefix = f"{mysql_config.database_prefix}_"
database_suffix = "_db"
# Initialize lists for tokens and contracts
token_list = []
# Step 1: Enumerate all databases asynchronously
async with await get_mysql_connection("information_schema", no_standardize=True, USE_ASYNC=True) as conn_info:
async with conn_info.cursor() as cursor:
query = f"""
SELECT SCHEMA_NAME
FROM SCHEMATA
WHERE SCHEMA_NAME LIKE '{database_prefix}%'
AND SCHEMA_NAME LIKE '%{database_suffix}'
"""
await cursor.execute(query)
all_databases = await cursor.fetchall()
# Filter token databases from smart contract databases
for db_name in all_databases:
stripped_name = db_name[0][len(database_prefix):-len(database_suffix)]
# Exclude "latestCache" and "system" databases
if stripped_name in ["latestCache", "system"]:
continue
parts = stripped_name.split('_')
if len(parts) == 1: # Token database format (e.g., usd, inr)
token_list.append(stripped_name)
# Step 2: Fetch smart contracts from the `system` database
async with await get_mysql_connection("system", USE_ASYNC=True) as conn_system:
async with conn_system.cursor() as cursor:
# Validate and process query parameters
contractName = request.args.get('contractName')
contractAddress = request.args.get('contractAddress')
if contractName:
contractName = contractName.strip().lower()
if contractAddress:
contractAddress = contractAddress.strip()
if not check_flo_address(contractAddress, is_testnet):
return jsonify(description='contractAddress validation failed'), 400
# Fetch smart contracts matching the filters
query = """
SELECT *
FROM activeContracts
WHERE (%s IS NULL OR LOWER(contractName) = %s)
AND (%s IS NULL OR contractAddress = %s)
"""
await cursor.execute(query, (contractName, contractName, contractAddress, contractAddress))
smart_contracts = await cursor.fetchall()
# Morph the smart contract data for response formatting
smart_contracts_morphed = await smartcontract_morph_helper(smart_contracts)
# Step 3: Fetch committee address list
committeeAddressList = await refresh_committee_list(APP_ADMIN, apiUrl, int(time.time()))
# Step 4: Prepare and send the response
response = {
"tokens": token_list,
"smartContracts": smart_contracts_morphed,
"smartContractCommittee": committeeAddressList
}
if not is_backend_ready():
response["warning"] = BACKEND_NOT_READY_WARNING
return jsonify(response), 206
else:
return jsonify(response), 200
except Exception as e:
print("tokenSmartContractList:", e)
return jsonify(description=INTERNAL_ERROR), 500
class ServerSentEvent:
def __init__(
self,
data: str,
*,
event: Optional[str] = None,
id: Optional[int] = None,
retry: Optional[int] = None,
) -> None:
self.data = data
self.event = event
self.id = id
self.retry = retry
def encode(self) -> bytes:
message = f"data: {self.data}"
if self.event is not None:
message = f"{message}\nevent: {self.event}"
if self.id is not None:
message = f"{message}\nid: {self.id}"
if self.retry is not None:
message = f"{message}\nretry: {self.retry}"
message = f"{message}\r\n\r\n"
return message.encode('utf-8')
@app.route('/sse')
async def sse():
queue = asyncio.Queue()
app.clients.add(queue)
async def send_events():
while True:
try:
data = await queue.get()
event = ServerSentEvent(data)
yield event.encode()
except asyncio.CancelledError as error:
app.clients.remove(queue)
response = await make_response(
send_events(),
{
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Transfer-Encoding': 'chunked',
},
)
response.timeout = None
return response
@app.route('/api/v2/prices', methods=['GET'])
async def priceData():
try:
# Standardize the system database name
db_name = standardize_db_name("system")
# Connect to the database asynchronously
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Query to fetch rate pairs and prices
query = "SELECT ratepair, price FROM ratepairs;"
await cursor.execute(query)
ratepairs = await cursor.fetchall()
# Prepare a dictionary of prices
prices = {ratepair[0]: ratepair[1] for ratepair in ratepairs}
# Return the prices
return jsonify(prices=prices), 200
except Exception as e:
print("priceData:", e)
return jsonify(description="Internal Error"), 500
#######################
#######################
async def initialize_db():
"""
Initializes the `ratepairs` table in the `system` database if it does not exist,
and populates it with default values.
"""
try:
# Standardize database name for the system database
db_name = standardize_db_name("system")
# Connect to the database asynchronously
async with await get_mysql_connection(db_name, USE_ASYNC=True) as conn:
async with conn.cursor() as cursor:
# Create the `ratepairs` table if it does not exist
await cursor.execute("""
CREATE TABLE IF NOT EXISTS ratepairs (
id INT AUTO_INCREMENT PRIMARY KEY,
ratepair VARCHAR(20) NOT NULL UNIQUE,
price FLOAT NOT NULL
)
""")
# Check if the table contains any data
await cursor.execute("SELECT COUNT(*) FROM ratepairs")
count = (await cursor.fetchone())[0]
if count == 0:
# Insert default rate pairs if the table is empty
default_ratepairs = [
('BTCBTC', 1),
('BTCUSD', -1),
('BTCINR', -1),
('FLOUSD', -1),
('FLOINR', -1),
('USDINR', -1)
]
await cursor.executemany(
"INSERT INTO ratepairs (ratepair, price) VALUES (%s, %s)",
default_ratepairs
)
await conn.commit()
# Update the prices
await updatePrices()
except Exception as e:
print(f"Error initializing the database: {e}")
def set_configs(config):
global DATA_PATH, apiUrl, FLO_DATA_DIR, API_VERIFY, debug_status, APIHOST, APIPORT, APP_ADMIN, NET, is_testnet
# Handle paths and API details
DATA_PATH = config.get("API", "dbfolder", fallback="")
apiUrl = config.get("API", "apiUrl", fallback="https://blockbook.ranchimall.net/api/")
FLO_DATA_DIR = config.get("API", "FLO_DATA_DIR", fallback="")
# Handle API verification
API_VERIFY = config.getboolean("DEFAULT", "API_VERIFY", fallback=True)
# Debug status and server details
debug_status = config.getboolean("API", "debug_status", fallback=False)
APIHOST = config.get("API", "HOST", fallback="localhost")
APIPORT = config.getint("API", "PORT", fallback=5432)
# Admin and network settings
APP_ADMIN = config.get("DEFAULT", "APP_ADMIN", fallback="")
NET = config.get("DEFAULT", "NET", fallback="mainnet")
# Determine if running on testnet
is_testnet = NET == 'testnet'
# This function will run the async updatePrices within the event loop
async def run_async_update_prices():
"""Runs the async updatePrices function within the event loop."""
await updatePrices() # Use await to run the async function
async def shutdown(loop, signal=None):
"""
Gracefully shuts down the event loop and cancels all pending tasks.
"""
if signal:
print(f"Received exit signal {signal.name}. Shutting down...")
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
loop.stop()
def init_process():
loop = asyncio.get_event_loop()
loop.create_task(initialize_db())
scheduler = BackgroundScheduler()
scheduler.add_job(lambda: asyncio.create_task(run_async_update_prices()), trigger="interval", seconds=600)
scheduler.start()
atexit.register(lambda: asyncio.run(shutdown(loop)))
def start_api_server(config):
set_configs(config)
init_process()
print("Starting API server at port=", APIPORT)
app.run(debug=debug_status, host=APIHOST, port=APIPORT)
if __name__ == "__main__":
start_api_server(config)