285 lines
9.2 KiB
Python
285 lines
9.2 KiB
Python
from flask import Flask, render_template, request, make_response, redirect
|
|
from flask_htmx import HTMX
|
|
import time
|
|
import re
|
|
import transaction_database
|
|
import transaction_details_database
|
|
import recent_blocks_database
|
|
import block_details_database
|
|
import config
|
|
import locale
|
|
from datetime import datetime
|
|
import account_details_database
|
|
|
|
#
|
|
# MAIN
|
|
#
|
|
|
|
print("Setting up Flask webapp...")
|
|
webapp = Flask(__name__)
|
|
htmx = HTMX(webapp)
|
|
|
|
|
|
print("SOLANA_CLUSTER", config.get_config()['cluster'])
|
|
print("LOCALE", locale.getlocale())
|
|
# transaction_database.run_query()
|
|
# recent_blocks_database.run_query()
|
|
# block_details_database.find_block_by_slotnumber(226352855)
|
|
# print("SELFTEST passed")
|
|
|
|
|
|
@webapp.route('/')
|
|
def index():
|
|
return redirect("/tx-errors", code=302)
|
|
|
|
|
|
# fly.io service health check
|
|
@webapp.route('/health')
|
|
def health():
|
|
return "UP\r\n", 200
|
|
|
|
|
|
@webapp.route('/dashboard')
|
|
def dashboard():
|
|
return redirect("/tx-errors", code=302)
|
|
|
|
|
|
@webapp.route('/tx-errors')
|
|
def tx_errors():
|
|
this_config = config.get_config()
|
|
rows_limit = request.args.get('limit', default = 50, type = int)
|
|
assert rows_limit > 0, "limit must be positive"
|
|
assert rows_limit <= 10000, "max limit is 10000"
|
|
|
|
start = time.time()
|
|
maprows = list(transaction_database.run_query(transaction_row_limit=rows_limit))
|
|
elapsed = time.time() - start
|
|
if elapsed > .5:
|
|
print("transaction_database.RunQuery() took", elapsed, "seconds")
|
|
return render_template('tx-errors.html', config=this_config, transactions=maprows)
|
|
|
|
|
|
@webapp.route('/recent-blocks')
|
|
def recent_blocks():
|
|
this_config = config.get_config()
|
|
to_slot = request.args.get('to_slot', default = 0, type = int)
|
|
if to_slot == 0:
|
|
to_slot = None
|
|
|
|
start = time.time()
|
|
maprows = list(recent_blocks_database.run_query(to_slot, blocks_row_limit=100))
|
|
elapsed = time.time() - start
|
|
if elapsed > .5:
|
|
print("recent_blocks_database.RunQuery() took", elapsed, "seconds")
|
|
|
|
enable_polling = "true" if to_slot is None else "false"
|
|
|
|
return render_template('recent_blocks.html', config=this_config, blocks=maprows, enable_polling=enable_polling)
|
|
|
|
|
|
@webapp.route('/block/<path:slot>')
|
|
def get_block(slot):
|
|
this_config = config.get_config()
|
|
start = time.time()
|
|
block = block_details_database.find_block_by_slotnumber(slot)
|
|
elapsed = time.time() - start
|
|
if elapsed > .5:
|
|
print("block_details_database.find_block_by_slotnumber() took", elapsed, "seconds")
|
|
return render_template('block_details.html', config=this_config, block=block)
|
|
|
|
@webapp.route('/account/<path:pubkey>')
|
|
def get_account(pubkey):
|
|
this_config = config.get_config()
|
|
start = time.time()
|
|
if not is_b58_44(pubkey):
|
|
return "Invalid account", 404
|
|
start = time.time()
|
|
(account, blocks, transactions) = account_details_database.build_account_details(pubkey, recent_blocks_row_limit=10, transaction_row_limit=100)
|
|
elapsed = time.time() - start
|
|
if elapsed > .5:
|
|
print("account_details_database.build_account_details() took", elapsed, "seconds")
|
|
return render_template('account_details.html', config=this_config, account=account, recent_blocks=blocks, transactions=transactions)
|
|
|
|
|
|
def is_slot_number(raw_string):
|
|
return re.fullmatch("[0-9,]+", raw_string) is not None
|
|
|
|
|
|
# used for blockhash AND account pubkey
|
|
def is_b58_44(raw_string):
|
|
return re.fullmatch("[0-9a-zA-Z]{43,44}", raw_string) is not None
|
|
|
|
|
|
def is_tx_sig(raw_string):
|
|
# regex is not perfect - feel free to improve
|
|
if is_b58_44(raw_string):
|
|
return False
|
|
return re.fullmatch("[0-9a-zA-Z]{86,88}", raw_string) is not None
|
|
|
|
|
|
# account address
|
|
# if NOT blockhash
|
|
def is_account_key(raw_string):
|
|
return re.fullmatch("[0-9a-zA-Z]{32,44}", raw_string) is not None
|
|
|
|
|
|
@webapp.route('/search', methods=["GET"])
|
|
def search_page():
|
|
this_config = config.get_config()
|
|
return render_template('search.html', config=this_config)
|
|
|
|
|
|
@webapp.route('/search/<path:searchstring>', methods=["GET"])
|
|
def search_deeplink(searchstring):
|
|
this_config = config.get_config()
|
|
return render_template('search.html', config=this_config, search_string=searchstring)
|
|
|
|
|
|
# please prefix all database methods with "search_" and use them only for search
|
|
@webapp.route('/search', methods=["POST"])
|
|
def search_form():
|
|
assert htmx, "htmx must be enabled"
|
|
search_string = request.form.get("search").strip()
|
|
return search_and_render(search_string)
|
|
|
|
|
|
def search_and_render(search_string):
|
|
this_config = config.get_config()
|
|
|
|
if search_string == "":
|
|
return render_template('_search_noresult.html', config=this_config)
|
|
|
|
if is_slot_number(search_string):
|
|
search_string = search_string.replace(',', '')
|
|
maprows = list(recent_blocks_database.search_block_by_slotnumber(int(search_string)))
|
|
if len(maprows):
|
|
# match
|
|
return (
|
|
render_template('_blockslist.html', config=this_config, blocks=maprows),
|
|
make_search_deeplink_header(search_string)
|
|
)
|
|
else:
|
|
return render_template('_search_noresult.html', config=this_config)
|
|
|
|
is_blockhash = block_details_database.is_matching_blockhash(search_string)
|
|
|
|
if is_blockhash:
|
|
print("blockhash search=", search_string)
|
|
maprows = list(recent_blocks_database.search_block_by_blockhash(search_string))
|
|
if len(maprows):
|
|
# match
|
|
return (
|
|
render_template('_blockslist.html', config=this_config, blocks=maprows),
|
|
make_search_deeplink_header(search_string)
|
|
)
|
|
else:
|
|
return render_template('_search_noresult.html', config=this_config)
|
|
elif not is_blockhash and is_b58_44(search_string):
|
|
print("account address search=", search_string)
|
|
maprows_account = account_details_database.search_account_by_key(search_string)
|
|
if len(maprows_account) != 1:
|
|
return render_template('_search_noresult.html', config=this_config)
|
|
account = maprows_account[0]
|
|
|
|
(maprows, is_limit_exceeded) = list(transaction_database.search_transactions_by_address(search_string))
|
|
if len(maprows):
|
|
# match
|
|
return (
|
|
render_template('_search_accountresult.html', config=this_config, account=account, transactions=maprows, limit_exceeded=is_limit_exceeded),
|
|
make_search_deeplink_header(search_string)
|
|
)
|
|
else:
|
|
return render_template('_search_noresult.html', config=this_config)
|
|
elif is_tx_sig(search_string):
|
|
print("txsig search=", search_string)
|
|
maprows = list(transaction_database.search_transaction_by_sig(search_string))
|
|
if len(maprows):
|
|
# match
|
|
return (
|
|
render_template('_txlist.html', config=this_config, transactions=maprows, limit_exceeded=False),
|
|
make_search_deeplink_header(search_string)
|
|
)
|
|
else:
|
|
return render_template('_search_noresult.html', config=this_config)
|
|
else:
|
|
return render_template('_search_unsupported.html', config=this_config, search_string=search_string)
|
|
|
|
|
|
def make_search_deeplink_header(search_string):
|
|
return {'HX-Replace-Url': '/search/' + search_string}
|
|
|
|
@webapp.route('/transaction/<path:signature>')
|
|
def get_transaction_details(signature):
|
|
this_config = config.get_config()
|
|
start = time.time()
|
|
maprows = list(transaction_details_database.find_transaction_details_by_sig(signature))
|
|
elapsed = time.time() - start
|
|
if elapsed > .5:
|
|
print("transaction_database.find_transaction_details_by_sig() took", elapsed, "seconds")
|
|
if len(maprows):
|
|
return render_template('transaction_details.html', config=this_config, transaction=maprows[0])
|
|
else:
|
|
return "Transaction not found", 404
|
|
|
|
|
|
# format 123456789 to "123,456,789"
|
|
@webapp.template_filter('lamports')
|
|
def lamports_filter(number: int):
|
|
if number is None:
|
|
return ""
|
|
else:
|
|
try:
|
|
return format(number, ",")
|
|
except TypeError:
|
|
print("FIELD_ERROR in template filter")
|
|
return "FIELD_ERROR"
|
|
|
|
|
|
@webapp.template_filter('slotnumber')
|
|
def slotnumber_filter(number: int):
|
|
if number is None:
|
|
return ""
|
|
else:
|
|
try:
|
|
return format(number, ",")
|
|
except TypeError:
|
|
print("FIELD_ERROR in template filter")
|
|
return "FIELD_ERROR"
|
|
|
|
|
|
@webapp.template_filter('count')
|
|
def count_filter(number: int):
|
|
if number is None:
|
|
return ""
|
|
else:
|
|
try:
|
|
return format(number, ",")
|
|
except TypeError:
|
|
print("FIELD_ERROR in template filter")
|
|
return "FIELD_ERROR"
|
|
|
|
|
|
# railway version: None -> None
|
|
@webapp.template_filter('map_count')
|
|
def mapcount_filter(number: int):
|
|
if number is None:
|
|
return None
|
|
else:
|
|
try:
|
|
return format(number, ",")
|
|
except TypeError:
|
|
print("FIELD_ERROR in template filter")
|
|
return "FIELD_ERROR"
|
|
|
|
|
|
@webapp.template_filter('timestamp')
|
|
def timestamp_filter(dt: datetime):
|
|
if dt is None:
|
|
return None
|
|
else:
|
|
try:
|
|
return dt.strftime('%a %d %b %H:%M:%SZ')
|
|
except TypeError:
|
|
print("FIELD_ERROR in template filter")
|
|
return "FIELD_ERROR"
|