53 Commits

Author SHA1 Message Date
7fc19a7f19 fix: Don't allow alerts without an ID to be dismissed
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m58s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m4s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m6s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m10s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 11m1s
2025-09-11 15:07:19 +10:00
eb6306bb83 feat: Move update check to alerts function
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Has been cancelled
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.10) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.11) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.13) (push) Has been cancelled
2025-09-11 15:04:31 +10:00
9f8daa8b88 feat: Replace most prints with logger calls to help with debugging
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 34s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 48s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 50s
2025-09-10 17:14:32 +10:00
63e0f0b804 feat: Add initial logggin system
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 37s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 37s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 37s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 48s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 51s
2025-09-10 16:56:31 +10:00
0c17c4ad9b feat: Add option to set host and port on main.py entry
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m52s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m56s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m3s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m5s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m6s
2025-09-10 16:18:46 +10:00
938fff8791 fix: Update wording of xPub
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 36s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 33s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 37s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 47s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 54s
2025-09-09 23:36:38 +10:00
155662d2b1 fix: Move Bootstrap design to lfs
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 35s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 35s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 42s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 52s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 59s
2025-09-09 23:21:57 +10:00
97569faf0e Merge pull request 'Add alerts to dashboard' (#7) from feat/notices into main
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 34s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 36s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 35s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 50s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 51s
Reviewed-on: #7
2025-09-09 22:51:03 +10:00
59000afa87 feat: Cleanup function layouts for alerts
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 35s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 40s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 37s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 51s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 50s
2025-09-09 17:34:36 +10:00
699a74f093 feat: Add api route to dismiss alert
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 29s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 36s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 37s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 42s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 47s
2025-09-09 17:29:11 +10:00
6096f82c4d feat: Add an alert framework
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 3m6s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 3m9s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m18s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m17s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m25s
2025-09-09 17:24:39 +10:00
4353eb8fa4 feat: Use new proxied script url
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 33s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 38s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 48s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 54s
2025-09-04 22:05:16 +10:00
344cde07d0 feat: Add more info about the installation script to readme
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 1m4s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 1m12s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 1m48s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 1m52s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m51s
2025-09-04 21:36:30 +10:00
2fb841aeaf fix: Update link for installation script to use raw
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Has been cancelled
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.10) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.13) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.11) (push) Has been cancelled
2025-09-04 21:34:19 +10:00
60df317f78 feat: Add install script to readme
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Has started running
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.11) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.10) (push) Has been cancelled
Tests and Linting / Tests-Linting (3.13) (push) Has been cancelled
2025-09-04 21:33:28 +10:00
4c1ea9fb12 feat: Add installation and start scripts 2025-09-04 21:29:47 +10:00
58ed636ce3 fix: Use git --version instead of short -v to allow for backwards compatability
The default git installed in ubuntu doesn't allow -v
2025-09-04 21:29:05 +10:00
e537c323c2 fix: Update paths to be consistent
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 43s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 45s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 51s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 59s
2025-09-02 18:15:56 +10:00
812fc84d3e feat: Allow zapping txs of a different age
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 31s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 34s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 35s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 42s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 45s
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
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 36s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 50s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 1m10s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m15s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m17s
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
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 3m7s
Test Python Compatibility / Python-Compatibility (3.10) (push) Successful in 4m59s
Test Python Compatibility / Python-Compatibility (3.11) (push) Successful in 4m57s
Test Python Compatibility / Python-Compatibility (3.13) (push) Successful in 1m58s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 7m3s
Reviewed-on: #6
2025-09-02 16:08:35 +10:00
86e174c337 fix: Lint default plugins
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 50s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 52s
Test Python Compatibility / Python-Compatibility (3.11) (push) Successful in 1m55s
Test Python Compatibility / Python-Compatibility (3.10) (push) Successful in 1m57s
Test Python Compatibility / Python-Compatibility (3.13) (push) Successful in 1m45s
2025-09-02 15:58:55 +10:00
e7b787c30b fix: Lint to follow ruff standards
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 45s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 44s
Test Python Compatibility / Python-Compatibility (3.11) (push) Failing after 22s
Test Python Compatibility / Python-Compatibility (3.13) (push) Failing after 22s
Test Python Compatibility / Python-Compatibility (3.10) (push) Failing after 1m37s
2025-09-02 15:55:45 +10:00
997828795a feat: Add ruff linting
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 43s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 46s
Test Python Compatibility / Python-Compatibility (3.11) (push) Failing after 1m40s
Test Python Compatibility / Python-Compatibility (3.10) (push) Failing after 1m44s
Test Python Compatibility / Python-Compatibility (3.13) (push) Failing after 1m40s
2025-09-02 15:48:21 +10:00
30de2d585e fix: Use single quote in sign message and reduce test versions
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 49s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 53s
Test Python Compatibility / Python-Compatibility (3.10) (push) Successful in 1m45s
Test Python Compatibility / Python-Compatibility (3.13) (push) Successful in 1m44s
Test Python Compatibility / Python-Compatibility (3.8) (push) Failing after 1m24s
2025-09-02 15:28:39 +10:00
56eabfc1fc feat: Add some inital tests
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 46s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 49s
Test Python Compatibility / Python-Compatibility (3.10) (push) Failing after 1m38s
Test Python Compatibility / Python-Compatibility (3.13) (push) Successful in 1m51s
Test Python Compatibility / Python-Compatibility (3.6) (push) Failing after 22s
Test Python Compatibility / Python-Compatibility (3.7) (push) Failing after 1m16s
Test Python Compatibility / Python-Compatibility (3.8) (push) Failing after 1m29s
Test Python Compatibility / Python-Compatibility (3.9) (push) Failing after 1m28s
2025-09-02 15:20:31 +10:00
e0f24267f5 test: Try a new container to run
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 57s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 1m0s
Test Python Compatibility / Python-Compatibility (3.10) (push) Failing after 3m23s
Test Python Compatibility / Python-Compatibility (3.13) (push) Failing after 3m27s
Test Python Compatibility / Python-Compatibility (3.6) (push) Failing after 23s
Test Python Compatibility / Python-Compatibility (3.7) (push) Failing after 1m19s
Test Python Compatibility / Python-Compatibility (3.8) (push) Failing after 1m42s
Test Python Compatibility / Python-Compatibility (3.9) (push) Failing after 1m35s
2025-09-02 15:10:23 +10:00
2d51882d20 fix: Specify python minor version number
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 49s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 48s
Test Python Compatibility / Python-Compatibility (3.10.18) (push) Failing after 19s
Test Python Compatibility / Python-Compatibility (3.13.7) (push) Failing after 19s
Test Python Compatibility / Python-Compatibility (3.6.15) (push) Failing after 21s
Test Python Compatibility / Python-Compatibility (3.7.17) (push) Failing after 20s
Test Python Compatibility / Python-Compatibility (3.8.18) (push) Failing after 20s
Test Python Compatibility / Python-Compatibility (3.9.23) (push) Failing after 19s
2025-09-02 15:00:40 +10:00
06b1eea9ef fix: Disable arm on testing workflow
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 52s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 52s
Test Python Compatibility / Python-Compatibility (3.10) (push) Failing after 1m2s
Test Python Compatibility / Python-Compatibility (3.6) (push) Failing after 1m1s
Test Python Compatibility / Python-Compatibility (3.7) (push) Failing after 22s
Test Python Compatibility / Python-Compatibility (3.8) (push) Failing after 21s
Test Python Compatibility / Python-Compatibility (3.9) (push) Failing after 18s
2025-09-02 14:55:40 +10:00
d483cfdcfd feat: Add testing CI workflow
Some checks failed
Test Python Compatibility / Python-Compatibility (3.6) (push) Failing after 45s
Test Python Compatibility / Python-Compatibility (3.10) (push) Failing after 47s
Test Python Compatibility / Python-Compatibility (3.7) (push) Failing after 11s
Test Python Compatibility / Python-Compatibility (3.8) (push) Failing after 9s
Test Python Compatibility / Python-Compatibility (3.9) (push) Failing after 7s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m10s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m13s
2025-09-02 14:49:14 +10:00
46ed0173d3 fix: Remove broken label
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 40s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 43s
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
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 51s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 4m18s
2025-08-29 23:11:47 +10:00
e8f052e0d1 feat: Try using matrix
Some checks failed
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Failing after 32s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Failing after 11m41s
2025-08-29 23:03:50 +10:00
7f450d620a revert: Use older checkout
Some checks failed
Build Docker / Build Image (push) Has been cancelled
Build Docker / Build Image with HSD (push) Has been cancelled
2025-08-29 22:55:32 +10:00
41a1bc743f feat: Try new syntax for gitea action
Some checks failed
Build Docker / Build Image (push) Failing after 17s
Build Docker / Build Image with HSD (push) Failing after 23s
2025-08-29 22:53:44 +10:00
30108e3bc5 fix: Extra backslash in CI/CD pipeline
All checks were successful
Build Docker / Build Image (push) Successful in 38s
Build Docker / Build Image with HSD (push) Successful in 50s
2025-08-29 22:42:59 +10:00
a2dc9f43e3 feat: Add docker support for inbuilt HSD
Some checks failed
Build Docker / Build Image with HSD (push) Failing after 36s
Build Docker / Build Image (push) Failing after 38s
2025-08-29 22:40:30 +10:00
1203719eac fix: Docker python version and fix reporting missing requirements
All checks were successful
Build Docker / Build Image (push) Successful in 2m58s
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
All checks were successful
Build Docker / Build Image (push) Successful in 2m13s
Reviewed-on: #4
2025-08-29 13:04:30 +10:00
b76b873036 feat: Update readme
All checks were successful
Build Docker / Build Image (push) Successful in 54s
2025-08-28 17:50:47 +10:00
23e714fad8 feat: Add additional node info to settings
All checks were successful
Build Docker / Build Image (push) Successful in 45s
2025-08-28 17:27:45 +10:00
a36c69ecfc fix: Add SPV support for getDNS()
All checks were successful
Build Docker / Build Image (push) Successful in 55s
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
All checks were successful
Build Docker / Build Image (push) Successful in 2m9s
2025-08-28 16:42:12 +10:00
26c5b4a4fa feat: Update configuration storage and overrides
All checks were successful
Build Docker / Build Image (push) Successful in 53s
2025-08-26 18:04:07 +10:00
7fdc4a3122 fix: SPV causes some domains to not be recognized as owned by the wallet
All checks were successful
Build Docker / Build Image (push) Successful in 50s
2025-08-26 17:31:25 +10:00
5ff8960b7b feat: Add initial internal node option
All checks were successful
Build Docker / Build Image (push) Successful in 10m29s
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
All checks were successful
Build Docker / Build Image (push) Successful in 2m7s
2025-08-26 12:52:14 +10:00
1c53547047 feat: Add env flag to disable WALLET DNS record lookup
All checks were successful
Build Docker / Build Image (push) Successful in 47s
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
All checks were successful
Build Docker / Build Image (push) Successful in 53s
Reviewed-on: #3
2025-08-25 13:59:15 +10:00
28 changed files with 1423 additions and 360 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

1
.gitattributes vendored Normal file
View File

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

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

5
.gitignore vendored
View File

@@ -16,3 +16,8 @@ customPlugins/
cache/ cache/
build/ build/
dist/ dist/
hsd/
hsd_data/
logs/
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

@@ -11,7 +11,6 @@ import dns.query
import dns.rdatatype 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 from cryptography.x509.oid import ExtensionOID
@@ -172,11 +171,11 @@ def resolve_TLSA_with_doh(query_name, doh_url="https://hnsdoh.com/dns-query"):
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"

429
main.py
View File

@@ -1,6 +1,7 @@
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
@@ -12,12 +13,12 @@ import re
from flask_qrcode import QRcode from flask_qrcode import QRcode
import domainLookup import domainLookup
import urllib.parse import urllib.parse
import importlib
import plugin as plugins_module import plugin as plugins_module
import gitinfo import gitinfo
import datetime import datetime
import functools
import time import time
import logging
from logging.handlers import RotatingFileHandler
dotenv.load_dotenv() dotenv.load_dotenv()
@@ -30,36 +31,25 @@ fees = 0.02
revokeCheck = random.randint(100000,999999) revokeCheck = random.randint(100000,999999)
THEME = os.getenv("THEME") 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"
# 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()
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)
@app.route('/') @app.route('/')
def index(): def index():
@@ -81,12 +71,13 @@ def index():
if not os.path.exists(".git"): if not os.path.exists(".git"):
return render_template("index.html", account=account, plugins=plugins) return render_template("index.html", account=account, plugins=plugins)
info = gitinfo.get_git_info() alerts = get_alerts(account)
branch = info['refs'] for alert in alerts:
commit = info['commit'] output_html = alert['output']
if commit != latestVersion(branch): if 'id' in alert:
print("New version available",flush=True) # Add a dismiss button
plugins += render_template('components/dashboard-alert.html', name='Update', output='A new version of FireWallet is available') output_html += f"&nbsp<a href='/dismiss/{alert['id']}' class='btn btn-secondary btn-sm' style='margin:none;'>Dismiss</a>"
plugins += render_template('components/dashboard-alert.html', name=alert['name'], output=output_html)
return render_template("index.html", account=account, plugins=plugins) return render_template("index.html", account=account, plugins=plugins)
@@ -116,7 +107,7 @@ def transactions():
page = request.args.get('page', 1) page = request.args.get('page', 1)
try: try:
page = int(page) page = int(page)
except: except ValueError:
page = 1 page = 1
if page < 1: if page < 1:
@@ -198,7 +189,7 @@ def send():
content = f"Are you sure you want to send {amount} HNS to {toAddress}<br><br>" content = f"Are you sure you want to send {amount} HNS to {toAddress}<br><br>"
content += f"This will cost {amount} HNS + mining fees and is not able to be undone." content += f"This will cost {amount} HNS + mining fees and is not able to be undone."
cancel = f"/send" cancel = "/send"
confirm = f"/send/confirm?address={address}&amount={amount}" confirm = f"/send/confirm?address={address}&amount={amount}"
@@ -213,7 +204,7 @@ def sendConfirmed():
address = request.args.get("address") address = request.args.get("address")
amount = float(request.args.get("amount","0")) amount = float(request.args.get("amount","0"))
response = account_module.send(request.cookies.get("account"),address,amount) response = account_module.send(request.cookies.get("account"),address,amount)
if 'error' in response and response['error'] != None: if 'error' in response and response['error'] is not None:
# If error is a dict get the message # If error is a dict get the message
if isinstance(response['error'], dict): if isinstance(response['error'], dict):
if 'message' in response['error']: if 'message' in response['error']:
@@ -284,7 +275,7 @@ def auctions():
# Sort # Sort
sort = request.args.get("sort") sort = request.args.get("sort")
if sort == None: if sort is None:
sort = "time" sort = "time"
sort = sort.lower() sort = sort.lower()
sort_price = "" sort_price = ""
@@ -298,7 +289,7 @@ def auctions():
reverse = False reverse = False
direction = request.args.get("direction") direction = request.args.get("direction")
if direction == None: if direction is None:
if sort == "time": if sort == "time":
direction = "" direction = ""
else: else:
@@ -367,7 +358,7 @@ def revealAllBids():
return redirect("/auctions?message=Failed to reveal bids") return redirect("/auctions?message=Failed to reveal bids")
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] is not None:
if response['error']['message'] == "Nothing to do.": if response['error']['message'] == "Nothing to do.":
return redirect("/auctions?message=No reveals pending") return redirect("/auctions?message=No reveals pending")
return redirect("/auctions?message=" + response['error']['message']) return redirect("/auctions?message=" + response['error']['message'])
@@ -390,7 +381,7 @@ def redeemAllBids():
return redirect("/auctions?message=Failed to redeem bids") return redirect("/auctions?message=Failed to redeem bids")
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] is not None:
if response['error']['message'] == "Nothing to do.": if response['error']['message'] == "Nothing to do.":
return redirect("/auctions?message=No redeems pending") return redirect("/auctions?message=No redeems pending")
return redirect("/auctions?message=" + response['error']['message']) return redirect("/auctions?message=" + response['error']['message'])
@@ -412,7 +403,7 @@ def registerAllDomains():
return redirect("/auctions?message=Failed to register domains") return redirect("/auctions?message=Failed to register domains")
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] is not None:
if response['error']['message'] == "Nothing to do.": if response['error']['message'] == "Nothing to do.":
return redirect("/auctions?message=No domains to register") return redirect("/auctions?message=No domains to register")
return redirect("/auctions?message=" + response['error']['message']) return redirect("/auctions?message=" + response['error']['message'])
@@ -431,7 +422,7 @@ def finalizeAllBids():
response = account_module.finalizeAll(request.cookies.get("account")) response = account_module.finalizeAll(request.cookies.get("account"))
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] is not None:
if response['error']['message'] == "Nothing to do.": if response['error']['message'] == "Nothing to do.":
return redirect("/dashboard?message=No domains to finalize") return redirect("/dashboard?message=No domains to finalize")
return redirect("/dashboard?message=" + response['error']['message']) return redirect("/dashboard?message=" + response['error']['message'])
@@ -509,7 +500,6 @@ def search():
domain_info = account_module.getDomain(search_term) domain_info = account_module.getDomain(search_term)
owner = 'Unknown' owner = 'Unknown'
dns = [] dns = []
txs = []
if domain_info: if domain_info:
# Check if info and info.owner # Check if info and info.owner
@@ -562,10 +552,10 @@ def manage(domain: str):
dns = render.dns(dns) dns = render.dns(dns)
errorMessage = request.args.get("error") errorMessage = request.args.get("error")
if errorMessage == None: if errorMessage is None:
errorMessage = "" errorMessage = ""
address = request.args.get("address") address = request.args.get("address")
if address == None: if address is None:
address = "" address = ""
finalize_time = "" finalize_time = ""
@@ -609,8 +599,8 @@ 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'] != 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'])
@@ -628,8 +618,8 @@ def cancelTransfer(domain: str):
domain = domain.lower() domain = domain.lower()
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'] != 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'])
@@ -647,9 +637,9 @@ def revokeInit(domain: str):
domain = domain.lower() domain = domain.lower()
content = f"Are you sure you want to revoke {domain}/?<br>" content = f"Are you sure you want to revoke {domain}/?<br>"
content += f"This will return the domain to the auction pool and you will lose any funds spent on the domain.<br>" content += "This will return the domain to the auction pool and you will lose any funds spent on the domain.<br>"
content += f"This cannot be undone after the transaction is sent.<br><br>" content += "This cannot be undone after the transaction is sent.<br><br>"
content += f"Please enter your password to confirm." content += "Please enter your password to confirm."
cancel = f"/manage/{domain}" cancel = f"/manage/{domain}"
confirm = f"/manage/{domain}/revoke/confirm" confirm = f"/manage/{domain}/revoke/confirm"
@@ -678,14 +668,14 @@ def revokeConfirm(domain: str):
return redirect("/manage/" + domain + "?error=An error occurred. Please try again.") return redirect("/manage/" + domain + "?error=An error occurred. Please try again.")
response = account_module.check_password(request.cookies.get("account"),password) response = account_module.check_password(request.cookies.get("account"),password)
if response == False: if not response:
return redirect("/manage/" + domain + "?error=Invalid password") return redirect("/manage/" + domain + "?error=Invalid password")
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'] != 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']}")
@@ -724,7 +714,7 @@ def editPage(domain: str):
user_edits = request.args.get("dns") user_edits = request.args.get("dns")
if user_edits != None: if user_edits is not None:
dns = urllib.parse.unquote(user_edits) dns = urllib.parse.unquote(user_edits)
else: else:
dns = account_module.getDNS(domain) dns = account_module.getDNS(domain)
@@ -737,7 +727,7 @@ def editPage(domain: str):
# Check if new records have been added # Check if new records have been added
dnsType = request.args.get("type") dnsType = request.args.get("type")
dnsValue = request.args.get("value") dnsValue = request.args.get("value")
if dnsType != None and dnsValue != None: if dnsType is not None and dnsValue is not None:
if dnsType != "DS": if dnsType != "DS":
dns.append({"type": dnsType, "value": dnsValue}) dns.append({"type": dnsType, "value": dnsValue})
else: else:
@@ -751,7 +741,7 @@ def editPage(domain: str):
key_tag = int(ds[0]) key_tag = int(ds[0])
algorithm = int(ds[1]) algorithm = int(ds[1])
digest_type = int(ds[2]) digest_type = int(ds[2])
except: except ValueError:
raw_dns = str(dns).replace("'",'"') raw_dns = str(dns).replace("'",'"')
return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(str(raw_dns)) + "&error=Invalid DS record") return redirect("/manage/" + domain + "/edit?dns=" + urllib.parse.quote(str(raw_dns)) + "&error=Invalid DS record")
@@ -763,7 +753,7 @@ def editPage(domain: str):
raw_dns = str(dns).replace("'",'"') raw_dns = str(dns).replace("'",'"')
dns = render.dns(dns,True) dns = render.dns(dns,True)
errorMessage = request.args.get("error") errorMessage = request.args.get("error")
if errorMessage == None: if errorMessage is None:
errorMessage = "" errorMessage = ""
@@ -791,7 +781,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']}")
@@ -822,7 +812,7 @@ def transfer(domain):
action = f"Send {domain}/ to {request.form.get('address')}" action = f"Send {domain}/ to {request.form.get('address')}"
content = f"Are you sure you want to send {domain}/ to {toAddress}<br><br>" content = f"Are you sure you want to send {domain}/ to {toAddress}<br><br>"
content += f"This requires sending a finalize transaction 2 days after the transfer is initiated." content += "This requires sending a finalize transaction 2 days after the transfer is initiated."
cancel = f"/manage/{domain}?address={address}" cancel = f"/manage/{domain}?address={address}"
confirm = f"/manage/{domain}/transfer/confirm?address={address}" confirm = f"/manage/{domain}/transfer/confirm?address={address}"
@@ -849,9 +839,9 @@ def signMessage(domain):
content = "Message to sign:<br><code>" + message + "</code><br><br>" content = "Message to sign:<br><code>" + message + "</code><br><br>"
signedMessage = account_module.signMessage(request.cookies.get("account"),domain,message) signedMessage = account_module.signMessage(request.cookies.get("account"),domain,message)
if signedMessage["error"] != None: if signedMessage["error"] is not None:
return redirect("/manage/" + domain + "?error=" + signedMessage["error"]) return redirect("/manage/" + domain + "?error=" + signedMessage["error"])
content += f"Signature:<br><code>{signedMessage["result"]}</code><br><br>" content += f"Signature:<br><code>{signedMessage['result']}</code><br><br>"
data = { data = {
"domain": domain, "domain": domain,
@@ -889,7 +879,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
@@ -908,7 +897,7 @@ def auction(domain):
domainInfo = account_module.getDomain(search_term) domainInfo = account_module.getDomain(search_term)
error = request.args.get("error") error = request.args.get("error")
if error == None: if error is None:
error = "" error = ""
if 'error' in domainInfo: if 'error' in domainInfo:
@@ -918,9 +907,9 @@ def auction(domain):
error=error) error=error)
if domainInfo['info'] is None: if domainInfo['info'] is None:
if 'registered' in domainInfo and domainInfo['registered'] == False and 'expired' in domainInfo and domainInfo['expired'] == False: if 'registered' in domainInfo and not domainInfo['registered'] and 'expired' in domainInfo and not domainInfo['expired']:
# Needs to be registered # Needs to be registered
next_action = f'ERROR GETTING NEXT STATE' next_action = 'ERROR GETTING NEXT STATE'
else: else:
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>' next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
return render_template("auction.html", account=account, return render_template("auction.html", account=account,
@@ -938,13 +927,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>'
@@ -967,7 +954,7 @@ def auction(domain):
elif stats['blocksUntilReveal'] == 2: elif stats['blocksUntilReveal'] == 2:
next += "<br>LAST CHANCE TO BID" next += "<br>LAST CHANCE TO BID"
elif stats['blocksUntilReveal'] == 3: elif stats['blocksUntilReveal'] == 3:
next += f"<br>Next block is last chance to bid" next += "<br>Next block is last chance to bid"
elif stats['blocksUntilReveal'] < 6: elif stats['blocksUntilReveal'] < 6:
next += f"<br>Last chance to bid in {stats['blocksUntilReveal']-2} blocks" next += f"<br>Last chance to bid in {stats['blocksUntilReveal']-2} blocks"
@@ -999,7 +986,7 @@ def rescan_auction(domain):
domain = domain.lower() domain = domain.lower()
response = account_module.rescan_auction(account,domain) account_module.rescan_auction(account,domain)
return redirect("/auction/" + domain) return redirect("/auction/" + domain)
@app.route('/auction/<domain>/bid') @app.route('/auction/<domain>/bid')
@@ -1095,7 +1082,7 @@ def open_auction(domain):
response = account_module.openAuction(request.cookies.get("account"),domain) response = account_module.openAuction(request.cookies.get("account"),domain)
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] is not None:
return redirect("/auction/" + domain + "?error=" + response['error']['message']) return redirect("/auction/" + domain + "?error=" + response['error']['message'])
return redirect(f"/success?tx={response['hash']}") return redirect(f"/success?tx={response['hash']}")
@@ -1144,22 +1131,26 @@ def settings():
return redirect("/logout") return redirect("/logout")
error = request.args.get("error") error = request.args.get("error")
if error == None: if error is None:
error = "" error = ""
success = request.args.get("success") success = request.args.get("success")
if success == None: if success is None:
success = "" success = ""
if not os.path.exists(".git"): if not os.path.exists(".git"):
return render_template("settings.html", account=account, return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version="Error") error=error,success=success,version="Error",
internal=account_module.HSD_INTERNAL_NODE,
spv=account_module.isSPV())
info = gitinfo.get_git_info() info = gitinfo.get_git_info()
if not info: if not info:
return render_template("settings.html", account=account, return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version="Error") error=error,success=success,version="Error",
internal=account_module.HSD_INTERNAL_NODE,
spv=account_module.isSPV())
branch = info['refs'] branch = info['refs']
if branch != "main": if branch != "main":
@@ -1174,7 +1165,8 @@ def settings():
version += ' (New version available)' version += ' (New version available)'
return render_template("settings.html", account=account, return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version=version) error=error,success=success,version=version,internal=account_module.HSD_INTERNAL_NODE,
spv=account_module.isSPV())
@app.route('/settings/<action>') @app.route('/settings/<action>')
def settings_action(action): def settings_action(action):
@@ -1191,19 +1183,29 @@ def settings_action(action):
if 'error' in resp: if 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Rescan started") return redirect("/settings?success=Rescan started")
elif action == "resend":
if action == "resend":
resp = account_module.resendTXs() resp = account_module.resendTXs()
if 'error' in resp: if 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Resent transactions") return redirect("/settings?success=Resent transactions")
elif action == "zap": if action == "zap":
resp = account_module.zapTXs(request.cookies.get("account")) age = request.args.get("age", 1200)
if type(resp) == dict and 'error' in resp: try:
age = int(age)
except ValueError:
age = 1200
if age < 0:
age = 1200
resp = account_module.zapTXs(request.cookies.get("account"),age)
if type(resp) is dict and 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Zapped transactions") return redirect("/settings?success=Zapped transactions")
elif action == "xpub":
if action == "xpub":
xpub = account_module.getxPub(request.cookies.get("account")) xpub = account_module.getxPub(request.cookies.get("account"))
content = "<br><br>" content = "<br><br>"
content += f"<textarea style='display: none;' id='data' rows='4' cols='50'>{xpub}</textarea>" content += f"<textarea style='display: none;' id='data' rows='4' cols='50'>{xpub}</textarea>"
@@ -1214,11 +1216,49 @@ def settings_action(action):
title="xPub Key", title="xPub Key",
content=f"<code>{xpub}</code>{content}") content=f"<code>{xpub}</code>{content}")
if action == "restart":
resp = account_module.hsdRestart()
return render_template("message.html", account=account,
title="Restarting",
content="The node is restarting. This may take a minute or two. You can close this window.")
if action == "api-info":
content = f"API URL: <code>http://{account_module.HSD_IP}:{account_module.HSD_NODE_PORT}</code><br>"
content += f"Wallet URL: <code>http://{account_module.HSD_IP}:{account_module.HSD_WALLET_PORT}</code><br>"
content += f"API Key: <code>{account_module.HSD_API}</code><br><br>"
return render_template("message.html", account=account,
title="API Information",
content=content)
if action == "logs":
if not os.path.exists(log_file):
return jsonify({"error": "Log file not found"}), 404
try:
with open(log_file, 'rb') as f:
response = requests.put(f"https://upload.woodburn.au/{os.path.basename(log_file)}", data=f)
if response.status_code == 200 or response.status_code == 201:
url = response.text.strip().split('\n')[-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'])
def upload_image(): def upload_image():
if not 'account' in request.cookies: if 'account' not in request.cookies:
return redirect("/login?message=Not logged in") return redirect("/login?message=Not logged in")
account = request.cookies.get("account") account = request.cookies.get("account")
@@ -1242,7 +1282,7 @@ def upload_image():
return redirect("/settings?error=An error occurred") return redirect("/settings?error=An error occurred")
def latestVersion(branch): def latestVersion(branch):
result = requests.get(f"https://git.woodburn.au/api/v1/repos/nathanwoodburn/firewalletbrowser/branches") result = requests.get("https://git.woodburn.au/api/v1/repos/nathanwoodburn/firewalletbrowser/branches")
if result.status_code != 200: if result.status_code != 200:
return "Error" return "Error"
@@ -1261,6 +1301,9 @@ def login():
wallets = account_module.listWallets() wallets = account_module.listWallets()
wallets = render.wallets(wallets) wallets = render.wallets(wallets)
# If there are no wallets redirect to either register or import
if len(wallets) == 0:
return redirect("/welcome")
if 'message' in request.args: if 'message' in request.args:
return render_template("login.html", return render_template("login.html",
@@ -1276,7 +1319,7 @@ def login_post():
account = request.form.get("account") account = request.form.get("account")
password = request.form.get("password") password = request.form.get("password")
if account == None or password == None: if account is None or password is None:
wallets = account_module.listWallets() wallets = account_module.listWallets()
wallets = render.wallets(wallets) wallets = render.wallets(wallets)
return render_template("login.html", return render_template("login.html",
@@ -1315,7 +1358,7 @@ def register():
password = request.form.get("password") password = request.form.get("password")
repeatPassword = request.form.get("password_repeat") repeatPassword = request.form.get("password_repeat")
if account == None or password == None or repeatPassword == None: if account is None or password is None or repeatPassword is None:
return render_template("register.html", return render_template("register.html",
error="Invalid account or password", error="Invalid account or password",
name=account,password=password,password_repeat=repeatPassword) name=account,password=password,password_repeat=repeatPassword)
@@ -1362,7 +1405,7 @@ def import_wallet():
repeatPassword = request.form.get("password_repeat") repeatPassword = request.form.get("password_repeat")
seed = request.form.get("seed") seed = request.form.get("seed")
if account == None or password == None or repeatPassword == None or seed == None: if account is None or password is None or repeatPassword is None or seed is None:
return render_template("import-wallet.html", return render_template("import-wallet.html",
error="Invalid account, password or seed", error="Invalid account, password or seed",
name=account,password=password,password_repeat=repeatPassword, name=account,password=password,password_repeat=repeatPassword,
@@ -1399,7 +1442,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)
@@ -1453,7 +1496,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)
@@ -1461,12 +1504,12 @@ def plugin(ptype,plugin):
functions = plugins_module.getPluginFunctions(plugin) functions = plugins_module.getPluginFunctions(plugin)
functions = render.plugin_functions(functions,plugin) functions = render.plugin_functions(functions,plugin)
if data['verified'] == False: if not data['verified']:
functions = "<div class='container-fluid'><div class='alert alert-warning' role='alert'>This plugin is not verified and is disabled for your protection. Please check the code before marking the plugin as verified <a href='/plugin/" + plugin + "/verify' class='btn btn-danger'>Verify</a></div></div>" + functions functions = "<div class='container-fluid'><div class='alert alert-warning' role='alert'>This plugin is not verified and is disabled for your protection. Please check the code before marking the plugin as verified <a href='/plugin/" + plugin + "/verify' class='btn btn-danger'>Verify</a></div></div>" + functions
error = request.args.get("error") error = request.args.get("error")
if error == None: if error is None:
error = "" error = ""
return render_template("plugin.html", account=account, return render_template("plugin.html", account=account,
@@ -1492,7 +1535,7 @@ def plugin_verify(ptype,plugin):
data = plugins_module.getPluginData(plugin) data = plugins_module.getPluginData(plugin)
if data['verified'] == False: if not data['verified']:
plugins_module.verifyPlugin(plugin) plugins_module.verifyPlugin(plugin)
return redirect("/plugin/" + plugin) return redirect("/plugin/" + plugin)
@@ -1579,7 +1622,7 @@ def api_hsd(function):
if not domain: if not domain:
return jsonify({"error": "No domain specified"}), 400 return jsonify({"error": "No domain specified"}), 400
domainInfo = account_module.getDomain(domain) domainInfo = account_module.getDomain(domain)
if 'error' in domainInfo and domainInfo['error'] != None: if 'error' in domainInfo and domainInfo['error'] is not None:
return jsonify({"error": domainInfo['error']}), 400 return jsonify({"error": domainInfo['error']}), 400
stats = domainInfo['info']['stats'] if 'stats' in domainInfo['info'] else {} stats = domainInfo['info']['stats'] if 'stats' in domainInfo['info'] else {}
state = domainInfo['info']['state'] state = domainInfo['info']['state']
@@ -1588,13 +1631,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>'
@@ -1614,7 +1655,7 @@ def api_hsd(function):
elif stats['blocksUntilReveal'] == 2: elif stats['blocksUntilReveal'] == 2:
next += "<br>LAST CHANCE TO BID" next += "<br>LAST CHANCE TO BID"
elif stats['blocksUntilReveal'] == 3: elif stats['blocksUntilReveal'] == 3:
next += f"<br>Next block is last chance to bid" next += "<br>Next block is last chance to bid"
elif stats['blocksUntilReveal'] < 6: elif stats['blocksUntilReveal'] < 6:
next += f"<br>Last chance to bid in {stats['blocksUntilReveal']-2} blocks" next += f"<br>Last chance to bid in {stats['blocksUntilReveal']-2} blocks"
@@ -1700,7 +1741,7 @@ def api_wallet(function):
if function == "domains": if function == "domains":
domains = account_module.getDomains(account) domains = account_module.getDomains(account)
if type(domains) == dict and 'error' in domains: if type(domains) is dict and 'error' in domains:
return jsonify({"result": [], "error": domains['error']}) return jsonify({"result": [], "error": domains['error']})
# Add nameRender to each domain # Add nameRender to each domain
@@ -1714,7 +1755,7 @@ def api_wallet(function):
page = request.args.get('page', 1) page = request.args.get('page', 1)
try: try:
page = int(page) page = int(page)
except: except ValueError:
page = 1 page = 1
if page < 1: if page < 1:
@@ -1769,9 +1810,9 @@ def api_wallet(function):
if function == "icon": if function == "icon":
# Check if there is an icon # Check if there is an icon
if not os.path.exists(f'user_data/images'): if not os.path.exists('user_data/images'):
return send_file('templates/assets/img/HNS.png') return send_file('templates/assets/img/HNS.png')
files = os.listdir(f'user_data/images') files = os.listdir('user_data/images')
for file in files: for file in files:
if file.startswith(account): if file.startswith(account):
return send_file(f'user_data/images/{file}') return send_file(f'user_data/images/{file}')
@@ -1787,7 +1828,6 @@ def api_wallet_mobile(function):
return jsonify({"error": "Not logged in"}) return jsonify({"error": "Not logged in"})
account = account_module.check_account(request.cookies.get("account")) account = account_module.check_account(request.cookies.get("account"))
password = request.cookies.get("account","").split(":")[1]
if not account: if not account:
return jsonify({"error": "Invalid account"}) return jsonify({"error": "Invalid account"})
@@ -1806,9 +1846,9 @@ def api_wallet_mobile(function):
@app.route('/api/v1/icon/<account>') @app.route('/api/v1/icon/<account>')
def api_icon(account): def api_icon(account):
if not os.path.exists(f'user_data/images'): if not os.path.exists('user_data/images'):
return send_file('templates/assets/img/HNS.png') return send_file('templates/assets/img/HNS.png')
files = os.listdir(f'user_data/images') files = os.listdir('user_data/images')
for file in files: for file in files:
if file.startswith(account): if file.startswith(account):
return send_file(f'user_data/images/{file}') return send_file(f'user_data/images/{file}')
@@ -1827,6 +1867,29 @@ def api_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:
""" """
@@ -1840,9 +1903,124 @@ 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 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",
"output": f"The wallet is not synced ({wallet_status}). 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
@@ -1880,15 +2058,44 @@ def try_path(path):
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
logger.warning(f"404 Not Found: {request.path}")
account = account_module.check_account(request.cookies.get("account")) account = account_module.check_account(request.cookies.get("account"))
return render_template('404.html',account=account), 404 return render_template('404.html',account=account), 404
#endregion #endregion
if __name__ == '__main__': if __name__ == '__main__':
#TODO add parsing to allow for custom port and host host = '127.0.0.1'
# Check to see if --debug is in the command line arguments port = 5000
# Check if --host is in the command line arguments
if "--host" in sys.argv:
host_index = sys.argv.index("--host") + 1
if host_index < len(sys.argv):
host = sys.argv[host_index]
# Check if --port is in the command line arguments
if "--port" in sys.argv:
port_index = sys.argv.index("--port") + 1
if port_index < len(sys.argv):
try:
port = int(sys.argv[port_index])
except ValueError:
pass
print(f"Starting FireWallet on http://{host}:{port}",flush=True)
if "--debug" in sys.argv: if "--debug" in sys.argv:
app.run(debug=True) console_handler = logging.StreamHandler(sys.stdout)
# Use a simple format for console
console_formatter = logging.Formatter('%(message)s')
console_handler.setFormatter(console_formatter)
console_handler.setLevel(logging.WARNING)
logger.addHandler(console_handler)
logger.setLevel(logging.DEBUG)
app.run(debug=True, host=host, port=port)
else: else:
app.run()
app.run(host=host, port=port)
def tests():
assert blocks_to_time(6) == "1 hrs"
assert blocks_to_time(3) == "30 mins"
assert blocks_to_time(1) == "10 mins"
assert blocks_to_time(10) == "1 hrs 40 mins"

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)
@@ -171,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)

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"]

View File

@@ -2,34 +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","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")
@@ -78,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'
@@ -205,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>"
@@ -276,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']
@@ -284,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
@@ -339,13 +314,13 @@ def bids(bids,reveals):
html += f"<td>{value:,.2f} HNS</td>" html += f"<td>{value:,.2f} HNS</td>"
html += f"<td>{bidValue:,.2f} HNS</td>" html += f"<td>{bidValue:,.2f} HNS</td>"
else: else:
html += f"<td>Hidden until reveal</td>" html += "<td>Hidden until reveal</td>"
html += f"<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 += f"<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 += 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>"
@@ -433,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":
@@ -472,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
@@ -491,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":
@@ -510,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):
@@ -521,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
@@ -540,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:
@@ -558,8 +533,10 @@ def renderDomainAsync(namehash: str) -> None:
if namehash in cache: if namehash in cache:
return return
# Fetch the name outside the lock (network call) # Fetch the name outside the lock (network call) using hsd.hns.au
name = hsd.rpc_getNameByHash(namehash) # name = account.hsd.rpc_getNameByHash(namehash)
name = requests.get(f"https://hsd.hns.au/api/v1/namehash/{namehash}").json()
if name["error"] is None: if name["error"] is None:
name = name["result"] name = name["result"]
rendered = renderDomain(name) rendered = renderDomain(name)

View File

@@ -1,4 +1,3 @@
import os
import sys import sys
import platform import platform
from main import app from main import app
@@ -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

@@ -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>
@@ -98,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">
@@ -121,7 +133,7 @@
<div class="card-body"> <div class="card-body">
<h4 class="card-title">About</h4> <h4 class="card-title">About</h4>
<h6 class="text-muted mb-2 card-subtitle">FireWallet is a UI to allow easy connection with HSD created by <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a> and freely available. Please contact him <a href="https://l.woodburn.au/contact" target="_blank">here</a> if you would like to request any features or report any bugs.<br>FireWallet version: <code>{{version}}</code></h6> <h6 class="text-muted mb-2 card-subtitle">FireWallet is a UI to allow easy connection with HSD created by <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a> and freely available. Please contact him <a href="https://l.woodburn.au/contact" target="_blank">here</a> if you would like to request any features or report any bugs.<br>FireWallet version: <code>{{version}}</code></h6>
<div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i>&nbsp;Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i>&nbsp;Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i>&nbsp;Donate to support development</a></div> <div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i>&nbsp;Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i>&nbsp;Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i>&nbsp;Donate to support development</a><a href="/settings/logs" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-help" style="color: var(--bs-emphasis-color);"></i>&nbsp;Upload logs for debugging</a></div>
</div> </div>
</div> </div>
</div> </div>

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>