69 Commits

Author SHA1 Message Date
59000afa87 feat: Cleanup function layouts for alerts 2025-09-09 17:34:36 +10:00
699a74f093 feat: Add api route to dismiss alert 2025-09-09 17:29:11 +10:00
6096f82c4d feat: Add an alert framework 2025-09-09 17:24:39 +10:00
4353eb8fa4 feat: Use new proxied script url 2025-09-04 22:05:16 +10:00
344cde07d0 feat: Add more info about the installation script to readme 2025-09-04 21:36:30 +10:00
2fb841aeaf fix: Update link for installation script to use raw 2025-09-04 21:34:19 +10:00
60df317f78 feat: Add install script to readme 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 2025-09-02 18:15:56 +10:00
812fc84d3e feat: Allow zapping txs of a different age 2025-09-02 17:58:40 +10:00
6d318a597b feat: Add API info to settings page
This is most useful when using an internal node with a random api key
2025-09-02 17:54:56 +10:00
83bd6b9643 feat: Reword workflow 2025-09-02 17:23:13 +10:00
c93b2652f5 Merge pull request 'Add CI workflow to test code with older versions of python' (#6) from feat/ci-testing into main
Reviewed-on: nathanwoodburn/firewalletbrowser#6
2025-09-02 16:08:35 +10:00
86e174c337 fix: Lint default plugins 2025-09-02 15:58:55 +10:00
e7b787c30b fix: Lint to follow ruff standards 2025-09-02 15:55:45 +10:00
997828795a feat: Add ruff linting 2025-09-02 15:48:21 +10:00
30de2d585e fix: Use single quote in sign message and reduce test versions 2025-09-02 15:28:39 +10:00
56eabfc1fc feat: Add some inital tests 2025-09-02 15:20:31 +10:00
e0f24267f5 test: Try a new container to run 2025-09-02 15:10:23 +10:00
2d51882d20 fix: Specify python minor version number 2025-09-02 15:00:40 +10:00
06b1eea9ef fix: Disable arm on testing workflow 2025-09-02 14:55:40 +10:00
d483cfdcfd feat: Add testing CI workflow 2025-09-02 14:49:14 +10:00
46ed0173d3 fix: Remove broken label 2025-08-29 23:18:49 +10:00
9dd50d1292 fix: Use full image name in compose 2025-08-29 23:17:10 +10:00
53148f573e revert: Use old manual install method 2025-08-29 23:11:47 +10:00
e8f052e0d1 feat: Try using matrix 2025-08-29 23:03:50 +10:00
7f450d620a revert: Use older checkout 2025-08-29 22:55:32 +10:00
41a1bc743f feat: Try new syntax for gitea action 2025-08-29 22:53:44 +10:00
30108e3bc5 fix: Extra backslash in CI/CD pipeline 2025-08-29 22:42:59 +10:00
a2dc9f43e3 feat: Add docker support for inbuilt HSD 2025-08-29 22:40:30 +10:00
1203719eac fix: Docker python version and fix reporting missing requirements 2025-08-29 13:28:12 +10:00
373a71f04d Merge pull request 'SPV support & add internal HSD node' (#4) from feat/internal_hsd into main
Reviewed-on: nathanwoodburn/firewalletbrowser#4
2025-08-29 13:04:30 +10:00
b76b873036 feat: Update readme 2025-08-28 17:50:47 +10:00
23e714fad8 feat: Add additional node info to settings 2025-08-28 17:27:45 +10:00
a36c69ecfc fix: Add SPV support for getDNS() 2025-08-28 17:18:52 +10:00
1fd9987bf1 feat: Upgrade tx page caches to a sqlite db 2025-08-28 17:13:23 +10:00
f2cda461ba feat: Add SPV features to fix accoutn balances 2025-08-28 16:42:12 +10:00
26c5b4a4fa feat: Update configuration storage and overrides 2025-08-26 18:04:07 +10:00
7fdc4a3122 fix: SPV causes some domains to not be recognized as owned by the wallet 2025-08-26 17:31:25 +10:00
5ff8960b7b feat: Add initial internal node option 2025-08-26 16:44:10 +10:00
4c84bc2bbe fix: Use hsd.hns.au to get name from namehash in order to only import account once 2025-08-26 15:26:19 +10:00
49e378803d fix: Use existing hsd from accounts module to get name from hash 2025-08-26 12:52:14 +10:00
1c53547047 feat: Add env flag to disable WALLET DNS record lookup 2025-08-25 18:10:03 +10:00
080c4402d8 Merge pull request 'Add WALLET DNS record for sending using domain alias' (#3) from feat/WALLETDNS into main
Reviewed-on: nathanwoodburn/firewalletbrowser#3
2025-08-25 13:59:15 +10:00
792688064e fix: More code cleanup 2025-08-25 12:43:12 +10:00
599c0df00c feat: Add more code cleanup 2025-08-25 12:36:11 +10:00
a619d78efd fix: Update types to make code more reliable 2025-08-25 12:28:26 +10:00
f090b7b71a feat: Add initial WALLET DNS record support 2025-08-25 11:55:15 +10:00
545a0b9475 fix: Add display for OPEN transactions in tx history 2025-08-18 11:33:47 +10:00
501091eeae Merge pull request 'feat/mempool-bids' (#2) from feat/mempool-bids into main
Reviewed-on: nathanwoodburn/firewalletbrowser#2
2025-07-17 16:54:38 +10:00
6911e3663c feat: Add mempool bids and dynamic loading 2025-07-16 18:44:18 +10:00
eda690544d feat: Add js to pull bids and auction state 2025-07-16 18:32:32 +10:00
e67c178ea7 feat: Add initial mempool bids function 2025-07-16 17:41:32 +10:00
631d558377 tmp: Add debugging for bids 2025-07-16 16:53:14 +10:00
1d5ed059b3 fix: Allow rescan while not in bidding 2025-07-16 16:30:08 +10:00
747ac575fa fix: Auto reset cache if incorrect format 2025-07-14 16:28:50 +10:00
e574933302 feat: Add link on names in tx history 2025-07-12 21:47:14 +10:00
c0f0dc5010 fix: Open tx for bid in new tab 2025-07-12 21:35:04 +10:00
060132bfec Merge pull request 'Update Auctions page to include more info and be easier to read' (#1) from dev into main
Reviewed-on: nathanwoodburn/firewalletbrowser#1
2025-07-12 15:10:56 +10:00
7bc1fad280 feat: Add bid sorting to auction page and add tx links 2025-07-12 14:13:40 +10:00
63e0b1c2f2 fix: Add new better method of validating domain owner 2025-07-12 13:50:30 +10:00
2fab7b3bc0 feat: Add info on when bidding closes 2025-07-12 13:28:21 +10:00
3fa57cc617 feat: Add time estimates to block times 2025-07-12 12:44:31 +10:00
4c3a738e43 feat: Cleanup urls in account module 2025-07-12 12:25:52 +10:00
988d03b48c fix: Update unknown owner message
Remove logging for DNS rendering
2025-07-12 12:03:36 +10:00
21043fc124 fix: Reveal from auction page crash
Reveal from the auction page had a missing function call
2025-07-12 11:56:22 +10:00
67e5276a13 feat: Cleanup any references to Niami
Niami has closed down so links to it no longer work
2025-07-07 12:43:55 +10:00
0164a9c3f2 fix: Remove Niami API requirement for searching for domains 2025-07-07 12:20:16 +10:00
31 changed files with 1910 additions and 521 deletions

32
.dockerignore Normal file
View File

@@ -0,0 +1,32 @@
.env
.env*
__pycache__/
templates/assets/css/styles.min.css
ignore/
plugins/signatures.json
.venv/
user_data/
customPlugins/
cache/
build/
dist/
hsd/
hsd-data/
hsd.lock
hsdconfig.json
Dockerfile
Dockerfile.hsd
FireWalletBrowser.bsdesign
LICENSE.md
README.md
docker-compose.yml
example.env
plugins.md

View File

@@ -4,11 +4,22 @@ on:
push: push:
jobs: jobs:
Build Image: Build Images:
runs-on: [ubuntu-latest, amd] runs-on: [ubuntu-latest, amd]
strategy:
matrix:
variant:
- target: default
tag_suffix: ""
dockerfile: "Dockerfile"
- target: hsd
tag_suffix: "-hsd"
dockerfile: "Dockerfile.hsd"
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install Docker - name: Install Docker
run : | run : |
apt-get install ca-certificates curl gnupg apt-get install ca-certificates curl gnupg
@@ -34,8 +45,8 @@ jobs:
fi fi
docker build -t firewallet:$tag_num . docker build --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg VCS_REF=$GITEA_SHA --build-arg VERSION=$GITEA_TAG --file ${{matrix.variant.dockerfile}} -t firewallet${{matrix.variant.tag_suffix}}:$tag_num .
docker tag firewallet:$tag_num git.woodburn.au/nathanwoodburn/firewallet:$tag_num docker tag firewallet${{matrix.variant.tag_suffix}}:$tag_num git.woodburn.au/nathanwoodburn/firewallet${{matrix.variant.tag_suffix}}:$tag_num
docker push git.woodburn.au/nathanwoodburn/firewallet:$tag_num docker push git.woodburn.au/nathanwoodburn/firewallet${{matrix.variant.tag_suffix}}:$tag_num
docker tag firewallet:$tag_num git.woodburn.au/nathanwoodburn/firewallet:$tag docker tag firewallet${{matrix.variant.tag_suffix}}:$tag_num git.woodburn.au/nathanwoodburn/firewallet${{matrix.variant.tag_suffix}}:$tag
docker push git.woodburn.au/nathanwoodburn/firewallet:$tag docker push git.woodburn.au/nathanwoodburn/firewallet${{matrix.variant.tag_suffix}}:$tag

40
.gitea/workflows/test.yml Normal file
View File

@@ -0,0 +1,40 @@
name: Tests and Linting
run-name: Python Compatibility and Linting tests
on:
push:
jobs:
Tests-Linting:
runs-on: [ubuntu-latest, amd]
container: catthehacker/ubuntu:act-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.13']
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then
pip install -r requirements.txt
fi
pip install pytest ruff
- name: Test with pytest
run: |
echo "Testing with Python ${{ matrix.python-version }}"
python -m pytest main.py
- name: Lint with ruff
run: |
echo "Linting with Python ${{ matrix.python-version }}"
ruff check

4
.gitignore vendored
View File

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

View File

@@ -1,7 +1,7 @@
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder FROM --platform=$BUILDPLATFORM python:3.13-alpine AS builder
WORKDIR /app WORKDIR /app
RUN apk add git openssl curl
COPY requirements.txt /app COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip3 install -r requirements.txt pip3 install -r requirements.txt
@@ -9,10 +9,21 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY . /app COPY . /app
# Add mount point for data volume # Add mount point for data volume
# VOLUME /data VOLUME /app/user_data
RUN apk add git openssl curl
ARG BUILD_DATE
ARG VCS_REF
LABEL org.opencontainers.image.title="FireWallet" \
org.opencontainers.image.description="The Handshake Wallet That is Fire" \
org.opencontainers.image.url="https://firewallet.au" \
org.opencontainers.image.source="https://git.woodburn.au/nathanwoodburn/firewalletbrowser" \
org.opencontainers.image.version="2.0.0" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.licenses="AGPL-3.0-only"
ENTRYPOINT ["python3"] ENTRYPOINT ["python3"]
CMD ["server.py"] CMD ["server.py"]
FROM builder as dev-envs FROM builder AS dev-envs

57
Dockerfile.hsd Normal file
View File

@@ -0,0 +1,57 @@
# ---- HSD build stage ----
FROM node:22-alpine AS hsd-build
WORKDIR /opt/hsd
RUN apk add --no-cache git bash unbound-dev gmp-dev g++ gcc make python3
RUN git clone --depth=1 --branch v8.0.0 https://github.com/handshake-org/hsd.git .
RUN npm install --omit=dev
# ---- Final stage ----
FROM python:3.13-alpine
WORKDIR /app
# Install runtime deps only
RUN apk add --no-cache unbound-dev gmp
# Copy node and npm from hsd-build stage
COPY --from=hsd-build /usr/local/bin/node /usr/local/bin/node
COPY --from=hsd-build /usr/local/lib/node_modules/npm /usr/local/lib/node
COPY --from=hsd-build /usr/local/bin/npm /usr/local/bin/npm
# Copy FireWallet dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy HSD from build stage
COPY --from=hsd-build /opt/hsd /app/hsd
# Copy FireWallet source
COPY . .
# Expose ports
EXPOSE 5000
# Optional HSD ports
# EXPOSE 12037
# EXPOSE 12039
ENV INTERNAL_HSD=true
ENV HSD_DOCKER_CONTAINER=true
ARG BUILD_DATE
ARG VCS_REF
LABEL org.opencontainers.image.title="FireWallet (HSD)" \
org.opencontainers.image.description="The Handshake Wallet That is Fire" \
org.opencontainers.image.url="https://firewallet.au" \
org.opencontainers.image.source="https://git.woodburn.au/nathanwoodburn/firewalletbrowser" \
org.opencontainers.image.version="2.0.0" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.licenses="AGPL-3.0-only"
VOLUME ["/app/hsd_data", "/app/user_data"]
ENTRYPOINT ["python3"]
CMD ["server.py"]

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
services:
firewallet:
image: git.woodburn.au/nathanwoodburn/firewallet-hsd:latest
ports:
- "5000:5000"
volumes:
- hsd_data:/app/hsd_data
- user_data:/app/user_data
volumes:
hsd_data:
user_data:

26
docker-compose.yml Normal file
View File

@@ -0,0 +1,26 @@
services:
hsd:
image: ghcr.io/handshake-org/hsd:8
volumes:
- hsd_data:/root/.hsd
environment:
- HSD_HTTP_HOST=0.0.0.0
- HSD_WALLET_HTTP_HOST=0.0.0.0
- HSD_LOG_LEVEL=error
- HSD_API_KEY=changeme
firewallet:
image: git.woodburn.au/nathanwoodburn/firewallet:latest
ports:
- "5000:5000"
environment:
- HSD_IP=hsd
- HSD_API=changeme
volumes:
- user_data:/app/user_data
volumes:
hsd_data:
user_data:

View File

@@ -6,10 +6,13 @@ import subprocess
import binascii import binascii
import datetime import datetime
import dns.asyncresolver import dns.asyncresolver
import dns.message
import dns.query
import dns.rdatatype
import httpx import httpx
from requests_doh import DNSOverHTTPSSession, add_dns_provider from requests_doh import DNSOverHTTPSSession, add_dns_provider
import requests
import urllib3 import urllib3
from cryptography.x509.oid import ExtensionOID
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Disable insecure request warnings (since we are manually verifying the certificate) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Disable insecure request warnings (since we are manually verifying the certificate)
@@ -56,7 +59,7 @@ def hip2(domain: str):
domains = [] domains = []
for ext in cert_obj.extensions: for ext in cert_obj.extensions:
if ext.oid == x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME: if ext.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
san_list = ext.value.get_values_for_type(x509.DNSName) san_list = ext.value.get_values_for_type(x509.DNSName)
domains.extend(san_list) domains.extend(san_list)
@@ -120,13 +123,39 @@ def hip2(domain: str):
print(f"Hip2: Lookup failed with error: {e}",flush=True) print(f"Hip2: Lookup failed with error: {e}",flush=True)
return "Hip2: Lookup failed." return "Hip2: Lookup failed."
def wallet_txt(domain: str, doh_url="https://hnsdoh.com/dns-query"):
with httpx.Client() as client:
q = dns.message.make_query(domain, dns.rdatatype.from_text("TYPE262"))
r = dns.query.https(q, doh_url, session=client)
if not r.answer:
return "No wallet address found for this domain"
wallet_record = "No WALLET record found"
for ans in r.answer:
raw = ans[0].to_wire() # type: ignore
try:
data = raw[1:].decode("utf-8", errors="ignore")
except UnicodeDecodeError:
return f"Unknown WALLET record format: {raw.hex()}"
if data.startswith("HNS:"):
wallet_record = data[4:]
break
elif data.startswith("HNS "):
wallet_record = data[4:]
break
elif data.startswith('"HNS" '):
wallet_record = data[6:].strip('"')
break
return wallet_record
def resolve_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"): def resolve_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
with httpx.Client() as client: with httpx.Client() as client:
q = dns.message.make_query(query_name, dns.rdatatype.A) q = dns.message.make_query(query_name, dns.rdatatype.A)
r = dns.query.https(q, doh_url, session=client) r = dns.query.https(q, doh_url, session=client)
ip = r.answer[0][0].address ip = r.answer[0][0].address # type: ignore
return ip return ip
def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"): def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
@@ -139,43 +168,14 @@ def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
return tlsa return tlsa
def niami_info(domain: str):
response = requests.get(f"https://api.niami.io/hsd/{domain}")
if response.status_code != 200:
return False
response = response.json()
if response["data"]["owner_tx_data"] is not None:
output = {
"owner": response["data"]["owner_tx_data"]["address"]
}
else:
output = {
"owner": None
}
if 'dnsData' in response["data"]:
output["dns"] = response["data"]["dnsData"]
else:
output["dns"] = []
transactions = requests.get(f"https://api.niami.io/txs/{domain}")
if transactions.status_code != 200:
return False
transactions = transactions.json()
output["txs"] = transactions["txs"]
return output
def emoji_to_punycode(emoji): def emoji_to_punycode(emoji):
try: try:
return emoji.encode("idna").decode("ascii") return emoji.encode("idna").decode("ascii")
except Exception as e: except Exception:
return emoji return emoji
def punycode_to_emoji(punycode): def punycode_to_emoji(punycode):
try: try:
return punycode.encode("ascii").decode("idna") return punycode.encode("ascii").decode("idna")
except Exception as e: except Exception:
return punycode return punycode

View File

@@ -3,3 +3,5 @@ HSD_IP=localhost
THEME=black THEME=black
SHOW_EXPIRED=false SHOW_EXPIRED=false
EXPLORER_TX=https://shakeshift.com/transaction/ EXPLORER_TX=https://shakeshift.com/transaction/
DISABLE_WALLETDNS=false
INTERNAL_HSD=false

View File

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

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"

589
main.py

File diff suppressed because it is too large Load Diff

View File

@@ -62,7 +62,8 @@ def listPlugins(update=False):
try: try:
with open("user_data/plugin_signatures.json", "r") as f: with open("user_data/plugin_signatures.json", "r") as f:
signatures = json.load(f) signatures = json.load(f)
except: except Exception as e:
print(f"Error loading plugin signatures: {e}")
# Write a new signatures file # Write a new signatures file
with open("user_data/plugin_signatures.json", "w") as f: with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f) json.dump(signatures, f)
@@ -87,7 +88,8 @@ def verifyPlugin(plugin: str):
try: try:
with open("user_data/plugin_signatures.json", "r") as f: with open("user_data/plugin_signatures.json", "r") as f:
signatures = json.load(f) signatures = json.load(f)
except: except Exception as e:
print(f"Error loading plugin signatures: {e}")
# Write a new signatures file # Write a new signatures file
with open("user_data/plugin_signatures.json", "w") as f: with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f) json.dump(signatures, f)
@@ -120,7 +122,8 @@ def getPluginData(pluginStr: str):
try: try:
with open("user_data/plugin_signatures.json", "r") as f: with open("user_data/plugin_signatures.json", "r") as f:
signatures = json.load(f) signatures = json.load(f)
except: except Exception as e:
print(f"Error loading plugin signatures: {e}")
# Write a new signatures file # Write a new signatures file
with open("user_data/plugin_signatures.json", "w") as f: with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f) json.dump(signatures, f)
@@ -148,11 +151,14 @@ def getPluginData(pluginStr: str):
def getPluginFunctions(plugin: str): def getPluginFunctions(plugin: str):
plugin = import_module(plugin.replace("/",".")) imported_plugin = import_module(plugin.replace("/","."))
return plugin.functions return imported_plugin.functions
def runPluginFunction(plugin: str, function: str, params: dict, authentication: str): def runPluginFunction(plugin: str, function: str, params: dict, authentication: (str|None)):
if not authentication:
return {"error": "Authentication required"}
plugin_module = import_module(plugin.replace("/",".")) plugin_module = import_module(plugin.replace("/","."))
if function not in plugin_module.functions: if function not in plugin_module.functions:
return {"error": "Function not found"} return {"error": "Function not found"}
@@ -168,7 +174,8 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
try: try:
with open("user_data/plugin_signatures.json", "r") as f: with open("user_data/plugin_signatures.json", "r") as f:
signatures = json.load(f) signatures = json.load(f)
except: except Exception as e:
print(f"Error loading plugin signatures: {e}")
# Write a new signatures file # Write a new signatures file
with open("user_data/plugin_signatures.json", "w") as f: with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f) json.dump(signatures, f)
@@ -189,13 +196,13 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
def getPluginFunctionInputs(plugin: str, function: str): def getPluginFunctionInputs(plugin: str, function: str):
plugin = import_module(plugin.replace("/",".")) imported_plugin = import_module(plugin.replace("/","."))
return plugin.functions[function]["params"] return imported_plugin.functions[function]["params"]
def getPluginFunctionReturns(plugin: str, function: str): def getPluginFunctionReturns(plugin: str, function: str):
plugin = import_module(plugin.replace("/",".")) imported_plugin = import_module(plugin.replace("/","."))
return plugin.functions[function]["returns"] return imported_plugin.functions[function]["returns"]
def getDomainFunctions(): def getDomainFunctions():

View File

@@ -1,4 +1,3 @@
import json
import account import account
import requests import requests
import threading import threading
@@ -127,7 +126,7 @@ def automations_background(authentication):
account_name = account.check_account(authentication) account_name = account.check_account(authentication)
password = ":".join(authentication.split(":")[1:]) password = ":".join(authentication.split(":")[1:])
if account_name == False: if not account_name:
return { return {
"error": { "error": {
"message": "Invalid account" "message": "Invalid account"

View File

@@ -1,7 +1,5 @@
import json
import account import account
import requests import requests
import os
@@ -384,7 +382,7 @@ def bid(params, authentication):
bid = float(params["bid"]) bid = float(params["bid"])
blind = float(params["blind"]) blind = float(params["blind"])
blind+=bid blind+=bid
except: except ValueError:
return { return {
"status":"Invalid bid amount", "status":"Invalid bid amount",
"transaction":None "transaction":None

View File

@@ -1,6 +1,4 @@
import json import json
import account
import requests
import os import os
# Plugin Data # Plugin Data

View File

@@ -1,7 +1,5 @@
import json
import account import account
import requests import requests
import os
# Plugin Data # Plugin Data
info = { info = {
@@ -90,7 +88,7 @@ def main(params, authentication):
return {"status": f"Failed: {batch['error']['message']}", "transaction": "None"} return {"status": f"Failed: {batch['error']['message']}", "transaction": "None"}
if 'result' in batch: if 'result' in batch:
if batch['result'] != None: if batch['result'] is not None:
tx = batch['result']['hash'] tx = batch['result']['hash']
return {"status": "Success", "transaction": tx} return {"status": "Success", "transaction": tx}
# Note only one batch can be sent at a time # Note only one batch can be sent at a time

View File

@@ -93,7 +93,7 @@ def status(params, authentication):
response = requests.post(f"https://{instance}/api", json=data, headers=headers) response = requests.post(f"https://{instance}/api", json=data, headers=headers)
if response.status_code != 200: if response.status_code != 200:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
if response.json()["success"] != True: if not response.json()["success"]:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
return {"status": f"Connected to {instance}"} return {"status": f"Connected to {instance}"}
@@ -110,7 +110,7 @@ def login(params, authentication):
if response.status_code != 200: if response.status_code != 200:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
if response.json()["success"] != True: if not response.json()["success"]:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
auth = { auth = {
@@ -146,7 +146,7 @@ def addDomain(params, authentication):
zones = requests.post(f"https://{instance}/api", json=data, headers=headers) zones = requests.post(f"https://{instance}/api", json=data, headers=headers)
if zones.status_code != 200: if zones.status_code != 200:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
if zones.json()["success"] != True: if not zones.json()["success"]:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
zones = zones.json()["data"] zones = zones.json()["data"]
@@ -169,7 +169,7 @@ def addDomain(params, authentication):
response = requests.post(f"https://{instance}/api", json=data, headers=headers) response = requests.post(f"https://{instance}/api", json=data, headers=headers)
if response.status_code != 200: if response.status_code != 200:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
if response.json()["success"] != True: if not response.json()["success"]:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
zoneID = response.json()["data"]["zone"] zoneID = response.json()["data"]["zone"]
data = { data = {
@@ -179,7 +179,7 @@ def addDomain(params, authentication):
response = requests.post(f"https://{instance}/api", json=data, headers=headers) response = requests.post(f"https://{instance}/api", json=data, headers=headers)
if response.status_code != 200: if response.status_code != 200:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
if response.json()["success"] != True: if not response.json()["success"]:
return {"status": "Error connecting to Varo"} return {"status": "Error connecting to Varo"}
zone = response.json()["data"] zone = response.json()["data"]

173
render.py
View File

@@ -2,36 +2,9 @@ import datetime
import json import json
import urllib.parse import urllib.parse
from flask import render_template from flask import render_template
from domainLookup import punycode_to_emoji
import os import os
from handywrapper import api
import threading import threading
import requests
HSD_API = os.getenv("HSD_API")
HSD_IP = os.getenv("HSD_IP")
if HSD_IP is None:
HSD_IP = "localhost"
HSD_NETWORK = os.getenv("HSD_NETWORK")
HSD_WALLET_PORT = 12039
HSD_NODE_PORT = 12037
if not HSD_NETWORK:
HSD_NETWORK = "main"
else:
HSD_NETWORK = HSD_NETWORK.lower()
if HSD_NETWORK == "simnet":
HSD_WALLET_PORT = 15039
HSD_NODE_PORT = 15037
elif HSD_NETWORK == "testnet":
HSD_WALLET_PORT = 13039
HSD_NODE_PORT = 13037
elif HSD_NETWORK == "regtest":
HSD_WALLET_PORT = 14039
HSD_NODE_PORT = 14037
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
# Get Explorer URL # Get Explorer URL
TX_EXPLORER_URL = os.getenv("EXPLORER_TX") TX_EXPLORER_URL = os.getenv("EXPLORER_TX")
@@ -40,6 +13,24 @@ if TX_EXPLORER_URL is None:
NAMEHASH_CACHE = 'user_data/namehash_cache.json' NAMEHASH_CACHE = 'user_data/namehash_cache.json'
# Validate cache version
if os.path.exists(NAMEHASH_CACHE):
with open(NAMEHASH_CACHE, 'r') as f:
cache = json.load(f)
if not isinstance(cache, dict):
print("Invalid namehash cache format. Resetting cache.")
with open(NAMEHASH_CACHE, 'w') as f:
json.dump({}, f)
# Check if cache entries are valid
for key in cache:
if not cache[key].startswith("<a href='/manage/"):
print(f"Invalid cache entry for {key}. Resetting cache.")
with open(NAMEHASH_CACHE, 'w') as f:
json.dump({}, f)
break
CACHE_LOCK = threading.Lock() CACHE_LOCK = threading.Lock()
@@ -62,7 +53,7 @@ def domains(domains, mobile=False):
link = f'/manage/{domain["name"]}' link = f'/manage/{domain["name"]}'
link_action = "Manage" link_action = "Manage"
if domain['registered'] == False: if not domain['registered']:
link_action = "Register" link_action = "Register"
link = f'/auction/{domain["name"]}/register' link = f'/auction/{domain["name"]}/register'
@@ -78,6 +69,7 @@ actionMap = {
"UPDATE": "Updated ", "UPDATE": "Updated ",
"REGISTER": "Registered ", "REGISTER": "Registered ",
"RENEW": "Renewed ", "RENEW": "Renewed ",
"OPEN": "Opened ",
"BID": "Bid on ", "BID": "Bid on ",
"REVEAL": "Revealed bid for ", "REVEAL": "Revealed bid for ",
"REDEEM": "Redeemed bid for ", "REDEEM": "Redeemed bid for ",
@@ -89,6 +81,7 @@ actionMapPlural = {
"UPDATE": "Updated multiple domains' records", "UPDATE": "Updated multiple domains' records",
"REGISTER": "Registered multiple domains", "REGISTER": "Registered multiple domains",
"RENEW": "Renewed multiple domains", "RENEW": "Renewed multiple domains",
"OPEN": "Opened multiple domains",
"BID": "Bid on multiple domains", "BID": "Bid on multiple domains",
"REVEAL": "Revealed multiple bids", "REVEAL": "Revealed multiple bids",
"REDEEM": "Redeemed multiple bids", "REDEEM": "Redeemed multiple bids",
@@ -187,7 +180,7 @@ def transactions(txs):
elif amount > 0: elif amount > 0:
amount = f"<span style='color: green;'>+{amount:,.2f}</span>" amount = f"<span style='color: green;'>+{amount:,.2f}</span>"
else: else:
amount = f"<span style='color: gray;'>0.00</span>" amount = "<span style='color: gray;'>0.00</span>"
# hash = f"<a target='_blank' href='{TX_EXPLORER_URL}{hash}'>{hash[:8]}...</a>" # hash = f"<a target='_blank' href='{TX_EXPLORER_URL}{hash}'>{hash[:8]}...</a>"
@@ -216,28 +209,28 @@ def dns(data, edit=False):
for key, value in entry.items(): for key, value in entry.items():
if key != 'type': if key != 'type':
if isinstance(value, list): if isinstance(value, list):
html_output += "<td>\n" if len(value) > 1:
html_output += '<td style="white-space: pre-wrap; font-family: monospace;">\n'
for val in value: for val in value:
html_output += f"{val}<br>\n" html_output += f"{val}<br>\n"
html_output += "</td>\n" html_output += "</td>\n"
else: else:
html_output += f"<td>{value}</td>\n" html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{value[0]}</td>\n'
else:
html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{value}</td>\n'
elif entry['type'] == 'DS': elif entry['type'] == 'DS':
ds = f"{entry['keyTag']} {entry['algorithm']} {entry['digestType']} {entry['digest']}" ds = f"{entry['keyTag']} {entry['algorithm']} {entry['digestType']} {entry['digest']}"
html_output += f"<td>{ds}</td>\n" html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{ds}</td>\n'
else: else:
value = "" value = ""
for key, val in entry.items(): for key, val in entry.items():
if key != 'type': if key != 'type':
value += f'{val} ' value += f'{val} '
html_output += f"<td>{value}</td>\n" html_output += f'<td style="white-space: pre-wrap; font-family: monospace;">{value.strip()}</td>\n'
if edit: if edit:
# Remove the current entry from the list
keptRecords = str(data[:index] + data[index+1:]).replace("'", '"') keptRecords = str(data[:index] + data[index+1:]).replace("'", '"')
keptRecords = urllib.parse.quote(keptRecords) keptRecords = urllib.parse.quote(keptRecords)
html_output += f"<td><a href='edit?dns={keptRecords}'>Remove</a></td>\n" html_output += f"<td><a href='edit?dns={keptRecords}'>Remove</a></td>\n"
@@ -246,6 +239,7 @@ def dns(data, edit=False):
index += 1 index += 1
return html_output return html_output
def txs(data): def txs(data):
data = data[::-1] data = data[::-1]
@@ -257,7 +251,7 @@ def txs(data):
amount = entry['amount'] amount = entry['amount']
amount = amount / 1000000 amount = amount / 1000000
if entry['blind'] == None: if entry['blind'] is None:
html_output += f"<td>{amount:,.2f} HNS</td>\n" html_output += f"<td>{amount:,.2f} HNS</td>\n"
else: else:
blind = entry['blind'] blind = entry['blind']
@@ -265,7 +259,7 @@ def txs(data):
html_output += f"<td>{amount:,.2f} + {blind:,.2f} HNS</td>\n" html_output += f"<td>{amount:,.2f} + {blind:,.2f} HNS</td>\n"
html_output += f"<td>{timestamp_to_readable_time(entry['time'])}</td>\n" html_output += f"<td>{timestamp_to_readable_time(entry['time'])}</td>\n"
html_output += f"</tr>\n" html_output += "</tr>\n"
return html_output return html_output
@@ -277,30 +271,60 @@ def timestamp_to_readable_time(timestamp):
return readable_time return readable_time
def bids(bids,reveals): def bids(bids,reveals):
html = '' # Create a list to hold bid data for sorting
bid_data = []
# Prepare data for sorting
for bid in bids: for bid in bids:
lockup = bid['lockup'] lockup = bid['lockup'] / 1000000
lockup = lockup / 1000000
html += "<tr>"
html += f"<td>{lockup:,.2f} HNS</td>"
revealed = False revealed = False
value = 0
# Check if this bid has been revealed
for reveal in reveals: for reveal in reveals:
if reveal['bid'] == bid['prevout']['hash']: if reveal['bid'] == bid['prevout']['hash']:
revealed = True revealed = True
value = reveal['value'] value = reveal['value'] / 1000000
value = value / 1000000
html += f"<td>{value:,.2f} HNS</td>"
bidValue = lockup - value
html += f"<td>{bidValue:,.2f} HNS</td>"
break break
if not revealed:
html += f"<td>Hidden until reveal</td>" # Store all relevant information for sorting and display
html += f"<td>Hidden until reveal</td>" bid_data.append({
'bid': bid,
'lockup': lockup,
'revealed': revealed,
'value': value,
'sort_value': value if revealed else lockup # Use value for sorting if revealed, otherwise lockup
})
# Sort by the sort_value in descending order (highest first)
bid_data.sort(key=lambda x: x['sort_value'], reverse=True)
# Generate HTML from sorted data
html = ''
for data in bid_data:
bid = data['bid']
lockup = data['lockup']
revealed = data['revealed']
value = data['value']
html += "<tr>"
html += f"<td>{lockup:,.2f} HNS</td>"
if revealed:
bidValue = lockup - value
html += f"<td>{value:,.2f} HNS</td>"
html += f"<td>{bidValue:,.2f} HNS</td>"
else:
html += "<td>Hidden until reveal</td>"
html += "<td>Hidden until reveal</td>"
if bid['own']: if bid['own']:
html += "<td>You</td>" html += "<td>You</td>"
else: else:
html += "<td>Unknown</td>" html += "<td>Unknown</td>"
html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' target='_blank' href='{TX_EXPLORER_URL}{bid['prevout']['hash']}'>Bid TX 🔗</a></td>"
html += "</tr>" html += "</tr>"
return html return html
@@ -384,22 +408,22 @@ def plugin_functions(functions, pluginName):
functionType = functions[function]["type"] functionType = functions[function]["type"]
html += f'<div class="card" style="margin-top: 50px;">' html += '<div class="card" style="margin-top: 50px;">'
html += f'<div class="card-body">' html += '<div class="card-body">'
html += f'<h4 class="card-title">{name}</h4>' html += f'<h4 class="card-title">{name}</h4>'
html += f'<h6 class="text-muted card-subtitle mb-2">{description}</h6>' html += f'<h6 class="text-muted card-subtitle mb-2">{description}</h6>'
html += f'<h6 class="text-muted card-subtitle mb-2">Function type: {functionType.capitalize()}</h6>' html += f'<h6 class="text-muted card-subtitle mb-2">Function type: {functionType.capitalize()}</h6>'
if functionType != "default": if functionType != "default":
html += f'<p class="card-text">Returns: {returns}</p>' html += f'<p class="card-text">Returns: {returns}</p>'
html += f'</div>' html += '</div>'
html += f'</div>' html += '</div>'
continue continue
# Form # Form
html += f'<form method="post" style="padding: 20px;" action="/plugin/{pluginName}/{function}">' html += f'<form method="post" style="padding: 20px;" action="/plugin/{pluginName}/{function}">'
for param in params: for param in params:
html += f'<div style="margin-bottom: 20px;">' html += '<div style="margin-bottom: 20px;">'
paramName = params[param]["name"] paramName = params[param]["name"]
paramType = params[param]["type"] paramType = params[param]["type"]
if paramType == "text": if paramType == "text":
@@ -423,14 +447,14 @@ def plugin_functions(functions, pluginName):
html += f'</div>' html += '</div>'
html += f'<button type="submit" class="btn btn-primary">Submit</button>' html += '<button type="submit" class="btn btn-primary">Submit</button>'
html += f'</form>' html += '</form>'
# For debugging # For debugging
html += f'<p class="card-text">Returns: {returns}</p>' html += f'<p class="card-text">Returns: {returns}</p>'
html += f'</div>' html += '</div>'
html += f'</div>' html += '</div>'
return html return html
@@ -442,16 +466,16 @@ def plugin_output(outputs, returns):
for returnOutput in returns: for returnOutput in returns:
if returnOutput not in outputs: if returnOutput not in outputs:
continue continue
html += f'<div class="card" style="margin-top: 50px; margin-bottom: 50px;">' html += '<div class="card" style="margin-top: 50px; margin-bottom: 50px;">'
html += f'<div class="card-body">' html += '<div class="card-body">'
html += f'<h4 class="card-title">{returns[returnOutput]["name"]}</h4>' html += f'<h4 class="card-title">{returns[returnOutput]["name"]}</h4>'
output = outputs[returnOutput] output = outputs[returnOutput]
if returns[returnOutput]["type"] == "list": if returns[returnOutput]["type"] == "list":
html += f'<ul>' html += '<ul>'
for item in output: for item in output:
html += f'<li>{item}</li>' html += f'<li>{item}</li>'
html += f'</ul>' html += '</ul>'
elif returns[returnOutput]["type"] == "text": elif returns[returnOutput]["type"] == "text":
html += f'<p>{output}</p>' html += f'<p>{output}</p>'
elif returns[returnOutput]["type"] == "tx": elif returns[returnOutput]["type"] == "tx":
@@ -461,8 +485,8 @@ def plugin_output(outputs, returns):
html += render_template('components/dns-output.html', dns=dns(output)) html += render_template('components/dns-output.html', dns=dns(output))
html += f'</div>' html += '</div>'
html += f'</div>' html += '</div>'
return html return html
def plugin_output_dash(outputs, returns): def plugin_output_dash(outputs, returns):
@@ -472,7 +496,7 @@ def plugin_output_dash(outputs, returns):
for returnOutput in returns: for returnOutput in returns:
if returnOutput not in outputs: if returnOutput not in outputs:
continue continue
if outputs[returnOutput] == None: if outputs[returnOutput] is None:
continue continue
html += render_template('components/dashboard-plugin.html', name=returns[returnOutput]["name"], output=outputs[returnOutput]) html += render_template('components/dashboard-plugin.html', name=returns[returnOutput]["name"], output=outputs[returnOutput])
return html return html
@@ -491,7 +515,7 @@ def renderDomain(name: str) -> str:
return f"{rendered}/ ({name})" return f"{rendered}/ ({name})"
except Exception as e: except Exception:
return f"{name}/" return f"{name}/"
def renderDomainAsync(namehash: str) -> None: def renderDomainAsync(namehash: str) -> None:
@@ -509,12 +533,15 @@ def renderDomainAsync(namehash: str) -> None:
if namehash in cache: if namehash in cache:
return return
# Fetch the name outside the lock (network call) using hsd.hns.au
# name = account.hsd.rpc_getNameByHash(namehash)
name = requests.get(f"https://hsd.hns.au/api/v1/namehash/{namehash}").json()
# Fetch the name outside the lock (network call)
name = hsd.rpc_getNameByHash(namehash)
if name["error"] is None: if name["error"] is None:
name = name["result"] name = name["result"]
rendered = renderDomain(name) rendered = renderDomain(name)
rendered = f"<a href='/manage/{name}' target='_blank' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));'>{rendered}</a>"
with CACHE_LOCK: with CACHE_LOCK:
with open(NAMEHASH_CACHE, 'r') as f: with open(NAMEHASH_CACHE, 'r') as f:
@@ -523,7 +550,7 @@ def renderDomainAsync(namehash: str) -> None:
with open(NAMEHASH_CACHE, 'w') as f: with open(NAMEHASH_CACHE, 'w') as f:
json.dump(cache, f) json.dump(cache, f)
return rendered return
else: else:
print(f"Error fetching name for hash {namehash}: {name['error']}", flush=True) print(f"Error fetching name for hash {namehash}: {name['error']}", flush=True)

View File

@@ -1,4 +1,3 @@
import os
import sys import sys
import platform import platform
from main import app from main import app
@@ -17,8 +16,8 @@ def gunicornServer():
def load_config(self): def load_config(self):
for key, value in self.options.items(): for key, value in self.options.items():
if key in self.cfg.settings and value is not None: if key in self.cfg.settings and value is not None: # type: ignore
self.cfg.set(key.lower(), value) self.cfg.set(key.lower(), value) # type: ignore
def load(self): def load(self):
return self.application return self.application
@@ -39,6 +38,6 @@ if __name__ == '__main__':
sys.exit() sys.exit()
print(f'Starting server with Waitress on {platform.system()} with {threads} threads...', flush=True) print(f'Starting server with Waitress on {platform.system()} with {threads} threads...', flush=True)
print(f'Press Ctrl+C to stop the server', flush=True) print('Press Ctrl+C to stop the server', flush=True)
print(f'Serving on http://0.0.0.0:5000/', flush=True) print('Serving on http://0.0.0.0:5000/', flush=True)
serve(app, host="0.0.0.0", port=5000, threads=threads) serve(app, host="0.0.0.0", port=5000, threads=threads)

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

@@ -66,9 +66,9 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="stick-right">{{next_action|safe}}</div> <div id="next-action" class="stick-right">{{next_action|safe}}</div>
<h4 class="card-title">{{rendered}}</h4> <h4 class="card-title">{{rendered}}</h4>
<h6 class="text-muted mb-2 card-subtitle">{{next}}</h6> <h6 class="text-muted mb-2 card-subtitle" id="next">{{next | safe}}</h6>
</div> </div>
</div> </div>
</div> </div>
@@ -93,13 +93,92 @@
<th>Bid</th> <th>Bid</th>
<th>Blind</th> <th>Blind</th>
<th>Owner</th> <th>Owner</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="bids-tbody">
{{bids | safe}} <tr id="loading-row">
<td colspan="5" class="text-center">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
Loading bids...
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<script>
async function loadBids(initial = false) {
const tbody = document.getElementById('bids-tbody');
try {
// Fetch all required data
const response = await fetch(`/api/v1/wallet/domainBids?domain={{search_term}}`);
const data = await response.json();
if (initial) {
if (response.ok && data.result) {
tbody.innerHTML = data.result;
} else {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No bids found. <a href="/auction/{{search_term}}/scan">Rescan Auction</a></td></tr>';
}
}
const mempoolResponse = await fetch('/api/v1/hsd/mempoolBids');
const nextStateResponse = await fetch(`/api/v1/hsd/nextAuctionState?domain={{search_term}}`);
if (!initial) {
if (response.ok && data.result) {
tbody.innerHTML = data.result;
} else {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No bids found. <a href="/auction/{{search_term}}/scan">Rescan Auction</a></td></tr>';
}
}
const nextStateData = await nextStateResponse.json();
if (nextStateResponse.ok && nextStateData.state) {
document.getElementById('next').innerHTML = nextStateData.next;
document.getElementById('next-action').innerHTML = nextStateData.next_action;
} else {
document.getElementById('next').innerHTML = 'Unknown';
document.getElementById('next-action').innerHTML = '';
}
const mempoolData = await mempoolResponse.json();
if (mempoolResponse.ok && mempoolData.result) {
const domainBids = mempoolData.result['{{search_term}}'];
if (domainBids && domainBids.length > 0) {
let mempoolRows = '';
domainBids.forEach(bid => {
const bidValue = bid.revealed ? `${(bid.value / 1000000).toFixed(2)} HNS` : 'Hidden until reveal';
const lockupValue = (bid.lockup / 1000000).toFixed(2);
const blindValue = bid.revealed ? `${((bid.lockup - bid.value) / 1000000).toFixed(2)} HNS` : 'Hidden until reveal';
const type = bid.revealed ? 'Reveal' : 'Bid';
mempoolRows += `<tr class="table-warning">
<td>${lockupValue} HNS</td>
<td>${bidValue}</td>
<td>${blindValue}</td>
<td>${bid.owner}</td>
<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' target='_blank' href='https://shakeshift.com/transaction/${bid.txid}'>Mempool ${type} 🔗</a></td>
</tr>`;
});
tbody.innerHTML += mempoolRows;
}
}
} catch (error) {
console.error('Error loading bids:', error);
}
}
// Load bids when page loads
document.addEventListener('DOMContentLoaded', () => loadBids(true));
// Auto-refresh bids every 20 seconds
setInterval(() => loadBids(false), 20000);
</script>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,4 @@
<span style="display: block;font-size: 12px;">TX: {{tx}}</span> <span style="display: block;font-size: 12px;">TX: {{tx}}</span>
<span style="display: block;">Check your transaction on a block explorer</span> <span style="display: block;">Check your transaction on a block explorer</span>
<a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a>
<a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a>
<a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a> <a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a>
<a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a>

View File

@@ -87,27 +87,6 @@
{{dns | safe}} {{dns | safe}}
</tbody> </tbody>
</table> </table>
</div>
</div>
</div>
</div>
<div class="container-fluid" style="margin-top: 50px;">
<div class="card">
<div class="card-body">
<h4 class="card-title">History</h4><div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Action</th>
<th>TX</th>
<th>Amount</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{{txs | safe}}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -67,7 +67,7 @@
</div> </div>
<div class="card" style="max-width: 500px;margin: auto;margin-top: 50px;"> <div class="card" style="max-width: 500px;margin: auto;margin-top: 50px;">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Your transaction has been sent and will be mined soon.</h4><span style="display: block;font-size: 12px;">TX: {{tx}}</span><span style="display: block;">Check your transaction on a block explorer</span><a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a><a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a><a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a> <h4 class="card-title">Your transaction has been sent and will be mined soon.</h4><span style="display: block;font-size: 12px;">TX: {{tx}}</span><span style="display: block;">Check your transaction on a block explorer</span><a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a><a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a>
</div> </div>
</div> </div>
</div> </div>

47
templates/welcome.html Normal file
View File

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