24 Commits

Author SHA1 Message Date
792688064e fix: More code cleanup 2025-08-25 12:43:12 +10:00
599c0df00c feat: Add more code cleanup 2025-08-25 12:36:11 +10:00
a619d78efd fix: Update types to make code more reliable 2025-08-25 12:28:26 +10:00
f090b7b71a feat: Add initial WALLET DNS record support 2025-08-25 11:55:15 +10:00
545a0b9475 fix: Add display for OPEN transactions in tx history 2025-08-18 11:33:47 +10:00
501091eeae Merge pull request 'feat/mempool-bids' (#2) from feat/mempool-bids into main
Reviewed-on: nathanwoodburn/firewalletbrowser#2
2025-07-17 16:54:38 +10:00
6911e3663c feat: Add mempool bids and dynamic loading 2025-07-16 18:44:18 +10:00
eda690544d feat: Add js to pull bids and auction state 2025-07-16 18:32:32 +10:00
e67c178ea7 feat: Add initial mempool bids function 2025-07-16 17:41:32 +10:00
631d558377 tmp: Add debugging for bids 2025-07-16 16:53:14 +10:00
1d5ed059b3 fix: Allow rescan while not in bidding 2025-07-16 16:30:08 +10:00
747ac575fa fix: Auto reset cache if incorrect format 2025-07-14 16:28:50 +10:00
e574933302 feat: Add link on names in tx history 2025-07-12 21:47:14 +10:00
c0f0dc5010 fix: Open tx for bid in new tab 2025-07-12 21:35:04 +10:00
060132bfec Merge pull request 'Update Auctions page to include more info and be easier to read' (#1) from dev into main
Reviewed-on: nathanwoodburn/firewalletbrowser#1
2025-07-12 15:10:56 +10:00
7bc1fad280 feat: Add bid sorting to auction page and add tx links 2025-07-12 14:13:40 +10:00
63e0b1c2f2 fix: Add new better method of validating domain owner 2025-07-12 13:50:30 +10:00
2fab7b3bc0 feat: Add info on when bidding closes 2025-07-12 13:28:21 +10:00
3fa57cc617 feat: Add time estimates to block times 2025-07-12 12:44:31 +10:00
4c3a738e43 feat: Cleanup urls in account module 2025-07-12 12:25:52 +10:00
988d03b48c fix: Update unknown owner message
Remove logging for DNS rendering
2025-07-12 12:03:36 +10:00
21043fc124 fix: Reveal from auction page crash
Reveal from the auction page had a missing function call
2025-07-12 11:56:22 +10:00
67e5276a13 feat: Cleanup any references to Niami
Niami has closed down so links to it no longer work
2025-07-07 12:43:55 +10:00
0164a9c3f2 fix: Remove Niami API requirement for searching for domains 2025-07-07 12:20:16 +10:00
11 changed files with 640 additions and 239 deletions

Binary file not shown.

View File

@@ -10,10 +10,8 @@ import time
dotenv.load_dotenv() dotenv.load_dotenv()
HSD_API = os.getenv("HSD_API") HSD_API = os.getenv("HSD_API","")
HSD_IP = os.getenv("HSD_IP") HSD_IP = os.getenv("HSD_IP","localhost")
if HSD_IP is None:
HSD_IP = "localhost"
HSD_NETWORK = os.getenv("HSD_NETWORK") HSD_NETWORK = os.getenv("HSD_NETWORK")
HSD_WALLET_PORT = 12039 HSD_WALLET_PORT = 12039
@@ -47,9 +45,7 @@ cacheTime = 3600
# Verify the connection # Verify the connection
response = hsd.getInfo() response = hsd.getInfo()
EXCLUDE = ["primary"] EXCLUDE = os.getenv("EXCLUDE","primary").split(",")
if os.getenv("EXCLUDE") is not None:
EXCLUDE = os.getenv("EXCLUDE").split(",")
def hsdConnected(): def hsdConnected():
@@ -68,7 +64,7 @@ def hsdVersion(format=True):
return info['version'] return info['version']
def check_account(cookie: str): def check_account(cookie: str | None):
if cookie is None: if cookie is None:
return False return False
@@ -84,7 +80,12 @@ def check_account(cookie: str):
return account return account
def check_password(cookie: str, password: str): def check_password(cookie: str|None, password: str|None):
if cookie is None:
return False
if password is None:
password = ""
account = check_account(cookie) account = check_account(cookie)
if account == False: if account == False:
return False return False
@@ -109,8 +110,7 @@ def createWallet(account: str, password: str):
} }
# Create the account # Create the account
# Python wrapper doesn't support this yet # Python wrapper doesn't support this yet
response = requests.put( response = requests.put(get_wallet_api_url(f"wallet/{account}"))
f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}")
if response.status_code != 200: if response.status_code != 200:
return { return {
"error": { "error": {
@@ -123,7 +123,7 @@ def createWallet(account: str, password: str):
seed = seed['mnemonic']['phrase'] seed = seed['mnemonic']['phrase']
# Encrypt the wallet (python wrapper doesn't support this yet) # Encrypt the wallet (python wrapper doesn't support this yet)
response = requests.post(f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/passphrase", response = requests.post(get_wallet_api_url(f"/wallet/{account}/passphrase"),
json={"passphrase": password}) json={"passphrase": password})
return { return {
@@ -147,8 +147,7 @@ def importWallet(account: str, password: str, seed: str):
"mnemonic": seed, "mnemonic": seed,
} }
response = requests.put( response = requests.put(get_wallet_api_url(f"/wallet/{account}"), json=data)
f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}", json=data)
if response.status_code != 200: if response.status_code != 200:
return { return {
"error": { "error": {
@@ -186,7 +185,6 @@ def selectWallet(account: str):
} }
} }
def getBalance(account: str): def getBalance(account: str):
# Get the total balance # Get the total balance
info = hsw.getBalance('default', account) info = hsw.getBalance('default', account)
@@ -251,11 +249,9 @@ def getPendingTX(account: str):
def getDomains(account, own=True): def getDomains(account, own=True):
if own: if own:
response = requests.get( response = requests.get(get_wallet_api_url(f"/wallet/{account}/name?own=true"))
f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/name?own=true")
else: else:
response = requests.get( response = requests.get(get_wallet_api_url(f"/wallet/{account}/name"))
f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/name")
info = response.json() info = response.json()
if SHOW_EXPIRED: if SHOW_EXPIRED:
@@ -339,11 +335,9 @@ def getTransactions(account, page=1, limit=100):
lastTX = getTXFromPage(account, page-1, limit) lastTX = getTXFromPage(account, page-1, limit)
if lastTX: if lastTX:
response = requests.get( response = requests.get(get_wallet_api_url(f"/wallet/{account}/tx/history?reverse=true&limit={limit}&after={lastTX}"))
f'http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/tx/history?reverse=true&limit={limit}&after={lastTX}')
elif page == 1: elif page == 1:
response = requests.get( response = requests.get(get_wallet_api_url(f"/wallet/{account}/tx/history?reverse=true&limit={limit}"))
f'http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/tx/history?reverse=true&limit={limit}')
else: else:
return [] return []
@@ -383,7 +377,7 @@ def check_address(address: str, allow_name: bool = True, return_address: bool =
return check_hip2(address[1:]) return check_hip2(address[1:])
# Check if the address is a valid HNS address # Check if the address is a valid HNS address
response = requests.post(f"http://x:{HSD_API}@{HSD_IP}:{HSD_NODE_PORT}", json={ response = requests.post(get_node_api_url(), json={
"method": "validateaddress", "method": "validateaddress",
"params": [address] "params": [address]
}).json() }).json()
@@ -410,17 +404,30 @@ def check_hip2(domain: str):
return 'Invalid domain' return 'Invalid domain'
address = domainLookup.hip2(domain) address = domainLookup.hip2(domain)
if address.startswith("Hip2: "): if not address.startswith("Hip2: "):
if not check_address(address, False, True):
return 'Hip2: Lookup succeeded but address is invalid'
return address return address
# Try using WALLET TXT record
address = domainLookup.wallet_txt(domain)
if not address.startswith("hs1"):
return "No HIP2 or WALLET record found for this domain"
if not check_address(address, False, True): if not check_address(address, False, True):
return 'Hip2: Lookup succeeded but address is invalid' return 'WALLET DNS record found but address is invalid'
return address return address
def send(account, address, amount): def send(account, address, amount):
account_name = check_account(account) account_name = check_account(account)
password = ":".join(account.split(":")[1:]) password = ":".join(account.split(":")[1:])
if not account_name:
return {
"error": {
"message": "Invalid account"
}
}
response = hsw.rpc_selectWallet(account_name) response = hsw.rpc_selectWallet(account_name)
if response['error'] is not None: if response['error'] is not None:
return { return {
@@ -431,8 +438,6 @@ def send(account, address, amount):
response = hsw.rpc_walletPassphrase(password, 10) response = hsw.rpc_walletPassphrase(password, 10)
# Unlock the account # Unlock the account
# response = requests.post(f"http://x:{APIKEY}@{ip}:{HSD_WALLET_PORT}/wallet/{account_name}/unlock",
# json={"passphrase": password,"timeout": 10})
if response['error'] is not None: if response['error'] is not None:
if response['error']['message'] != "Wallet is not encrypted.": if response['error']['message'] != "Wallet is not encrypted.":
return { return {
@@ -454,12 +459,38 @@ def send(account, address, amount):
def isOwnDomain(account, name: str): def isOwnDomain(account, name: str):
domains = getDomains(account) # Get domain
for domain in domains: domain_info = getDomain(name)
if domain['name'] == name: owner = getAddressFromCoin(domain_info['info']['owner']['hash'],domain_info['info']['owner']['index'])
return True # Select the account
hsw.rpc_selectWallet(account)
account = hsw.rpc_getAccount(owner)
if 'error' in account and account['error'] is not None:
return False
if 'result' not in account:
return False
if account['result'] == 'default':
return True
return False return False
def isOwnPrevout(account, prevout: dict):
if 'hash' not in prevout or 'index' not in prevout:
return False
# Get the address from the prevout
address = getAddressFromCoin(prevout['hash'], prevout['index'])
# Select the account
hsw.rpc_selectWallet(account)
account = hsw.rpc_getAccount(address)
if 'error' in account and account['error'] is not None:
return False
if 'result' not in account:
return False
if account['result'] == 'default':
return True
return False
def getDomain(domain: str): def getDomain(domain: str):
# Get the domain # Get the domain
@@ -472,6 +503,18 @@ def getDomain(domain: str):
} }
return response['result'] return response['result']
def getAddressFromCoin(coinhash: str, coinindex = 0):
# Get the address from the hash
response = requests.get(get_node_api_url(f"coin/{coinhash}/{coinindex}"))
if response.status_code != 200:
print(f"Error getting address from coin: {response.text}")
return "No Owner"
data = response.json()
if 'address' not in data:
print(json.dumps(data, indent=4))
return "No Owner"
return data['address']
def renewDomain(account, domain): def renewDomain(account, domain):
account_name = check_account(account) account_name = check_account(account)
@@ -689,9 +732,13 @@ def getPendingFinalizes(account, password):
pending = [] pending = []
try: try:
for output in tx['outputs']: for output in tx['outputs']:
if output['covenant']['type'] != 10: if type(output) != dict:
continue continue
if output['covenant']['action'] != "FINALIZE": if not 'covenant' in output:
continue
if output['covenant'].get("type") != 10:
continue
if output['covenant'].get('action') != "FINALIZE":
continue continue
nameHash = output['covenant']['items'][0] nameHash = output['covenant']['items'][0]
# Try to get the name from hash # Try to get the name from hash
@@ -765,7 +812,7 @@ def revealAll(account):
} }
} }
return requests.post(f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}", json={"method": "sendbatch", "params": [[["REVEAL"]]]}).json() return requests.post(get_wallet_api_url(), json={"method": "sendbatch", "params": [[["REVEAL"]]]}).json()
except Exception as e: except Exception as e:
return { return {
"error": { "error": {
@@ -799,7 +846,7 @@ def redeemAll(account):
} }
} }
return requests.post(f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}", json={"method": "sendbatch", "params": [[["REDEEM"]]]}).json() return requests.post(get_wallet_api_url(), json={"method": "sendbatch", "params": [[["REDEEM"]]]}).json()
except Exception as e: except Exception as e:
return { return {
"error": { "error": {
@@ -855,10 +902,12 @@ def rescan_auction(account, domain):
return { return {
"error": "Invalid domain" "error": "Invalid domain"
} }
if 'bidPeriodStart' not in response['result']['info']['stats']: if 'height' not in response['result']['info']:
return { return {
"error": "Not in auction" "error": "Can't find start"
} }
height = response['result']['info']['height']-1 height = response['result']['info']['height']-1
response = hsw.rpc_importName(domain, height) response = hsw.rpc_importName(domain, height)
return response return response
@@ -1071,7 +1120,7 @@ def sendBatch(account, batch):
"message": response['error']['message'] "message": response['error']['message']
} }
} }
response = requests.post(f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}", json={ response = requests.post(get_wallet_api_url(), json={
"method": "sendbatch", "method": "sendbatch",
"params": [batch] "params": [batch]
}).json() }).json()
@@ -1120,7 +1169,7 @@ def createBatch(account, batch):
"message": response['error']['message'] "message": response['error']['message']
} }
} }
response = requests.post(f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}", json={ response = requests.post(get_wallet_api_url(), json={
"method": "createbatch", "method": "createbatch",
"params": [batch] "params": [batch]
}).json() }).json()
@@ -1141,6 +1190,80 @@ def createBatch(account, batch):
} }
} }
# region Mempool
def getMempoolTxs():
# hsd-cli rpc getrawmempool
response = hsd.rpc_getRawMemPool()
if 'error' in response and response['error'] is not None:
return []
return response['result'] if 'result' in response else []
def getMempoolBids():
mempoolTxs = getMempoolTxs()
bids = {}
for txid in mempoolTxs:
tx = hsd.getTxByHash(txid)
if 'error' in tx and tx['error'] is not None:
print(f"Error getting tx {txid}: {tx['error']}")
continue
if 'outputs' not in tx:
print(f"Error getting outputs for tx {txid}")
continue
for output in tx['outputs']:
if output['covenant']['action'] not in ["BID", "REVEAL"]:
continue
if output['covenant']['action'] == "REVEAL":
# Try to find bid tx from inputs
namehash = output['covenant']['items'][0]
for txInput in tx['inputs']:
if txInput['coin']['covenant']['action'] != "BID":
continue
if txInput['coin']['covenant']['items'][0] != namehash:
continue
name = txInput['coin']['covenant']['items'][2]
# Convert name from hex to ascii
name = bytes.fromhex(name).decode('ascii')
bid = {
'txid': txid,
'lockup': txInput['coin']['value'],
'revealed': True,
'height': -1,
'value': output['value'],
'sort_value': txInput['coin']['value'],
'owner': "Unknown"
}
if name not in bids:
bids[name] = []
bids[name].append(bid)
continue
name = output['covenant']['items'][2]
# Convert name from hex to ascii
name = bytes.fromhex(name).decode('ascii')
if name not in bids:
bids[name] = []
bid = {
'txid': txid,
'value': -1000000, # Default value if not found
'lockup': output['value'],
'revealed': False,
'height': -1,
'sort_value': output['value'],
'owner': "Unknown"
}
bids[name].append(bid)
return bids
# endregion
# region settingsAPIs # region settingsAPIs
def rescan(): def rescan():
@@ -1180,7 +1303,7 @@ def zapTXs(account):
} }
try: try:
response = requests.post(f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account_name}/zap", response = requests.post(get_wallet_api_url(f"/wallet/{account_name}/zap"),
json={"age": age, json={"age": age,
"account": "default" "account": "default"
}) })
@@ -1309,3 +1432,25 @@ def generateReport(account, format="{name},{expiry},{value},{maxBid}"):
def convertHNS(value: int): def convertHNS(value: int):
return value/1000000 return value/1000000
return value/1000000
def get_node_api_url(path=''):
"""Construct a URL for the HSD node API."""
base_url = f"http://x:{HSD_API}@{HSD_IP}:{HSD_NODE_PORT}"
if path:
# Ensure path starts with a slash if it's not empty
if not path.startswith('/'):
path = f'/{path}'
return f"{base_url}{path}"
return base_url
def get_wallet_api_url(path=''):
"""Construct a URL for the HSD wallet API."""
base_url = f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}"
if path:
# Ensure path starts with a slash if it's not empty
if not path.startswith('/'):
path = f'/{path}'
return f"{base_url}{path}"
return base_url

View File

@@ -6,10 +6,14 @@ import subprocess
import binascii import binascii
import datetime import datetime
import dns.asyncresolver import dns.asyncresolver
import dns.message
import dns.query
import dns.rdatatype
import httpx import httpx
from requests_doh import DNSOverHTTPSSession, add_dns_provider from requests_doh import DNSOverHTTPSSession, add_dns_provider
import requests import requests
import urllib3 import urllib3
from cryptography.x509.oid import ExtensionOID
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Disable insecure request warnings (since we are manually verifying the certificate) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Disable insecure request warnings (since we are manually verifying the certificate)
@@ -56,7 +60,7 @@ def hip2(domain: str):
domains = [] domains = []
for ext in cert_obj.extensions: for ext in cert_obj.extensions:
if ext.oid == x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME: if ext.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
san_list = ext.value.get_values_for_type(x509.DNSName) san_list = ext.value.get_values_for_type(x509.DNSName)
domains.extend(san_list) domains.extend(san_list)
@@ -120,13 +124,39 @@ def hip2(domain: str):
print(f"Hip2: Lookup failed with error: {e}",flush=True) print(f"Hip2: Lookup failed with error: {e}",flush=True)
return "Hip2: Lookup failed." return "Hip2: Lookup failed."
def wallet_txt(domain: str, doh_url="https://hnsdoh.com/dns-query"):
with httpx.Client() as client:
q = dns.message.make_query(domain, dns.rdatatype.from_text("TYPE262"))
r = dns.query.https(q, doh_url, session=client)
if not r.answer:
return "No wallet address found for this domain"
wallet_record = "No WALLET record found"
for ans in r.answer:
raw = ans[0].to_wire() # type: ignore
try:
data = raw[1:].decode("utf-8", errors="ignore")
except UnicodeDecodeError:
return f"Unknown WALLET record format: {raw.hex()}"
if data.startswith("HNS:"):
wallet_record = data[4:]
break
elif data.startswith("HNS "):
wallet_record = data[4:]
break
elif data.startswith('"HNS" '):
wallet_record = data[6:].strip('"')
break
return wallet_record
def resolve_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"): def resolve_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
with httpx.Client() as client: with httpx.Client() as client:
q = dns.message.make_query(query_name, dns.rdatatype.A) q = dns.message.make_query(query_name, dns.rdatatype.A)
r = dns.query.https(q, doh_url, session=client) r = dns.query.https(q, doh_url, session=client)
ip = r.answer[0][0].address ip = r.answer[0][0].address # type: ignore
return ip return ip
def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"): def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
@@ -138,35 +168,6 @@ def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
tlsa = r.answer[0][0] tlsa = r.answer[0][0]
return tlsa return tlsa
def niami_info(domain: str):
response = requests.get(f"https://api.niami.io/hsd/{domain}")
if response.status_code != 200:
return False
response = response.json()
if response["data"]["owner_tx_data"] is not None:
output = {
"owner": response["data"]["owner_tx_data"]["address"]
}
else:
output = {
"owner": None
}
if 'dnsData' in response["data"]:
output["dns"] = response["data"]["dnsData"]
else:
output["dns"] = []
transactions = requests.get(f"https://api.niami.io/txs/{domain}")
if transactions.status_code != 200:
return False
transactions = transactions.json()
output["txs"] = transactions["txs"]
return output
def emoji_to_punycode(emoji): def emoji_to_punycode(emoji):
try: try:

347
main.py
View File

@@ -32,12 +32,40 @@ revokeCheck = random.randint(100000,999999)
THEME = os.getenv("THEME") THEME = os.getenv("THEME")
def blocks_to_time(blocks: int) -> str:
"""
Convert blocks to time in a human-readable format.
Blocks are mined approximately every 10 minutes.
"""
if blocks < 0:
return "Invalid time"
if blocks < 6:
return f"{blocks * 10} mins"
elif blocks < 144:
hours = blocks // 6
minutes = (blocks % 6) * 10
if minutes == 0:
return f"{hours} hrs"
return f"{hours} hrs {minutes} mins"
else:
days = blocks // 144
hours = (blocks % 144) // 6
if hours == 0:
return f"{days} days"
return f"{days} days {hours} hrs"
@app.route('/') @app.route('/')
def index(): def index():
# Check if the user is logged in # Check if the user is logged in
if request.cookies.get("account") is None: if request.cookies.get("account") is None:
return redirect("/login") return redirect("/login")
account = account_module.check_account(request.cookies.get("account")) account = account_module.check_account(request.cookies.get("account"))
if not account: if not account:
return redirect("/logout") return redirect("/logout")
@@ -85,7 +113,7 @@ def transactions():
return redirect("/logout") return redirect("/logout")
# Get the page parameter # Get the page parameter
page = request.args.get('page') page = request.args.get('page', 1)
try: try:
page = int(page) page = int(page)
except: except:
@@ -106,6 +134,8 @@ def send_page():
return redirect("/login") return redirect("/login")
account = account_module.check_account(request.cookies.get("account")) account = account_module.check_account(request.cookies.get("account"))
if not account:
return redirect("/logout")
max = account_module.getBalance(account)['available'] max = account_module.getBalance(account)['available']
# Subtract approx fee # Subtract approx fee
max = max - fees max = max - fees
@@ -141,28 +171,28 @@ def send():
amount = request.form.get("amount") amount = request.form.get("amount")
if address is None or amount is None: if address is None or amount is None:
return redirect("/send?message=Invalid address or amount&address=" + address + "&amount=" + amount) return redirect(f"/send?message=Invalid address or amount&address={address}&amount={amount}")
address_check = account_module.check_address(address.strip(),True,True) address_check = account_module.check_address(address.strip(),True,True)
if not address_check: if not address_check:
return redirect("/send?message=Invalid address&address=" + address + "&amount=" + amount) return redirect(f"/send?message=Invalid address&address={address}&amount={amount}")
address = address_check address = address_check
# Check if the amount is valid # Check if the amount is valid
if re.match(r"^\d+(\.\d+)?$", amount) is None: if re.match(r"^\d+(\.\d+)?$", amount) is None:
return redirect("/send?message=Invalid amount&address=" + address + "&amount=" + amount) return redirect(f"/send?message=Invalid amount&address={address}&amount={amount}")
# Check if the amount is valid # Check if the amount is valid
amount = float(amount) amount = float(amount)
if amount <= 0: if amount <= 0:
return redirect("/send?message=Invalid amount&address=" + address + "&amount=" + str(amount)) return redirect(f"/send?message=Invalid amount&address={address}&amount={amount}")
if amount > account_module.getBalance(account)['available'] - fees: if amount > account_module.getBalance(account)['available'] - fees:
return redirect("/send?message=Not enough funds to transfer&address=" + address + "&amount=" + str(amount)) return redirect(f"/send?message=Not enough funds to transfer&address={address}&amount={amount}")
toAddress = address toAddress = address
if request.form.get('address') != address: if request.form.get('address') != address:
toAddress = request.form.get('address') + "<br>" + address toAddress = f"{request.form.get('address')}<br>{address}"
action = f"Send HNS to {request.form.get('address')}" action = f"Send HNS to {request.form.get('address')}"
content = f"Are you sure you want to send {amount} HNS to {toAddress}<br><br>" content = f"Are you sure you want to send {amount} HNS to {toAddress}<br><br>"
@@ -173,7 +203,6 @@ def send():
return render_template("confirm.html", account=account_module.check_account(request.cookies.get("account")), return render_template("confirm.html", account=account_module.check_account(request.cookies.get("account")),
action=action, action=action,
content=content,cancel=cancel,confirm=confirm) content=content,cancel=cancel,confirm=confirm)
@@ -182,20 +211,20 @@ def send():
def sendConfirmed(): def sendConfirmed():
address = request.args.get("address") address = request.args.get("address")
amount = float(request.args.get("amount")) amount = float(request.args.get("amount","0"))
response = account_module.send(request.cookies.get("account"),address,amount) response = account_module.send(request.cookies.get("account"),address,amount)
if 'error' in response and response['error'] != None: if 'error' in response and response['error'] != None:
# If error is a dict get the message # If error is a dict get the message
if isinstance(response['error'], dict): if isinstance(response['error'], dict):
if 'message' in response['error']: if 'message' in response['error']:
return redirect("/send?message=" + response['error']['message'] + "&address=" + address + "&amount=" + str(amount)) return redirect(f"/send?message={response['error']['message']}&address={address}&amount={amount}")
else: else:
return redirect("/send?message=" + str(response['error']) + "&address=" + address + "&amount=" + str(amount)) return redirect(f"/send?message={response['error']}&address={address}&amount={amount}")
# If error is a string # If error is a string
return redirect("/send?message=" + response['error'] + "&address=" + address + "&amount=" + str(amount)) return redirect(f"/send?message={response['error']}&address={address}&amount={amount}")
return redirect("/success?tx=" + response['tx']) return redirect(f"/success?tx={response['tx']}")
@@ -334,6 +363,9 @@ def revealAllBids():
return redirect("/logout") return redirect("/logout")
response = account_module.revealAll(request.cookies.get("account")) response = account_module.revealAll(request.cookies.get("account"))
if not response:
return redirect("/auctions?message=Failed to reveal bids")
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] != None:
if response['error']['message'] == "Nothing to do.": if response['error']['message'] == "Nothing to do.":
@@ -354,6 +386,9 @@ def redeemAllBids():
return redirect("/logout") return redirect("/logout")
response = account_module.redeemAll(request.cookies.get("account")) response = account_module.redeemAll(request.cookies.get("account"))
if not response:
return redirect("/auctions?message=Failed to redeem bids")
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] != None:
if response['error']['message'] == "Nothing to do.": if response['error']['message'] == "Nothing to do.":
@@ -373,13 +408,16 @@ def registerAllDomains():
return redirect("/logout") return redirect("/logout")
response = account_module.registerAll(request.cookies.get("account")) response = account_module.registerAll(request.cookies.get("account"))
if not response:
return redirect("/auctions?message=Failed to register domains")
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] != None:
if response['error']['message'] == "Nothing to do.": if response['error']['message'] == "Nothing to do.":
return redirect("/auctions?message=No domains to register") return redirect("/auctions?message=No domains to register")
return redirect("/auctions?message=" + response['error']['message']) return redirect("/auctions?message=" + response['error']['message'])
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
@app.route('/all/finalize') @app.route('/all/finalize')
def finalizeAllBids(): def finalizeAllBids():
@@ -398,7 +436,7 @@ def finalizeAllBids():
return redirect("/dashboard?message=No domains to finalize") return redirect("/dashboard?message=No domains to finalize")
return redirect("/dashboard?message=" + response['error']['message']) return redirect("/dashboard?message=" + response['error']['message'])
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
#endregion #endregion
@app.route('/search') @app.route('/search')
@@ -412,6 +450,8 @@ def search():
return redirect("/logout") return redirect("/logout")
search_term = request.args.get("q") search_term = request.args.get("q")
if search_term is None:
return redirect("/")
search_term = search_term.lower().strip() search_term = search_term.lower().strip()
# Replace spaces with hyphens # Replace spaces with hyphens
@@ -428,7 +468,7 @@ def search():
# Execute domain plugins # Execute domain plugins
searchFunctions = plugins_module.getSearchFunctions() searchFunctions = plugins_module.getSearchFunctions()
for function in searchFunctions: for function in searchFunctions:
functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{"domain":search_term},account_module.check_account(request.cookies.get("account"))) functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{"domain":search_term},account)
plugins += render.plugin_output(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"])) plugins += render.plugin_output(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"]))
plugins += "</div>" plugins += "</div>"
@@ -445,10 +485,12 @@ def search():
state="AVAILABLE", next="Available Now",plugins=plugins) state="AVAILABLE", next="Available Now",plugins=plugins)
state = domain['info']['state'] state = domain['info']['state']
stats = domain['info']['stats']
next = ""
if state == 'CLOSED': if state == 'CLOSED':
if domain['info']['registered']: if domain['info']['registered']:
state = 'REGISTERED' state = 'REGISTERED'
expires = domain['info']['stats']['daysUntilExpire'] expires = stats['daysUntilExpire']
next = f"Expires in ~{expires} days" next = f"Expires in ~{expires} days"
else: else:
state = 'AVAILABLE' state = 'AVAILABLE'
@@ -456,23 +498,25 @@ def search():
elif state == "REVOKED": elif state == "REVOKED":
next = "Available Now" next = "Available Now"
elif state == 'OPENING': elif state == 'OPENING':
next = "Bidding opens in ~" + str(domain['info']['stats']['blocksUntilBidding']) + " blocks" next = f"Bidding opens in {str(stats['blocksUntilBidding'])} blocks (~{blocks_to_time(stats['blocksUntilBidding'])})"
elif state == 'BIDDING': elif state == 'BIDDING':
next = "Reveal in ~" + str(domain['info']['stats']['blocksUntilReveal']) + " blocks" next = f"Reveal in {str(stats['blocksUntilReveal'])} blocks (~{blocks_to_time(stats['blocksUntilReveal'])})"
elif state == 'REVEAL': elif state == 'REVEAL':
next = "Reveal ends in ~" + str(domain['info']['stats']['blocksUntilClose']) + " blocks" next = f"Reveal ends in {str(stats['blocksUntilClose'])} blocks (~{blocks_to_time(stats['blocksUntilClose'])})"
domain_info = domainLookup.niami_info(search_term) domain_info = account_module.getDomain(search_term)
owner = 'Unknown' owner = 'Unknown'
dns = [] dns = []
txs = [] txs = []
if domain_info: if domain_info:
owner = domain_info['owner'] # Check if info and info.owner
dns = domain_info['dns'] if 'info' in domain_info and 'owner' in domain_info['info']:
txs = domain_info['txs'] owner = account_module.getAddressFromCoin(domain_info['info']['owner']['hash'],domain_info['info']['owner']['index'])
dns = account_module.getDNS(search_term)
own_domains = account_module.getDomains(account) own_domains = account_module.getDomains(account)
own_domains = [x['name'] for x in own_domains] own_domains = [x['name'] for x in own_domains]
@@ -481,13 +525,12 @@ def search():
owner = "You" owner = "You"
dns = render.dns(dns) dns = render.dns(dns)
txs = render.txs(txs)
return render_template("search.html", account=account, return render_template("search.html", account=account,
rendered=renderDomain(search_term), rendered=renderDomain(search_term),
search_term=search_term,domain=domain['info']['name'], search_term=search_term,domain=domain['info']['name'],
raw=domain,state=state, next=next, owner=owner, raw=domain,state=state, next=next, owner=owner,
dns=dns, txs=txs,plugins=plugins) dns=dns,plugins=plugins)
@app.route('/manage/<domain>') @app.route('/manage/<domain>')
def manage(domain: str): def manage(domain: str):
@@ -500,11 +543,8 @@ def manage(domain: str):
return redirect("/logout") return redirect("/logout")
domain = domain.lower() domain = domain.lower()
own_domains = account_module.getDomains(account) if not account_module.isOwnDomain(account, domain):
own_domains = [x['name'] for x in own_domains]
own_domains = [x.lower() for x in own_domains]
if domain not in own_domains:
return redirect("/search?q=" + domain) return redirect("/search?q=" + domain)
domain_info = account_module.getDomain(domain) domain_info = account_module.getDomain(domain)
@@ -513,7 +553,10 @@ def manage(domain: str):
rendered=renderDomain(domain), rendered=renderDomain(domain),
domain=domain, error=domain_info['error']) domain=domain, error=domain_info['error'])
expiry = domain_info['info']['stats']['daysUntilExpire'] if domain_info['info'] is not None and 'stats' in domain_info['info'] and 'daysUntilExpire' in domain_info['info']['stats']:
expiry = domain_info['info']['stats']['daysUntilExpire']
else:
expiry = "Unknown"
dns = account_module.getDNS(domain) dns = account_module.getDNS(domain)
raw_dns = str(dns).replace("'",'"') raw_dns = str(dns).replace("'",'"')
dns = render.dns(dns) dns = render.dns(dns)
@@ -540,7 +583,7 @@ def manage(domain: str):
# Execute domain plugins # Execute domain plugins
domainFunctions = plugins_module.getDomainFunctions() domainFunctions = plugins_module.getDomainFunctions()
for function in domainFunctions: for function in domainFunctions:
functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{"domain":domain},account_module.check_account(request.cookies.get("account"))) functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{"domain":domain},account)
plugins += render.plugin_output(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"])) plugins += render.plugin_output(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"]))
plugins += "</div>" plugins += "</div>"
@@ -645,7 +688,7 @@ def revokeConfirm(domain: str):
print(response) print(response)
return redirect("/manage/" + domain + "?error=" + response['error']['message']) return redirect("/manage/" + domain + "?error=" + response['error']['message'])
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
@app.route('/manage/<domain>/renew') @app.route('/manage/<domain>/renew')
def renew(domain: str): def renew(domain: str):
@@ -659,7 +702,7 @@ def renew(domain: str):
domain = domain.lower() domain = domain.lower()
response = account_module.renewDomain(request.cookies.get("account"),domain) response = account_module.renewDomain(request.cookies.get("account"),domain)
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
@app.route('/manage/<domain>/edit') @app.route('/manage/<domain>/edit')
def editPage(domain: str): def editPage(domain: str):
@@ -685,8 +728,11 @@ def editPage(domain: str):
dns = urllib.parse.unquote(user_edits) dns = urllib.parse.unquote(user_edits)
else: else:
dns = account_module.getDNS(domain) dns = account_module.getDNS(domain)
dns = json.loads(dns) if dns and isinstance(dns, str):
dns = json.loads(dns)
else:
dns = []
# Check if new records have been added # Check if new records have been added
dnsType = request.args.get("type") dnsType = request.args.get("type")
@@ -702,14 +748,14 @@ def editPage(domain: str):
return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(str(raw_dns)) + "&error=Invalid DS record") return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(str(raw_dns)) + "&error=Invalid DS record")
try: try:
ds[0] = int(ds[0]) key_tag = int(ds[0])
ds[1] = int(ds[1]) algorithm = int(ds[1])
ds[2] = int(ds[2]) digest_type = int(ds[2])
except: except:
raw_dns = str(dns).replace("'",'"') raw_dns = str(dns).replace("'",'"')
return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(str(raw_dns)) + "&error=Invalid DS record") return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(str(raw_dns)) + "&error=Invalid DS record")
finally:
dns.append({"type": dnsType, "keyTag": ds[0], "algorithm": ds[1], "digestType": ds[2], "digest": ds[3]}) dns.append({"type": dnsType, "keyTag": key_tag, "algorithm": algorithm, "digestType": digest_type, "digest": ds[3]})
dns = json.dumps(dns).replace("'",'"') dns = json.dumps(dns).replace("'",'"')
return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(dns)) return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(dns))
@@ -739,13 +785,15 @@ def editSave(domain: str):
domain = domain.lower() domain = domain.lower()
dns = request.args.get("dns") dns = request.args.get("dns")
if dns is None:
return redirect(f"/manage/{domain}/edit?error=No DNS records provided")
raw_dns = dns raw_dns = dns
dns = urllib.parse.unquote(dns) dns = urllib.parse.unquote(dns)
response = account_module.setDNS(request.cookies.get("account"),domain,dns) response = account_module.setDNS(request.cookies.get("account"),domain,dns)
if 'error' in response: if 'error' in response:
print(response) print(response)
return redirect("/manage/" + domain + "/edit?dns="+raw_dns+"&error=" + str(response['error'])) return redirect(f"/manage/{domain}/edit?dns={raw_dns}&error={response['error']}")
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
@app.route('/manage/<domain>/transfer') @app.route('/manage/<domain>/transfer')
def transfer(domain): def transfer(domain):
@@ -770,7 +818,7 @@ def transfer(domain):
toAddress = address toAddress = address
if request.form.get('address') != address: if request.form.get('address') != address:
toAddress = request.args.get('address') + "<br>" + address toAddress = f"{request.args.get('address')}<br>{address}"
action = f"Send {domain}/ to {request.form.get('address')}" action = f"Send {domain}/ to {request.form.get('address')}"
content = f"Are you sure you want to send {domain}/ to {toAddress}<br><br>" content = f"Are you sure you want to send {domain}/ to {toAddress}<br><br>"
@@ -780,9 +828,7 @@ def transfer(domain):
confirm = f"/manage/{domain}/transfer/confirm?address={address}" confirm = f"/manage/{domain}/transfer/confirm?address={address}"
return render_template("confirm.html", account=account_module.check_account(request.cookies.get("account")), return render_template("confirm.html", account=account,action=action,
action=action,
content=content,cancel=cancel,confirm=confirm) content=content,cancel=cancel,confirm=confirm)
@app.route('/manage/<domain>/sign') @app.route('/manage/<domain>/sign')
@@ -805,7 +851,7 @@ def signMessage(domain):
signedMessage = account_module.signMessage(request.cookies.get("account"),domain,message) signedMessage = account_module.signMessage(request.cookies.get("account"),domain,message)
if signedMessage["error"] != None: if signedMessage["error"] != None:
return redirect("/manage/" + domain + "?error=" + signedMessage["error"]) return redirect("/manage/" + domain + "?error=" + signedMessage["error"])
content += "Signature:<br><code>" + signedMessage["result"] + "</code><br><br>" content += f"Signature:<br><code>{signedMessage["result"]}</code><br><br>"
data = { data = {
"domain": domain, "domain": domain,
@@ -822,8 +868,7 @@ def signMessage(domain):
return render_template("message.html", account=account, return render_template("message.html", account=account,
title="Sign Message",content=content) title="Sign Message",content=content)
@@ -842,7 +887,7 @@ def transferConfirm(domain):
if 'error' in response: if 'error' in response:
return redirect("/manage/" + domain + "?error=" + response['error']) return redirect("/manage/" + domain + "?error=" + response['error'])
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
@app.route('/auction/<domain>') @app.route('/auction/<domain>')
@@ -886,20 +931,10 @@ def auction(domain):
state = domainInfo['info']['state'] state = domainInfo['info']['state']
next_action = '' next_action = ''
next = ""
bids = account_module.getBids(account,search_term) bids = []
if bids == []: stats = domainInfo['info']['stats'] if 'stats' in domainInfo['info'] else {}
bids = "No bids found"
next_action = f'<a href="/auction/{domain}/scan">Rescan Auction</a>'
else:
reveals = account_module.getReveals(account,search_term)
for reveal in reveals:
# Get TX
revealInfo = account_module.getRevealTX(reveal)
reveal['bid'] = revealInfo
bids = render.bids(bids,reveals)
if state == 'CLOSED': if state == 'CLOSED':
if not domainInfo['info']['registered']: if not domainInfo['info']['registered']:
if account_module.isOwnDomain(account,domain): if account_module.isOwnDomain(account,domain):
@@ -918,20 +953,27 @@ def auction(domain):
expires = domainInfo['info']['stats']['daysUntilExpire'] expires = domainInfo['info']['stats']['daysUntilExpire']
next = f"Expires in ~{expires} days" next = f"Expires in ~{expires} days"
own_domains = account_module.getDomains(account) if account_module.isOwnDomain(account,domain):
own_domains = [x['name'] for x in own_domains]
own_domains = [x.lower() for x in own_domains]
if search_term in own_domains:
next_action = f'<a href="/manage/{domain}">Manage</a>' next_action = f'<a href="/manage/{domain}">Manage</a>'
elif state == "REVOKED": elif state == "REVOKED":
next = "Available Now" next = "Available Now"
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>' next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
elif state == 'OPENING': elif state == 'OPENING':
next = "Bidding opens in ~" + str(domainInfo['info']['stats']['blocksUntilBidding']) + " blocks" next = f"Bidding opens in {str(stats['blocksUntilBidding'])} blocks (~{blocks_to_time(stats['blocksUntilBidding'])})"
elif state == 'BIDDING': elif state == 'BIDDING':
next = "Reveal in ~" + str(domainInfo['info']['stats']['blocksUntilReveal']) + " blocks" next = f"Reveal in {stats['blocksUntilReveal']} blocks (~{blocks_to_time(stats['blocksUntilReveal'])})"
if stats['blocksUntilReveal'] == 1:
next += "<br>Bidding no longer possible"
elif stats['blocksUntilReveal'] == 2:
next += "<br>LAST CHANCE TO BID"
elif stats['blocksUntilReveal'] == 3:
next += f"<br>Next block is last chance to bid"
elif stats['blocksUntilReveal'] < 6:
next += f"<br>Last chance to bid in {stats['blocksUntilReveal']-2} blocks"
elif state == 'REVEAL': elif state == 'REVEAL':
next = "Reveal ends in ~" + str(domainInfo['info']['stats']['blocksUntilClose']) + " blocks" next = f"Reveal ends in {str(stats['blocksUntilClose'])} blocks (~{blocks_to_time(stats['blocksUntilClose'])})"
next_action = f'<a href="/auction/{domain}/reveal">Reveal All</a>' next_action = f'<a href="/auction/{domain}/reveal">Reveal All</a>'
message = '' message = ''
@@ -971,8 +1013,8 @@ def bid(domain):
return redirect("/logout") return redirect("/logout")
domain = domain.lower() domain = domain.lower()
bid = request.args.get("bid") bid = request.args.get("bid","")
blind = request.args.get("blind") blind = request.args.get("blind","")
if bid == "": if bid == "":
bid = 0 bid = 0
@@ -1017,8 +1059,8 @@ def bid_confirm(domain):
return redirect("/logout") return redirect("/logout")
domain = domain.lower() domain = domain.lower()
bid = request.args.get("bid") bid = request.args.get("bid","")
blind = request.args.get("blind") blind = request.args.get("blind","")
if bid == "": if bid == "":
bid = 0 bid = 0
@@ -1037,7 +1079,7 @@ def bid_confirm(domain):
if 'error' in response: if 'error' in response:
return redirect("/auction/" + domain + "?error=" + response['error']['message']) return redirect("/auction/" + domain + "?error=" + response['error']['message'])
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
@app.route('/auction/<domain>/open') @app.route('/auction/<domain>/open')
def open_auction(domain): def open_auction(domain):
@@ -1056,7 +1098,7 @@ def open_auction(domain):
if response['error'] != None: if response['error'] != None:
return redirect("/auction/" + domain + "?error=" + response['error']['message']) return redirect("/auction/" + domain + "?error=" + response['error']['message'])
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
@app.route('/auction/<domain>/reveal') @app.route('/auction/<domain>/reveal')
def reveal_auction(domain): def reveal_auction(domain):
@@ -1069,10 +1111,10 @@ def reveal_auction(domain):
return redirect("/logout") return redirect("/logout")
domain = domain.lower() domain = domain.lower()
response = account_module(request.cookies.get("account"),domain) response = account_module.revealAuction(request.cookies.get("account"),domain)
if 'error' in response: if 'error' in response:
return redirect("/auction/" + domain + "?message=" + response['error']['message']) return redirect(f"/auction/{domain}?message={response['error']}")
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
@app.route('/auction/<domain>/register') @app.route('/auction/<domain>/register')
def registerdomain(domain): def registerdomain(domain):
@@ -1087,7 +1129,7 @@ def registerdomain(domain):
response = account_module.register(request.cookies.get("account"),domain) response = account_module.register(request.cookies.get("account"),domain)
if 'error' in response: if 'error' in response:
return redirect("/auction/" + domain + "?message=" + response['error']['message']) return redirect("/auction/" + domain + "?message=" + response['error']['message'])
return redirect("/success?tx=" + response['hash']) return redirect(f"/success?tx={response['hash']}")
#endregion #endregion
#region Settings #region Settings
@@ -1114,6 +1156,11 @@ def settings():
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version="Error") error=error,success=success,version="Error")
info = gitinfo.get_git_info() info = gitinfo.get_git_info()
if not info:
return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False),
error=error,success=success,version="Error")
branch = info['refs'] branch = info['refs']
if branch != "main": if branch != "main":
branch = f"({branch})" branch = f"({branch})"
@@ -1153,20 +1200,19 @@ def settings_action(action):
elif action == "zap": elif action == "zap":
resp = account_module.zapTXs(request.cookies.get("account")) resp = account_module.zapTXs(request.cookies.get("account"))
if 'error' in resp: if type(resp) == dict and 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Zapped transactions") return redirect("/settings?success=Zapped transactions")
elif action == "xpub": elif action == "xpub":
xpub = account_module.getxPub(request.cookies.get("account")) xpub = account_module.getxPub(request.cookies.get("account"))
content = "<br><br>" content = "<br><br>"
content += "<textarea style='display: none;' id='data' rows='4' cols='50'>"+xpub+"</textarea>" content += f"<textarea style='display: none;' id='data' rows='4' cols='50'>{xpub}</textarea>"
content += "<script>function copyToClipboard() {var copyText = document.getElementById('data');copyText.style.display = 'block';copyText.select();copyText.setSelectionRange(0, 99999);document.execCommand('copy');copyText.style.display = 'none';var copyButton = document.getElementById('copyButton');copyButton.innerHTML='Copied';}</script>" content += "<script>function copyToClipboard() {var copyText = document.getElementById('data');copyText.style.display = 'block';copyText.select();copyText.setSelectionRange(0, 99999);document.execCommand('copy');copyText.style.display = 'none';var copyButton = document.getElementById('copyButton');copyButton.innerHTML='Copied';}</script>"
content += "<button id='copyButton' onclick='copyToClipboard()' class='btn btn-secondary'>Copy to clipboard</button>" content += "<button id='copyButton' onclick='copyToClipboard()' class='btn btn-secondary'>Copy to clipboard</button>"
return render_template("message.html", account=account, return render_template("message.html", account=account,
title="xPub Key", title="xPub Key",
content="<code>"+xpub+"</code>" + content) content=f"<code>{xpub}</code>{content}")
return redirect("/settings?error=Invalid action") return redirect("/settings?error=Invalid action")
@@ -1176,6 +1222,9 @@ def upload_image():
return redirect("/login?message=Not logged in") return redirect("/login?message=Not logged in")
account = request.cookies.get("account") account = request.cookies.get("account")
account = account_module.check_account(account)
if not account:
return redirect("/logout")
if not os.path.exists('user_data/images'): if not os.path.exists('user_data/images'):
os.mkdir('user_data/images') os.mkdir('user_data/images')
@@ -1185,11 +1234,12 @@ def upload_image():
file = request.files['image'] file = request.files['image']
if file.filename == '': if file.filename == '':
return redirect("/settings?error=No file selected") return redirect("/settings?error=No file selected")
if file: if file and file.filename:
filepath = os.path.join(f'user_data/images/{account.split(":")[0]}.{file.filename.split(".")[-1]}') filepath = os.path.join(f'user_data/images/{account}.{file.filename.split(".")[-1]}')
file.save(filepath) file.save(filepath)
return redirect("/settings?success=File uploaded successfully") return redirect("/settings?success=File uploaded successfully")
return redirect("/settings?error=An error occurred")
def latestVersion(branch): def latestVersion(branch):
result = requests.get(f"https://git.woodburn.au/api/v1/repos/nathanwoodburn/firewalletbrowser/branches") result = requests.get(f"https://git.woodburn.au/api/v1/repos/nathanwoodburn/firewalletbrowser/branches")
@@ -1226,6 +1276,12 @@ def login_post():
account = request.form.get("account") account = request.form.get("account")
password = request.form.get("password") password = request.form.get("password")
if account == None or password == None:
wallets = account_module.listWallets()
wallets = render.wallets(wallets)
return render_template("login.html",
error="Invalid account or password",wallets=wallets)
# Check if the account is valid # Check if the account is valid
if account.count(":") > 0: if account.count(":") > 0:
wallets = account_module.listWallets() wallets = account_module.listWallets()
@@ -1241,8 +1297,6 @@ def login_post():
wallets = render.wallets(wallets) wallets = render.wallets(wallets)
return render_template("login.html", return render_template("login.html",
error="Invalid account or password",wallets=wallets) error="Invalid account or password",wallets=wallets)
# Set the cookie # Set the cookie
response = make_response(redirect("/")) response = make_response(redirect("/"))
response.set_cookie("account", account) response.set_cookie("account", account)
@@ -1261,6 +1315,11 @@ def register():
password = request.form.get("password") password = request.form.get("password")
repeatPassword = request.form.get("password_repeat") repeatPassword = request.form.get("password_repeat")
if account == None or password == None or repeatPassword == None:
return render_template("register.html",
error="Invalid account or password",
name=account,password=password,password_repeat=repeatPassword)
# Check if the passwords match # Check if the passwords match
if password != repeatPassword: if password != repeatPassword:
return render_template("register.html", return render_template("register.html",
@@ -1290,10 +1349,8 @@ def register():
# Set the cookie # Set the cookie
response = make_response(render_template("message.html", response = make_response(render_template("message.html",title="Account Created",
content=f"Your account has been created. Here is your seed phrase. Please write it down and keep it safe as it will not be shown again<br><br>{response['seed']}"))
title="Account Created",
content="Your account has been created. Here is your seed phrase. Please write it down and keep it safe as it will not be shown again<br><br>" + response['seed']))
response.set_cookie("account", account+":"+password) response.set_cookie("account", account+":"+password)
return response return response
@@ -1305,6 +1362,12 @@ def import_wallet():
repeatPassword = request.form.get("password_repeat") repeatPassword = request.form.get("password_repeat")
seed = request.form.get("seed") seed = request.form.get("seed")
if account == None or password == None or repeatPassword == None or seed == None:
return render_template("import-wallet.html",
error="Invalid account, password or seed",
name=account,password=password,password_repeat=repeatPassword,
seed=seed)
# Check if the passwords match # Check if the passwords match
if password != repeatPassword: if password != repeatPassword:
return render_template("import-wallet.html", return render_template("import-wallet.html",
@@ -1506,7 +1569,67 @@ def api_hsd(function):
return jsonify({"result": account_module.hsdVersion(False)}) return jsonify({"result": account_module.hsdVersion(False)})
if function == "height": if function == "height":
return jsonify({"result": account_module.getBlockHeight()}) return jsonify({"result": account_module.getBlockHeight()})
if function == "mempool":
return jsonify({"result": account_module.getMempoolTxs()})
if function == "mempoolBids":
return jsonify({"result": account_module.getMempoolBids()})
if function == "nextAuctionState":
# Get the domain from the query parameters
domain = request.args.get('domain')
if not domain:
return jsonify({"error": "No domain specified"}), 400
domainInfo = account_module.getDomain(domain)
if 'error' in domainInfo and domainInfo['error'] != None:
return jsonify({"error": domainInfo['error']}), 400
stats = domainInfo['info']['stats'] if 'stats' in domainInfo['info'] else {}
state = domainInfo['info']['state']
next_action = ""
next = ""
if state == 'CLOSED':
if not domainInfo['info']['registered']:
if account_module.isOwnDomain(account,domain):
print("Waiting to be registered")
state = 'PENDING REGISTER'
next = "Pending Register"
next_action = f'<a href="/auction/{domain}/register">Register Domain</a>'
else:
print("Not registered")
state = 'AVAILABLE'
next = "Available Now"
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
else:
state = 'REGISTERED'
expires = domainInfo['info']['stats']['daysUntilExpire']
next = f"Expires in ~{expires} days"
elif state == "REVOKED":
next = "Available Now"
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
elif state == 'OPENING':
next = f"Bidding opens in {str(stats['blocksUntilBidding'])} blocks (~{blocks_to_time(stats['blocksUntilBidding'])})"
elif state == 'BIDDING':
next = f"Reveal in {stats['blocksUntilReveal']} blocks (~{blocks_to_time(stats['blocksUntilReveal'])})"
if stats['blocksUntilReveal'] == 1:
next += "<br>Bidding no longer possible"
elif stats['blocksUntilReveal'] == 2:
next += "<br>LAST CHANCE TO BID"
elif stats['blocksUntilReveal'] == 3:
next += f"<br>Next block is last chance to bid"
elif stats['blocksUntilReveal'] < 6:
next += f"<br>Last chance to bid in {stats['blocksUntilReveal']-2} blocks"
elif state == 'REVEAL':
next = f"Reveal ends in {str(stats['blocksUntilClose'])} blocks (~{blocks_to_time(stats['blocksUntilClose'])})"
next_action = f'<a href="/auction/{domain}/reveal">Reveal All</a>'
return jsonify({
"state": state,
"next": next,
"next_action": next_action
})
return jsonify({"error": "Invalid function", "result": "Invalid function"}), 400 return jsonify({"error": "Invalid function", "result": "Invalid function"}), 400
@@ -1540,7 +1663,10 @@ def api_wallet(function):
return jsonify({"error": "Not logged in"}) return jsonify({"error": "Not logged in"})
account = account_module.check_account(request.cookies.get("account")) account = account_module.check_account(request.cookies.get("account"))
password = request.cookies.get("account").split(":")[1] if not account:
return jsonify({"error": "Invalid account"})
password = request.cookies.get("account","").split(":")[1]
if not account: if not account:
return jsonify({"error": "Invalid account"}) return jsonify({"error": "Invalid account"})
@@ -1574,7 +1700,7 @@ def api_wallet(function):
if function == "domains": if function == "domains":
domains = account_module.getDomains(account) domains = account_module.getDomains(account)
if 'error' in domains: if type(domains) == dict and 'error' in domains:
return jsonify({"result": [], "error": domains['error']}) return jsonify({"result": [], "error": domains['error']})
# Add nameRender to each domain # Add nameRender to each domain
@@ -1585,7 +1711,7 @@ def api_wallet(function):
if function == "transactions": if function == "transactions":
# Get the page parameter # Get the page parameter
page = request.args.get('page') page = request.args.get('page', 1)
try: try:
page = int(page) page = int(page)
except: except:
@@ -1625,6 +1751,21 @@ def api_wallet(function):
"page": page "page": page
}) })
if function == "domainBids":
domain = request.args.get('domain')
if not domain:
return jsonify({"error": "No domain specified"}), 400
bids = account_module.getBids(account,domain)
if bids == []:
return jsonify({"result": [], "error": "No bids found"}), 404
else:
reveals = account_module.getReveals(account,domain)
for reveal in reveals:
# Get TX
revealInfo = account_module.getRevealTX(reveal)
reveal['bid'] = revealInfo
bids = render.bids(bids,reveals)
return jsonify({"result": bids})
if function == "icon": if function == "icon":
# Check if there is an icon # Check if there is an icon
@@ -1646,7 +1787,7 @@ def api_wallet_mobile(function):
return jsonify({"error": "Not logged in"}) return jsonify({"error": "Not logged in"})
account = account_module.check_account(request.cookies.get("account")) account = account_module.check_account(request.cookies.get("account"))
password = request.cookies.get("account").split(":")[1] password = request.cookies.get("account","").split(":")[1]
if not account: if not account:
return jsonify({"error": "Invalid account"}) return jsonify({"error": "Invalid account"})
@@ -1708,7 +1849,11 @@ def renderDomain(name: str) -> str:
#region Assets and default pages #region Assets and default pages
@app.route('/qr/<data>') @app.route('/qr/<data>')
def qr(data): def qr(data):
return send_file(qrcode(data, mode="raw"), mimetype="image/png")
output = qrcode(data, mode="raw")
if output is None:
return jsonify({"error": "Invalid data"}), 400
return send_file(output, mimetype="image/png")
# Theme # Theme
@app.route('/assets/css/styles.min.css') @app.route('/assets/css/styles.min.css')

View File

@@ -148,11 +148,14 @@ def getPluginData(pluginStr: str):
def getPluginFunctions(plugin: str): def getPluginFunctions(plugin: str):
plugin = import_module(plugin.replace("/",".")) imported_plugin = import_module(plugin.replace("/","."))
return plugin.functions return imported_plugin.functions
def runPluginFunction(plugin: str, function: str, params: dict, authentication: str): def runPluginFunction(plugin: str, function: str, params: dict, authentication: (str|None)):
if not authentication:
return {"error": "Authentication required"}
plugin_module = import_module(plugin.replace("/",".")) plugin_module = import_module(plugin.replace("/","."))
if function not in plugin_module.functions: if function not in plugin_module.functions:
return {"error": "Function not found"} return {"error": "Function not found"}
@@ -189,13 +192,13 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
def getPluginFunctionInputs(plugin: str, function: str): def getPluginFunctionInputs(plugin: str, function: str):
plugin = import_module(plugin.replace("/",".")) imported_plugin = import_module(plugin.replace("/","."))
return plugin.functions[function]["params"] return imported_plugin.functions[function]["params"]
def getPluginFunctionReturns(plugin: str, function: str): def getPluginFunctionReturns(plugin: str, function: str):
plugin = import_module(plugin.replace("/",".")) imported_plugin = import_module(plugin.replace("/","."))
return plugin.functions[function]["returns"] return imported_plugin.functions[function]["returns"]
def getDomainFunctions(): def getDomainFunctions():

106
render.py
View File

@@ -7,10 +7,8 @@ import os
from handywrapper import api from handywrapper import api
import threading import threading
HSD_API = os.getenv("HSD_API") HSD_API = os.getenv("HSD_API","")
HSD_IP = os.getenv("HSD_IP") HSD_IP = os.getenv("HSD_IP","localhost")
if HSD_IP is None:
HSD_IP = "localhost"
HSD_NETWORK = os.getenv("HSD_NETWORK") HSD_NETWORK = os.getenv("HSD_NETWORK")
HSD_WALLET_PORT = 12039 HSD_WALLET_PORT = 12039
@@ -40,6 +38,24 @@ if TX_EXPLORER_URL is None:
NAMEHASH_CACHE = 'user_data/namehash_cache.json' NAMEHASH_CACHE = 'user_data/namehash_cache.json'
# Validate cache version
if os.path.exists(NAMEHASH_CACHE):
with open(NAMEHASH_CACHE, 'r') as f:
cache = json.load(f)
if not isinstance(cache, dict):
print("Invalid namehash cache format. Resetting cache.")
with open(NAMEHASH_CACHE, 'w') as f:
json.dump({}, f)
# Check if cache entries are valid
for key in cache:
if not cache[key].startswith("<a href='/manage/"):
print(f"Invalid cache entry for {key}. Resetting cache.")
with open(NAMEHASH_CACHE, 'w') as f:
json.dump({}, f)
break
CACHE_LOCK = threading.Lock() CACHE_LOCK = threading.Lock()
@@ -78,6 +94,7 @@ actionMap = {
"UPDATE": "Updated ", "UPDATE": "Updated ",
"REGISTER": "Registered ", "REGISTER": "Registered ",
"RENEW": "Renewed ", "RENEW": "Renewed ",
"OPEN": "Opened ",
"BID": "Bid on ", "BID": "Bid on ",
"REVEAL": "Revealed bid for ", "REVEAL": "Revealed bid for ",
"REDEEM": "Redeemed bid for ", "REDEEM": "Redeemed bid for ",
@@ -89,6 +106,7 @@ actionMapPlural = {
"UPDATE": "Updated multiple domains' records", "UPDATE": "Updated multiple domains' records",
"REGISTER": "Registered multiple domains", "REGISTER": "Registered multiple domains",
"RENEW": "Renewed multiple domains", "RENEW": "Renewed multiple domains",
"OPEN": "Opened multiple domains",
"BID": "Bid on multiple domains", "BID": "Bid on multiple domains",
"REVEAL": "Revealed multiple bids", "REVEAL": "Revealed multiple bids",
"REDEEM": "Redeemed multiple bids", "REDEEM": "Redeemed multiple bids",
@@ -216,28 +234,28 @@ def dns(data, edit=False):
for key, value in entry.items(): for key, value in entry.items():
if key != 'type': if key != 'type':
if isinstance(value, list): if isinstance(value, list):
html_output += "<td>\n" if len(value) > 1:
for val in value: html_output += '<td style="white-space: pre-wrap; font-family: monospace;">\n'
html_output += f"{val}<br>\n" for val in value:
html_output += f"{val}<br>\n"
html_output += "</td>\n" html_output += "</td>\n"
else:
html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{value[0]}</td>\n'
else: else:
html_output += f"<td>{value}</td>\n" html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{value}</td>\n'
elif entry['type'] == 'DS': elif entry['type'] == 'DS':
ds = f"{entry['keyTag']} {entry['algorithm']} {entry['digestType']} {entry['digest']}" ds = f"{entry['keyTag']} {entry['algorithm']} {entry['digestType']} {entry['digest']}"
html_output += f"<td>{ds}</td>\n" html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{ds}</td>\n'
else: else:
value = "" value = ""
for key, val in entry.items(): for key, val in entry.items():
if key != 'type': if key != 'type':
value += f'{val} ' value += f'{val} '
html_output += f"<td>{value}</td>\n" html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{value.strip()}</td>\n'
if edit: if edit:
# Remove the current entry from the list
keptRecords = str(data[:index] + data[index+1:]).replace("'", '"') keptRecords = str(data[:index] + data[index+1:]).replace("'", '"')
keptRecords = urllib.parse.quote(keptRecords) keptRecords = urllib.parse.quote(keptRecords)
html_output += f"<td><a href='edit?dns={keptRecords}'>Remove</a></td>\n" html_output += f"<td><a href='edit?dns={keptRecords}'>Remove</a></td>\n"
@@ -246,6 +264,7 @@ def dns(data, edit=False):
index += 1 index += 1
return html_output return html_output
def txs(data): def txs(data):
data = data[::-1] data = data[::-1]
@@ -277,30 +296,60 @@ def timestamp_to_readable_time(timestamp):
return readable_time return readable_time
def bids(bids,reveals): def bids(bids,reveals):
html = '' # Create a list to hold bid data for sorting
bid_data = []
# Prepare data for sorting
for bid in bids: for bid in bids:
lockup = bid['lockup'] lockup = bid['lockup'] / 1000000
lockup = lockup / 1000000
html += "<tr>"
html += f"<td>{lockup:,.2f} HNS</td>"
revealed = False revealed = False
value = 0
# Check if this bid has been revealed
for reveal in reveals: for reveal in reveals:
if reveal['bid'] == bid['prevout']['hash']: if reveal['bid'] == bid['prevout']['hash']:
revealed = True revealed = True
value = reveal['value'] value = reveal['value'] / 1000000
value = value / 1000000
html += f"<td>{value:,.2f} HNS</td>"
bidValue = lockup - value
html += f"<td>{bidValue:,.2f} HNS</td>"
break break
if not revealed:
# Store all relevant information for sorting and display
bid_data.append({
'bid': bid,
'lockup': lockup,
'revealed': revealed,
'value': value,
'sort_value': value if revealed else lockup # Use value for sorting if revealed, otherwise lockup
})
# Sort by the sort_value in descending order (highest first)
bid_data.sort(key=lambda x: x['sort_value'], reverse=True)
# Generate HTML from sorted data
html = ''
for data in bid_data:
bid = data['bid']
lockup = data['lockup']
revealed = data['revealed']
value = data['value']
html += "<tr>"
html += f"<td>{lockup:,.2f} HNS</td>"
if revealed:
bidValue = lockup - value
html += f"<td>{value:,.2f} HNS</td>"
html += f"<td>{bidValue:,.2f} HNS</td>"
else:
html += f"<td>Hidden until reveal</td>" html += f"<td>Hidden until reveal</td>"
html += f"<td>Hidden until reveal</td>" html += f"<td>Hidden until reveal</td>"
if bid['own']: if bid['own']:
html += "<td>You</td>" html += "<td>You</td>"
else: else:
html += "<td>Unknown</td>" html += f"<td>Unknown</td>"
html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' target='_blank' href='{TX_EXPLORER_URL}{bid['prevout']['hash']}'>Bid TX 🔗</a></td>"
html += "</tr>" html += "</tr>"
return html return html
@@ -509,12 +558,13 @@ def renderDomainAsync(namehash: str) -> None:
if namehash in cache: if namehash in cache:
return return
# Fetch the name outside the lock (network call) # Fetch the name outside the lock (network call)
name = hsd.rpc_getNameByHash(namehash) name = hsd.rpc_getNameByHash(namehash)
if name["error"] is None: if name["error"] is None:
name = name["result"] name = name["result"]
rendered = renderDomain(name) rendered = renderDomain(name)
rendered = f"<a href='/manage/{name}' target='_blank' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));'>{rendered}</a>"
with CACHE_LOCK: with CACHE_LOCK:
with open(NAMEHASH_CACHE, 'r') as f: with open(NAMEHASH_CACHE, 'r') as f:
@@ -523,7 +573,7 @@ def renderDomainAsync(namehash: str) -> None:
with open(NAMEHASH_CACHE, 'w') as f: with open(NAMEHASH_CACHE, 'w') as f:
json.dump(cache, f) json.dump(cache, f)
return rendered return
else: else:
print(f"Error fetching name for hash {namehash}: {name['error']}", flush=True) print(f"Error fetching name for hash {namehash}: {name['error']}", flush=True)

View File

@@ -17,8 +17,8 @@ def gunicornServer():
def load_config(self): def load_config(self):
for key, value in self.options.items(): for key, value in self.options.items():
if key in self.cfg.settings and value is not None: if key in self.cfg.settings and value is not None: # type: ignore
self.cfg.set(key.lower(), value) self.cfg.set(key.lower(), value) # type: ignore
def load(self): def load(self):
return self.application return self.application

View File

@@ -66,9 +66,9 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="stick-right">{{next_action|safe}}</div> <div id="next-action" class="stick-right">{{next_action|safe}}</div>
<h4 class="card-title">{{rendered}}</h4> <h4 class="card-title">{{rendered}}</h4>
<h6 class="text-muted mb-2 card-subtitle">{{next}}</h6> <h6 class="text-muted mb-2 card-subtitle" id="next">{{next | safe}}</h6>
</div> </div>
</div> </div>
</div> </div>
@@ -93,13 +93,92 @@
<th>Bid</th> <th>Bid</th>
<th>Blind</th> <th>Blind</th>
<th>Owner</th> <th>Owner</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="bids-tbody">
{{bids | safe}} <tr id="loading-row">
<td colspan="5" class="text-center">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
Loading bids...
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<script>
async function loadBids(initial = false) {
const tbody = document.getElementById('bids-tbody');
try {
// Fetch all required data
const response = await fetch(`/api/v1/wallet/domainBids?domain={{search_term}}`);
const data = await response.json();
if (initial) {
if (response.ok && data.result) {
tbody.innerHTML = data.result;
} else {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No bids found. <a href="/auction/{{search_term}}/scan">Rescan Auction</a></td></tr>';
}
}
const mempoolResponse = await fetch('/api/v1/hsd/mempoolBids');
const nextStateResponse = await fetch(`/api/v1/hsd/nextAuctionState?domain={{search_term}}`);
if (!initial) {
if (response.ok && data.result) {
tbody.innerHTML = data.result;
} else {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No bids found. <a href="/auction/{{search_term}}/scan">Rescan Auction</a></td></tr>';
}
}
const nextStateData = await nextStateResponse.json();
if (nextStateResponse.ok && nextStateData.state) {
document.getElementById('next').innerHTML = nextStateData.next;
document.getElementById('next-action').innerHTML = nextStateData.next_action;
} else {
document.getElementById('next').innerHTML = 'Unknown';
document.getElementById('next-action').innerHTML = '';
}
const mempoolData = await mempoolResponse.json();
if (mempoolResponse.ok && mempoolData.result) {
const domainBids = mempoolData.result['{{search_term}}'];
if (domainBids && domainBids.length > 0) {
let mempoolRows = '';
domainBids.forEach(bid => {
const bidValue = bid.revealed ? `${(bid.value / 1000000).toFixed(2)} HNS` : 'Hidden until reveal';
const lockupValue = (bid.lockup / 1000000).toFixed(2);
const blindValue = bid.revealed ? `${((bid.lockup - bid.value) / 1000000).toFixed(2)} HNS` : 'Hidden until reveal';
const type = bid.revealed ? 'Reveal' : 'Bid';
mempoolRows += `<tr class="table-warning">
<td>${lockupValue} HNS</td>
<td>${bidValue}</td>
<td>${blindValue}</td>
<td>${bid.owner}</td>
<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' target='_blank' href='https://shakeshift.com/transaction/${bid.txid}'>Mempool ${type} 🔗</a></td>
</tr>`;
});
tbody.innerHTML += mempoolRows;
}
}
} catch (error) {
console.error('Error loading bids:', error);
}
}
// Load bids when page loads
document.addEventListener('DOMContentLoaded', () => loadBids(true));
// Auto-refresh bids every 20 seconds
setInterval(() => loadBids(false), 20000);
</script>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,4 @@
<span style="display: block;font-size: 12px;">TX: {{tx}}</span> <span style="display: block;font-size: 12px;">TX: {{tx}}</span>
<span style="display: block;">Check your transaction on a block explorer</span> <span style="display: block;">Check your transaction on a block explorer</span>
<a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a> <a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a>
<a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a> <a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a>
<a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a>

View File

@@ -87,27 +87,6 @@
{{dns | safe}} {{dns | safe}}
</tbody> </tbody>
</table> </table>
</div>
</div>
</div>
</div>
<div class="container-fluid" style="margin-top: 50px;">
<div class="card">
<div class="card-body">
<h4 class="card-title">History</h4><div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Action</th>
<th>TX</th>
<th>Amount</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{{txs | safe}}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -67,7 +67,7 @@
</div> </div>
<div class="card" style="max-width: 500px;margin: auto;margin-top: 50px;"> <div class="card" style="max-width: 500px;margin: auto;margin-top: 50px;">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Your transaction has been sent and will be mined soon.</h4><span style="display: block;font-size: 12px;">TX: {{tx}}</span><span style="display: block;">Check your transaction on a block explorer</span><a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a><a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a><a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a> <h4 class="card-title">Your transaction has been sent and will be mined soon.</h4><span style="display: block;font-size: 12px;">TX: {{tx}}</span><span style="display: block;">Check your transaction on a block explorer</span><a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a><a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a>
</div> </div>
</div> </div>
</div> </div>