Compare commits
9 Commits
5ff8960b7b
...
v2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
1203719eac
|
|||
|
373a71f04d
|
|||
|
b76b873036
|
|||
|
23e714fad8
|
|||
|
a36c69ecfc
|
|||
|
1fd9987bf1
|
|||
|
f2cda461ba
|
|||
|
26c5b4a4fa
|
|||
|
7fdc4a3122
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,3 +19,4 @@ dist/
|
|||||||
hsd/
|
hsd/
|
||||||
hsd-data/
|
hsd-data/
|
||||||
hsd.lock
|
hsd.lock
|
||||||
|
hsdconfig.json
|
||||||
|
|||||||
@@ -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.
33
README.md
33
README.md
@@ -129,6 +129,39 @@ 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
|
||||||
|
|||||||
446
account.py
446
account.py
@@ -11,6 +11,9 @@ import subprocess
|
|||||||
import atexit
|
import atexit
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
|
import sqlite3
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
@@ -46,26 +49,36 @@ if SHOW_EXPIRED is None:
|
|||||||
SHOW_EXPIRED = False
|
SHOW_EXPIRED = False
|
||||||
|
|
||||||
HSD_PROCESS = None
|
HSD_PROCESS = None
|
||||||
|
SPV_MODE = None
|
||||||
|
|
||||||
# Get hsdconfig.json
|
# Get hsdconfig.json
|
||||||
HSD_CONFIG = {}
|
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'):
|
if not os.path.exists('hsdconfig.json'):
|
||||||
# Pull from the latest git
|
with open('hsdconfig.json', 'w') as f:
|
||||||
response = requests.get("https://git.woodburn.au/nathanwoodburn/firewalletbrowser/raw/branch/main/hsdconfig.json")
|
f.write(json.dumps(HSD_CONFIG, indent=4))
|
||||||
if response.status_code == 200:
|
|
||||||
with open('hsdconfig.json', 'w') as f:
|
|
||||||
f.write(response.text)
|
|
||||||
HSD_CONFIG = response.json()
|
|
||||||
else:
|
else:
|
||||||
with open('hsdconfig.json') as f:
|
with open('hsdconfig.json') as f:
|
||||||
HSD_CONFIG = json.load(f)
|
hsdConfigTMP = json.load(f)
|
||||||
|
for key in hsdConfigTMP:
|
||||||
|
HSD_CONFIG[key] = hsdConfigTMP[key]
|
||||||
|
|
||||||
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
|
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
|
||||||
hsw = api.hsw(HSD_API, HSD_IP, HSD_WALLET_PORT)
|
hsw = api.hsw(HSD_API, HSD_IP, HSD_WALLET_PORT)
|
||||||
|
|
||||||
cacheTime = 3600
|
|
||||||
|
|
||||||
# Verify the connection
|
# Verify the connection
|
||||||
response = hsd.getInfo()
|
response = hsd.getInfo()
|
||||||
|
|
||||||
@@ -82,6 +95,13 @@ def hsdVersion(format=True):
|
|||||||
info = hsd.getInfo()
|
info = hsd.getInfo()
|
||||||
if 'error' in info:
|
if 'error' in info:
|
||||||
return -1
|
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:
|
if format:
|
||||||
return float('.'.join(info['version'].split(".")[:2]))
|
return float('.'.join(info['version'].split(".")[:2]))
|
||||||
else:
|
else:
|
||||||
@@ -208,6 +228,124 @@ def selectWallet(account: str):
|
|||||||
"message": response['error']['message']
|
"message": response['error']['message']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def getBalance(account: str):
|
||||||
# Get the total balance
|
# Get the total balance
|
||||||
@@ -225,9 +363,66 @@ def getBalance(account: str):
|
|||||||
|
|
||||||
domains = getDomains(account)
|
domains = getDomains(account)
|
||||||
domainValue = 0
|
domainValue = 0
|
||||||
for domain in domains:
|
domains_to_update = [] # Track domains that need cache updates
|
||||||
if domain['state'] == "CLOSED":
|
|
||||||
domainValue += domain['value']
|
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)
|
total = total - (domainValue/1000000)
|
||||||
locked = locked - (domainValue/1000000)
|
locked = locked - (domainValue/1000000)
|
||||||
|
|
||||||
@@ -292,40 +487,74 @@ def getDomains(account, own=True):
|
|||||||
|
|
||||||
return domains
|
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):
|
def getPageTXCache(account, page, size=100):
|
||||||
page = f"{page}-{size}"
|
"""Get cached transaction ID from SQLite database."""
|
||||||
if not os.path.exists(f'cache'):
|
account = getxPub(account)
|
||||||
os.mkdir(f'cache')
|
page_key = f"{page}-{size}"
|
||||||
|
|
||||||
if not os.path.exists(f'cache/{account}_page.json'):
|
# Initialize database if needed
|
||||||
with open(f'cache/{account}_page.json', 'w') as f:
|
init_tx_page_db()
|
||||||
f.write('{}')
|
|
||||||
with open(f'cache/{account}_page.json') as f:
|
db_path = os.path.join('cache', 'tx_pages.db')
|
||||||
pageCache = json.load(f)
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
if page in pageCache and pageCache[page]['time'] > int(time.time()) - cacheTime:
|
|
||||||
return pageCache[page]['txid']
|
# 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
|
return None
|
||||||
|
|
||||||
|
|
||||||
def pushPageTXCache(account, page, txid, size=100):
|
def pushPageTXCache(account, page, txid, size=100):
|
||||||
page = f"{page}-{size}"
|
"""Store transaction ID in SQLite database."""
|
||||||
if not os.path.exists(f'cache/{account}_page.json'):
|
account = getxPub(account)
|
||||||
with open(f'cache/{account}_page.json', 'w') as f:
|
page_key = f"{page}-{size}"
|
||||||
f.write('{}')
|
|
||||||
with open(f'cache/{account}_page.json') as f:
|
# Initialize database if needed
|
||||||
pageCache = json.load(f)
|
init_tx_page_db()
|
||||||
|
|
||||||
pageCache[page] = {
|
db_path = os.path.join('cache', 'tx_pages.db')
|
||||||
'time': int(time.time()),
|
conn = sqlite3.connect(db_path)
|
||||||
'txid': txid
|
cursor = conn.cursor()
|
||||||
}
|
|
||||||
with open(f'cache/{account}_page.json', 'w') as f:
|
# Insert or replace the transaction ID
|
||||||
json.dump(pageCache, f, indent=4)
|
cursor.execute(
|
||||||
|
'INSERT OR REPLACE INTO tx_pages (account, page_key, txid, timestamp) VALUES (?, ?, ?, ?)',
|
||||||
return pageCache[page]['txid']
|
(account, page_key, txid, int(time.time()))
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return txid
|
||||||
|
|
||||||
def getTXFromPage(account, page, size=100):
|
def getTXFromPage(account, page, size=100):
|
||||||
if page == 1:
|
if page == 1:
|
||||||
@@ -489,7 +718,12 @@ def send(account, address, amount):
|
|||||||
def isOwnDomain(account, name: str):
|
def isOwnDomain(account, name: str):
|
||||||
# Get domain
|
# Get domain
|
||||||
domain_info = getDomain(name)
|
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
|
# Select the account
|
||||||
hsw.rpc_selectWallet(account)
|
hsw.rpc_selectWallet(account)
|
||||||
account = hsw.rpc_getAccount(owner)
|
account = hsw.rpc_getAccount(owner)
|
||||||
@@ -521,6 +755,16 @@ def isOwnPrevout(account, prevout: dict):
|
|||||||
|
|
||||||
|
|
||||||
def getDomain(domain: str):
|
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
|
# Get the domain
|
||||||
response = hsd.rpc_getNameInfo(domain)
|
response = hsd.rpc_getNameInfo(domain)
|
||||||
if response['error'] is not None:
|
if response['error'] is not None:
|
||||||
@@ -531,11 +775,21 @@ def getDomain(domain: str):
|
|||||||
}
|
}
|
||||||
return response['result']
|
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):
|
def getAddressFromCoin(coinhash: str, coinindex = 0):
|
||||||
# Get the address from the hash
|
# Get the address from the hash
|
||||||
response = requests.get(get_node_api_url(f"coin/{coinhash}/{coinindex}"))
|
response = requests.get(get_node_api_url(f"coin/{coinhash}/{coinindex}"))
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print(f"Error getting address from coin: {response.text}")
|
print(f"Error getting address from coin")
|
||||||
return "No Owner"
|
return "No Owner"
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if 'address' not in data:
|
if 'address' not in data:
|
||||||
@@ -561,6 +815,17 @@ def renewDomain(account, domain):
|
|||||||
|
|
||||||
def getDNS(domain: str):
|
def getDNS(domain: str):
|
||||||
# Get the DNS
|
# 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)
|
response = hsd.rpc_getNameResource(domain)
|
||||||
if response['error'] is not None:
|
if response['error'] is not None:
|
||||||
return {
|
return {
|
||||||
@@ -748,7 +1013,9 @@ def getPendingRegisters(account):
|
|||||||
for bid in bids:
|
for bid in bids:
|
||||||
if bid['name'] == domain['name']:
|
if bid['name'] == domain['name']:
|
||||||
if bid['value'] == domain['highest']:
|
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
|
return pending
|
||||||
|
|
||||||
|
|
||||||
@@ -1345,7 +1612,9 @@ def zapTXs(account):
|
|||||||
|
|
||||||
|
|
||||||
def getxPub(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:
|
if account_name == False:
|
||||||
return {
|
return {
|
||||||
@@ -1363,8 +1632,6 @@ def getxPub(account):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response['accountKey']
|
return response['accountKey']
|
||||||
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
@@ -1461,10 +1728,20 @@ def generateReport(account, format="{name},{expiry},{value},{maxBid}"):
|
|||||||
def convertHNS(value: int):
|
def convertHNS(value: int):
|
||||||
return value/1000000
|
return value/1000000
|
||||||
|
|
||||||
|
SPV_EXTERNAL_ROUTES = [
|
||||||
|
"name",
|
||||||
|
"coin",
|
||||||
|
"tx",
|
||||||
|
"block"
|
||||||
|
]
|
||||||
|
|
||||||
def get_node_api_url(path=''):
|
def get_node_api_url(path=''):
|
||||||
"""Construct a URL for the HSD node API."""
|
"""Construct a URL for the HSD node API."""
|
||||||
base_url = f"http://x:{HSD_API}@{HSD_IP}:{HSD_NODE_PORT}"
|
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:
|
if path:
|
||||||
# Ensure path starts with a slash if it's not empty
|
# Ensure path starts with a slash if it's not empty
|
||||||
if not path.startswith('/'):
|
if not path.startswith('/'):
|
||||||
@@ -1482,7 +1759,19 @@ def get_wallet_api_url(path=''):
|
|||||||
return f"{base_url}{path}"
|
return f"{base_url}{path}"
|
||||||
return base_url
|
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
|
# region HSD Internal Node
|
||||||
|
|
||||||
@@ -1495,25 +1784,34 @@ def checkPreRequisites() -> dict[str, bool]:
|
|||||||
"git": False,
|
"git": False,
|
||||||
"hsd": False
|
"hsd": False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 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
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Check if node is installed and get version
|
try:
|
||||||
nodeSubprocess = subprocess.run(["node", "-v"], capture_output=True, text=True)
|
# Check if npm is installed
|
||||||
if nodeSubprocess.returncode == 0:
|
npmSubprocess = subprocess.run(["npm", "-v"], capture_output=True, text=True)
|
||||||
major_version = int(nodeSubprocess.stdout.strip().lstrip('v').split('.')[0])
|
if npmSubprocess.returncode == 0:
|
||||||
if major_version >= HSD_CONFIG.get("minNodeVersion", 20):
|
major_version = int(npmSubprocess.stdout.strip().split('.')[0])
|
||||||
prerequisites["node"] = True
|
if major_version >= HSD_CONFIG.get("minNPMVersion", 8):
|
||||||
|
prerequisites["npm"] = True
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Check if npm is installed
|
try:
|
||||||
npmSubprocess = subprocess.run(["npm", "-v"], capture_output=True, text=True)
|
# Check if git is installed
|
||||||
if npmSubprocess.returncode == 0:
|
gitSubprocess = subprocess.run(["git", "-v"], capture_output=True, text=True)
|
||||||
major_version = int(npmSubprocess.stdout.strip().split('.')[0])
|
if gitSubprocess.returncode == 0:
|
||||||
if major_version >= HSD_CONFIG.get("minNPMVersion", 8):
|
prerequisites["git"] = True
|
||||||
prerequisites["npm"] = True
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
# 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
|
# Check if hsd is installed
|
||||||
if os.path.exists("./hsd/bin/hsd"):
|
if os.path.exists("./hsd/bin/hsd"):
|
||||||
@@ -1543,7 +1841,7 @@ def hsdInit():
|
|||||||
for key, value in prerequisites.items():
|
for key, value in prerequisites.items():
|
||||||
if not value:
|
if not value:
|
||||||
print(f" - {key} is missing or does not meet the version requirement.")
|
print(f" - {key} is missing or does not meet the version requirement.")
|
||||||
exit(1)
|
exit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if hsd is installed
|
# Check if hsd is installed
|
||||||
@@ -1571,6 +1869,7 @@ def hsdInit():
|
|||||||
|
|
||||||
def hsdStart():
|
def hsdStart():
|
||||||
global HSD_PROCESS
|
global HSD_PROCESS
|
||||||
|
global SPV_MODE
|
||||||
if not HSD_INTERNAL_NODE:
|
if not HSD_INTERNAL_NODE:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1592,15 +1891,16 @@ def hsdStart():
|
|||||||
chain_migrate = HSD_CONFIG.get("chainMigrate", False)
|
chain_migrate = HSD_CONFIG.get("chainMigrate", False)
|
||||||
wallet_migrate = HSD_CONFIG.get("walletMigrate", False)
|
wallet_migrate = HSD_CONFIG.get("walletMigrate", False)
|
||||||
spv = HSD_CONFIG.get("spv", False)
|
spv = HSD_CONFIG.get("spv", False)
|
||||||
|
prefix = HSD_CONFIG.get("prefix", os.path.join(os.getcwd(), "hsd-data"))
|
||||||
|
|
||||||
|
|
||||||
# Base command
|
# Base command
|
||||||
cmd = [
|
cmd = [
|
||||||
"node",
|
"node",
|
||||||
"./hsd/bin/hsd",
|
"./hsd/bin/hsd",
|
||||||
f"--network={HSD_NETWORK}",
|
f"--network={HSD_NETWORK}",
|
||||||
f"--prefix={os.path.join(os.getcwd(), 'hsd-data')}",
|
f"--prefix={prefix}",
|
||||||
f"--api-key={HSD_API}",
|
f"--api-key={HSD_API}",
|
||||||
"--agent=FireWallet",
|
|
||||||
"--http-host=127.0.0.1",
|
"--http-host=127.0.0.1",
|
||||||
"--log-console=false"
|
"--log-console=false"
|
||||||
]
|
]
|
||||||
@@ -1610,9 +1910,14 @@ def hsdStart():
|
|||||||
cmd.append(f"--chain-migrate={chain_migrate}")
|
cmd.append(f"--chain-migrate={chain_migrate}")
|
||||||
if wallet_migrate:
|
if wallet_migrate:
|
||||||
cmd.append(f"--wallet-migrate={wallet_migrate}")
|
cmd.append(f"--wallet-migrate={wallet_migrate}")
|
||||||
|
SPV_MODE = spv
|
||||||
if spv:
|
if spv:
|
||||||
cmd.append("--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
|
# Launch process
|
||||||
HSD_PROCESS = subprocess.Popen(
|
HSD_PROCESS = subprocess.Popen(
|
||||||
@@ -1662,7 +1967,6 @@ def hsdRestart():
|
|||||||
hsdStart()
|
hsdStart()
|
||||||
|
|
||||||
|
|
||||||
checkPreRequisites()
|
|
||||||
hsdInit()
|
hsdInit()
|
||||||
hsdStart()
|
hsdStart()
|
||||||
# endregion
|
# endregion
|
||||||
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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "v8.0.0",
|
|
||||||
"chainMigrate":4,
|
|
||||||
"walletMigrate":7,
|
|
||||||
"minNodeVersion":20,
|
|
||||||
"minNpmVersion":8,
|
|
||||||
"spv": true
|
|
||||||
}
|
|
||||||
12
main.py
12
main.py
@@ -1148,15 +1148,20 @@ 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",internal=account_module.HSD_INTERNAL_NODE)
|
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:
|
if not info:
|
||||||
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",internal=account_module.HSD_INTERNAL_NODE)
|
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":
|
||||||
@@ -1171,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,internal=account_module.HSD_INTERNAL_NODE)
|
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):
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<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}} 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">
|
<h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6><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>
|
||||||
|
|||||||
Reference in New Issue
Block a user