16 Commits

Author SHA1 Message Date
525b068f14 feat: Pull updates from main
Merges updates from nathanwoodburn/firewalletbrowser#1
2025-07-12 16:44:38 +10:00
6b69f933c3 feat: Do some more optimization from AI
Double check this all works
2025-07-12 16:35:56 +10:00
6271cf810e feat: Try some more optimizations 2025-07-12 16:35:07 +10:00
61d9f209b7 feat: Optimize some of the auction routes 2025-07-12 16:35:07 +10:00
b2db24c08e feat: Add red warning on auction page for potential outbids 2025-07-12 16:35:07 +10:00
7dda41bda7 feat: Add api route for possible outbidded domains 2025-07-12 16:34:42 +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
10 changed files with 528 additions and 226 deletions

Binary file not shown.

View File

@@ -7,6 +7,7 @@ import re
import domainLookup
import json
import time
import concurrent.futures
dotenv.load_dotenv()
@@ -109,8 +110,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 +123,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 +147,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 +185,6 @@ def selectWallet(account: str):
}
}
def getBalance(account: str):
# Get the total balance
info = hsw.getBalance('default', account)
@@ -251,11 +249,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 +335,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 +377,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 +425,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 +446,18 @@ 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
@@ -472,6 +473,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)
@@ -599,11 +612,35 @@ def getWalletStatus():
return "Error wallet ahead of node"
# Add a simple cache for bid data
_bid_cache = {}
_bid_cache_time = {}
_cache_duration = 300 # Increased cache duration to 5 minutes for bids
# Add domain info cache
_domain_info_cache = {}
_domain_info_time = {}
_domain_info_duration = 600 # Cache domain info for 10 minutes
# Add wallet authentication cache
_wallet_auth_cache = {}
_wallet_auth_time = {}
_wallet_auth_duration = 300 # Increased to 5 minutes for wallet auth
def getBids(account, domain="NONE"):
cache_key = f"{account}:{domain}"
current_time = time.time()
# Return cached data if available and fresh
if cache_key in _bid_cache and current_time - _bid_cache_time.get(cache_key, 0) < _cache_duration:
return _bid_cache[cache_key]
try:
if domain == "NONE":
response = hsw.getWalletBids(account)
else:
response = hsw.getWalletBidsByName(domain, account)
# Add backup for bids with no value
bids = []
for bid in response:
@@ -614,30 +651,163 @@ def getBids(account, domain="NONE"):
if 'height' not in bid:
bid['height'] = 0
bids.append(bid)
return bids
# Cache the results
_bid_cache[cache_key] = bids
_bid_cache_time[cache_key] = current_time
return bids
except Exception as e:
print(f"Error fetching bids: {str(e)}")
return []
def _fetch_domain_info(domain):
"""Helper function to fetch domain info with caching"""
current_time = time.time()
# Check cache first
if (domain in _domain_info_cache and
current_time - _domain_info_time.get(domain, 0) < _domain_info_duration):
return _domain_info_cache[domain]
# Fetch domain info
domain_info = getDomain(domain)
# Store in cache
_domain_info_cache[domain] = domain_info
_domain_info_time[domain] = current_time
return domain_info
def _fetch_domain_batch(domains, max_workers=10):
"""Fetch multiple domains in parallel"""
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
# Create a mapping of futures to domains
future_to_domain = {executor.submit(_fetch_domain_info, domain): domain for domain in domains}
# Process as they complete
for future in concurrent.futures.as_completed(future_to_domain):
domain = future_to_domain[future]
try:
results[domain] = future.result()
except Exception as e:
print(f"Error fetching domain {domain}: {str(e)}")
results[domain] = {"error": str(e)}
return results
def getPossibleOutbids(account):
# Get all bids
bids = getBids(account)
if not bids or 'error' in bids:
return []
# Get current height
current_height = getBlockHeight()
# Sort out bids older than 720 blocks and extract domain names
filtered_bids = []
domains_to_check = set()
for bid in bids:
if (current_height - bid['height']) <= 720:
filtered_bids.append(bid)
domains_to_check.add(bid['name'])
if not domains_to_check:
return []
# Fetch all domain info in parallel
domain_info_map = _fetch_domain_batch(domains_to_check)
# Pre-filter domains in bidding state
bidding_domains = {
domain: info for domain, info in domain_info_map.items()
if ('info' in info and 'state' in info['info'] and
info['info']['state'] == "BIDDING")
}
# Process the results
possible_outbids = []
processed_domains = set()
# Group bids by domain name for efficient processing
bids_by_domain = {}
for bid in filtered_bids:
domain = bid['name']
if domain not in bids_by_domain:
bids_by_domain[domain] = []
bids_by_domain[domain].append(bid)
# Analyze each domain in bidding state
for domain, info in bidding_domains.items():
if domain in processed_domains or domain not in bids_by_domain:
continue
processed_domains.add(domain)
# Get all bids for this domain in one call
domain_bids = getBids(account, domain)
# Find the highest bid we've made
our_highest_bid = max(
(bid['value'] for bid in domain_bids if bid.get("own", False)),
default=0
)
# Quick check if any unrevealed bids could outbid us
if any(
bid["lockup"] > our_highest_bid
for bid in domain_bids
if not bid.get("own", False) and bid.get('value', 0) == -1000000
):
possible_outbids.append(domain)
return possible_outbids
def getReveals(account, domain):
return hsw.getWalletRevealsByName(domain, account)
def getPendingReveals(account):
bids = getBids(account)
domains = getDomains(account, False)
pending = []
for domain in domains:
if domain['state'] == "REVEAL":
reveals = getReveals(account, domain['name'])
for bid in bids:
if bid['name'] == domain['name']:
state_found = False
for reveal in reveals:
if reveal['own'] == True:
if bid['value'] == reveal['value']:
state_found = True
# Only get domains in REVEAL state to reduce API calls
domains = [d for d in getDomains(account, False) if d['state'] == "REVEAL"]
if not state_found:
if not domains: # Early return if no domains in REVEAL state
return []
pending = []
# Create a dictionary for O(1) lookups
domain_names = {domain['name']: domain for domain in domains}
# Group bids by name to batch process reveals
bids_by_name = {}
for bid in bids:
if bid['name'] in domain_names:
if bid['name'] not in bids_by_name:
bids_by_name[bid['name']] = []
bids_by_name[bid['name']].append(bid)
# Fetch reveals for each domain once
reveals_by_name = {}
for domain_name in bids_by_name:
reveals_by_name[domain_name] = getReveals(account, domain_name)
# Check each bid against the reveals
for domain_name, domain_bids in bids_by_name.items():
domain_reveals = reveals_by_name[domain_name]
for bid in domain_bids:
# Check if this bid has been revealed
bid_revealed = any(
reveal['own'] == True and bid['value'] == reveal['value']
for reveal in domain_reveals
)
if not bid_revealed:
pending.append(bid)
return pending
@@ -650,20 +820,27 @@ def getPendingRedeems(account, password):
pending = []
try:
# Collect all nameHashes first
name_hashes = []
for output in tx['result']['outputs']:
if output['covenant']['type'] != 5:
continue
if output['covenant']['action'] != "REDEEM":
continue
nameHash = output['covenant']['items'][0]
# Try to get the name from hash
name = hsd.rpc_getNameByHash(nameHash)
name_hashes.append(output['covenant']['items'][0])
# Batch processing name hashes
name_lookup = {}
for name_hash in name_hashes:
name = hsd.rpc_getNameByHash(name_hash)
if name['error']:
pending.append(nameHash)
pending.append(name_hash)
else:
pending.append(name['result'])
except:
print("Failed to parse redeems")
name_lookup[name_hash] = name['result']
except Exception as e:
print(f"Failed to parse redeems: {str(e)}")
return pending
@@ -671,13 +848,22 @@ def getPendingRedeems(account, password):
def getPendingRegisters(account):
bids = getBids(account)
domains = getDomains(account, False)
# Create dictionaries for O(1) lookups
bids_by_name = {}
for bid in bids:
if bid['name'] not in bids_by_name:
bids_by_name[bid['name']] = []
bids_by_name[bid['name']].append(bid)
pending = []
for domain in domains:
if domain['state'] == "CLOSED" and domain['registered'] == False:
for bid in bids:
if bid['name'] == domain['name']:
if domain['name'] in bids_by_name:
for bid in bids_by_name[domain['name']]:
if bid['value'] == domain['highest']:
pending.append(bid)
return pending
@@ -688,20 +874,26 @@ def getPendingFinalizes(account, password):
pending = []
try:
# Collect all nameHashes first
name_hashes = []
for output in tx['outputs']:
if output['covenant']['type'] != 10:
continue
if output['covenant']['action'] != "FINALIZE":
continue
nameHash = output['covenant']['items'][0]
# Try to get the name from hash
name = hsd.rpc_getNameByHash(nameHash)
name_hashes.append(output['covenant']['items'][0])
# Batch lookup for name hashes
for name_hash in name_hashes:
name = hsd.rpc_getNameByHash(name_hash)
if name['error']:
pending.append(nameHash)
pending.append(name_hash)
else:
pending.append(name['result'])
except:
print("Failed to parse finalizes")
except Exception as e:
print(f"Failed to parse finalizes: {str(e)}")
return pending
@@ -765,7 +957,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 +991,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": {
@@ -1044,19 +1236,51 @@ def revoke(account, domain):
}
def sendBatch(account, batch):
account_name = check_account(account)
password = ":".join(account.split(":")[1:])
def _prepare_wallet_for_batch(account_name, password):
"""Helper function to prepare wallet for batch operations with caching"""
cache_key = f"{account_name}:{password}"
current_time = time.time()
if account_name == False:
return {
"error": {
"message": "Invalid account"
}
}
# Return cached authentication if available and fresh
if (cache_key in _wallet_auth_cache and
current_time - _wallet_auth_time.get(cache_key, 0) < _wallet_auth_duration):
return _wallet_auth_cache[cache_key]
# Select and unlock wallet
result = {'success': False, 'error': None}
# Try to select the wallet
select_response = hsw.rpc_selectWallet(account_name)
if select_response['error'] is not None:
result['error'] = {"message": select_response['error']['message']}
return result
# Try to unlock the wallet
unlock_response = hsw.rpc_walletPassphrase(password, 30) # Increased timeout to reduce future unlocks
if (unlock_response['error'] is not None and
unlock_response['error']['message'] != "Wallet is not encrypted."):
result['error'] = {"message": unlock_response['error']['message']}
return result
# Authentication successful
result['success'] = True
# Cache the authentication result
_wallet_auth_cache[cache_key] = result
_wallet_auth_time[cache_key] = current_time
return result
def _execute_batch_operation(account_name, batch, operation_type="sendbatch"):
"""Execute a batch operation with the specified wallet"""
# Make the batch request
try:
response = hsw.rpc_selectWallet(account_name)
response = requests.post(
get_wallet_api_url(),
json={"method": operation_type, "params": [batch]},
timeout=30 # Add timeout to prevent hanging
).json()
if response['error'] is not None:
return {
"error": {
@@ -1071,38 +1295,49 @@ 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()
if response['error'] is not None:
return response
if 'result' not in response:
return {
"error": {
"message": "No result"
}
}
return {"error": {"message": "No result"}}
return response['result']
except Exception as e:
return {
"error": {
"message": str(e)
}
}
return {"error": {"message": str(e)}}
def sendBatch(account, batch):
account_name = check_account(account)
if account_name == False:
return {"error": {"message": "Invalid account"}}
password = ":".join(account.split(":")[1:])
# Prepare the wallet (this uses caching)
auth_result = _prepare_wallet_for_batch(account_name, password)
if not auth_result['success']:
return auth_result['error']
# Execute the batch operation
return _execute_batch_operation(account_name, batch, "sendbatch")
def createBatch(account, batch):
account_name = check_account(account)
if account_name == False:
return {"error": {"message": "Invalid account"}}
password = ":".join(account.split(":")[1:])
if account_name == False:
return {
"error": {
"message": "Invalid account"
}
}
# Prepare the wallet (this uses caching)
auth_result = _prepare_wallet_for_batch(account_name, password)
if not auth_result['success']:
return auth_result['error']
# Execute the batch operation
return _execute_batch_operation(account_name, batch, "createbatch")
try:
response = hsw.rpc_selectWallet(account_name)
@@ -1120,7 +1355,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()
@@ -1180,7 +1415,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 +1544,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")

144
main.py
View File

@@ -32,6 +32,38 @@ 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
return f"{hours} hrs {minutes} mins"
else:
days = blocks // 144
hours = (blocks % 144) // 6
return f"{days} days {hours} hrs"
# Add a cache for transactions with a timeout
tx_cache = {}
TX_CACHE_TIMEOUT = 60*5 # Cache timeout in seconds
# Add a cache for outbids with a timeout
outbids_cache = {}
OUTBIDS_CACHE_TIMEOUT = 60*2 # Cache timeout in seconds
@app.route('/')
def index():
# Check if the user is logged in
@@ -70,10 +102,6 @@ def reverseDirection(direction: str):
#region Transactions
# Add a cache for transactions with a timeout
tx_cache = {}
TX_CACHE_TIMEOUT = 60*5 # Cache timeout in seconds
@app.route('/tx')
def transactions():
# Check if the user is logged in
@@ -294,8 +322,12 @@ def auctions():
sort_time = direction
sort_time_next = reverseDirection(direction)
# Check if bids list is empty to avoid IndexError
if not bids:
domains = sorted(domains, key=lambda k: k['height'],reverse=reverse)
sortbyDomain = True
# If older HSD version sort by domain height
if bids[0]['height'] == 0:
elif bids[0]['height'] == 0:
domains = sorted(domains, key=lambda k: k['height'],reverse=reverse)
sortbyDomain = True
else:
@@ -306,7 +338,27 @@ def auctions():
sort_domain = direction
sort_domain_next = reverseDirection(direction)
bidsHtml = render.bidDomains(bids,domains,sortbyDomain)
# Check if outbids set to true
outbids = request.args.get("outbids")
if outbids is not None and outbids.lower() == "true":
# Check cache before making expensive call
cache_key = f"outbids_{account}"
current_time = time.time()
if cache_key in outbids_cache and (current_time - outbids_cache[cache_key]['time'] < OUTBIDS_CACHE_TIMEOUT):
outbids = outbids_cache[cache_key]['data']
else:
# Get outbid domains
outbids = account_module.getPossibleOutbids(account)
# Store in cache
outbids_cache[cache_key] = {
'data': outbids,
'time': current_time
}
else:
outbids = []
bidsHtml = render.bidDomains(bids,domains,sortbyDomain,outbids)
plugins = ""
message = ''
if 'message' in request.args:
@@ -334,11 +386,12 @@ def revealAllBids():
return redirect("/logout")
response = account_module.revealAll(request.cookies.get("account"))
if 'error' in response:
if response['error'] != None:
if response['error']['message'] == "Nothing to do.":
# Simplified error handling
if 'error' in response and response['error']:
error_msg = response['error'].get('message', str(response['error']))
if error_msg == "Nothing to do.":
return redirect("/auctions?message=No reveals pending")
return redirect("/auctions?message=" + response['error']['message'])
return redirect("/auctions?message=" + error_msg)
return redirect("/success?tx=" + response['result']['hash'])
@@ -445,10 +498,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,38 +510,36 @@ 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'])
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:
dns = account_module.getDNS(search_term)
if account_module.isOwnDomain(account, search_term):
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 +553,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 +562,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)
@@ -673,10 +725,7 @@ def editPage(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)
@@ -899,7 +948,7 @@ def auction(domain):
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 +967,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 +1125,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'])
@@ -1637,6 +1693,12 @@ def api_wallet(function):
return send_file('templates/assets/img/HNS.png')
if function == "possibleOutbids":
return jsonify({"result": account_module.getPossibleOutbids(account)})
return jsonify({"error": "Invalid function", "result": "Invalid function"}), 400
@app.route('/api/v1/wallet/<function>/mobile', methods=["GET"])

View File

@@ -216,28 +216,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 +246,7 @@ def dns(data, edit=False):
index += 1
return html_output
def txs(data):
data = data[::-1]
@@ -277,36 +278,66 @@ 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)));' href='{TX_EXPLORER_URL}{bid['prevout']['hash']}'>Bid TX 🔗</a></td>"
html += "</tr>"
return html
def bidDomains(bids,domains, sortbyDomains=False):
def bidDomains(bids,domains, sortbyDomains=False, outbids=[]):
html = ''
if not sortbyDomains:
for bid in bids:
for domain in domains:
@@ -321,12 +352,14 @@ def bidDomains(bids,domains, sortbyDomains=False):
else:
bidDisplay = f'<b>{bidValue:,.2f}</b> HNS'
html += "<tr>"
if domain['name'] in outbids:
html += f"<td style='background-color: red;'><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{renderDomain(domain['name'])}</a></td>"
else:
html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{renderDomain(domain['name'])}</a></td>"
html += f"<td>{domain['state']}</td>"
html += f"<td style='white-space: nowrap;'>{bidDisplay}</td>"
html += f"<td class='hide-mobile'>{domain['height']:,}</td>"
html += f"<td class='hide-mobile'>{bid['height']:,}</td>"
html += "</tr>"
else:
for domain in domains:
@@ -342,7 +375,7 @@ def bidDomains(bids,domains, sortbyDomains=False):
html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{renderDomain(domain['name'])}</a></td>"
html += f"<td>{domain['state']}</td>"
html += f"<td>{bidDisplay}</td>"
html += f"<td class='hide-mobile'>{domain['height']:,}</td>"
html += f"<td class='hide-mobile'>{bid['height']:,}</td>"
html += "</tr>"
return html

View File

@@ -1 +1 @@
function createCard(e,n,t){if(document.getElementById(t)&&document.getElementById(t).remove(),n<=0)return;const a=document.createElement("div");a.classList.add("col-md-6","col-xl-3","mb-4"),a.id=t,html=`\n <div class="card shadow border-start-warning py-2">\n <div class="card-body">\n <div class="row align-items-center no-gutters">\n <div class="col me-2">\n <div class="text-uppercase text-warning fw-bold text-xs mb-1"><span>${e}</span></div>\n <div class="text-dark fw-bold h5 mb-0"><span id="${e}">${n}</span></div>\n </div>\n <div class="col"><a class="btn btn-primary" role="button" href="/all/${t.toLowerCase()}">${t} All</a></div>\n <div class="col-auto"><svg class="fa-2x text-gray-300" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor">\n <g>\n <rect fill="none" height="24" width="24"></rect>\n </g>\n <g>\n <path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M7,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C8.5,12.83,7.83,13.5,7,13.5z M12,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C13.5,12.83,12.83,13.5,12,13.5z M17,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C18.5,12.83,17.83,13.5,17,13.5z"></path>\n </g>\n </svg></div>\n </div>\n </div>`,a.innerHTML=html,document.getElementById("actions-row").appendChild(a)}async function updateActions(){const e={Finalize:"Pending Finalizes",Register:"Pending Register",Redeem:"Pending Redeem",Reveal:"Pending Reveal"};for(const n in e){const t=await request(`wallet/pending${n}`);"Error"!=t&&createCard(e[n],t.length,n)}}window.addEventListener("load",(async()=>{updateActions()})),setInterval((async function(){updateActions()}),2e4);
function createCard(e,n,t){if(document.getElementById(t)&&document.getElementById(t).remove(),n<=0)return;const s=document.createElement("div");s.classList.add("col-md-6","col-xl-3","mb-4"),s.id=t,html=`\n <div class="card shadow border-start-warning py-2">\n <div class="card-body">\n <div class="row align-items-center no-gutters">\n <div class="col me-2">\n <div class="text-uppercase text-warning fw-bold text-xs mb-1"><span>${e}</span></div>\n <div class="text-dark fw-bold h5 mb-0"><span id="${e}">${n}</span></div>\n </div>\n <div class="col"><a class="btn btn-primary" role="button" href="/all/${t.toLowerCase()}">${t} All</a></div>\n <div class="col-auto"><svg class="fa-2x text-gray-300" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor">\n <g>\n <rect fill="none" height="24" width="24"></rect>\n </g>\n <g>\n <path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M7,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C8.5,12.83,7.83,13.5,7,13.5z M12,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C13.5,12.83,12.83,13.5,12,13.5z M17,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C18.5,12.83,17.83,13.5,17,13.5z"></path>\n </g>\n </svg></div>\n </div>\n </div>`,s.innerHTML=html,document.getElementById("actions-row").appendChild(s)}async function updateActions(){const e={Finalize:"Pending Finalizes",Register:"Pending Register",Redeem:"Pending Redeem",Reveal:"Pending Reveal"},n=Object.keys(e).map((e=>request(`wallet/pending${e}`).then((n=>({id:e,result:n}))))),t=await Promise.all(n);for(const{id:n,result:s}of t)"Error"!==s&&createCard(e[n],s.length,n);const s=await request("wallet/possibleOutbids");if("Error"===s)return;const d=document.getElementById("outbids");if(d&&d.remove(),s.length<=0)return;const i=document.createElement("div");i.classList.add("col-md-6","col-xl-3","mb-4"),i.id="outbids",i.innerHTML=`\n <div class="card shadow border-start-warning py-2">\n <div class="card-body">\n <div class="row align-items-center no-gutters">\n <div class="col me-2">\n <div class="text-uppercase text-warning fw-bold text-xs mb-1"><span>Names with possible outbids</span></div>\n <div class="text-dark fw-bold h5 mb-0"><span id="outbids-count">${s.length}</span></div>\n </div>\n <div class="col"><a class="btn btn-primary" role="button" href="/auctions?outbids=true">Show All</a></div>\n <div class="col-auto">\n <svg class="fa-2x text-gray-300" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor">\n <g><rect fill="none" height="24" width="24"></rect></g>\n <g>\n <path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M7,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C8.5,12.83,7.83,13.5,7,13.5z M12,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C13.5,12.83,12.83,13.5,12,13.5z M17,13.5c-0.83,0-1.5-0.67-1.5-1.5 c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5C18.5,12.83,17.83,13.5,17,13.5z"></path>\n </g>\n </svg>\n </div>\n </div>\n </div>\n </div>\n `,document.getElementById("actions-row").appendChild(i)}window.addEventListener("load",(async()=>{updateActions()})),setInterval((async function(){updateActions()}),2e4);

View File

@@ -68,7 +68,7 @@
<div class="card-body">
<div 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">{{next | safe}}</h6>
</div>
</div>
</div>
@@ -93,6 +93,7 @@
<th>Bid</th>
<th>Blind</th>
<th>Owner</th>
<th></th>
</tr>
</thead>
<tbody>

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>