Compare commits

18 Commits

Author SHA1 Message Date
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
9 changed files with 463 additions and 144 deletions

Binary file not shown.

View File

@@ -109,8 +109,7 @@ def createWallet(account: str, password: str):
}
# Create the account
# Python wrapper doesn't support this yet
response = requests.put(
f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}")
response = requests.put(get_wallet_api_url(f"wallet/{account}"))
if response.status_code != 200:
return {
"error": {
@@ -123,7 +122,7 @@ def createWallet(account: str, password: str):
seed = seed['mnemonic']['phrase']
# 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})
return {
@@ -147,8 +146,7 @@ def importWallet(account: str, password: str, seed: str):
"mnemonic": seed,
}
response = requests.put(
f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}", json=data)
response = requests.put(get_wallet_api_url(f"/wallet/{account}"), json=data)
if response.status_code != 200:
return {
"error": {
@@ -186,7 +184,6 @@ def selectWallet(account: str):
}
}
def getBalance(account: str):
# Get the total balance
info = hsw.getBalance('default', account)
@@ -251,11 +248,9 @@ def getPendingTX(account: str):
def getDomains(account, own=True):
if own:
response = requests.get(
f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/name?own=true")
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name?own=true"))
else:
response = requests.get(
f"http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/name")
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name"))
info = response.json()
if SHOW_EXPIRED:
@@ -339,11 +334,9 @@ def getTransactions(account, page=1, limit=100):
lastTX = getTXFromPage(account, page-1, limit)
if lastTX:
response = requests.get(
f'http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/tx/history?reverse=true&limit={limit}&after={lastTX}')
response = requests.get(get_wallet_api_url(f"/wallet/{account}/tx/history?reverse=true&limit={limit}&after={lastTX}"))
elif page == 1:
response = requests.get(
f'http://x:{HSD_API}@{HSD_IP}:{HSD_WALLET_PORT}/wallet/{account}/tx/history?reverse=true&limit={limit}')
response = requests.get(get_wallet_api_url(f"/wallet/{account}/tx/history?reverse=true&limit={limit}"))
else:
return []
@@ -383,7 +376,7 @@ def check_address(address: str, allow_name: bool = True, return_address: bool =
return check_hip2(address[1:])
# 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",
"params": [address]
}).json()
@@ -431,8 +424,6 @@ def send(account, address, amount):
response = hsw.rpc_walletPassphrase(password, 10)
# 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']['message'] != "Wallet is not encrypted.":
return {
@@ -454,9 +445,35 @@ def send(account, address, amount):
def isOwnDomain(account, name: str):
domains = getDomains(account)
for domain in domains:
if domain['name'] == name:
# Get domain
domain_info = getDomain(name)
owner = getAddressFromCoin(domain_info['info']['owner']['hash'],domain_info['info']['owner']['index'])
# 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
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
@@ -472,6 +489,18 @@ def getDomain(domain: str):
}
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):
account_name = check_account(account)
@@ -765,7 +794,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:
return {
"error": {
@@ -799,7 +828,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:
return {
"error": {
@@ -855,10 +884,12 @@ def rescan_auction(account, domain):
return {
"error": "Invalid domain"
}
if 'bidPeriodStart' not in response['result']['info']['stats']:
if 'height' not in response['result']['info']:
return {
"error": "Not in auction"
"error": "Can't find start"
}
height = response['result']['info']['height']-1
response = hsw.rpc_importName(domain, height)
return response
@@ -1071,7 +1102,7 @@ def sendBatch(account, batch):
"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",
"params": [batch]
}).json()
@@ -1120,7 +1151,7 @@ def createBatch(account, batch):
"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",
"params": [batch]
}).json()
@@ -1141,6 +1172,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
def rescan():
@@ -1180,7 +1285,7 @@ def zapTXs(account):
}
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,
"account": "default"
})
@@ -1309,3 +1414,25 @@ def generateReport(account, format="{name},{expiry},{value},{maxBid}"):
def convertHNS(value: int):
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

@@ -139,35 +139,6 @@ def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
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):
try:
return emoji.encode("idna").decode("ascii")

181
main.py
View File

@@ -32,6 +32,35 @@ revokeCheck = random.randint(100000,999999)
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('/')
def index():
# Check if the user is logged in
@@ -445,10 +474,11 @@ def search():
state="AVAILABLE", next="Available Now",plugins=plugins)
state = domain['info']['state']
stats = domain['info']['stats']
if state == 'CLOSED':
if domain['info']['registered']:
state = 'REGISTERED'
expires = domain['info']['stats']['daysUntilExpire']
expires = stats['daysUntilExpire']
next = f"Expires in ~{expires} days"
else:
state = 'AVAILABLE'
@@ -456,23 +486,25 @@ def search():
elif state == "REVOKED":
next = "Available Now"
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':
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':
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'
dns = []
txs = []
if domain_info:
owner = domain_info['owner']
dns = domain_info['dns']
txs = domain_info['txs']
# Check if info and info.owner
if 'info' in domain_info and 'owner' in domain_info['info']:
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 = [x['name'] for x in own_domains]
@@ -481,13 +513,12 @@ def search():
owner = "You"
dns = render.dns(dns)
txs = render.txs(txs)
return render_template("search.html", account=account,
rendered=renderDomain(search_term),
search_term=search_term,domain=domain['info']['name'],
raw=domain,state=state, next=next, owner=owner,
dns=dns, txs=txs,plugins=plugins)
dns=dns,plugins=plugins)
@app.route('/manage/<domain>')
def manage(domain: str):
@@ -501,10 +532,7 @@ def manage(domain: str):
domain = domain.lower()
own_domains = account_module.getDomains(account)
own_domains = [x['name'] for x in own_domains]
own_domains = [x.lower() for x in own_domains]
if domain not in own_domains:
if not account_module.isOwnDomain(account, domain):
return redirect("/search?q=" + domain)
domain_info = account_module.getDomain(domain)
@@ -513,7 +541,10 @@ def manage(domain: str):
rendered=renderDomain(domain),
domain=domain, error=domain_info['error'])
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)
raw_dns = str(dns).replace("'",'"')
dns = render.dns(dns)
@@ -887,19 +918,20 @@ def auction(domain):
state = domainInfo['info']['state']
next_action = ''
bids = account_module.getBids(account,search_term)
if bids == []:
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)
# bids = account_module.getBids(account,search_term)
bids = []
# if bids == []:
# 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)
stats = domainInfo['info']['stats'] if 'stats' in domainInfo['info'] else {}
if state == 'CLOSED':
if not domainInfo['info']['registered']:
if account_module.isOwnDomain(account,domain):
@@ -918,20 +950,27 @@ def auction(domain):
expires = domainInfo['info']['stats']['daysUntilExpire']
next = f"Expires in ~{expires} days"
own_domains = account_module.getDomains(account)
own_domains = [x['name'] for x in own_domains]
own_domains = [x.lower() for x in own_domains]
if search_term in own_domains:
if account_module.isOwnDomain(account,domain):
next_action = f'<a href="/manage/{domain}">Manage</a>'
elif state == "REVOKED":
next = "Available Now"
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
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':
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':
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>'
message = ''
@@ -1069,7 +1108,7 @@ def reveal_auction(domain):
return redirect("/logout")
domain = domain.lower()
response = account_module(request.cookies.get("account"),domain)
response = account_module.revealAuction(request.cookies.get("account"),domain)
if 'error' in response:
return redirect("/auction/" + domain + "?message=" + response['error']['message'])
return redirect("/success?tx=" + response['hash'])
@@ -1506,6 +1545,65 @@ def api_hsd(function):
return jsonify({"result": account_module.hsdVersion(False)})
if function == "height":
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 = ""
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
@@ -1625,6 +1723,21 @@ def api_wallet(function):
"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":
# Check if there is an icon

View File

@@ -40,6 +40,24 @@ if TX_EXPLORER_URL is None:
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()
@@ -216,28 +234,28 @@ def dns(data, edit=False):
for key, value in entry.items():
if key != 'type':
if isinstance(value, list):
html_output += "<td>\n"
if len(value) > 1:
html_output += '<td style="white-space: pre-wrap; font-family: monospace;">\n'
for val in value:
html_output += f"{val}<br>\n"
html_output += "</td>\n"
else:
html_output += f"<td>{value}</td>\n"
html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{value[0]}</td>\n'
else:
html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{value}</td>\n'
elif entry['type'] == 'DS':
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:
value = ""
for key, val in entry.items():
if key != 'type':
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:
# 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"
@@ -246,6 +264,7 @@ def dns(data, edit=False):
index += 1
return html_output
def txs(data):
data = data[::-1]
@@ -277,30 +296,60 @@ def timestamp_to_readable_time(timestamp):
return readable_time
def bids(bids,reveals):
html = ''
# Create a list to hold bid data for sorting
bid_data = []
# Prepare data for sorting
for bid in bids:
lockup = bid['lockup']
lockup = lockup / 1000000
html += "<tr>"
html += f"<td>{lockup:,.2f} HNS</td>"
lockup = bid['lockup'] / 1000000
revealed = False
value = 0
# Check if this bid has been revealed
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>"
value = reveal['value'] / 1000000
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>"
if bid['own']:
html += "<td>You</td>"
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>"
return html
@@ -515,6 +564,8 @@ def renderDomainAsync(namehash: str) -> None:
if name["error"] is None:
name = name["result"]
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 open(NAMEHASH_CACHE, 'r') as f:

View File

@@ -66,9 +66,9 @@
<div class="container-fluid">
<div class="card">
<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>
<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>
@@ -93,13 +93,92 @@
<th>Bid</th>
<th>Blind</th>
<th>Owner</th>
<th></th>
</tr>
</thead>
<tbody>
{{bids | safe}}
<tbody id="bids-tbody">
<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>
</table>
</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>

View File

@@ -1,5 +1,4 @@
<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>
<a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a>

View File

@@ -87,27 +87,6 @@
{{dns | safe}}
</tbody>
</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>

View File

@@ -67,7 +67,7 @@
</div>
<div class="card" style="max-width: 500px;margin: auto;margin-top: 50px;">
<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>