Compare commits
30 Commits
dev
...
fix/balanc
| Author | SHA1 | Date | |
|---|---|---|---|
|
c4cd2bc443
|
|||
|
608933c228
|
|||
|
d9e847a995
|
|||
|
15d919ca97
|
|||
|
aa52911823
|
|||
|
2ee294cab8
|
|||
|
19771fe30d
|
|||
|
12d3958b9d
|
|||
|
d20fc1eb55
|
|||
|
148e5f325a
|
|||
|
6442aa4df6
|
|||
|
2e86e64dd0
|
|||
|
7fc19a7f19
|
|||
|
eb6306bb83
|
|||
|
9f8daa8b88
|
|||
|
63e0f0b804
|
|||
|
0c17c4ad9b
|
|||
|
938fff8791
|
|||
|
155662d2b1
|
|||
|
97569faf0e
|
|||
|
59000afa87
|
|||
|
699a74f093
|
|||
|
6096f82c4d
|
|||
|
4353eb8fa4
|
|||
|
344cde07d0
|
|||
|
2fb841aeaf
|
|||
|
60df317f78
|
|||
|
4c1ea9fb12
|
|||
|
58ed636ce3
|
|||
|
e537c323c2
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.bsdesign filter=lfs diff=lfs merge=lfs -text
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,7 @@ cache/
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
hsd/
|
hsd/
|
||||||
hsd-data/
|
hsd_data/
|
||||||
|
logs/
|
||||||
hsd.lock
|
hsd.lock
|
||||||
hsdconfig.json
|
hsdconfig.json
|
||||||
|
|||||||
@@ -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.
13
README.md
13
README.md
@@ -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:
|
||||||
@@ -157,7 +168,7 @@ walletMigrate: <int> (for users migrating from older versions of HSD)
|
|||||||
|
|
||||||
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.
|
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`
|
HNS donations can be sent to: `hs1qh7uzytf2ftwkd9dmjjs7az9qfver5m7dd7x4ej`
|
||||||
Other donation options can be found at [my website](https://nathan.woodburn.au/donate)
|
Other donation options can be found at [my website](https://nathan.woodburn.au/donate)
|
||||||
|
|
||||||
Thank you for your support!
|
Thank you for your support!
|
||||||
|
|||||||
162
account.py
162
account.py
@@ -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:")
|
||||||
exit(1)
|
print(npmInstall.stderr,flush=True)
|
||||||
print("Installed hsd dependencies.")
|
logger.error(npmInstall.stderr)
|
||||||
|
exit(1)
|
||||||
|
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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ THEME=black
|
|||||||
SHOW_EXPIRED=false
|
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
81
install.sh
Executable 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"
|
||||||
442
main.py
442
main.py
@@ -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,17 +73,16 @@ 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" <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)
|
||||||
|
|
||||||
def reverseDirection(direction: str):
|
def reverseDirection(direction: str):
|
||||||
if direction == "⬆":
|
if direction == "⬆":
|
||||||
return "⬇"
|
return "⬇"
|
||||||
@@ -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,14 +1607,7 @@ 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()})
|
||||||
if function == "version":
|
if function == "version":
|
||||||
@@ -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,21 +1715,30 @@ 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:
|
if function == "sync":
|
||||||
|
# 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"})
|
return jsonify({"error": "Not logged in"})
|
||||||
|
|
||||||
account = account_module.check_account(request.cookies.get("account"))
|
if function == "balance":
|
||||||
if not account:
|
return jsonify({"result": account_module.getBalance(account)})
|
||||||
return jsonify({"error": "Invalid account"})
|
|
||||||
|
|
||||||
password = request.cookies.get("account","").split(":")[1]
|
|
||||||
if not account:
|
|
||||||
return jsonify({"error": "Invalid account"})
|
|
||||||
|
|
||||||
if function == "sync":
|
|
||||||
return jsonify({"result": account_module.getWalletStatus()})
|
|
||||||
|
|
||||||
if function == "available":
|
if function == "available":
|
||||||
return jsonify({"result": account_module.getBalance(account)['available']})
|
return jsonify({"result": account_module.getBalance(account)['available']})
|
||||||
if function == "total":
|
if function == "total":
|
||||||
@@ -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)
|
||||||
else:
|
# Use a simple format for console
|
||||||
app.run()
|
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:
|
||||||
|
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
9
start.sh
Executable 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
|
||||||
@@ -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> 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> 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> 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> 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> 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> 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> Upload logs for debugging</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user