30 Commits

Author SHA1 Message Date
c4cd2bc443 fix: Check if blocksSinceExpired to get expired domains
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m56s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 3m4s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m6s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m24s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m24s
2025-09-18 16:35:34 +10:00
608933c228 fix: Calculate balance with expired domains 2025-09-18 16:34:45 +10:00
d9e847a995 feat: Add support for verbose sync status and don't require auth for some api routes
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 28s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 30s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 30s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 45s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 48s
2025-09-16 22:16:29 +10:00
15d919ca97 feat: Add option to use http basic auth for api routes
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 3m2s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 3m8s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m12s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m19s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m24s
This shoudl make it easier to use curl to access info
2025-09-16 16:42:09 +10:00
aa52911823 fix: Style settings links as inline-blocks
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m53s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m59s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m2s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m5s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m13s
This fixes mobile views from splitting the text and icons for the links
2025-09-15 11:07:17 +10:00
2ee294cab8 feat: Add git version to status route
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 30s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 35s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 36s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 46s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 52s
2025-09-12 15:40:39 +10:00
19771fe30d feat: Add better status check for internal nodes and error logging
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 27s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 28s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 28s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 46s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 46s
2025-09-12 15:23:56 +10:00
12d3958b9d fix: Auction sort crashes on 0 bids
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m14s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 2m17s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 2m23s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m36s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 2m39s
2025-09-12 12:07:16 +10:00
d20fc1eb55 feat: Use inline link for logs to improve readability
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 31s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 36s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 40s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 44s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 54s
2025-09-11 16:55:51 +10:00
148e5f325a fix: Move logger before imports to ensure it is initialized before logs start
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 29s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 31s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 32s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 46s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 47s
2025-09-11 16:48:43 +10:00
6442aa4df6 fix: Return error message if logs are nonexistent or empty 2025-09-11 15:28:37 +10:00
2e86e64dd0 Merge pull request 'Add logging system' (#8) from feat/logging into main
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 31s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 39s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 49s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 54s
Reviewed-on: #8
2025-09-11 15:19:24 +10:00
7fc19a7f19 fix: Don't allow alerts without an ID to be dismissed
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m58s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m4s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m6s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m10s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 11m1s
2025-09-11 15:07:19 +10:00
eb6306bb83 feat: Move update check to alerts function
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Has been cancelled
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.10) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.11) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.13) (push) Has been cancelled
2025-09-11 15:04:31 +10:00
9f8daa8b88 feat: Replace most prints with logger calls to help with debugging
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 34s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 48s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 50s
2025-09-10 17:14:32 +10:00
63e0f0b804 feat: Add initial logggin system
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 37s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 37s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 37s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 48s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 51s
2025-09-10 16:56:31 +10:00
0c17c4ad9b feat: Add option to set host and port on main.py entry
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m52s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m56s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m3s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m5s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m6s
2025-09-10 16:18:46 +10:00
938fff8791 fix: Update wording of xPub
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 36s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 33s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 37s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 47s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 54s
2025-09-09 23:36:38 +10:00
155662d2b1 fix: Move Bootstrap design to lfs
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 35s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 35s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 42s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 52s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 59s
2025-09-09 23:21:57 +10:00
97569faf0e Merge pull request 'Add alerts to dashboard' (#7) from feat/notices into main
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 34s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 36s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 35s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 50s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 51s
Reviewed-on: #7
2025-09-09 22:51:03 +10:00
59000afa87 feat: Cleanup function layouts for alerts
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 35s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 40s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 37s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 51s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 50s
2025-09-09 17:34:36 +10:00
699a74f093 feat: Add api route to dismiss alert
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 29s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 36s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 37s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 42s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 47s
2025-09-09 17:29:11 +10:00
6096f82c4d feat: Add an alert framework
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 3m6s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 3m9s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m18s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m17s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m25s
2025-09-09 17:24:39 +10:00
4353eb8fa4 feat: Use new proxied script url
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 33s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 38s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 48s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 54s
2025-09-04 22:05:16 +10:00
344cde07d0 feat: Add more info about the installation script to readme
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 1m4s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 1m12s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 1m48s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 1m52s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m51s
2025-09-04 21:36:30 +10:00
2fb841aeaf fix: Update link for installation script to use raw
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Has been cancelled
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.10) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.13) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.11) (push) Has been cancelled
2025-09-04 21:34:19 +10:00
60df317f78 feat: Add install script to readme
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Has started running
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.11) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.10) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.13) (push) Has been cancelled
2025-09-04 21:33:28 +10:00
4c1ea9fb12 feat: Add installation and start scripts 2025-09-04 21:29:47 +10:00
58ed636ce3 fix: Use git --version instead of short -v to allow for backwards compatability
The default git installed in ubuntu doesn't allow -v
2025-09-04 21:29:05 +10:00
e537c323c2 fix: Update paths to be consistent
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 43s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 45s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 51s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 59s
2025-09-02 18:15:56 +10:00
12 changed files with 570 additions and 154 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.bsdesign filter=lfs diff=lfs merge=lfs -text

3
.gitignore vendored
View File

@@ -17,6 +17,7 @@ cache/
build/ build/
dist/ dist/
hsd/ hsd/
hsd-data/ hsd_data/
logs/
hsd.lock hsd.lock
hsdconfig.json hsdconfig.json

View File

@@ -50,7 +50,7 @@ LABEL org.opencontainers.image.title="FireWallet (HSD)" \
VOLUME ["/app/hsd-data", "/app/user_data"] VOLUME ["/app/hsd_data", "/app/user_data"]
ENTRYPOINT ["python3"] ENTRYPOINT ["python3"]

Binary file not shown.

View File

@@ -13,6 +13,17 @@ cp example.env .env
Edit .env to have your HSD api key. Edit .env to have your HSD api key.
If you have HSD runnning on a separate computer also add the IP here If you have HSD runnning on a separate computer also add the IP here
For a quick and easy installation on ubuntu/debian you can run the install.sh script
```bash
curl https://firewallet.au/install.sh | bash
```
This will install all dependencies (including Node/NPM for an internal HSD node), create a python virtual environment and install the required python packages.
After the script has run you can start the wallet with
```bash
./start.sh
```
## Usage ## Usage
Make sure HSD is running then run the following commands: Make sure HSD is running then run the following commands:

View File

@@ -13,6 +13,8 @@ import signal
import sys import sys
import threading import threading
import sqlite3 import sqlite3
import logging
logger = logging.getLogger("firewallet")
dotenv.load_dotenv() dotenv.load_dotenv()
@@ -43,9 +45,7 @@ if HSD_INTERNAL_NODE:
HSD_API = "firewallet-" + str(int(time.time())) HSD_API = "firewallet-" + str(int(time.time()))
HSD_IP = "localhost" HSD_IP = "localhost"
SHOW_EXPIRED = os.getenv("SHOW_EXPIRED") SHOW_EXPIRED = os.getenv("SHOW_EXPIRED","false").lower() in ["1","true","yes"]
if SHOW_EXPIRED is None:
SHOW_EXPIRED = False
HSD_PROCESS = None HSD_PROCESS = None
SPV_MODE = None SPV_MODE = None
@@ -93,6 +93,7 @@ def hsdConnected():
def hsdVersion(format=True): def hsdVersion(format=True):
info = hsd.getInfo() info = hsd.getInfo()
if 'error' in info: if 'error' in info:
logger.error(f"HSD connection error: {info.get('error', 'Unknown error')}")
return -1 return -1
# Check if SPV mode is enabled # Check if SPV mode is enabled
@@ -116,9 +117,12 @@ def check_account(cookie: str | None):
return False return False
account = cookie.split(":")[0] account = cookie.split(":")[0]
if len(account) < 1:
return False
# Check if the account is valid # Check if the account is valid
info = hsw.getAccountInfo(account, 'default') info = hsw.getAccountInfo(account, 'default')
if 'error' in info: if 'error' in info:
logger.error(f"HSW error checking account {account}: {info.get('error', 'Unknown error')}")
return False return False
return account return account
@@ -270,7 +274,7 @@ def getCachedDomains():
domain_cache[row['name']] = json.loads(row['info']) domain_cache[row['name']] = json.loads(row['info'])
domain_cache[row['name']]['last_updated'] = row['last_updated'] domain_cache[row['name']]['last_updated'] = row['last_updated']
except json.JSONDecodeError: except json.JSONDecodeError:
print(f"Error parsing cached data for domain {row['name']}") logger.error(f"Error parsing cached data for domain {row['name']}")
conn.close() conn.close()
return domain_cache return domain_cache
@@ -310,7 +314,7 @@ def update_domain_cache(domain_names: list):
domain_info = getDomain(domain_name) domain_info = getDomain(domain_name)
if 'error' in domain_info or not domain_info.get('info'): 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) logger.error(f"Failed to get info for domain {domain_name}: {domain_info.get('error', 'Unknown error')}")
continue continue
# Update or insert into database # Update or insert into database
@@ -322,9 +326,9 @@ def update_domain_cache(domain_names: list):
(domain_name, serialized_info, now) (domain_name, serialized_info, now)
) )
print(f"Updated cache for domain {domain_name}") logger.info(f"Updated cache for domain {domain_name}")
except Exception as e: except Exception as e:
print(f"Error updating cache for domain {domain_name}: {str(e)}") logger.error(f"Error updating cache for domain {domain_name}: {str(e)}", exc_info=True)
finally: finally:
# Always remove from active set, even if there was an error # Always remove from active set, even if there was an error
with DOMAIN_UPDATE_LOCK: with DOMAIN_UPDATE_LOCK:
@@ -336,20 +340,21 @@ def update_domain_cache(domain_names: list):
conn.close() conn.close()
except Exception as e: except Exception as e:
print(f"Error updating domain cache: {str(e)}", flush=True) logger.error(f"Error updating domain cache: {str(e)}", exc_info=True)
# Make sure to clean up the active set on any exception # Make sure to clean up the active set on any exception
with DOMAIN_UPDATE_LOCK: with DOMAIN_UPDATE_LOCK:
for domain in domains_to_update: for domain in domains_to_update:
if domain in ACTIVE_DOMAIN_UPDATES: if domain in ACTIVE_DOMAIN_UPDATES:
ACTIVE_DOMAIN_UPDATES.remove(domain) ACTIVE_DOMAIN_UPDATES.remove(domain)
print("Updated cache for domains") logger.info("Updated cache for domains")
def getBalance(account: str): def getBalance(account: str):
# Get the total balance # Get the total balance
info = hsw.getBalance('default', account) info = hsw.getBalance('default', account)
if 'error' in info: if 'error' in info:
logger.error(f"Error getting balance for account {account}: {info['error']}")
return {'available': 0, 'total': 0} return {'available': 0, 'total': 0}
total = info['confirmed'] total = info['confirmed']
@@ -359,8 +364,9 @@ def getBalance(account: str):
# Convert to HNS # Convert to HNS
total = total / 1000000 total = total / 1000000
available = available / 1000000 available = available / 1000000
logger.debug(f"Initial balance for account {account}: total={total}, available={available}, locked={locked}")
domains = getDomains(account) domains = getDomains(account,True,True)
domainValue = 0 domainValue = 0
domains_to_update = [] # Track domains that need cache updates domains_to_update = [] # Track domains that need cache updates
@@ -402,6 +408,7 @@ def getBalance(account: str):
if domain_info.get('info', {}).get('state', "") == "CLOSED": if domain_info.get('info', {}).get('state', "") == "CLOSED":
domainValue += domain_info.get('info', {}).get('value', 0) domainValue += domain_info.get('info', {}).get('value', 0)
except json.JSONDecodeError: except json.JSONDecodeError:
logger.warning(f"Error parsing cached data for domain {domain_name}")
# Only add for update if not already being updated # Only add for update if not already being updated
with DOMAIN_UPDATE_LOCK: with DOMAIN_UPDATE_LOCK:
if domain_name not in ACTIVE_DOMAIN_UPDATES: if domain_name not in ACTIVE_DOMAIN_UPDATES:
@@ -424,6 +431,7 @@ def getBalance(account: str):
total = total - (domainValue/1000000) total = total - (domainValue/1000000)
locked = locked - (domainValue/1000000) locked = locked - (domainValue/1000000)
logger.debug(f"Adjusted balance for account {account}: total={total}, available={available}, locked={locked}")
# Only keep 2 decimal places # Only keep 2 decimal places
total = round(total, 2) total = round(total, 2)
@@ -465,22 +473,27 @@ def getPendingTX(account: str):
return pending return pending
def getDomains(account, own=True): def getDomains(account, own: bool = True, expired: bool = SHOW_EXPIRED):
if own: if own:
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name?own=true")) response = requests.get(get_wallet_api_url(f"/wallet/{account}/name?own=true"))
else: else:
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name")) response = requests.get(get_wallet_api_url(f"/wallet/{account}/name"))
info = response.json() info = response.json()
if SHOW_EXPIRED: if expired:
return info return info
# Remove any expired domains # Remove any expired domains
domains = [] domains = []
for domain in info: for domain in info:
if 'stats' in domain: if 'stats' in domain and domain['stats'] is not None:
if 'daysUntilExpire' in domain['stats']: if 'daysUntilExpire' in domain['stats']:
if domain['stats']['daysUntilExpire'] < 0: if domain['stats']['daysUntilExpire'] < 0:
logger.debug(f"Excluding expired domain: {domain['name']} due to daysUntilExpire")
continue
if 'blocksSinceExpired' in domain['stats']:
if domain['stats']['blocksSinceExpired'] > 0:
logger.debug(f"Excluding expired domain: {domain['name']} due to blocksSinceExpired")
continue continue
domains.append(domain) domains.append(domain)
@@ -577,6 +590,7 @@ def getTransactions(account, page=1, limit=100):
return [] return []
info = hsw.getWalletTxHistory(account) info = hsw.getWalletTxHistory(account)
if 'error' in info: if 'error' in info:
logger.error(f"Error getting transactions for account {account}: {info['error']}")
return [] return []
return info[::-1] return info[::-1]
@@ -594,14 +608,14 @@ def getTransactions(account, page=1, limit=100):
return [] return []
if response.status_code != 200: if response.status_code != 200:
print(response.text) logger.error(f"Error fetching transactions: {response.status_code} - {response.text}")
return [] return []
data = response.json() data = response.json()
# Refresh the cache if the next page is different # Refresh the cache if the next page is different
nextPage = getPageTXCache(account, page, limit) nextPage = getPageTXCache(account, page, limit)
if nextPage is not None and nextPage != data[-1]['hash']: if nextPage is not None and nextPage != data[-1]['hash']:
print(f'Refreshing page {page}') logger.info(f'Refreshing tx page {page}')
pushPageTXCache(account, page, data[-1]['hash'], limit) pushPageTXCache(account, page, data[-1]['hash'], limit)
return data return data
@@ -788,11 +802,12 @@ 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("Error getting address from coin") logger.error("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:
print(json.dumps(data, indent=4)) logger.error("Error getting address from coin")
logger.error(json.dumps(data, indent=4))
return "No Owner" return "No Owner"
return data['address'] return data['address']
@@ -909,6 +924,7 @@ def register(account, domain):
def getNodeSync(): def getNodeSync():
response = hsd.getInfo() response = hsd.getInfo()
if 'error' in response: if 'error' in response:
logger.error(f"Error getting node sync status: {response['error']}")
return 0 return 0
sync = response['chain']['progress']*100 sync = response['chain']['progress']*100
@@ -916,11 +932,14 @@ def getNodeSync():
return sync return sync
def getWalletStatus(): def getWalletStatus(verbose: bool = False):
response = hsw.rpc_getWalletInfo() response = hsw.rpc_getWalletInfo()
if 'error' in response and response['error'] is not None: if 'error' in response and response['error'] is not None:
return "Error" return "Error"
if verbose:
return response.get('result', {})
# return response # return response
walletHeight = response['result']['height'] walletHeight = response['result']['height']
# Get the current block height # Get the current block height
@@ -998,7 +1017,7 @@ def getPendingRedeems(account, password):
else: else:
pending.append(name['result']) pending.append(name['result'])
except Exception as e: except Exception as e:
print(f"Failed to parse redeems: {str(e)}") logger.error(f"Failed to parse redeems: {str(e)}", exc_info=True)
return pending return pending
@@ -1042,7 +1061,7 @@ def getPendingFinalizes(account, password):
else: else:
pending.append(name['result']) pending.append(name['result'])
except Exception as e: except Exception as e:
print(f"Failed to parse finalizes: {str(e)}") logger.error(f"Failed to parse finalizes: {str(e)}", exc_info=True)
return pending return pending
@@ -1052,9 +1071,8 @@ def getRevealTX(reveal):
index = prevout['index'] index = prevout['index']
tx = hsd.getTxByHash(hash) tx = hsd.getTxByHash(hash)
if 'inputs' not in tx: if 'inputs' not in tx:
print(f'Something is up with this tx: {hash}') logger.error(f'Something is up with this tx: {hash}')
print(tx) logger.error(tx)
print('---')
# No idea what happened here # No idea what happened here
# Check if registered? # Check if registered?
return None return None
@@ -1498,10 +1516,10 @@ def getMempoolBids():
for txid in mempoolTxs: for txid in mempoolTxs:
tx = hsd.getTxByHash(txid) tx = hsd.getTxByHash(txid)
if 'error' in tx and tx['error'] is not None: if 'error' in tx and tx['error'] is not None:
print(f"Error getting tx {txid}: {tx['error']}") logger.error(f"Error getting tx {txid}: {tx['error']}")
continue continue
if 'outputs' not in tx: if 'outputs' not in tx:
print(f"Error getting outputs for tx {txid}") logger.error(f"Error getting outputs for tx {txid}")
continue continue
for output in tx['outputs']: for output in tx['outputs']:
if output['covenant']['action'] not in ["BID", "REVEAL"]: if output['covenant']['action'] not in ["BID", "REVEAL"]:
@@ -1558,9 +1576,15 @@ def getMempoolBids():
# region settingsAPIs # region settingsAPIs
def rescan(): def rescan(height:int = 0):
try: try:
response = hsw.walletRescan(0) response = hsw.walletRescan(height)
if 'success' in response and response['success'] is False:
return {
"error": {
"message": "Rescan already in progress"
}
}
return response return response
except Exception as e: except Exception as e:
return { return {
@@ -1679,7 +1703,7 @@ def verifyMessageWithName(domain, signature, message):
return response['result'] return response['result']
return False return False
except Exception as e: except Exception as e:
print(f"Error verifying message with name: {str(e)}") logger.error(f"Error verifying message with name: {str(e)}", exc_info=True)
return False return False
@@ -1690,7 +1714,7 @@ def verifyMessage(address, signature, message):
return response['result'] return response['result']
return False return False
except Exception as e: except Exception as e:
print(f"Error verifying message: {str(e)}") logger.error(f"Error verifying message: {str(e)}", exc_info=True)
return False return False
# endregion # endregion
@@ -1804,7 +1828,7 @@ def checkPreRequisites() -> dict[str, bool]:
try: try:
# Check if git is installed # Check if git is installed
gitSubprocess = subprocess.run(["git", "-v"], capture_output=True, text=True,timeout=2) gitSubprocess = subprocess.run(["git", "--version"], capture_output=True, text=True,timeout=2)
if gitSubprocess.returncode == 0: if gitSubprocess.returncode == 0:
prerequisites["git"] = True prerequisites["git"] = True
except Exception: except Exception:
@@ -1835,7 +1859,7 @@ def hsdInit():
prerequisites = checkPreRequisites() prerequisites = checkPreRequisites()
minNodeVersion = HSD_CONFIG.get("minNodeVersion", 20) minNodeVersion = HSD_CONFIG.get("minNodeVersion", 20)
minNPMVersion = HSD_CONFIG.get("minNPMVersion", 8) minNPMVersion = HSD_CONFIG.get("minNpmVersion", 8)
PREREQ_MESSAGES = { PREREQ_MESSAGES = {
"node": f"Install Node.js from https://nodejs.org/en/download (Version >= {minNodeVersion})", "node": f"Install Node.js from https://nodejs.org/en/download (Version >= {minNodeVersion})",
"npm": f"Install npm (version >= {minNPMVersion}) - usually comes with Node.js", "npm": f"Install npm (version >= {minNPMVersion}) - usually comes with Node.js",
@@ -1844,18 +1868,21 @@ def hsdInit():
# Check if all prerequisites are met (except hsd) # Check if all prerequisites are met (except hsd)
if not all(prerequisites[key] for key in prerequisites if key != "hsd"): if not all(prerequisites[key] for key in prerequisites if key != "hsd"):
print("HSD Internal Node prerequisites not met:") print("HSD Internal Node prerequisites not met:",flush=True)
logger.error("HSD Internal Node prerequisites not met:")
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.",flush=True) print(f" - {key} is missing or does not meet the version requirement.",flush=True)
logger.error(f" - {key} is missing or does not meet the version requirement.")
if key in PREREQ_MESSAGES: if key in PREREQ_MESSAGES:
print(PREREQ_MESSAGES[key],flush=True) print(PREREQ_MESSAGES[key],flush=True)
logger.error(PREREQ_MESSAGES[key])
exit(1) exit(1)
return return
# Check if hsd is installed # Check if hsd is installed
if not prerequisites["hsd"]: if not prerequisites["hsd"]:
print("HSD not found, installing...") logger.info("HSD not found, installing...")
# If hsd folder exists, remove it # If hsd folder exists, remove it
if os.path.exists("hsd"): if os.path.exists("hsd"):
os.rmdir("hsd") os.rmdir("hsd")
@@ -1863,19 +1890,22 @@ def hsdInit():
# Clone hsd repo # 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) 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: if gitClone.returncode != 0:
print("Failed to clone hsd repository:") print("Failed to clone hsd repository:",flush=True)
print(gitClone.stderr) logger.error("Failed to clone hsd repository:")
print(gitClone.stderr,flush=True)
logger.error(gitClone.stderr)
exit(1) exit(1)
print("Cloned hsd repository.") logger.info("Cloned hsd repository.")
logger.info("Installing hsd dependencies...")
# Install hsd dependencies # Install hsd dependencies
print("Installing hsd dependencies...")
npmInstall = subprocess.run(["npm", "install"], cwd="hsd", capture_output=True, text=True) npmInstall = subprocess.run(["npm", "install"], cwd="hsd", capture_output=True, text=True)
if npmInstall.returncode != 0: if npmInstall.returncode != 0:
print("Failed to install hsd dependencies:") print("Failed to install hsd dependencies:",flush=True)
print(npmInstall.stderr) logger.error("Failed to install hsd dependencies:")
print(npmInstall.stderr,flush=True)
logger.error(npmInstall.stderr)
exit(1) exit(1)
print("Installed hsd dependencies.") logger.info("Installed hsd dependencies.")
def hsdStart(): def hsdStart():
global HSD_PROCESS global HSD_PROCESS
global SPV_MODE global SPV_MODE
@@ -1886,12 +1916,12 @@ def hsdStart():
if os.path.exists("hsd.lock"): if os.path.exists("hsd.lock"):
lock_time = os.path.getmtime("hsd.lock") lock_time = os.path.getmtime("hsd.lock")
if time.time() - lock_time < 30: if time.time() - lock_time < 30:
print("HSD was started recently, skipping start.") logger.info("HSD was started recently, skipping start.")
return return
else: else:
os.remove("hsd.lock") os.remove("hsd.lock")
print("Starting HSD...") logger.info("Starting HSD...")
# Create a lock file # Create a lock file
with open("hsd.lock", "w") as f: with open("hsd.lock", "w") as f:
f.write(str(time.time())) f.write(str(time.time()))
@@ -1900,7 +1930,7 @@ 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")) prefix = HSD_CONFIG.get("prefix", os.path.join(os.getcwd(), "hsd_data"))
# Base command # Base command
@@ -1929,13 +1959,17 @@ def hsdStart():
cmd.append(flag) cmd.append(flag)
# Launch process # Launch process
HSD_PROCESS = subprocess.Popen( try:
cmd, HSD_PROCESS = subprocess.Popen(
cwd=os.getcwd(), cmd,
text=True cwd=os.getcwd(),
) text=True
)
print(f"HSD started with PID {HSD_PROCESS.pid}") logger.info(f"HSD started with PID {HSD_PROCESS.pid}")
except Exception as e:
logger.error(f"Failed to start HSD: {str(e)}", exc_info=True)
return
atexit.register(hsdStop) atexit.register(hsdStop)
@@ -1944,9 +1978,22 @@ def hsdStart():
signal.signal(signal.SIGINT, lambda s, f: (hsdStop(), sys.exit(0))) signal.signal(signal.SIGINT, lambda s, f: (hsdStop(), sys.exit(0)))
signal.signal(signal.SIGTERM, lambda s, f: (hsdStop(), sys.exit(0))) signal.signal(signal.SIGTERM, lambda s, f: (hsdStop(), sys.exit(0)))
except Exception as e: except Exception as e:
print(f"Failed to set signal handlers: {str(e)}") logger.error(f"Failed to set signal handlers: {str(e)}", exc_info=True)
pass pass
def hsdRunning() -> bool:
global HSD_PROCESS
if not HSD_INTERNAL_NODE:
return False
if HSD_PROCESS is None:
return False
# Check if process has terminated
poll_result = HSD_PROCESS.poll()
if poll_result is not None:
logger.error(f"HSD process has terminated with exit code: {poll_result}")
return False
return True
def hsdStop(): def hsdStop():
global HSD_PROCESS global HSD_PROCESS
@@ -1954,16 +2001,16 @@ def hsdStop():
if HSD_PROCESS is None: if HSD_PROCESS is None:
return return
print("Stopping HSD...") logger.info("Stopping HSD...")
# Send SIGINT (like Ctrl+C) # Send SIGINT (like Ctrl+C)
HSD_PROCESS.send_signal(signal.SIGINT) HSD_PROCESS.send_signal(signal.SIGINT)
try: try:
HSD_PROCESS.wait(timeout=10) # wait for graceful exit HSD_PROCESS.wait(timeout=10) # wait for graceful exit
print("HSD shut down cleanly.") logger.info("HSD shut down cleanly.")
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
print("HSD did not exit yet, is it alright???") logger.warning("HSD did not exit yet, is it alright???")
# Clean up lock file # Clean up lock file
if os.path.exists("hsd.lock"): if os.path.exists("hsd.lock"):
@@ -1976,7 +2023,6 @@ def hsdRestart():
time.sleep(2) time.sleep(2)
hsdStart() hsdStart()
hsdInit() hsdInit()
hsdStart() hsdStart()
# endregion # endregion

View File

@@ -4,10 +4,8 @@ services:
ports: ports:
- "5000:5000" - "5000:5000"
volumes: volumes:
- hsd_data:/app/hsd-data - hsd_data:/app/hsd_data
- user_data:/app/user_data - user_data:/app/user_data
environment:
- INTERNAL_HSD=true
volumes: volumes:
hsd_data: hsd_data:

View File

@@ -5,3 +5,4 @@ SHOW_EXPIRED=false
EXPLORER_TX=https://shakeshift.com/transaction/ EXPLORER_TX=https://shakeshift.com/transaction/
DISABLE_WALLETDNS=false DISABLE_WALLETDNS=false
INTERNAL_HSD=false INTERNAL_HSD=false
LOG_LEVEL=WARNING

81
install.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
install_command=""
# Check if currently in the FireWalletBrowser directory
if [ -f "server.py" ]; then
echo "Please run this script from outside the FireWalletBrowser directory."
exit 1
fi
echo "Starting installation of FireWalletBrowser..."
# Check if OS is using apt package manager (Debian/Ubuntu)
if command -v apt-get &> /dev/null; then
install_command="sudo apt-get install -y"
dependencies=(git curl wget python3 python3-pip python3-venv)
echo "Detected apt package manager."
# Check if OS is using pacman package manager (Arch Linux)
elif command -v pacman &> /dev/null; then
install_command="sudo pacman -S"
dependencies=(git curl wget python3 python-pip)
echo "Detected pacman package manager."
else
echo "Unsupported package manager. Please install dependencies manually."
exit 1
fi
# List of dependencies to install
# Install dependencies
for package in "${dependencies[@]}"; do
# Check if the package is already installed
if command -v $package &> /dev/null || dpkg -s $package &> /dev/null || pacman -Qi $package &> /dev/null; then
echo "$package is already installed."
continue
fi
echo "Installing $package..."
$install_command $package
# Check if the installation was successful
if [ $? -ne 0 ]; then
echo "Failed to install $package. Please check your package manager settings."
exit 1
fi
done
if ! command -v node &> /dev/null || ! command -v npm &> /dev/null; then
echo "Installing Node.js and npm..."
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"
nvm install 20
if [ $? -ne 0 ]; then
echo "Failed to install Node.js and npm. Please install them manually."
exit 1
fi
else
echo "Node.js and npm are already installed."
fi
# Clone repo
git clone https://git.woodburn.au/nathanwoodburn/firewalletbrowser.git
# Setup venv
cd firewalletbrowser || exit 1
python3 -m venv .venv
source .venv/bin/activate
# Install python dependencies
python3 -m pip install -r requirements.txt
# Write .env file
if [ ! -f ".env" ]; then
echo "Creating .env file..."
echo "INTERNAL_HSD=true" > .env
echo "Created .env file with INTERNAL Node enabled."
fi
echo "Installation complete. You can start the application by running ./start.sh"

436
main.py
View File

@@ -1,60 +1,58 @@
import io import io
import json import json
import random import random
import sqlite3
import sys import sys
from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory,send_file from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory,send_file
import os import os
import dotenv import dotenv
import requests import requests
import account as account_module
import render
import re import re
from flask_qrcode import QRcode from flask_qrcode import QRcode
import domainLookup
import urllib.parse import urllib.parse
import plugin as plugins_module
import gitinfo import gitinfo
import datetime import datetime
import time import time
import logging
from logging.handlers import RotatingFileHandler
dotenv.load_dotenv() dotenv.load_dotenv()
# Setup logging
if not os.path.exists('logs'):
os.makedirs('logs')
log_file = 'logs/firewallet.log'
handler = RotatingFileHandler(log_file, maxBytes=1024*1024, backupCount=3)
formatter = logging.Formatter('[%(asctime)s] %(levelname)s in %(module)s: %(message)s')
handler.setFormatter(formatter)
logger = logging.getLogger()
log_level = os.getenv("LOG_LEVEL", "WARNING").upper()
if log_level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
logger.setLevel(getattr(logging, log_level))
else:
logger.setLevel(logging.WARNING)
# Disable werkzeug logging
logging.getLogger('werkzeug').setLevel(logging.INFO)
logging.getLogger("urllib3").setLevel(logging.ERROR)
logging.getLogger("requests").setLevel(logging.ERROR)
logger.addHandler(handler)
# Import after logger setup to capture logs from these modules
import render # noqa: E402
import domainLookup # noqa: E402
import account as account_module # noqa: E402
import plugin as plugins_module # noqa: E402
app = Flask(__name__) app = Flask(__name__)
qrcode = QRcode(app) qrcode = QRcode(app)
# Change this if network fees change # Change this if network fees change
fees = 0.02 fees = 0.02
revokeCheck = random.randint(100000,999999) revokeCheck = random.randint(100000,999999)
THEME = os.getenv("THEME", "black") THEME = os.getenv("THEME", "black")
def blocks_to_time(blocks: int) -> str:
"""
Convert blocks to time in a human-readable format.
Blocks are mined approximately every 10 minutes.
"""
if blocks < 0:
return "Invalid time"
if blocks < 6:
return f"{blocks * 10} mins"
elif blocks < 144:
hours = blocks // 6
minutes = (blocks % 6) * 10
if minutes == 0:
return f"{hours} hrs"
return f"{hours} hrs {minutes} mins"
else:
days = blocks // 144
hours = (blocks % 144) // 6
if hours == 0:
return f"{days} days"
return f"{days} days {hours} hrs"
@app.route('/') @app.route('/')
def index(): def index():
# Check if the user is logged in # Check if the user is logged in
@@ -75,14 +73,13 @@ def index():
if not os.path.exists(".git"): if not os.path.exists(".git"):
return render_template("index.html", account=account, plugins=plugins) return render_template("index.html", account=account, plugins=plugins)
info = gitinfo.get_git_info() alerts = get_alerts(account)
if info is None: for alert in alerts:
return render_template("index.html", account=account, plugins=plugins) output_html = alert['output']
branch = info['refs'] if 'id' in alert:
commit = info['commit'] # Add a dismiss button
if commit != latestVersion(branch): output_html += f"&nbsp<a href='/dismiss/{alert['id']}' class='btn btn-secondary btn-sm' style='margin:none;'>Dismiss</a>"
print("New version available",flush=True) plugins += render_template('components/dashboard-alert.html', name=alert['name'], output=output_html)
plugins += render_template('components/dashboard-alert.html', name='Update', output='A new version of FireWallet is available')
return render_template("index.html", account=account, plugins=plugins) return render_template("index.html", account=account, plugins=plugins)
@@ -320,11 +317,12 @@ def auctions():
sort_time_next = reverseDirection(direction) sort_time_next = reverseDirection(direction)
# If older HSD version sort by domain height # If older HSD version sort by domain height
if bids[0]['height'] == 0: if len(bids) > 0:
domains = sorted(domains, key=lambda k: k['height'],reverse=reverse) if bids[0]['height'] == 0:
sortbyDomain = True domains = sorted(domains, key=lambda k: k['height'],reverse=reverse)
else: sortbyDomain = True
bids = sorted(bids, key=lambda k: k['height'],reverse=reverse) else:
bids = sorted(bids, key=lambda k: k['height'],reverse=reverse)
else: else:
# Sort by domain # Sort by domain
bids = sorted(bids, key=lambda k: k['name'],reverse=reverse) bids = sorted(bids, key=lambda k: k['name'],reverse=reverse)
@@ -605,7 +603,7 @@ def finalize(domain: str):
domain = domain.lower() domain = domain.lower()
response = account_module.finalize(request.cookies.get("account"),domain) response = account_module.finalize(request.cookies.get("account"),domain)
if response['error'] is not None: if response['error'] is not None:
print(response) logger.error(f"Error finalizing transfer for {domain}: {response['error']}")
return redirect("/manage/" + domain + "?error=" + response['error']['message']) return redirect("/manage/" + domain + "?error=" + response['error']['message'])
return redirect("/success?tx=" + response['result']['hash']) return redirect("/success?tx=" + response['result']['hash'])
@@ -624,7 +622,7 @@ def cancelTransfer(domain: str):
response = account_module.cancelTransfer(request.cookies.get("account"),domain) response = account_module.cancelTransfer(request.cookies.get("account"),domain)
if 'error' in response: if 'error' in response:
if response['error'] is not None: if response['error'] is not None:
print(response) logger.error(f"Error canceling transfer for {domain}: {response['error']}")
return redirect("/manage/" + domain + "?error=" + response['error']['message']) return redirect("/manage/" + domain + "?error=" + response['error']['message'])
return redirect("/success?tx=" + response['result']['hash']) return redirect("/success?tx=" + response['result']['hash'])
@@ -680,7 +678,7 @@ def revokeConfirm(domain: str):
response = account_module.revoke(request.cookies.get("account"),domain) response = account_module.revoke(request.cookies.get("account"),domain)
if 'error' in response: if 'error' in response:
if response['error'] is not None: if response['error'] is not None:
print(response) logger.error(f"Error revoking {domain}: {response['error']}")
return redirect("/manage/" + domain + "?error=" + response['error']['message']) return redirect("/manage/" + domain + "?error=" + response['error']['message'])
return redirect(f"/success?tx={response['hash']}") return redirect(f"/success?tx={response['hash']}")
@@ -786,7 +784,7 @@ def editSave(domain: str):
dns = urllib.parse.unquote(dns) dns = urllib.parse.unquote(dns)
response = account_module.setDNS(request.cookies.get("account"),domain,dns) response = account_module.setDNS(request.cookies.get("account"),domain,dns)
if 'error' in response: if 'error' in response:
print(response) logger.error(f"Error setting DNS for {domain}: {response['error']}")
return redirect(f"/manage/{domain}/edit?dns={raw_dns}&error={response['error']}") return redirect(f"/manage/{domain}/edit?dns={raw_dns}&error={response['error']}")
return redirect(f"/success?tx={response['hash']}") return redirect(f"/success?tx={response['hash']}")
@@ -884,7 +882,6 @@ def transferConfirm(domain):
return redirect(f"/success?tx={response['hash']}") return redirect(f"/success?tx={response['hash']}")
@app.route('/auction/<domain>') @app.route('/auction/<domain>')
def auction(domain): def auction(domain):
# Check if the user is logged in # Check if the user is logged in
@@ -933,13 +930,11 @@ def auction(domain):
if state == 'CLOSED': if state == 'CLOSED':
if not domainInfo['info']['registered']: if not domainInfo['info']['registered']:
if account_module.isOwnDomain(account,domain): if account_module.isOwnDomain(account,domain):
print("Waiting to be registered")
state = 'PENDING REGISTER' state = 'PENDING REGISTER'
next = "Pending Register" next = "Pending Register"
next_action = f'<a href="/auction/{domain}/register">Register Domain</a>' next_action = f'<a href="/auction/{domain}/register">Register Domain</a>'
else: else:
print("Not registered")
state = 'AVAILABLE' state = 'AVAILABLE'
next = "Available Now" next = "Available Now"
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>' next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
@@ -1241,7 +1236,33 @@ def settings_action(action):
title="API Information", title="API Information",
content=content) content=content)
if action == "logs":
if not os.path.exists(log_file):
return redirect("/settings?error=No log file found")
# Check if log file is empty
if os.path.getsize(log_file) == 0:
return redirect("/settings?error=Log file is empty")
try:
with open(log_file, 'rb') as f:
response = requests.put(f"https://upload.woodburn.au/{os.path.basename(log_file)}", data=f,
headers={"Max-Days": "5"})
if response.status_code == 200 or response.status_code == 201:
token = response.text.strip().split('\n')[-1].split('/')[-2:]
url = f"https://upload.woodburn.au/inline/{token[0]}/{token[1]}"
logger.info(f"Log upload successful: {url}")
return redirect(url)
else:
logger.error(f"Failed to upload log: {response.status_code} {response.text}")
return redirect(f"/settings?error=Failed to upload log: {response.status_code}")
except Exception as e:
logger.error(f"Exception during log upload: {e}", exc_info=True)
return redirect("/settings?error=An error occurred during log upload")
logger.warning(f"Unknown settings action: {action}")
return redirect("/settings?error=Invalid action") return redirect("/settings?error=Invalid action")
@app.route('/settings/upload', methods=['POST']) @app.route('/settings/upload', methods=['POST'])
@@ -1430,7 +1451,7 @@ def import_wallet():
name=account,password=password,password_repeat=repeatPassword, name=account,password=password,password_repeat=repeatPassword,
seed=seed) seed=seed)
add_alert("Rescan needed", "Please rescan the wallet after importing to see all transactions", account)
# Set the cookie # Set the cookie
response = make_response(redirect("/")) response = make_response(redirect("/"))
response.set_cookie("account", account+":"+password) response.set_cookie("account", account+":"+password)
@@ -1484,7 +1505,7 @@ def plugin(ptype,plugin):
plugin = f"{ptype}/{plugin}" plugin = f"{ptype}/{plugin}"
if not plugins_module.pluginExists(plugin): if not plugins_module.pluginExists(plugin):
print(f"Plugin {plugin} not found") logger.warning(f"Plugin not found: {plugin}")
return redirect("/plugins") return redirect("/plugins")
data = plugins_module.getPluginData(plugin) data = plugins_module.getPluginData(plugin)
@@ -1586,13 +1607,6 @@ def plugin_function(ptype,plugin,function):
#region API Routes #region API Routes
@app.route('/api/v1/hsd/<function>', methods=["GET"]) @app.route('/api/v1/hsd/<function>', methods=["GET"])
def api_hsd(function): def api_hsd(function):
# Check if the user is logged in
if request.cookies.get("account") is None:
return jsonify({"error": "Not logged in"})
account = account_module.check_account(request.cookies.get("account"))
if not account:
return jsonify({"error": "Invalid account"})
if function == "sync": if function == "sync":
return jsonify({"result": account_module.getNodeSync()}) return jsonify({"result": account_module.getNodeSync()})
@@ -1602,8 +1616,22 @@ def api_hsd(function):
return jsonify({"result": account_module.getBlockHeight()}) return jsonify({"result": account_module.getBlockHeight()})
if function == "mempool": if function == "mempool":
return jsonify({"result": account_module.getMempoolTxs()}) return jsonify({"result": account_module.getMempoolTxs()})
if function == "mempoolBids":
# Check if the user is logged in for all other functions
account = None
if request.cookies.get("account") is not None:
account = account_module.check_account(request.cookies.get("account"))
# Allow login using http basic auth
if account is None and request.authorization is not None:
account = account_module.check_account(f"{request.authorization.username}:{request.authorization.password}")
if not account:
return jsonify({"error": "Not logged in"})
if function == "mempoolBids": # This is a heavy function so only allow for logged in users
return jsonify({"result": account_module.getMempoolBids()}) return jsonify({"result": account_module.getMempoolBids()})
if function == "nextAuctionState": if function == "nextAuctionState":
# Get the domain from the query parameters # Get the domain from the query parameters
domain = request.args.get('domain') domain = request.args.get('domain')
@@ -1619,13 +1647,11 @@ def api_hsd(function):
if state == 'CLOSED': if state == 'CLOSED':
if not domainInfo['info']['registered']: if not domainInfo['info']['registered']:
if account_module.isOwnDomain(account,domain): if account_module.isOwnDomain(account,domain):
print("Waiting to be registered")
state = 'PENDING REGISTER' state = 'PENDING REGISTER'
next = "Pending Register" next = "Pending Register"
next_action = f'<a href="/auction/{domain}/register">Register Domain</a>' next_action = f'<a href="/auction/{domain}/register">Register Domain</a>'
else: else:
print("Not registered")
state = 'AVAILABLE' state = 'AVAILABLE'
next = "Available Now" next = "Available Now"
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>' next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
@@ -1689,20 +1715,29 @@ def api_hsd_mobile(function):
@app.route('/api/v1/wallet/<function>', methods=["GET"]) @app.route('/api/v1/wallet/<function>', methods=["GET"])
def api_wallet(function): def api_wallet(function):
# Check if the user is logged in
if request.cookies.get("account") is None:
return jsonify({"error": "Not logged in"})
account = account_module.check_account(request.cookies.get("account"))
if not account:
return jsonify({"error": "Invalid account"})
password = request.cookies.get("account","").split(":")[1]
if not account:
return jsonify({"error": "Invalid account"})
if function == "sync": if function == "sync":
return jsonify({"result": account_module.getWalletStatus()}) # Check if arg verbose is set
verbose = request.args.get('verbose', 'false').lower() == 'true'
return jsonify({"result": account_module.getWalletStatus(verbose)})
# Check if the user is logged in for all other functions
account = None
password = None
if request.cookies.get("account") is not None:
account = account_module.check_account(request.cookies.get("account"))
password = request.cookies.get("account","").split(":")[1]
# Allow login using http basic auth
if account is None and request.authorization is not None:
account = account_module.check_account(f"{request.authorization.username}:{request.authorization.password}")
password = request.authorization.password
if not account:
return jsonify({"error": "Not logged in"})
if function == "balance":
return jsonify({"result": account_module.getBalance(account)})
if function == "available": if function == "available":
return jsonify({"result": account_module.getBalance(account)['available']}) return jsonify({"result": account_module.getBalance(account)['available']})
@@ -1849,14 +1884,71 @@ def api_icon(account):
def api_status(): def api_status():
# This doesn't require a login # This doesn't require a login
# Check if the node is connected # Check if the node is connected
if not account_module.hsdConnected(): node_status = {
return jsonify({"status":503,"error": "Node not connected"}), 503 "connected": account_module.hsdConnected(),
return jsonify({"status": 200,"result": "FireWallet is running"}) "internal": account_module.HSD_INTERNAL_NODE,
"internal_running": False,
"version": "N/A"
}
status = 200
error = None
node_status['version'] = account_module.hsdVersion(False)
if node_status['internal']:
node_status['internal_running'] = account_module.hsdRunning()
# If the node is not connected, return an error
if not node_status['connected']:
error = "Node not connected"
status = 503
if node_status['version'] == -1:
error = "Error connecting to HSD"
status = 503
if node_status['internal'] and not node_status['internal_running']:
error = "Internal node not running"
status = 503
commit = currentCurrentCommit()
return jsonify({
"node": node_status,
"version": {
"commit": commit,
"branch": currentCurrentBranch(),
"latest": runningLatestVersion(),
"url": f"https://git.woodburn.au/nathanwoodburn/firewalletbrowser/commit/{commit}" if commit != "Error" else None
},
"error": error,
"status": status
}), status
#endregion #endregion
#region Helper functions #region Helper functions
def blocks_to_time(blocks: int) -> str:
"""
Convert blocks to time in a human-readable format.
Blocks are mined approximately every 10 minutes.
"""
if blocks < 0:
return "Invalid time"
if blocks < 6:
return f"{blocks * 10} mins"
elif blocks < 144:
hours = blocks // 6
minutes = (blocks % 6) * 10
if minutes == 0:
return f"{hours} hrs"
return f"{hours} hrs {minutes} mins"
else:
days = blocks // 144
hours = (blocks % 144) // 6
if hours == 0:
return f"{days} days"
return f"{days} days {hours} hrs"
def renderDomain(name: str) -> str: def renderDomain(name: str) -> str:
""" """
@@ -1873,6 +1965,160 @@ def renderDomain(name: str) -> str:
except Exception: except Exception:
return f"{name}/" return f"{name}/"
def currentCurrentCommit() -> str:
"""
Get the current commit of the application.
"""
if not os.path.exists(".git"):
return "Error"
info = gitinfo.get_git_info()
if info is None:
return "Error"
commit = info['commit']
return commit
def currentCurrentBranch() -> str:
"""
Get the current branch of the application.
"""
if not os.path.exists(".git"):
return "Error"
info = gitinfo.get_git_info()
if info is None:
return "Error"
branch = info['refs']
return branch
def runningLatestVersion() -> bool:
"""
Check if the current version is the latest version.
"""
if not os.path.exists(".git"):
return False
info = gitinfo.get_git_info()
if info is None:
return False
branch = info['refs']
commit = info['commit']
if commit != latestVersion(branch):
return False
return True
def get_alerts(account:str) -> list:
"""
Get alerts to show on the dashboard.
"""
alerts = []
info = gitinfo.get_git_info()
if info is not None:
branch = info['refs']
commit = info['commit']
if commit != latestVersion(branch):
logger.info("New version available")
alerts.append({
"name": "Update Available",
"output": f"A new version of FireWallet is available. <a href='https://git.woodburn.au/nathanwoodburn/firewalletbrowser/compare/{commit}...{branch}' target='_blank'>Changelog</a>"
})
# Check if the node is connected
if not account_module.hsdConnected():
alerts.append({
"name": "Node",
"output": "HSD node is not connected. Please check your settings."
})
return alerts
# Check if the wallet is synced
wallet_status = account_module.getWalletStatus()
if wallet_status != "Ready":
alerts.append({
"name": "Wallet Not Synced",
"output": "Please wait for it to sync."
})
# Try to read from notifications sqlite database
if os.path.exists("user_data/notifications.db"):
try:
conn = sqlite3.connect("user_data/notifications.db")
c = conn.cursor()
c.execute("SELECT id, name, message FROM notifications WHERE read=0 AND (account=? OR account='all')", (account,))
rows = c.fetchall()
for row in rows:
alerts.append({
"id": row[0],
"name": row[1],
"output": row[2]
})
conn.close()
except Exception as e:
logger.error(f"Error reading notifications: {e}")
pass
return alerts
def add_alert(name:str,output:str,account:str="all"):
"""
Add an alert to the notifications database.
name: Name of the alert
output: Message of the alert
account: Account to add the alert for (default: all)
"""
if not os.path.exists("user_data/notifications.db"):
conn = sqlite3.connect("user_data/notifications.db")
c = conn.cursor()
c.execute("CREATE TABLE notifications (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, message TEXT, account TEXT, read INTEGER DEFAULT 0)")
conn.commit()
conn.close()
try:
conn = sqlite3.connect("user_data/notifications.db")
c = conn.cursor()
c.execute("INSERT INTO notifications (name, message, account) VALUES (?, ?, ?)", (name, output, account))
conn.commit()
conn.close()
except Exception as e:
logger.error(f"Error adding notification: {e}")
pass
def dismiss_alert(alert_id:int,account:str="all"):
"""
Mark an alert as read.
alert_id: ID of the alert to dismiss
account: Account to dismiss the alert for (default: all)
"""
if not os.path.exists("user_data/notifications.db"):
return
try:
conn = sqlite3.connect("user_data/notifications.db")
c = conn.cursor()
c.execute("UPDATE notifications SET read=1 WHERE id=?", (alert_id,))
conn.commit()
conn.close()
except Exception as e:
logger.error(f"Error dismissing notification: {e}")
pass
@app.route('/dismiss/<int:alert_id>')
@app.route('/api/v1/dismiss/<int:alert_id>')
def dismiss_alert_route(alert_id):
# 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")
dismiss_alert(alert_id,account)
return redirect(request.referrer or "/")
#endregion #endregion
@@ -1910,18 +2156,40 @@ def try_path(path):
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
logger.warning(f"404 Not Found: {request.path}")
account = account_module.check_account(request.cookies.get("account")) account = account_module.check_account(request.cookies.get("account"))
return render_template('404.html',account=account), 404 return render_template('404.html',account=account), 404
#endregion #endregion
if __name__ == '__main__': if __name__ == '__main__':
#TODO add parsing to allow for custom port and host host = '127.0.0.1'
# Check to see if --debug is in the command line arguments port = 5000
# Check if --host is in the command line arguments
if "--host" in sys.argv:
host_index = sys.argv.index("--host") + 1
if host_index < len(sys.argv):
host = sys.argv[host_index]
# Check if --port is in the command line arguments
if "--port" in sys.argv:
port_index = sys.argv.index("--port") + 1
if port_index < len(sys.argv):
try:
port = int(sys.argv[port_index])
except ValueError:
pass
print(f"Starting FireWallet on http://{host}:{port}",flush=True)
if "--debug" in sys.argv: if "--debug" in sys.argv:
app.run(debug=True) console_handler = logging.StreamHandler(sys.stdout)
# Use a simple format for console
console_formatter = logging.Formatter('%(message)s')
console_handler.setFormatter(console_formatter)
console_handler.setLevel(logging.WARNING)
logger.addHandler(console_handler)
logger.setLevel(logging.DEBUG)
app.run(debug=True, host=host, port=port)
else: else:
app.run() app.run(host=host, port=port)
def tests(): def tests():
assert blocks_to_time(6) == "1 hrs" assert blocks_to_time(6) == "1 hrs"

9
start.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Find if .venv exists
if [ -d ".venv" ]; then
echo "Virtual environment found. Activating..."
source .venv/bin/activate
fi
python3 server.py

View File

@@ -110,7 +110,7 @@
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">
<div><a class="btn btn-primary stick-right" role="button" href="/settings/xpub">xPub</a> <div><a class="btn btn-primary stick-right" role="button" href="/settings/xpub">xPub</a>
<h3>xPub Key</h3><span>Get your xPub key</span> <h3>xPub Key</h3><span>View your Extended Public (xPub) key</span>
</div> </div>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
@@ -133,7 +133,7 @@
<div class="card-body"> <div class="card-body">
<h4 class="card-title">About</h4> <h4 class="card-title">About</h4>
<h6 class="text-muted mb-2 card-subtitle">FireWallet is a UI to allow easy connection with HSD created by <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a> and freely available. Please contact him <a href="https://l.woodburn.au/contact" target="_blank">here</a> if you would like to request any features or report any bugs.<br>FireWallet version: <code>{{version}}</code></h6> <h6 class="text-muted mb-2 card-subtitle">FireWallet is a UI to allow easy connection with HSD created by <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a> and freely available. Please contact him <a href="https://l.woodburn.au/contact" target="_blank">here</a> if you would like to request any features or report any bugs.<br>FireWallet version: <code>{{version}}</code></h6>
<div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i>&nbsp;Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i>&nbsp;Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i>&nbsp;Donate to support development</a></div> <div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i>&nbsp;Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i>&nbsp;Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i>&nbsp;Donate to support development</a><a href="/settings/logs" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-help" style="color: var(--bs-emphasis-color);"></i>&nbsp;Upload logs for debugging</a></div>
</div> </div>
</div> </div>
</div> </div>