Files
firewalletbrowser/render.py
Nathan Woodburn 7494b77f32
All checks were successful
Build Docker / Build Image (push) Successful in 1m3s
feat: Update actions page for mobile
2025-06-26 14:05:43 +10:00

475 lines
16 KiB
Python

import datetime
import json
import urllib.parse
from flask import render_template
from domainLookup import punycode_to_emoji
import os
from handywrapper import api
HSD_API = os.getenv("HSD_API")
HSD_IP = os.getenv("HSD_IP")
if HSD_IP is None:
HSD_IP = "localhost"
HSD_NETWORK = os.getenv("HSD_NETWORK")
HSD_WALLET_PORT = 12039
HSD_NODE_PORT = 12037
if not HSD_NETWORK:
HSD_NETWORK = "main"
else:
HSD_NETWORK = HSD_NETWORK.lower()
if HSD_NETWORK == "simnet":
HSD_WALLET_PORT = 15039
HSD_NODE_PORT = 15037
elif HSD_NETWORK == "testnet":
HSD_WALLET_PORT = 13039
HSD_NODE_PORT = 13037
elif HSD_NETWORK == "regtest":
HSD_WALLET_PORT = 14039
HSD_NODE_PORT = 14037
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
# Get Explorer URL
TX_EXPLORER_URL = os.getenv("EXPLORER_TX")
if TX_EXPLORER_URL is None:
TX_EXPLORER_URL = "https://shakeshift.com/transaction/"
def domains(domains, mobile=False):
html = ''
for domain in domains:
expires = domain['stats']
if 'daysUntilExpire' in expires:
expires = expires['daysUntilExpire']
else:
expires = "No expiration date"
paid = domain['value']
paid = paid / 1000000
# Handle punycodes
name = renderDomain(domain['name'])
link = f'/manage/{domain["name"]}'
link_action = "Manage"
if domain['registered'] == False:
link_action = "Register"
link = f'/auction/{domain["name"]}/register'
if not mobile:
html += f'<tr><td>{name}</td><td>{expires} days</td><td>{paid:,.2f} HNS</td><td><a href="{link}">{link_action}</a></td></tr>'
else:
html += f'<tr><td><a href="{link}">{name}</a></td><td>{expires} days</td></tr>'
return html
actionMap = {
"UPDATE": "Updated ",
"REGISTER": "Registered ",
"RENEW": "Renewed ",
"BID": "Bid on ",
"REVEAL": "Revealed Bid for ",
"REDEEM": "Redeemed Bid for ",
"TRANSFER": "Started Transfer for ",
"NONE": "Multiple Actions"
}
actionMapPlural = {
"UPDATE": "Updated Multiple Domains' Records",
"REGISTER": "Registered Multiple Domains",
"RENEW": "Renewed Multiple Domains",
"BID": "Bid on Domains",
"REVEAL": "Revealed Bids",
"REDEEM": "Redeemed Bids",
"TRANSFER": "Started Multiple Domain Transfers",
"NONE": "Multiple Actions"
}
def transactions(txs):
if len(txs) == 0:
return '<tr><td colspan="5">No transactions found</td></tr>'
html = ''
for tx in txs:
action = "HNS Transfer"
address = tx["outputs"][0]["address"]
hash = tx["hash"]
confirmations=tx["confirmations"]
incomming = True
amount = 0
isMulti = False
nameHashes = []
for txInput in tx["inputs"]:
if txInput["path"]:
incomming = False
amount -= txInput["value"]
for output in tx["outputs"]:
if output["covenant"]["action"] != "NONE":
if action == "HNS Transfer":
action = output["covenant"]["action"]
elif action == output["covenant"]["action"]:
isMulti = True
else:
action = "Multiple Actions"
if output["covenant"]["items"] and len(output["covenant"]["items"]) > 0:
nameHashes.append(output["covenant"]["items"][0])
if not incomming:
if output["path"]:
amount += output["value"]
else:
if output["path"] and output["covenant"]["action"] == "NONE":
amount += output["value"]
if not incomming:
# Subtract fee to make it easier to read
amount += tx["fee"]
amount = amount / 1000000
humanAction = action
if action == "HNS Transfer":
if amount > 0:
humanAction = "Received HNS"
else:
humanAction = "Sent HNS"
elif action == "FINALIZE":
if incomming and not isMulti:
name = hsd.rpc_getNameByHash(nameHashes[0])
if name["error"] is None:
name = name["result"]
humanAction = f"Received {renderDomain(name)}"
elif incomming and isMulti:
humanAction = "Received Multiple Domains"
elif not isMulti:
name = hsd.rpc_getNameByHash(nameHashes[0])
if name["error"] is None:
name = name["result"]
humanAction = f"Finalized {renderDomain(name)}"
else:
humanAction = "Finalized Multiple Domain Transfers"
elif isMulti:
humanAction = actionMapPlural.get(action, "Unknown Action")
else:
humanAction = actionMap.get(action, "Unknown Action")
name = hsd.rpc_getNameByHash(nameHashes[0])
if name["error"] is None:
name = name["result"]
else:
name = None
humanAction += renderDomain(name) if name else "domain"
if amount < 0:
amount = f"<span style='color: red;'>{amount:,.2f}</span>"
elif amount > 0:
amount = f"<span style='color: green;'>+{amount:,.2f}</span>"
else:
amount = f"<span style='color: gray;'>0.00</span>"
# hash = f"<a target='_blank' href='{TX_EXPLORER_URL}{hash}'>{hash[:8]}...</a>"
if confirmations < 5:
confirmations = f"<td class='hide-mobile' style='background-color: red;'>{confirmations}</td>"
else:
confirmations = f"<td class='hide-mobile'>{confirmations:,}</td>"
html += f'<tr><td><a style="color:var(--bs-body-color);" target="_blank" href="{TX_EXPLORER_URL}{hash}">{humanAction}</a></td><td class="hide-mobile">{address}</td>{confirmations}<td class="amount-column">{amount} HNS</td></tr>'
return html
def dns(data, edit=False):
html_output = ""
index = 0
for entry in data:
html_output += f"<tr><td>{entry['type']}</td>\n"
if entry['type'] != 'DS' and not entry['type'].startswith("GLUE") and not entry['type'].startswith("SYNTH"):
for key, value in entry.items():
if key != 'type':
if isinstance(value, list):
html_output += "<td>\n"
for val in value:
html_output += f"{val}<br>\n"
html_output += "</td>\n"
else:
html_output += f"<td>{value}</td>\n"
elif entry['type'] == 'DS':
ds = f"{entry['keyTag']} {entry['algorithm']} {entry['digestType']} {entry['digest']}"
html_output += f"<td>{ds}</td>\n"
else:
value = ""
for key, val in entry.items():
if key != 'type':
value += f'{val} '
html_output += f"<td>{value}</td>\n"
if edit:
# Remove the current entry from the list
keptRecords = str(data[:index] + data[index+1:]).replace("'", '"')
keptRecords = urllib.parse.quote(keptRecords)
html_output += f"<td><a href='edit?dns={keptRecords}'>Remove</a></td>\n"
html_output += " </tr>\n"
index += 1
return html_output
def txs(data):
data = data[::-1]
html_output = ""
for entry in data:
html_output += f"<tr><td>{entry['action']}</td>\n"
html_output += f"<td><a target='_blank' href='{TX_EXPLORER_URL}{entry['txid']}'>{entry['txid'][:8]}...</a></td>\n"
amount = entry['amount']
amount = amount / 1000000
if entry['blind'] == None:
html_output += f"<td>{amount:,.2f} HNS</td>\n"
else:
blind = entry['blind']
blind = blind / 1000000
html_output += f"<td>{amount:,.2f} + {blind:,.2f} HNS</td>\n"
html_output += f"<td>{timestamp_to_readable_time(entry['time'])}</td>\n"
html_output += f"</tr>\n"
return html_output
def timestamp_to_readable_time(timestamp):
# Assuming the timestamp is in seconds
dt_object = datetime.datetime.fromtimestamp(timestamp)
readable_time = dt_object.strftime("%H:%M:%S %d %b %Y")
return readable_time
def bids(bids,reveals):
html = ''
for bid in bids:
lockup = bid['lockup']
lockup = lockup / 1000000
html += "<tr>"
html += f"<td>{lockup:,.2f} HNS</td>"
revealed = False
for reveal in reveals:
if reveal['bid'] == bid['prevout']['hash']:
revealed = True
value = reveal['value']
value = value / 1000000
html += f"<td>{value:,.2f} HNS</td>"
bidValue = lockup - value
html += f"<td>{bidValue:,.2f} HNS</td>"
break
if not revealed:
html += f"<td>Hidden until reveal</td>"
html += f"<td>Hidden until reveal</td>"
if bid['own']:
html += "<td>You</td>"
else:
html += "<td>Unknown</td>"
html += "</tr>"
return html
def bidDomains(bids,domains, sortbyDomains=False):
html = ''
if not sortbyDomains:
for bid in bids:
for domain in domains:
if bid['name'] == domain['name']:
lockup = bid['lockup']
lockup = lockup / 1000000
bidValue = bid['value'] / 1000000
blind = lockup - bidValue
bidDisplay = f'<b>{bidValue:,.2f} HNS</b> + {blind:,.2f} HNS blind'
html += "<tr>"
html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{renderDomain(domain['name'])}</a></td>"
html += f"<td>{domain['state']}</td>"
html += f"<td>{bidDisplay}</td>"
html += f"<td class='hide-mobile'>{domain['height']:,}</td>"
html += "</tr>"
else:
for domain in domains:
for bid in bids:
if bid['name'] == domain['name']:
lockup = bid['lockup']
lockup = lockup / 1000000
bidValue = bid['value'] / 1000000
blind = lockup - bidValue
bidDisplay = f'<b>{bidValue:,.2f} HNS</b> + {blind:,.2f} HNS blind'
html += "<tr>"
html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{renderDomain(domain['name'])}</a></td>"
html += f"<td>{domain['state']}</td>"
html += f"<td>{bidDisplay}</td>"
html += f"<td class='hide-mobile'>{domain['height']:,}</td>"
html += "</tr>"
return html
def wallets(wallets):
html = ''
for wallet in wallets:
html += f'<option value="{wallet}">{wallet}</option>'
return html
def plugins(plugins):
html = ''
for plugin in plugins:
name = plugin['name']
link = plugin['link']
if plugin['verified']:
html += f'<li class="list-group-item"><a class="btn btn-secondary" style="width:100%;height:100%;margin:0px;font-size: x-large;" role="button" href="/plugin/{link}">{name}</a></li>'
else:
html += f'<li class="list-group-item"><a class="btn btn-danger" style="width:100%;height:100%;margin:0px;font-size: x-large;" role="button" href="/plugin/{link}">{name} (Not verified)</a></li>'
return html
def plugin_functions(functions, pluginName):
html = ''
for function in functions:
name = functions[function]['name']
description = functions[function]['description']
params = functions[function]['params']
returnsRaw = functions[function]['returns']
returns = ""
for output in returnsRaw:
returns += f"{returnsRaw[output]['name']}, "
returns = returns.removesuffix(', ')
functionType = "default"
if "type" in functions[function]:
functionType = functions[function]["type"]
html += f'<div class="card" style="margin-top: 50px;">'
html += f'<div class="card-body">'
html += f'<h4 class="card-title">{name}</h4>'
html += f'<h6 class="text-muted card-subtitle mb-2">{description}</h6>'
html += f'<h6 class="text-muted card-subtitle mb-2">Function type: {functionType.capitalize()}</h6>'
if functionType != "default":
html += f'<p class="card-text">Returns: {returns}</p>'
html += f'</div>'
html += f'</div>'
continue
# Form
html += f'<form method="post" style="padding: 20px;" action="/plugin/{pluginName}/{function}">'
for param in params:
html += f'<div style="margin-bottom: 20px;">'
paramName = params[param]["name"]
paramType = params[param]["type"]
if paramType == "text":
html += f'<label for="{param}">{paramName}</label>'
html += f'<input class="form-control" type="text" name="{param}" />'
elif paramType == "longText":
html += f'<label for="{param}">{paramName}</label>'
html += f'<textarea class="form-control" name="{param}" rows="4" cols="50"></textarea>'
elif paramType == "number":
html += f'<label for="{param}">{paramName}</label>'
html += f'<input class="form-control" type="number" name="{param}" />'
elif paramType == "checkbox":
html += f'<div class="form-check"><input id="{param}" class="form-check-input" type="checkbox" name="{param}" /><label class="form-check-label" for="{param}">{paramName}</label></div>'
elif paramType == "address":
# render components/address.html
address = render_template('components/address.html', paramName=paramName, param=param)
html += address
elif paramType == "dns":
html += render_template('components/dns-input.html', paramName=paramName, param=param)
html += f'</div>'
html += f'<button type="submit" class="btn btn-primary">Submit</button>'
html += f'</form>'
# For debugging
html += f'<p class="card-text">Returns: {returns}</p>'
html += f'</div>'
html += f'</div>'
return html
def plugin_output(outputs, returns):
html = ''
for returnOutput in returns:
if returnOutput not in outputs:
continue
html += f'<div class="card" style="margin-top: 50px; margin-bottom: 50px;">'
html += f'<div class="card-body">'
html += f'<h4 class="card-title">{returns[returnOutput]["name"]}</h4>'
output = outputs[returnOutput]
if returns[returnOutput]["type"] == "list":
html += f'<ul>'
for item in output:
html += f'<li>{item}</li>'
html += f'</ul>'
elif returns[returnOutput]["type"] == "text":
html += f'<p>{output}</p>'
elif returns[returnOutput]["type"] == "tx":
html += render_template('components/tx.html', tx=output)
elif returns[returnOutput]["type"] == "dns":
output = json.loads(output)
html += render_template('components/dns-output.html', dns=dns(output))
html += f'</div>'
html += f'</div>'
return html
def plugin_output_dash(outputs, returns):
html = ''
for returnOutput in returns:
if returnOutput not in outputs:
continue
if outputs[returnOutput] == None:
continue
html += render_template('components/dashboard-plugin.html', name=returns[returnOutput]["name"], output=outputs[returnOutput])
return html
def renderDomain(name: str) -> str:
"""
Render a domain name with emojis and other special characters.
"""
# Convert emoji to punycode
try:
rendered = name.encode("ascii").decode("idna")
if rendered == name:
return f"{name}/"
return f"{rendered}/ ({name})"
except Exception as e:
return f"{name}/"