Compare commits
18 Commits
feat/mempo
...
feat/inter
| Author | SHA1 | Date | |
|---|---|---|---|
|
b76b873036
|
|||
|
23e714fad8
|
|||
|
a36c69ecfc
|
|||
|
1fd9987bf1
|
|||
|
f2cda461ba
|
|||
|
26c5b4a4fa
|
|||
|
7fdc4a3122
|
|||
|
5ff8960b7b
|
|||
|
4c84bc2bbe
|
|||
|
49e378803d
|
|||
|
1c53547047
|
|||
|
080c4402d8
|
|||
|
792688064e
|
|||
|
599c0df00c
|
|||
|
a619d78efd
|
|||
|
f090b7b71a
|
|||
|
545a0b9475
|
|||
|
501091eeae
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -16,3 +16,7 @@ customPlugins/
|
||||
cache/
|
||||
build/
|
||||
dist/
|
||||
hsd/
|
||||
hsd-data/
|
||||
hsd.lock
|
||||
hsdconfig.json
|
||||
|
||||
Binary file not shown.
35
README.md
35
README.md
@@ -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)
|
||||
EXPLORER_TX: URL for exploring transactions (default https://shakeshift.com/transaction/)
|
||||
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
|
||||
|
||||
- This is a work in progress and is not guaranteed to work
|
||||
|
||||
634
account.py
634
account.py
@@ -7,22 +7,25 @@ import re
|
||||
import domainLookup
|
||||
import json
|
||||
import time
|
||||
import subprocess
|
||||
import atexit
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import sqlite3
|
||||
from functools import wraps
|
||||
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
HSD_API = os.getenv("HSD_API")
|
||||
HSD_IP = os.getenv("HSD_IP")
|
||||
if HSD_IP is None:
|
||||
HSD_IP = "localhost"
|
||||
HSD_API = os.getenv("HSD_API","")
|
||||
HSD_IP = os.getenv("HSD_IP","localhost")
|
||||
|
||||
HSD_NETWORK = os.getenv("HSD_NETWORK")
|
||||
HSD_NETWORK = os.getenv("HSD_NETWORK", "main")
|
||||
HSD_WALLET_PORT = 12039
|
||||
HSD_NODE_PORT = 12037
|
||||
|
||||
if not HSD_NETWORK:
|
||||
HSD_NETWORK = "main"
|
||||
else:
|
||||
HSD_NETWORK = HSD_NETWORK.lower()
|
||||
HSD_NETWORK = HSD_NETWORK.lower()
|
||||
|
||||
if HSD_NETWORK == "simnet":
|
||||
HSD_WALLET_PORT = 15039
|
||||
@@ -34,22 +37,52 @@ elif HSD_NETWORK == "regtest":
|
||||
HSD_WALLET_PORT = 14039
|
||||
HSD_NODE_PORT = 14037
|
||||
|
||||
HSD_INTERNAL_NODE = os.getenv("INTERNAL_HSD","false").lower() in ["1","true","yes"]
|
||||
if HSD_INTERNAL_NODE:
|
||||
if HSD_API == "":
|
||||
# Use a random API KEY
|
||||
HSD_API = "firewallet-" + str(int(time.time()))
|
||||
HSD_IP = "localhost"
|
||||
|
||||
SHOW_EXPIRED = os.getenv("SHOW_EXPIRED")
|
||||
if SHOW_EXPIRED is None:
|
||||
SHOW_EXPIRED = False
|
||||
|
||||
HSD_PROCESS = None
|
||||
SPV_MODE = None
|
||||
|
||||
# Get hsdconfig.json
|
||||
HSD_CONFIG = {
|
||||
"version": "v8.0.0",
|
||||
"chainMigrate": 4,
|
||||
"walletMigrate": 7,
|
||||
"minNodeVersion": 20,
|
||||
"minNpmVersion": 8,
|
||||
"spv": False,
|
||||
"flags": [
|
||||
"--agent=FireWallet"
|
||||
]
|
||||
}
|
||||
|
||||
TX_CACHE_TTL = 3600
|
||||
DOMAIN_CACHE_TTL = int(os.getenv("CACHE_TTL",90))
|
||||
|
||||
if not os.path.exists('hsdconfig.json'):
|
||||
with open('hsdconfig.json', 'w') as f:
|
||||
f.write(json.dumps(HSD_CONFIG, indent=4))
|
||||
else:
|
||||
with open('hsdconfig.json') as f:
|
||||
hsdConfigTMP = json.load(f)
|
||||
for key in hsdConfigTMP:
|
||||
HSD_CONFIG[key] = hsdConfigTMP[key]
|
||||
|
||||
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
|
||||
hsw = api.hsw(HSD_API, HSD_IP, HSD_WALLET_PORT)
|
||||
|
||||
cacheTime = 3600
|
||||
|
||||
# Verify the connection
|
||||
response = hsd.getInfo()
|
||||
|
||||
EXCLUDE = ["primary"]
|
||||
if os.getenv("EXCLUDE") is not None:
|
||||
EXCLUDE = os.getenv("EXCLUDE").split(",")
|
||||
EXCLUDE = os.getenv("EXCLUDE","primary").split(",")
|
||||
|
||||
|
||||
def hsdConnected():
|
||||
@@ -62,13 +95,20 @@ def hsdVersion(format=True):
|
||||
info = hsd.getInfo()
|
||||
if 'error' in info:
|
||||
return -1
|
||||
|
||||
# Check if SPV mode is enabled
|
||||
global SPV_MODE
|
||||
if info.get('chain',{}).get('options',{}).get('spv',False):
|
||||
SPV_MODE = True
|
||||
else:
|
||||
SPV_MODE = False
|
||||
if format:
|
||||
return float('.'.join(info['version'].split(".")[:2]))
|
||||
else:
|
||||
return info['version']
|
||||
|
||||
|
||||
def check_account(cookie: str):
|
||||
def check_account(cookie: str | None):
|
||||
if cookie is None:
|
||||
return False
|
||||
|
||||
@@ -84,7 +124,12 @@ def check_account(cookie: str):
|
||||
return account
|
||||
|
||||
|
||||
def check_password(cookie: str, password: str):
|
||||
def check_password(cookie: str|None, password: str|None):
|
||||
if cookie is None:
|
||||
return False
|
||||
if password is None:
|
||||
password = ""
|
||||
|
||||
account = check_account(cookie)
|
||||
if account == False:
|
||||
return False
|
||||
@@ -184,6 +229,124 @@ def selectWallet(account: str):
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def init_domain_db():
|
||||
"""Initialize the SQLite database for domain cache."""
|
||||
os.makedirs('cache', exist_ok=True)
|
||||
db_path = os.path.join('cache', 'domains.db')
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create the domains table if it doesn't exist
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS domains (
|
||||
name TEXT PRIMARY KEY,
|
||||
info TEXT,
|
||||
last_updated INTEGER
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def getCachedDomains():
|
||||
"""Get cached domain information from SQLite database."""
|
||||
init_domain_db() # Ensure DB exists
|
||||
|
||||
db_path = os.path.join('cache', 'domains.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row # This allows accessing columns by name
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all domains from the database
|
||||
cursor.execute('SELECT name, info, last_updated FROM domains')
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Convert to dictionary format
|
||||
domain_cache = {}
|
||||
for row in rows:
|
||||
try:
|
||||
domain_cache[row['name']] = json.loads(row['info'])
|
||||
domain_cache[row['name']]['last_updated'] = row['last_updated']
|
||||
except json.JSONDecodeError:
|
||||
print(f"Error parsing cached data for domain {row['name']}")
|
||||
|
||||
conn.close()
|
||||
return domain_cache
|
||||
|
||||
|
||||
ACTIVE_DOMAIN_UPDATES = set() # Track domains being updated
|
||||
DOMAIN_UPDATE_LOCK = threading.Lock() # For thread-safe access to ACTIVE_DOMAIN_UPDATES
|
||||
|
||||
def update_domain_cache(domain_names: list):
|
||||
"""Fetch domain info and update the SQLite cache."""
|
||||
if not domain_names:
|
||||
return
|
||||
|
||||
# Filter out domains that are already being updated
|
||||
domains_to_update = []
|
||||
with DOMAIN_UPDATE_LOCK:
|
||||
for domain in domain_names:
|
||||
if domain not in ACTIVE_DOMAIN_UPDATES:
|
||||
ACTIVE_DOMAIN_UPDATES.add(domain)
|
||||
domains_to_update.append(domain)
|
||||
|
||||
if not domains_to_update:
|
||||
# All requested domains are already being updated
|
||||
return
|
||||
|
||||
try:
|
||||
# Initialize database
|
||||
init_domain_db()
|
||||
|
||||
db_path = os.path.join('cache', 'domains.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
for domain_name in domains_to_update:
|
||||
try:
|
||||
# Get domain info from node
|
||||
domain_info = getDomain(domain_name)
|
||||
|
||||
if 'error' in domain_info or not domain_info.get('info'):
|
||||
print(f"Failed to get info for domain {domain_name}: {domain_info.get('error', 'Unknown error')}", flush=True)
|
||||
continue
|
||||
|
||||
# Update or insert into database
|
||||
now = int(time.time())
|
||||
serialized_info = json.dumps(domain_info)
|
||||
|
||||
cursor.execute(
|
||||
'INSERT OR REPLACE INTO domains (name, info, last_updated) VALUES (?, ?, ?)',
|
||||
(domain_name, serialized_info, now)
|
||||
)
|
||||
|
||||
print(f"Updated cache for domain {domain_name}")
|
||||
except Exception as e:
|
||||
print(f"Error updating cache for domain {domain_name}: {str(e)}")
|
||||
finally:
|
||||
# Always remove from active set, even if there was an error
|
||||
with DOMAIN_UPDATE_LOCK:
|
||||
if domain_name in ACTIVE_DOMAIN_UPDATES:
|
||||
ACTIVE_DOMAIN_UPDATES.remove(domain_name)
|
||||
|
||||
# Commit all changes at once
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating domain cache: {str(e)}", flush=True)
|
||||
# Make sure to clean up the active set on any exception
|
||||
with DOMAIN_UPDATE_LOCK:
|
||||
for domain in domains_to_update:
|
||||
if domain in ACTIVE_DOMAIN_UPDATES:
|
||||
ACTIVE_DOMAIN_UPDATES.remove(domain)
|
||||
|
||||
print("Updated cache for domains")
|
||||
|
||||
|
||||
def getBalance(account: str):
|
||||
# Get the total balance
|
||||
info = hsw.getBalance('default', account)
|
||||
@@ -200,9 +363,66 @@ def getBalance(account: str):
|
||||
|
||||
domains = getDomains(account)
|
||||
domainValue = 0
|
||||
for domain in domains:
|
||||
if domain['state'] == "CLOSED":
|
||||
domainValue += domain['value']
|
||||
domains_to_update = [] # Track domains that need cache updates
|
||||
|
||||
if isSPV():
|
||||
# Initialize database if needed
|
||||
init_domain_db()
|
||||
|
||||
# Connect to the database directly for efficient querying
|
||||
db_path = os.path.join('cache', 'domains.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
now = int(time.time())
|
||||
cache_cutoff = now - (DOMAIN_CACHE_TTL * 86400) # Cache TTL in days
|
||||
|
||||
for domain in domains:
|
||||
domain_name = domain['name']
|
||||
|
||||
# Check if domain is in cache and still fresh
|
||||
cursor.execute(
|
||||
'SELECT info, last_updated FROM domains WHERE name = ?',
|
||||
(domain_name,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
# Only add domain for update if:
|
||||
# 1. Not in cache or stale
|
||||
# 2. Not currently being updated by another thread
|
||||
with DOMAIN_UPDATE_LOCK:
|
||||
if (not row or row['last_updated'] < cache_cutoff) and domain_name not in ACTIVE_DOMAIN_UPDATES:
|
||||
domains_to_update.append(domain_name)
|
||||
continue
|
||||
|
||||
# Use the cached info
|
||||
try:
|
||||
if row: # Make sure we have data
|
||||
domain_info = json.loads(row['info'])
|
||||
if domain_info.get('info', {}).get('state', "") == "CLOSED":
|
||||
domainValue += domain_info.get('info', {}).get('value', 0)
|
||||
except json.JSONDecodeError:
|
||||
# Only add for update if not already being updated
|
||||
with DOMAIN_UPDATE_LOCK:
|
||||
if domain_name not in ACTIVE_DOMAIN_UPDATES:
|
||||
domains_to_update.append(domain_name)
|
||||
|
||||
conn.close()
|
||||
else:
|
||||
for domain in domains:
|
||||
if domain['state'] == "CLOSED":
|
||||
domainValue += domain['value']
|
||||
|
||||
# Start background thread to update cache for missing domains
|
||||
if domains_to_update:
|
||||
thread = threading.Thread(
|
||||
target=update_domain_cache,
|
||||
args=(domains_to_update,),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
total = total - (domainValue/1000000)
|
||||
locked = locked - (domainValue/1000000)
|
||||
|
||||
@@ -267,40 +487,74 @@ def getDomains(account, own=True):
|
||||
|
||||
return domains
|
||||
|
||||
def init_tx_page_db():
|
||||
"""Initialize the SQLite database for transaction page cache."""
|
||||
os.makedirs('cache', exist_ok=True)
|
||||
db_path = os.path.join('cache', 'tx_pages.db')
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create the tx_pages table if it doesn't exist
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS tx_pages (
|
||||
account TEXT,
|
||||
page_key TEXT,
|
||||
txid TEXT,
|
||||
timestamp INTEGER,
|
||||
PRIMARY KEY (account, page_key)
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def getPageTXCache(account, page, size=100):
|
||||
page = f"{page}-{size}"
|
||||
if not os.path.exists(f'cache'):
|
||||
os.mkdir(f'cache')
|
||||
"""Get cached transaction ID from SQLite database."""
|
||||
account = getxPub(account)
|
||||
page_key = f"{page}-{size}"
|
||||
|
||||
if not os.path.exists(f'cache/{account}_page.json'):
|
||||
with open(f'cache/{account}_page.json', 'w') as f:
|
||||
f.write('{}')
|
||||
with open(f'cache/{account}_page.json') as f:
|
||||
pageCache = json.load(f)
|
||||
# Initialize database if needed
|
||||
init_tx_page_db()
|
||||
|
||||
if page in pageCache and pageCache[page]['time'] > int(time.time()) - cacheTime:
|
||||
return pageCache[page]['txid']
|
||||
db_path = os.path.join('cache', 'tx_pages.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Query for the cached transaction ID
|
||||
cursor.execute(
|
||||
'SELECT txid, timestamp FROM tx_pages WHERE account = ? AND page_key = ?',
|
||||
(account, page_key)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row and row[1] > int(time.time()) - TX_CACHE_TTL:
|
||||
return row[0] # Return the cached txid
|
||||
return None
|
||||
|
||||
|
||||
def pushPageTXCache(account, page, txid, size=100):
|
||||
page = f"{page}-{size}"
|
||||
if not os.path.exists(f'cache/{account}_page.json'):
|
||||
with open(f'cache/{account}_page.json', 'w') as f:
|
||||
f.write('{}')
|
||||
with open(f'cache/{account}_page.json') as f:
|
||||
pageCache = json.load(f)
|
||||
"""Store transaction ID in SQLite database."""
|
||||
account = getxPub(account)
|
||||
page_key = f"{page}-{size}"
|
||||
|
||||
pageCache[page] = {
|
||||
'time': int(time.time()),
|
||||
'txid': txid
|
||||
}
|
||||
with open(f'cache/{account}_page.json', 'w') as f:
|
||||
json.dump(pageCache, f, indent=4)
|
||||
# Initialize database if needed
|
||||
init_tx_page_db()
|
||||
|
||||
return pageCache[page]['txid']
|
||||
db_path = os.path.join('cache', 'tx_pages.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Insert or replace the transaction ID
|
||||
cursor.execute(
|
||||
'INSERT OR REPLACE INTO tx_pages (account, page_key, txid, timestamp) VALUES (?, ?, ?, ?)',
|
||||
(account, page_key, txid, int(time.time()))
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return txid
|
||||
|
||||
def getTXFromPage(account, page, size=100):
|
||||
if page == 1:
|
||||
@@ -403,17 +657,34 @@ def check_hip2(domain: str):
|
||||
return 'Invalid domain'
|
||||
|
||||
address = domainLookup.hip2(domain)
|
||||
if address.startswith("Hip2: "):
|
||||
if not address.startswith("Hip2: "):
|
||||
if not check_address(address, False, True):
|
||||
return 'Hip2: Lookup succeeded but address is invalid'
|
||||
return address
|
||||
|
||||
# Check if DISABLE_WALLETDNS is set
|
||||
if os.getenv("DISABLE_WALLETDNS","").lower() in ["1","true","yes"]:
|
||||
return "No HIP2 record found for this domain"
|
||||
# Try using WALLET TXT record
|
||||
address = domainLookup.wallet_txt(domain)
|
||||
if not address.startswith("hs1"):
|
||||
return "No HIP2 or WALLET record found for this domain"
|
||||
if not check_address(address, False, True):
|
||||
return 'Hip2: Lookup succeeded but address is invalid'
|
||||
return 'WALLET DNS record found but address is invalid'
|
||||
return address
|
||||
|
||||
|
||||
|
||||
|
||||
def send(account, address, amount):
|
||||
account_name = check_account(account)
|
||||
password = ":".join(account.split(":")[1:])
|
||||
if not account_name:
|
||||
return {
|
||||
"error": {
|
||||
"message": "Invalid account"
|
||||
}
|
||||
}
|
||||
response = hsw.rpc_selectWallet(account_name)
|
||||
if response['error'] is not None:
|
||||
return {
|
||||
@@ -447,7 +718,12 @@ def send(account, address, amount):
|
||||
def isOwnDomain(account, name: str):
|
||||
# Get domain
|
||||
domain_info = getDomain(name)
|
||||
owner = getAddressFromCoin(domain_info['info']['owner']['hash'],domain_info['info']['owner']['index'])
|
||||
if 'info' not in domain_info or domain_info['info'] is None:
|
||||
return False
|
||||
if 'owner' not in domain_info['info']:
|
||||
return False
|
||||
|
||||
owner = getAddressFromCoin(domain_info['info']['owner'].get("hash"),domain_info['info']['owner'].get("index"))
|
||||
# Select the account
|
||||
hsw.rpc_selectWallet(account)
|
||||
account = hsw.rpc_getAccount(owner)
|
||||
@@ -479,6 +755,16 @@ def isOwnPrevout(account, prevout: dict):
|
||||
|
||||
|
||||
def getDomain(domain: str):
|
||||
if isSPV():
|
||||
response = requests.get(f"https://hsd.hns.au/api/v1/name/{domain}").json()
|
||||
if 'error' in response:
|
||||
return {
|
||||
"error": {
|
||||
"message": response['error']
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
# Get the domain
|
||||
response = hsd.rpc_getNameInfo(domain)
|
||||
if response['error'] is not None:
|
||||
@@ -489,11 +775,21 @@ def getDomain(domain: str):
|
||||
}
|
||||
return response['result']
|
||||
|
||||
def isKnownDomain(domain: str) -> bool:
|
||||
# Get the domain
|
||||
response = hsd.rpc_getNameInfo(domain)
|
||||
if response['error'] is not None:
|
||||
return False
|
||||
|
||||
if response['result'] is None or response['result'].get('info') is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
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}")
|
||||
print(f"Error getting address from coin")
|
||||
return "No Owner"
|
||||
data = response.json()
|
||||
if 'address' not in data:
|
||||
@@ -519,6 +815,17 @@ def renewDomain(account, domain):
|
||||
|
||||
def getDNS(domain: str):
|
||||
# Get the DNS
|
||||
|
||||
if isSPV():
|
||||
response = requests.get(f"https://hsd.hns.au/api/v1/nameresource/{domain}")
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"error": f"Error fetching DNS records: {response.status_code}"
|
||||
}
|
||||
response = response.json()
|
||||
return response.get('records', [])
|
||||
|
||||
|
||||
response = hsd.rpc_getNameResource(domain)
|
||||
if response['error'] is not None:
|
||||
return {
|
||||
@@ -706,7 +1013,9 @@ def getPendingRegisters(account):
|
||||
for bid in bids:
|
||||
if bid['name'] == domain['name']:
|
||||
if bid['value'] == domain['highest']:
|
||||
pending.append(bid)
|
||||
# Double check the domain is actually in the node
|
||||
if isKnownDomain(domain['name']):
|
||||
pending.append(bid)
|
||||
return pending
|
||||
|
||||
|
||||
@@ -718,9 +1027,13 @@ def getPendingFinalizes(account, password):
|
||||
pending = []
|
||||
try:
|
||||
for output in tx['outputs']:
|
||||
if output['covenant']['type'] != 10:
|
||||
if type(output) != dict:
|
||||
continue
|
||||
if output['covenant']['action'] != "FINALIZE":
|
||||
if not 'covenant' in output:
|
||||
continue
|
||||
if output['covenant'].get("type") != 10:
|
||||
continue
|
||||
if output['covenant'].get('action') != "FINALIZE":
|
||||
continue
|
||||
nameHash = output['covenant']['items'][0]
|
||||
# Try to get the name from hash
|
||||
@@ -1299,7 +1612,9 @@ def zapTXs(account):
|
||||
|
||||
|
||||
def getxPub(account):
|
||||
account_name = check_account(account)
|
||||
account_name = account
|
||||
if account.count(":") > 0:
|
||||
account_name = check_account(account)
|
||||
|
||||
if account_name == False:
|
||||
return {
|
||||
@@ -1317,8 +1632,6 @@ def getxPub(account):
|
||||
}
|
||||
}
|
||||
return response['accountKey']
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": {
|
||||
@@ -1414,12 +1727,21 @@ def generateReport(account, format="{name},{expiry},{value},{maxBid}"):
|
||||
|
||||
def convertHNS(value: int):
|
||||
return value/1000000
|
||||
return value/1000000
|
||||
|
||||
SPV_EXTERNAL_ROUTES = [
|
||||
"name",
|
||||
"coin",
|
||||
"tx",
|
||||
"block"
|
||||
]
|
||||
|
||||
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 isSPV() and any(path.startswith(route) for route in SPV_EXTERNAL_ROUTES):
|
||||
# If in SPV mode and the path is one of the external routes, use the external API
|
||||
base_url = f"https://hsd.hns.au/api/v1"
|
||||
|
||||
if path:
|
||||
# Ensure path starts with a slash if it's not empty
|
||||
if not path.startswith('/'):
|
||||
@@ -1436,3 +1758,207 @@ def get_wallet_api_url(path=''):
|
||||
path = f'/{path}'
|
||||
return f"{base_url}{path}"
|
||||
return base_url
|
||||
|
||||
def isSPV() -> bool:
|
||||
global SPV_MODE
|
||||
if SPV_MODE is None:
|
||||
info = hsd.getInfo()
|
||||
if 'error' in info:
|
||||
return False
|
||||
|
||||
# Check if SPV mode is enabled
|
||||
if info.get('chain',{}).get('options',{}).get('spv',False):
|
||||
SPV_MODE = True
|
||||
else:
|
||||
SPV_MODE = False
|
||||
return SPV_MODE
|
||||
|
||||
# region HSD Internal Node
|
||||
|
||||
|
||||
|
||||
def checkPreRequisites() -> dict[str, bool]:
|
||||
prerequisites = {
|
||||
"node": False,
|
||||
"npm": False,
|
||||
"git": False,
|
||||
"hsd": False
|
||||
}
|
||||
|
||||
# Check if node is installed and get version
|
||||
nodeSubprocess = subprocess.run(["node", "-v"], capture_output=True, text=True)
|
||||
if nodeSubprocess.returncode == 0:
|
||||
major_version = int(nodeSubprocess.stdout.strip().lstrip('v').split('.')[0])
|
||||
if major_version >= HSD_CONFIG.get("minNodeVersion", 20):
|
||||
prerequisites["node"] = True
|
||||
|
||||
# Check if npm is installed
|
||||
npmSubprocess = subprocess.run(["npm", "-v"], capture_output=True, text=True)
|
||||
if npmSubprocess.returncode == 0:
|
||||
major_version = int(npmSubprocess.stdout.strip().split('.')[0])
|
||||
if major_version >= HSD_CONFIG.get("minNPMVersion", 8):
|
||||
prerequisites["npm"] = True
|
||||
|
||||
# Check if git is installed
|
||||
gitSubprocess = subprocess.run(["git", "-v"], capture_output=True, text=True)
|
||||
if gitSubprocess.returncode == 0:
|
||||
prerequisites["git"] = True
|
||||
|
||||
# Check if hsd is installed
|
||||
if os.path.exists("./hsd/bin/hsd"):
|
||||
prerequisites["hsd"] = True
|
||||
|
||||
|
||||
|
||||
|
||||
return prerequisites
|
||||
|
||||
|
||||
|
||||
def hsdInit():
|
||||
if not HSD_INTERNAL_NODE:
|
||||
return
|
||||
prerequisites = checkPreRequisites()
|
||||
|
||||
PREREQ_MESSAGES = {
|
||||
"node": "Install Node.js from https://nodejs.org/en/download (Version >= {minNodeVersion})",
|
||||
"npm": "Install npm (version >= {minNPMVersion}) - usually comes with Node.js",
|
||||
"git": "Install Git from https://git-scm.com/downloads"}
|
||||
|
||||
|
||||
# Check if all prerequisites are met (except hsd)
|
||||
if not all(prerequisites[key] for key in prerequisites if key != "hsd"):
|
||||
print("HSD Internal Node prerequisites not met:")
|
||||
for key, value in prerequisites.items():
|
||||
if not value:
|
||||
print(f" - {key} is missing or does not meet the version requirement.")
|
||||
exit(1)
|
||||
return
|
||||
|
||||
# Check if hsd is installed
|
||||
if not prerequisites["hsd"]:
|
||||
print("HSD not found, installing...")
|
||||
# If hsd folder exists, remove it
|
||||
if os.path.exists("hsd"):
|
||||
os.rmdir("hsd")
|
||||
|
||||
# Clone hsd repo
|
||||
gitClone = subprocess.run(["git", "clone", "--depth", "1", "--branch", HSD_CONFIG.get("version", "latest"), "https://github.com/handshake-org/hsd.git", "hsd"], capture_output=True, text=True)
|
||||
if gitClone.returncode != 0:
|
||||
print("Failed to clone hsd repository:")
|
||||
print(gitClone.stderr)
|
||||
exit(1)
|
||||
print("Cloned hsd repository.")
|
||||
# Install hsd dependencies
|
||||
print("Installing hsd dependencies...")
|
||||
npmInstall = subprocess.run(["npm", "install"], cwd="hsd", capture_output=True, text=True)
|
||||
if npmInstall.returncode != 0:
|
||||
print("Failed to install hsd dependencies:")
|
||||
print(npmInstall.stderr)
|
||||
exit(1)
|
||||
print("Installed hsd dependencies.")
|
||||
|
||||
def hsdStart():
|
||||
global HSD_PROCESS
|
||||
global SPV_MODE
|
||||
if not HSD_INTERNAL_NODE:
|
||||
return
|
||||
|
||||
# Check if hsd was started in the last 30 seconds
|
||||
if os.path.exists("hsd.lock"):
|
||||
lock_time = os.path.getmtime("hsd.lock")
|
||||
if time.time() - lock_time < 30:
|
||||
print("HSD was started recently, skipping start.")
|
||||
return
|
||||
else:
|
||||
os.remove("hsd.lock")
|
||||
|
||||
print("Starting HSD...")
|
||||
# Create a lock file
|
||||
with open("hsd.lock", "w") as f:
|
||||
f.write(str(time.time()))
|
||||
|
||||
# Config lookups with defaults
|
||||
chain_migrate = HSD_CONFIG.get("chainMigrate", False)
|
||||
wallet_migrate = HSD_CONFIG.get("walletMigrate", False)
|
||||
spv = HSD_CONFIG.get("spv", False)
|
||||
prefix = HSD_CONFIG.get("prefix", os.path.join(os.getcwd(), "hsd-data"))
|
||||
|
||||
|
||||
# Base command
|
||||
cmd = [
|
||||
"node",
|
||||
"./hsd/bin/hsd",
|
||||
f"--network={HSD_NETWORK}",
|
||||
f"--prefix={prefix}",
|
||||
f"--api-key={HSD_API}",
|
||||
"--http-host=127.0.0.1",
|
||||
"--log-console=false"
|
||||
]
|
||||
|
||||
# Conditionally add migration flags
|
||||
if chain_migrate:
|
||||
cmd.append(f"--chain-migrate={chain_migrate}")
|
||||
if wallet_migrate:
|
||||
cmd.append(f"--wallet-migrate={wallet_migrate}")
|
||||
SPV_MODE = spv
|
||||
if spv:
|
||||
cmd.append("--spv")
|
||||
|
||||
# Add flags
|
||||
if len(HSD_CONFIG.get("flags",[])) > 0:
|
||||
for flag in HSD_CONFIG.get("flags",[]):
|
||||
cmd.append(flag)
|
||||
|
||||
# Launch process
|
||||
HSD_PROCESS = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=os.getcwd(),
|
||||
text=True
|
||||
)
|
||||
|
||||
print(f"HSD started with PID {HSD_PROCESS.pid}")
|
||||
|
||||
atexit.register(hsdStop)
|
||||
|
||||
# Handle Ctrl+C
|
||||
try:
|
||||
signal.signal(signal.SIGINT, lambda s, f: (hsdStop(), sys.exit(0)))
|
||||
signal.signal(signal.SIGTERM, lambda s, f: (hsdStop(), sys.exit(0)))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def hsdStop():
|
||||
global HSD_PROCESS
|
||||
|
||||
if HSD_PROCESS is None:
|
||||
return
|
||||
|
||||
print("Stopping HSD...")
|
||||
|
||||
# Send SIGINT (like Ctrl+C)
|
||||
HSD_PROCESS.send_signal(signal.SIGINT)
|
||||
|
||||
try:
|
||||
HSD_PROCESS.wait(timeout=10) # wait for graceful exit
|
||||
print("HSD shut down cleanly.")
|
||||
except subprocess.TimeoutExpired:
|
||||
print("HSD did not exit yet, is it alright???")
|
||||
|
||||
# Clean up lock file
|
||||
if os.path.exists("hsd.lock"):
|
||||
os.remove("hsd.lock")
|
||||
|
||||
HSD_PROCESS = None
|
||||
|
||||
def hsdRestart():
|
||||
hsdStop()
|
||||
time.sleep(2)
|
||||
hsdStart()
|
||||
|
||||
|
||||
checkPreRequisites()
|
||||
hsdInit()
|
||||
hsdStart()
|
||||
# endregion
|
||||
@@ -6,10 +6,14 @@ import subprocess
|
||||
import binascii
|
||||
import datetime
|
||||
import dns.asyncresolver
|
||||
import dns.message
|
||||
import dns.query
|
||||
import dns.rdatatype
|
||||
import httpx
|
||||
from requests_doh import DNSOverHTTPSSession, add_dns_provider
|
||||
import requests
|
||||
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)
|
||||
|
||||
@@ -56,7 +60,7 @@ def hip2(domain: str):
|
||||
|
||||
domains = []
|
||||
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)
|
||||
domains.extend(san_list)
|
||||
|
||||
@@ -120,13 +124,39 @@ def hip2(domain: str):
|
||||
print(f"Hip2: Lookup failed with error: {e}",flush=True)
|
||||
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"):
|
||||
with httpx.Client() as client:
|
||||
q = dns.message.make_query(query_name, dns.rdatatype.A)
|
||||
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
|
||||
|
||||
def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
|
||||
|
||||
@@ -3,3 +3,5 @@ HSD_IP=localhost
|
||||
THEME=black
|
||||
SHOW_EXPIRED=false
|
||||
EXPLORER_TX=https://shakeshift.com/transaction/
|
||||
DISABLE_WALLETDNS=false
|
||||
INTERNAL_HSD=false
|
||||
45
grant.md
45
grant.md
@@ -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
|
||||
212
main.py
212
main.py
@@ -57,16 +57,11 @@ def blocks_to_time(blocks: int) -> str:
|
||||
return f"{days} days"
|
||||
return f"{days} days {hours} hrs"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# Check if the user is logged in
|
||||
if request.cookies.get("account") is None:
|
||||
return redirect("/login")
|
||||
|
||||
account = account_module.check_account(request.cookies.get("account"))
|
||||
if not account:
|
||||
return redirect("/logout")
|
||||
@@ -83,6 +78,8 @@ def index():
|
||||
return render_template("index.html", account=account, plugins=plugins)
|
||||
|
||||
info = gitinfo.get_git_info()
|
||||
if info is None:
|
||||
return render_template("index.html", account=account, plugins=plugins)
|
||||
branch = info['refs']
|
||||
commit = info['commit']
|
||||
if commit != latestVersion(branch):
|
||||
@@ -114,7 +111,7 @@ def transactions():
|
||||
return redirect("/logout")
|
||||
|
||||
# Get the page parameter
|
||||
page = request.args.get('page')
|
||||
page = request.args.get('page', 1)
|
||||
try:
|
||||
page = int(page)
|
||||
except:
|
||||
@@ -135,6 +132,8 @@ def send_page():
|
||||
return redirect("/login")
|
||||
|
||||
account = account_module.check_account(request.cookies.get("account"))
|
||||
if not account:
|
||||
return redirect("/logout")
|
||||
max = account_module.getBalance(account)['available']
|
||||
# Subtract approx fee
|
||||
max = max - fees
|
||||
@@ -170,28 +169,28 @@ def send():
|
||||
amount = request.form.get("amount")
|
||||
|
||||
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)
|
||||
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
|
||||
# Check if the amount is valid
|
||||
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
|
||||
amount = float(amount)
|
||||
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:
|
||||
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
|
||||
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')}"
|
||||
content = f"Are you sure you want to send {amount} HNS to {toAddress}<br><br>"
|
||||
@@ -202,7 +201,6 @@ def send():
|
||||
|
||||
|
||||
return render_template("confirm.html", account=account_module.check_account(request.cookies.get("account")),
|
||||
|
||||
action=action,
|
||||
content=content,cancel=cancel,confirm=confirm)
|
||||
|
||||
@@ -211,20 +209,20 @@ def send():
|
||||
def sendConfirmed():
|
||||
|
||||
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)
|
||||
if 'error' in response and response['error'] != None:
|
||||
# If error is a dict get the message
|
||||
if isinstance(response['error'], dict):
|
||||
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:
|
||||
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
|
||||
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']}")
|
||||
|
||||
|
||||
|
||||
@@ -363,6 +361,9 @@ def revealAllBids():
|
||||
return redirect("/logout")
|
||||
|
||||
response = account_module.revealAll(request.cookies.get("account"))
|
||||
if not response:
|
||||
return redirect("/auctions?message=Failed to reveal bids")
|
||||
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
if response['error']['message'] == "Nothing to do.":
|
||||
@@ -383,6 +384,9 @@ def redeemAllBids():
|
||||
return redirect("/logout")
|
||||
|
||||
response = account_module.redeemAll(request.cookies.get("account"))
|
||||
if not response:
|
||||
return redirect("/auctions?message=Failed to redeem bids")
|
||||
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
if response['error']['message'] == "Nothing to do.":
|
||||
@@ -402,13 +406,16 @@ def registerAllDomains():
|
||||
return redirect("/logout")
|
||||
|
||||
response = account_module.registerAll(request.cookies.get("account"))
|
||||
if not response:
|
||||
return redirect("/auctions?message=Failed to register domains")
|
||||
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
if response['error']['message'] == "Nothing to do.":
|
||||
return redirect("/auctions?message=No domains to register")
|
||||
return redirect("/auctions?message=" + response['error']['message'])
|
||||
|
||||
return redirect("/success?tx=" + response['hash'])
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
|
||||
@app.route('/all/finalize')
|
||||
def finalizeAllBids():
|
||||
@@ -427,7 +434,7 @@ def finalizeAllBids():
|
||||
return redirect("/dashboard?message=No domains to finalize")
|
||||
return redirect("/dashboard?message=" + response['error']['message'])
|
||||
|
||||
return redirect("/success?tx=" + response['hash'])
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
#endregion
|
||||
|
||||
@app.route('/search')
|
||||
@@ -441,6 +448,8 @@ def search():
|
||||
return redirect("/logout")
|
||||
|
||||
search_term = request.args.get("q")
|
||||
if search_term is None:
|
||||
return redirect("/")
|
||||
search_term = search_term.lower().strip()
|
||||
|
||||
# Replace spaces with hyphens
|
||||
@@ -457,7 +466,7 @@ def search():
|
||||
# Execute domain plugins
|
||||
searchFunctions = plugins_module.getSearchFunctions()
|
||||
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 += "</div>"
|
||||
@@ -475,6 +484,7 @@ def search():
|
||||
|
||||
state = domain['info']['state']
|
||||
stats = domain['info']['stats']
|
||||
next = ""
|
||||
if state == 'CLOSED':
|
||||
if domain['info']['registered']:
|
||||
state = 'REGISTERED'
|
||||
@@ -571,7 +581,7 @@ def manage(domain: str):
|
||||
# Execute domain plugins
|
||||
domainFunctions = plugins_module.getDomainFunctions()
|
||||
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 += "</div>"
|
||||
@@ -676,7 +686,7 @@ def revokeConfirm(domain: str):
|
||||
print(response)
|
||||
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')
|
||||
def renew(domain: str):
|
||||
@@ -690,7 +700,7 @@ def renew(domain: str):
|
||||
|
||||
domain = domain.lower()
|
||||
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')
|
||||
def editPage(domain: str):
|
||||
@@ -717,7 +727,10 @@ def editPage(domain: str):
|
||||
else:
|
||||
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
|
||||
dnsType = request.args.get("type")
|
||||
@@ -733,14 +746,14 @@ def editPage(domain: str):
|
||||
return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(str(raw_dns)) + "&error=Invalid DS record")
|
||||
|
||||
try:
|
||||
ds[0] = int(ds[0])
|
||||
ds[1] = int(ds[1])
|
||||
ds[2] = int(ds[2])
|
||||
key_tag = int(ds[0])
|
||||
algorithm = int(ds[1])
|
||||
digest_type = int(ds[2])
|
||||
except:
|
||||
raw_dns = str(dns).replace("'",'"')
|
||||
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("'",'"')
|
||||
return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(dns))
|
||||
@@ -770,13 +783,15 @@ def editSave(domain: str):
|
||||
|
||||
domain = domain.lower()
|
||||
dns = request.args.get("dns")
|
||||
if dns is None:
|
||||
return redirect(f"/manage/{domain}/edit?error=No DNS records provided")
|
||||
raw_dns = dns
|
||||
dns = urllib.parse.unquote(dns)
|
||||
response = account_module.setDNS(request.cookies.get("account"),domain,dns)
|
||||
if 'error' in response:
|
||||
print(response)
|
||||
return redirect("/manage/" + domain + "/edit?dns="+raw_dns+"&error=" + str(response['error']))
|
||||
return redirect("/success?tx=" + response['hash'])
|
||||
return redirect(f"/manage/{domain}/edit?dns={raw_dns}&error={response['error']}")
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
|
||||
@app.route('/manage/<domain>/transfer')
|
||||
def transfer(domain):
|
||||
@@ -801,7 +816,7 @@ def transfer(domain):
|
||||
|
||||
toAddress = 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')}"
|
||||
content = f"Are you sure you want to send {domain}/ to {toAddress}<br><br>"
|
||||
@@ -811,9 +826,7 @@ def transfer(domain):
|
||||
confirm = f"/manage/{domain}/transfer/confirm?address={address}"
|
||||
|
||||
|
||||
return render_template("confirm.html", account=account_module.check_account(request.cookies.get("account")),
|
||||
|
||||
action=action,
|
||||
return render_template("confirm.html", account=account,action=action,
|
||||
content=content,cancel=cancel,confirm=confirm)
|
||||
|
||||
@app.route('/manage/<domain>/sign')
|
||||
@@ -836,7 +849,7 @@ def signMessage(domain):
|
||||
signedMessage = account_module.signMessage(request.cookies.get("account"),domain,message)
|
||||
if signedMessage["error"] != None:
|
||||
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 = {
|
||||
"domain": domain,
|
||||
@@ -854,7 +867,6 @@ def signMessage(domain):
|
||||
|
||||
|
||||
return render_template("message.html", account=account,
|
||||
|
||||
title="Sign Message",content=content)
|
||||
|
||||
|
||||
@@ -873,7 +885,7 @@ def transferConfirm(domain):
|
||||
if 'error' in response:
|
||||
return redirect("/manage/" + domain + "?error=" + response['error'])
|
||||
|
||||
return redirect("/success?tx=" + response['hash'])
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
|
||||
|
||||
@app.route('/auction/<domain>')
|
||||
@@ -917,20 +929,9 @@ def auction(domain):
|
||||
|
||||
state = domainInfo['info']['state']
|
||||
next_action = ''
|
||||
next = ""
|
||||
|
||||
# bids = account_module.getBids(account,search_term)
|
||||
bids = []
|
||||
# if bids == []:
|
||||
# bids = "No bids found"
|
||||
# next_action = f'<a href="/auction/{domain}/scan">Rescan Auction</a>'
|
||||
# else:
|
||||
# reveals = account_module.getReveals(account,search_term)
|
||||
# for reveal in reveals:
|
||||
# # Get TX
|
||||
# revealInfo = account_module.getRevealTX(reveal)
|
||||
# reveal['bid'] = revealInfo
|
||||
# bids = render.bids(bids,reveals)
|
||||
|
||||
stats = domainInfo['info']['stats'] if 'stats' in domainInfo['info'] else {}
|
||||
if state == 'CLOSED':
|
||||
if not domainInfo['info']['registered']:
|
||||
@@ -1010,8 +1011,8 @@ def bid(domain):
|
||||
return redirect("/logout")
|
||||
|
||||
domain = domain.lower()
|
||||
bid = request.args.get("bid")
|
||||
blind = request.args.get("blind")
|
||||
bid = request.args.get("bid","")
|
||||
blind = request.args.get("blind","")
|
||||
|
||||
if bid == "":
|
||||
bid = 0
|
||||
@@ -1056,8 +1057,8 @@ def bid_confirm(domain):
|
||||
return redirect("/logout")
|
||||
|
||||
domain = domain.lower()
|
||||
bid = request.args.get("bid")
|
||||
blind = request.args.get("blind")
|
||||
bid = request.args.get("bid","")
|
||||
blind = request.args.get("blind","")
|
||||
|
||||
if bid == "":
|
||||
bid = 0
|
||||
@@ -1076,7 +1077,7 @@ def bid_confirm(domain):
|
||||
if 'error' in response:
|
||||
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')
|
||||
def open_auction(domain):
|
||||
@@ -1095,7 +1096,7 @@ def open_auction(domain):
|
||||
if response['error'] != None:
|
||||
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')
|
||||
def reveal_auction(domain):
|
||||
@@ -1110,8 +1111,8 @@ def reveal_auction(domain):
|
||||
domain = domain.lower()
|
||||
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'])
|
||||
return redirect(f"/auction/{domain}?message={response['error']}")
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
|
||||
@app.route('/auction/<domain>/register')
|
||||
def registerdomain(domain):
|
||||
@@ -1126,7 +1127,7 @@ def registerdomain(domain):
|
||||
response = account_module.register(request.cookies.get("account"),domain)
|
||||
if 'error' in response:
|
||||
return redirect("/auction/" + domain + "?message=" + response['error']['message'])
|
||||
return redirect("/success?tx=" + response['hash'])
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
|
||||
#endregion
|
||||
#region Settings
|
||||
@@ -1147,12 +1148,21 @@ def settings():
|
||||
if success == None:
|
||||
success = ""
|
||||
|
||||
|
||||
if not os.path.exists(".git"):
|
||||
return render_template("settings.html", account=account,
|
||||
|
||||
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()
|
||||
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']
|
||||
if branch != "main":
|
||||
branch = f"({branch})"
|
||||
@@ -1166,7 +1176,8 @@ def settings():
|
||||
version += ' (New version available)'
|
||||
return render_template("settings.html", account=account,
|
||||
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>')
|
||||
def settings_action(action):
|
||||
@@ -1183,29 +1194,36 @@ def settings_action(action):
|
||||
if 'error' in resp:
|
||||
return redirect("/settings?error=" + str(resp['error']))
|
||||
return redirect("/settings?success=Rescan started")
|
||||
elif action == "resend":
|
||||
|
||||
if action == "resend":
|
||||
resp = account_module.resendTXs()
|
||||
if 'error' in resp:
|
||||
return redirect("/settings?error=" + str(resp['error']))
|
||||
return redirect("/settings?success=Resent transactions")
|
||||
|
||||
|
||||
elif action == "zap":
|
||||
if action == "zap":
|
||||
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?success=Zapped transactions")
|
||||
elif action == "xpub":
|
||||
|
||||
if action == "xpub":
|
||||
xpub = account_module.getxPub(request.cookies.get("account"))
|
||||
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 += "<button id='copyButton' onclick='copyToClipboard()' class='btn btn-secondary'>Copy to clipboard</button>"
|
||||
|
||||
return render_template("message.html", account=account,
|
||||
|
||||
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")
|
||||
|
||||
@@ -1215,6 +1233,9 @@ def upload_image():
|
||||
return redirect("/login?message=Not logged in")
|
||||
|
||||
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'):
|
||||
os.mkdir('user_data/images')
|
||||
@@ -1224,11 +1245,12 @@ def upload_image():
|
||||
file = request.files['image']
|
||||
if file.filename == '':
|
||||
return redirect("/settings?error=No file selected")
|
||||
if file:
|
||||
filepath = os.path.join(f'user_data/images/{account.split(":")[0]}.{file.filename.split(".")[-1]}')
|
||||
if file and file.filename:
|
||||
filepath = os.path.join(f'user_data/images/{account}.{file.filename.split(".")[-1]}')
|
||||
file.save(filepath)
|
||||
return redirect("/settings?success=File uploaded successfully")
|
||||
|
||||
return redirect("/settings?error=An error occurred")
|
||||
|
||||
def latestVersion(branch):
|
||||
result = requests.get(f"https://git.woodburn.au/api/v1/repos/nathanwoodburn/firewalletbrowser/branches")
|
||||
@@ -1250,6 +1272,9 @@ def login():
|
||||
wallets = account_module.listWallets()
|
||||
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:
|
||||
return render_template("login.html",
|
||||
@@ -1265,6 +1290,12 @@ def login_post():
|
||||
account = request.form.get("account")
|
||||
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
|
||||
if account.count(":") > 0:
|
||||
wallets = account_module.listWallets()
|
||||
@@ -1280,8 +1311,6 @@ def login_post():
|
||||
wallets = render.wallets(wallets)
|
||||
return render_template("login.html",
|
||||
error="Invalid account or password",wallets=wallets)
|
||||
|
||||
|
||||
# Set the cookie
|
||||
response = make_response(redirect("/"))
|
||||
response.set_cookie("account", account)
|
||||
@@ -1300,6 +1329,11 @@ def register():
|
||||
password = request.form.get("password")
|
||||
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
|
||||
if password != repeatPassword:
|
||||
return render_template("register.html",
|
||||
@@ -1329,10 +1363,8 @@ def register():
|
||||
|
||||
|
||||
# Set the cookie
|
||||
response = make_response(render_template("message.html",
|
||||
|
||||
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 = 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']}"))
|
||||
response.set_cookie("account", account+":"+password)
|
||||
return response
|
||||
|
||||
@@ -1344,6 +1376,12 @@ def import_wallet():
|
||||
repeatPassword = request.form.get("password_repeat")
|
||||
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
|
||||
if password != repeatPassword:
|
||||
return render_template("import-wallet.html",
|
||||
@@ -1560,6 +1598,7 @@ def api_hsd(function):
|
||||
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):
|
||||
@@ -1638,7 +1677,10 @@ def api_wallet(function):
|
||||
return jsonify({"error": "Not logged in"})
|
||||
|
||||
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:
|
||||
return jsonify({"error": "Invalid account"})
|
||||
|
||||
@@ -1672,7 +1714,7 @@ def api_wallet(function):
|
||||
|
||||
if function == "domains":
|
||||
domains = account_module.getDomains(account)
|
||||
if 'error' in domains:
|
||||
if type(domains) == dict and 'error' in domains:
|
||||
return jsonify({"result": [], "error": domains['error']})
|
||||
|
||||
# Add nameRender to each domain
|
||||
@@ -1683,7 +1725,7 @@ def api_wallet(function):
|
||||
|
||||
if function == "transactions":
|
||||
# Get the page parameter
|
||||
page = request.args.get('page')
|
||||
page = request.args.get('page', 1)
|
||||
try:
|
||||
page = int(page)
|
||||
except:
|
||||
@@ -1759,7 +1801,7 @@ def api_wallet_mobile(function):
|
||||
return jsonify({"error": "Not logged in"})
|
||||
|
||||
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:
|
||||
return jsonify({"error": "Invalid account"})
|
||||
|
||||
@@ -1821,7 +1863,11 @@ def renderDomain(name: str) -> str:
|
||||
#region Assets and default pages
|
||||
@app.route('/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
|
||||
@app.route('/assets/css/styles.min.css')
|
||||
|
||||
17
plugin.py
17
plugin.py
@@ -148,11 +148,14 @@ def getPluginData(pluginStr: str):
|
||||
|
||||
|
||||
def getPluginFunctions(plugin: str):
|
||||
plugin = import_module(plugin.replace("/","."))
|
||||
return plugin.functions
|
||||
imported_plugin = import_module(plugin.replace("/","."))
|
||||
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("/","."))
|
||||
if function not in plugin_module.functions:
|
||||
return {"error": "Function not found"}
|
||||
@@ -189,13 +192,13 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
|
||||
|
||||
|
||||
def getPluginFunctionInputs(plugin: str, function: str):
|
||||
plugin = import_module(plugin.replace("/","."))
|
||||
return plugin.functions[function]["params"]
|
||||
imported_plugin = import_module(plugin.replace("/","."))
|
||||
return imported_plugin.functions[function]["params"]
|
||||
|
||||
|
||||
def getPluginFunctionReturns(plugin: str, function: str):
|
||||
plugin = import_module(plugin.replace("/","."))
|
||||
return plugin.functions[function]["returns"]
|
||||
imported_plugin = import_module(plugin.replace("/","."))
|
||||
return imported_plugin.functions[function]["returns"]
|
||||
|
||||
|
||||
def getDomainFunctions():
|
||||
|
||||
36
render.py
36
render.py
@@ -6,32 +6,7 @@ from domainLookup import punycode_to_emoji
|
||||
import os
|
||||
from handywrapper import api
|
||||
import threading
|
||||
|
||||
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)
|
||||
import requests
|
||||
|
||||
# Get Explorer URL
|
||||
TX_EXPLORER_URL = os.getenv("EXPLORER_TX")
|
||||
@@ -96,6 +71,7 @@ actionMap = {
|
||||
"UPDATE": "Updated ",
|
||||
"REGISTER": "Registered ",
|
||||
"RENEW": "Renewed ",
|
||||
"OPEN": "Opened ",
|
||||
"BID": "Bid on ",
|
||||
"REVEAL": "Revealed bid for ",
|
||||
"REDEEM": "Redeemed bid for ",
|
||||
@@ -107,6 +83,7 @@ actionMapPlural = {
|
||||
"UPDATE": "Updated multiple domains' records",
|
||||
"REGISTER": "Registered multiple domains",
|
||||
"RENEW": "Renewed multiple domains",
|
||||
"OPEN": "Opened multiple domains",
|
||||
"BID": "Bid on multiple domains",
|
||||
"REVEAL": "Revealed multiple bids",
|
||||
"REDEEM": "Redeemed multiple bids",
|
||||
@@ -558,9 +535,10 @@ def renderDomainAsync(namehash: str) -> None:
|
||||
|
||||
if namehash in cache:
|
||||
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:
|
||||
name = name["result"]
|
||||
rendered = renderDomain(name)
|
||||
@@ -574,7 +552,7 @@ def renderDomainAsync(namehash: str) -> None:
|
||||
with open(NAMEHASH_CACHE, 'w') as f:
|
||||
json.dump(cache, f)
|
||||
|
||||
return rendered
|
||||
return
|
||||
else:
|
||||
print(f"Error fetching name for hash {namehash}: {name['error']}", flush=True)
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ def gunicornServer():
|
||||
|
||||
def load_config(self):
|
||||
for key, value in self.options.items():
|
||||
if key in self.cfg.settings and value is not None:
|
||||
self.cfg.set(key.lower(), value)
|
||||
if key in self.cfg.settings and value is not None: # type: ignore
|
||||
self.cfg.set(key.lower(), value) # type: ignore
|
||||
|
||||
def load(self):
|
||||
return self.application
|
||||
|
||||
@@ -68,25 +68,31 @@
|
||||
<h3 class="mb-1" style="text-align: center;color: rgb(0,255,0);">{{success}}</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Node Settings</h4><small>HSD Version: v{{hsd_version}}</small>
|
||||
<h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<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't been mined yet.</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 class="card-title">Node Settings</h4><small>HSD Version: v{{hsd_version}} 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><ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<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't been mined yet.</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
{% if internal %}
|
||||
<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>
|
||||
|
||||
47
templates/welcome.html
Normal file
47
templates/welcome.html
Normal 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&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("/assets/img/favicon.png") 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>
|
||||
Reference in New Issue
Block a user