38 Commits
dev ... v2.0

Author SHA1 Message Date
1203719eac fix: Docker python version and fix reporting missing requirements 2025-08-29 13:28:12 +10:00
373a71f04d Merge pull request 'SPV support & add internal HSD node' (#4) from feat/internal_hsd into main
Reviewed-on: nathanwoodburn/firewalletbrowser#4
2025-08-29 13:04:30 +10:00
b76b873036 feat: Update readme 2025-08-28 17:50:47 +10:00
23e714fad8 feat: Add additional node info to settings 2025-08-28 17:27:45 +10:00
a36c69ecfc fix: Add SPV support for getDNS() 2025-08-28 17:18:52 +10:00
1fd9987bf1 feat: Upgrade tx page caches to a sqlite db 2025-08-28 17:13:23 +10:00
f2cda461ba feat: Add SPV features to fix accoutn balances 2025-08-28 16:42:12 +10:00
26c5b4a4fa feat: Update configuration storage and overrides 2025-08-26 18:04:07 +10:00
7fdc4a3122 fix: SPV causes some domains to not be recognized as owned by the wallet 2025-08-26 17:31:25 +10:00
5ff8960b7b feat: Add initial internal node option 2025-08-26 16:44:10 +10:00
4c84bc2bbe fix: Use hsd.hns.au to get name from namehash in order to only import account once 2025-08-26 15:26:19 +10:00
49e378803d fix: Use existing hsd from accounts module to get name from hash 2025-08-26 12:52:14 +10:00
1c53547047 feat: Add env flag to disable WALLET DNS record lookup 2025-08-25 18:10:03 +10:00
080c4402d8 Merge pull request 'Add WALLET DNS record for sending using domain alias' (#3) from feat/WALLETDNS into main
Reviewed-on: nathanwoodburn/firewalletbrowser#3
2025-08-25 13:59:15 +10:00
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
18 changed files with 1337 additions and 379 deletions

4
.gitignore vendored
View File

@@ -16,3 +16,7 @@ customPlugins/
cache/ cache/
build/ build/
dist/ dist/
hsd/
hsd-data/
hsd.lock
hsdconfig.json

View File

@@ -1,7 +1,7 @@
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder FROM --platform=$BUILDPLATFORM python:3.13-alpine AS builder
WORKDIR /app WORKDIR /app
RUN apk add git openssl curl
COPY requirements.txt /app COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip3 install -r requirements.txt pip3 install -r requirements.txt
@@ -10,9 +10,8 @@ COPY . /app
# Add mount point for data volume # Add mount point for data volume
# VOLUME /data # VOLUME /data
RUN apk add git openssl curl
ENTRYPOINT ["python3"] ENTRYPOINT ["python3"]
CMD ["server.py"] CMD ["server.py"]
FROM builder as dev-envs FROM builder AS dev-envs

Binary file not shown.

View File

@@ -124,9 +124,44 @@ SHOW_EXPIRED: Show expired domains (true/false)
EXCLUDE: Comma separated list of wallets to exclude from the wallet list (default primary) EXCLUDE: Comma separated list of wallets to exclude from the wallet list (default primary)
EXPLORER_TX: URL for exploring transactions (default https://shakeshift.com/transaction/) EXPLORER_TX: URL for exploring transactions (default https://shakeshift.com/transaction/)
HSD_NETWORK: Network to connect to (main, regtest, simnet) HSD_NETWORK: Network to connect to (main, regtest, simnet)
DISABLE_WALLETDNS: Disable Wallet DNS records when sending HNS to domains (true/false)
INTERNAL_HSD: Use internal HSD node (true/false)
``` ```
# Internal HSD
If you set INTERNAL_HSD=true in the .env file the wallet will start and manage its own HSD node. If you want to override the default HSD config create a file called hsdconfig.json in the same directory as main.py and change the values you want to override. For example to disable SPV and use an existing bob wallet sync (on linux) and set the agent to "SuperCoolDev" you could use the following:
```json
{
"spv": false,
"prefix":"~/.config/Bob/hsd_data",
"flags":[
"--agent=SuperCoolDev"
]
}
```
Supported config options are:
```yaml
spv: true/false
prefix: path to hsd data directory
flags: list of additional flags to pass to hsd
version: version of hsd to use (used when installing HSD from source)
chainMigrate: <int> (for users migrating from older versions of HSD)
walletMigrate: <int> (for users migrating from older versions of HSD)
```
## Support the Project
If you find FireWallet useful and would like to support its continued development, please consider making a donation. Your contributions help maintain the project and develop new features.
HNS donations can be sent to: `hs1qh7uzytf2ftwkd9dmjjs7az9qfver5m7dd7x4ej`
Other donation options can be found at [my website](https://nathan.woodburn.au/donate)
Thank you for your support!
## Warnings ## Warnings
- This is a work in progress and is not guaranteed to work - This is a work in progress and is not guaranteed to work

File diff suppressed because it is too large Load Diff

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:

View File

@@ -2,4 +2,6 @@ HSD_API=123480615465636893475aCwyaae6s45
HSD_IP=localhost HSD_IP=localhost
THEME=black THEME=black
SHOW_EXPIRED=false SHOW_EXPIRED=false
EXPLORER_TX=https://shakeshift.com/transaction/ EXPLORER_TX=https://shakeshift.com/transaction/
DISABLE_WALLETDNS=false
INTERNAL_HSD=false

View File

@@ -1,45 +0,0 @@
What have you built previously?
- [HNSHosting](https://hnshosting.au)
- [ShakeCities](https://shakecities.com)
- [FireWallet](https://firewallet.au)
- [Git Profile](https://github.com/nathanwoodburn)
Project summary
A Handshake wallet web ui. This will be a HSD wallet web ui that will allow users to manage their Handshake domains via a web interface. This will allow users to easily manage their domains without having to use the command line or bob. One benefit of this is that it will allow users to easily manage their domains from their mobile devices that don't have access to any HNS wallet. This could be done in a secure way by only allowing connections on local network devices (in addition to requiring the wallet credentials).
Features:
- Login with HSD wallet name + password (by default don't show a list of wallets to login to as this could be a security risk)
- View account information in a dashboard
- Available balance
- Total balance
- Pending Transactions
- List of domains
- List of transactions
- Manage domains
- Transfer domains
- Finalize domains
- Edit domains
- Revoke domains (with a warning and requiring the account password)
- Manage wallet
- Send HNS
- Receive HNS
- Auctions
- View bids on domain
- Open auction
- Bid on auction
- Reveal bid
- Redeem bid
- Register domain
Completion requirements:
- Basic functionality including
- View info
- Send/Receive HNS
- Manage domains
After the initial version is completed I will be looking to add more features including the above mentioned features.
The initial version will be completed in 2-3 weeks with a fully fledged version released later as the features are developed and tested.
You can contact me at handshake @ nathan.woodburn.au

373
main.py
View File

@@ -32,12 +32,36 @@ 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")
@@ -54,6 +78,8 @@ def index():
return render_template("index.html", account=account, plugins=plugins) return render_template("index.html", account=account, plugins=plugins)
info = gitinfo.get_git_info() info = gitinfo.get_git_info()
if info is None:
return render_template("index.html", account=account, plugins=plugins)
branch = info['refs'] branch = info['refs']
commit = info['commit'] commit = info['commit']
if commit != latestVersion(branch): if commit != latestVersion(branch):
@@ -85,7 +111,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 +132,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 +169,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 +201,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 +209,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 +361,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 +384,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 +406,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 +434,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 +448,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 +466,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 +483,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 +496,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 +523,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 +541,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 +551,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 +581,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 +686,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 +700,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 +726,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 +746,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 +783,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 +816,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 +826,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 +849,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 +866,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 +885,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 +929,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 +951,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 +1011,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 +1057,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 +1077,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 +1096,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 +1109,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 +1127,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
@@ -1108,12 +1148,21 @@ def settings():
if success == None: if success == None:
success = "" success = ""
if not os.path.exists(".git"): if not os.path.exists(".git"):
return render_template("settings.html", account=account, return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version="Error") error=error,success=success,version="Error",
internal=account_module.HSD_INTERNAL_NODE,
spv=account_module.isSPV())
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",
internal=account_module.HSD_INTERNAL_NODE,
spv=account_module.isSPV())
branch = info['refs'] branch = info['refs']
if branch != "main": if branch != "main":
branch = f"({branch})" branch = f"({branch})"
@@ -1127,7 +1176,8 @@ def settings():
version += ' (New version available)' version += ' (New version available)'
return render_template("settings.html", account=account, return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version=version) error=error,success=success,version=version,internal=account_module.HSD_INTERNAL_NODE,
spv=account_module.isSPV())
@app.route('/settings/<action>') @app.route('/settings/<action>')
def settings_action(action): def settings_action(action):
@@ -1144,29 +1194,36 @@ def settings_action(action):
if 'error' in resp: if 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Rescan started") return redirect("/settings?success=Rescan started")
elif action == "resend":
if action == "resend":
resp = account_module.resendTXs() resp = account_module.resendTXs()
if 'error' in resp: if 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Resent transactions") return redirect("/settings?success=Resent transactions")
elif action == "zap": if 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":
if 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}")
if action == "restart":
resp = account_module.hsdRestart()
return render_template("message.html", account=account,
title="Restarting",
content="The node is restarting. This may take a minute or two. You can close this window.")
return redirect("/settings?error=Invalid action") return redirect("/settings?error=Invalid action")
@@ -1176,6 +1233,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 +1245,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")
@@ -1211,6 +1272,9 @@ def login():
wallets = account_module.listWallets() wallets = account_module.listWallets()
wallets = render.wallets(wallets) wallets = render.wallets(wallets)
# If there are no wallets redirect to either register or import
if len(wallets) == 0:
return redirect("/welcome")
if 'message' in request.args: if 'message' in request.args:
return render_template("login.html", return render_template("login.html",
@@ -1226,6 +1290,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 +1311,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 +1329,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 +1363,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 +1376,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 +1583,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 +1677,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 +1714,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 +1725,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 +1765,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 +1801,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 +1863,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():

131
render.py
View File

@@ -6,32 +6,7 @@ from domainLookup import punycode_to_emoji
import os import os
from handywrapper import api from handywrapper import api
import threading import threading
import requests
HSD_API = os.getenv("HSD_API")
HSD_IP = os.getenv("HSD_IP")
if HSD_IP is None:
HSD_IP = "localhost"
HSD_NETWORK = os.getenv("HSD_NETWORK")
HSD_WALLET_PORT = 12039
HSD_NODE_PORT = 12037
if not HSD_NETWORK:
HSD_NETWORK = "main"
else:
HSD_NETWORK = HSD_NETWORK.lower()
if HSD_NETWORK == "simnet":
HSD_WALLET_PORT = 15039
HSD_NODE_PORT = 15037
elif HSD_NETWORK == "testnet":
HSD_WALLET_PORT = 13039
HSD_NODE_PORT = 13037
elif HSD_NETWORK == "regtest":
HSD_WALLET_PORT = 14039
HSD_NODE_PORT = 14037
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
# Get Explorer URL # Get Explorer URL
TX_EXPLORER_URL = os.getenv("EXPLORER_TX") TX_EXPLORER_URL = os.getenv("EXPLORER_TX")
@@ -40,6 +15,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 +71,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 +83,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 +211,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 +241,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 +273,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 +535,15 @@ def renderDomainAsync(namehash: str) -> None:
if namehash in cache: if namehash in cache:
return return
# Fetch the name outside the lock (network call) using hsd.hns.au
# name = account.hsd.rpc_getNameByHash(namehash)
name = requests.get(f"https://hsd.hns.au/api/v1/namehash/{namehash}").json()
# Fetch the name outside the lock (network call)
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 +552,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
@@ -32,7 +32,7 @@ def gunicornServer():
gunicorn_app.run() gunicorn_app.run()
if __name__ == '__main__': if __name__ == '__main__':
# Check if --gunicorn is in the command line arguments # Check if --gunicorn is in the command line arguments
if "--gunicorn" in sys.argv: if "--gunicorn" in sys.argv:
gunicornServer() gunicornServer()

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

@@ -68,25 +68,31 @@
<h3 class="mb-1" style="text-align: center;color: rgb(0,255,0);">{{success}}</h3> <h3 class="mb-1" style="text-align: center;color: rgb(0,255,0);">{{success}}</h3>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Node Settings</h4><small>HSD Version: v{{hsd_version}}</small> <h4 class="card-title">Node Settings</h4><small>HSD Version: v{{hsd_version}}&nbsp; Type: {% if internal %} Internal {% else %} Remote {% endif %} ({% if spv %}SPV{% else %}Full Node{% endif %})</small>
<h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6> <h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6><ul class="list-group">
<ul class="list-group"> <li class="list-group-item">
<li class="list-group-item"> <div><a class="btn btn-primary stick-right" role="button" href="/settings/rescan">Rescan</a>
<div><a class="btn btn-primary stick-right" role="button" href="/settings/rescan">Rescan</a> <h3>Rescan</h3><span>Rescan the blockchain for transactions</span>
<h3>Rescan</h3><span>Rescan the blockchain for transactions</span> </div>
</div> </li>
</li> <li class="list-group-item">
<li class="list-group-item"> <div><a class="btn btn-primary stick-right" role="button" href="/settings/resend">Resend</a>
<div><a class="btn btn-primary stick-right" role="button" href="/settings/resend">Resend</a> <h3>Resend unconfirmed transactions</h3><span>Resend any transactions that haven&#39;t been mined yet.</span>
<h3>Resend&nbsp;unconfirmed transactions</h3><span>Resend any transactions that haven't been mined yet.</span> </div>
</div> </li>
</li> <li class="list-group-item">
<li class="list-group-item"> <div><a class="btn btn-primary stick-right" role="button" href="/settings/zap">Zap</a>
<div><a class="btn btn-primary stick-right" role="button" href="/settings/zap">Zap</a> <h3>Delete unconfirmed transactions</h3><span>This will only remove pending tx from the wallet older than 20 minutes (~ 2 blocks)</span>
<h3>Delete unconfirmed transactions</h3><span>This will only remove pending tx from the wallet older than 20 minutes (~ 2 blocks)</span> </div>
</div> </li>
</li> {% if internal %}
</ul> <li class="list-group-item">
<div><a class="btn btn-primary stick-right" role="button" href="/settings/restart">Restart Node</a>
<h3>Restart Internal Node</h3><span>This will attempt to restart the HSD node</span>
</div>
</li>
{% endif %}
</ul>
</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>

47
templates/welcome.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html data-bs-theme="dark" lang="en-au" style="height: 100%;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Welcome to FireWallet</title>
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i&amp;display=swap">
<link rel="stylesheet" href="/assets/css/styles.min.css">
</head>
<body class="d-flex align-items-center bg-gradient-primary" style="height: 100%;">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-9 col-lg-12 col-xl-10">
<h1 class="text-center" style="color: var(--bs-danger);background: var(--bs-primary);">{{error}}</h1>
<div class="card shadow-lg my-5 o-hidden border-0" style="padding-top: 50px;padding-bottom: 50px;">
<div class="card-body p-0">
<div class="row">
<div class="col-lg-6 d-none d-lg-flex">
<div class="flex-grow-1 bg-login-image" style="background: url(&quot;/assets/img/favicon.png&quot;) center / contain no-repeat;"></div>
</div>
<div class="col-lg-6">
<div class="text-center p-5">
<div class="text-center">
<h4 class="mb-4">Welcome to FireWallet!</h4>
</div>
<div class="btn-group-vertical btn-group-lg gap-1" role="group"><a class="btn btn-primary" role="button" href="/register">Create a new wallet</a><a class="btn btn-primary" role="button" href="/import-wallet">Import an existing wallet</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
</body>
</html>