Compare commits
53 Commits
feat/WALLE
...
feat/loggi
| Author | SHA1 | Date | |
|---|---|---|---|
|
7fc19a7f19
|
|||
|
eb6306bb83
|
|||
|
9f8daa8b88
|
|||
|
63e0f0b804
|
|||
|
0c17c4ad9b
|
|||
|
938fff8791
|
|||
|
155662d2b1
|
|||
|
97569faf0e
|
|||
|
59000afa87
|
|||
|
699a74f093
|
|||
|
6096f82c4d
|
|||
|
4353eb8fa4
|
|||
|
344cde07d0
|
|||
|
2fb841aeaf
|
|||
|
60df317f78
|
|||
|
4c1ea9fb12
|
|||
|
58ed636ce3
|
|||
|
e537c323c2
|
|||
|
812fc84d3e
|
|||
|
6d318a597b
|
|||
|
83bd6b9643
|
|||
|
c93b2652f5
|
|||
|
86e174c337
|
|||
|
e7b787c30b
|
|||
|
997828795a
|
|||
|
30de2d585e
|
|||
|
56eabfc1fc
|
|||
|
e0f24267f5
|
|||
|
2d51882d20
|
|||
|
06b1eea9ef
|
|||
|
d483cfdcfd
|
|||
|
46ed0173d3
|
|||
|
9dd50d1292
|
|||
|
53148f573e
|
|||
|
e8f052e0d1
|
|||
|
7f450d620a
|
|||
|
41a1bc743f
|
|||
|
30108e3bc5
|
|||
|
a2dc9f43e3
|
|||
|
1203719eac
|
|||
|
373a71f04d
|
|||
|
b76b873036
|
|||
|
23e714fad8
|
|||
|
a36c69ecfc
|
|||
|
1fd9987bf1
|
|||
|
f2cda461ba
|
|||
|
26c5b4a4fa
|
|||
|
7fdc4a3122
|
|||
|
5ff8960b7b
|
|||
|
4c84bc2bbe
|
|||
|
49e378803d
|
|||
|
1c53547047
|
|||
|
080c4402d8
|
32
.dockerignore
Normal file
32
.dockerignore
Normal 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
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.bsdesign filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -4,11 +4,22 @@ on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
Build Image:
|
||||
Build Images:
|
||||
runs-on: [ubuntu-latest, amd]
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- target: default
|
||||
tag_suffix: ""
|
||||
dockerfile: "Dockerfile"
|
||||
- target: hsd
|
||||
tag_suffix: "-hsd"
|
||||
dockerfile: "Dockerfile.hsd"
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Docker
|
||||
run : |
|
||||
apt-get install ca-certificates curl gnupg
|
||||
@@ -17,7 +28,7 @@ jobs:
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
apt-get update
|
||||
apt-get install docker-ce-cli -y
|
||||
apt-get install docker-ce-cli -y
|
||||
- name: Build Docker image
|
||||
run : |
|
||||
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
|
||||
@@ -34,8 +45,8 @@ jobs:
|
||||
fi
|
||||
|
||||
|
||||
docker build -t firewallet:$tag_num .
|
||||
docker tag firewallet:$tag_num git.woodburn.au/nathanwoodburn/firewallet:$tag_num
|
||||
docker push git.woodburn.au/nathanwoodburn/firewallet:$tag_num
|
||||
docker tag firewallet:$tag_num git.woodburn.au/nathanwoodburn/firewallet:$tag
|
||||
docker push git.woodburn.au/nathanwoodburn/firewallet:$tag
|
||||
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${{matrix.variant.tag_suffix}}:$tag_num git.woodburn.au/nathanwoodburn/firewallet${{matrix.variant.tag_suffix}}:$tag_num
|
||||
docker push git.woodburn.au/nathanwoodburn/firewallet${{matrix.variant.tag_suffix}}:$tag_num
|
||||
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${{matrix.variant.tag_suffix}}:$tag
|
||||
40
.gitea/workflows/test.yml
Normal file
40
.gitea/workflows/test.yml
Normal 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
5
.gitignore
vendored
@@ -16,3 +16,8 @@ customPlugins/
|
||||
cache/
|
||||
build/
|
||||
dist/
|
||||
hsd/
|
||||
hsd_data/
|
||||
logs/
|
||||
hsd.lock
|
||||
hsdconfig.json
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,7 +1,7 @@
|
||||
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM python:3.13-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add git openssl curl
|
||||
COPY requirements.txt /app
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip3 install -r requirements.txt
|
||||
@@ -9,10 +9,21 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
COPY . /app
|
||||
|
||||
# Add mount point for data volume
|
||||
# VOLUME /data
|
||||
RUN apk add git openssl curl
|
||||
VOLUME /app/user_data
|
||||
|
||||
|
||||
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"]
|
||||
CMD ["server.py"]
|
||||
|
||||
FROM builder as dev-envs
|
||||
FROM builder AS dev-envs
|
||||
|
||||
57
Dockerfile.hsd
Normal file
57
Dockerfile.hsd
Normal 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.
46
README.md
46
README.md
@@ -13,6 +13,17 @@ cp example.env .env
|
||||
Edit .env to have your HSD api key.
|
||||
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
|
||||
|
||||
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)
|
||||
EXPLORER_TX: URL for exploring transactions (default https://shakeshift.com/transaction/)
|
||||
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
|
||||
|
||||
- This is a work in progress and is not guaranteed to work
|
||||
|
||||
715
account.py
715
account.py
File diff suppressed because it is too large
Load Diff
12
docker-compose-internal.yml
Normal file
12
docker-compose-internal.yml
Normal 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
26
docker-compose.yml
Normal 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:
|
||||
@@ -11,7 +11,6 @@ import dns.query
|
||||
import dns.rdatatype
|
||||
import httpx
|
||||
from requests_doh import DNSOverHTTPSSession, add_dns_provider
|
||||
import requests
|
||||
import urllib3
|
||||
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):
|
||||
try:
|
||||
return emoji.encode("idna").decode("ascii")
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return emoji
|
||||
|
||||
def punycode_to_emoji(punycode):
|
||||
try:
|
||||
return punycode.encode("ascii").decode("idna")
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return punycode
|
||||
@@ -2,4 +2,6 @@ HSD_API=123480615465636893475aCwyaae6s45
|
||||
HSD_IP=localhost
|
||||
THEME=black
|
||||
SHOW_EXPIRED=false
|
||||
EXPLORER_TX=https://shakeshift.com/transaction/
|
||||
EXPLORER_TX=https://shakeshift.com/transaction/
|
||||
DISABLE_WALLETDNS=false
|
||||
INTERNAL_HSD=false
|
||||
45
grant.md
45
grant.md
@@ -1,45 +0,0 @@
|
||||
What have you built previously?
|
||||
- [HNSHosting](https://hnshosting.au)
|
||||
- [ShakeCities](https://shakecities.com)
|
||||
- [FireWallet](https://firewallet.au)
|
||||
- [Git Profile](https://github.com/nathanwoodburn)
|
||||
|
||||
Project summary
|
||||
A Handshake wallet web ui. This will be a HSD wallet web ui that will allow users to manage their Handshake domains via a web interface. This will allow users to easily manage their domains without having to use the command line or bob. One benefit of this is that it will allow users to easily manage their domains from their mobile devices that don't have access to any HNS wallet. This could be done in a secure way by only allowing connections on local network devices (in addition to requiring the wallet credentials).
|
||||
|
||||
Features:
|
||||
- Login with HSD wallet name + password (by default don't show a list of wallets to login to as this could be a security risk)
|
||||
- View account information in a dashboard
|
||||
- Available balance
|
||||
- Total balance
|
||||
- Pending Transactions
|
||||
- List of domains
|
||||
- List of transactions
|
||||
- Manage domains
|
||||
- Transfer domains
|
||||
- Finalize domains
|
||||
- Edit domains
|
||||
- Revoke domains (with a warning and requiring the account password)
|
||||
- Manage wallet
|
||||
- Send HNS
|
||||
- Receive HNS
|
||||
- Auctions
|
||||
- View bids on domain
|
||||
- Open auction
|
||||
- Bid on auction
|
||||
- Reveal bid
|
||||
- Redeem bid
|
||||
- Register domain
|
||||
|
||||
Completion requirements:
|
||||
- Basic functionality including
|
||||
- View info
|
||||
- Send/Receive HNS
|
||||
- Manage domains
|
||||
|
||||
After the initial version is completed I will be looking to add more features including the above mentioned features.
|
||||
|
||||
|
||||
The initial version will be completed in 2-3 weeks with a fully fledged version released later as the features are developed and tested.
|
||||
|
||||
You can contact me at handshake @ nathan.woodburn.au
|
||||
81
install.sh
Executable file
81
install.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
install_command=""
|
||||
|
||||
# Check if currently in the FireWalletBrowser directory
|
||||
if [ -f "server.py" ]; then
|
||||
echo "Please run this script from outside the FireWalletBrowser directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting installation of FireWalletBrowser..."
|
||||
|
||||
# Check if OS is using apt package manager (Debian/Ubuntu)
|
||||
if command -v apt-get &> /dev/null; then
|
||||
install_command="sudo apt-get install -y"
|
||||
dependencies=(git curl wget python3 python3-pip python3-venv)
|
||||
echo "Detected apt package manager."
|
||||
# Check if OS is using pacman package manager (Arch Linux)
|
||||
elif command -v pacman &> /dev/null; then
|
||||
install_command="sudo pacman -S"
|
||||
dependencies=(git curl wget python3 python-pip)
|
||||
echo "Detected pacman package manager."
|
||||
else
|
||||
echo "Unsupported package manager. Please install dependencies manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# List of dependencies to install
|
||||
# Install dependencies
|
||||
for package in "${dependencies[@]}"; do
|
||||
# Check if the package is already installed
|
||||
if command -v $package &> /dev/null || dpkg -s $package &> /dev/null || pacman -Qi $package &> /dev/null; then
|
||||
echo "$package is already installed."
|
||||
continue
|
||||
fi
|
||||
|
||||
|
||||
echo "Installing $package..."
|
||||
$install_command $package
|
||||
# Check if the installation was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install $package. Please check your package manager settings."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! command -v node &> /dev/null || ! command -v npm &> /dev/null; then
|
||||
echo "Installing Node.js and npm..."
|
||||
# Download and install nvm:
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
||||
# in lieu of restarting the shell
|
||||
\. "$HOME/.nvm/nvm.sh"
|
||||
nvm install 20
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install Node.js and npm. Please install them manually."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Node.js and npm are already installed."
|
||||
fi
|
||||
|
||||
# Clone repo
|
||||
git clone https://git.woodburn.au/nathanwoodburn/firewalletbrowser.git
|
||||
|
||||
# Setup venv
|
||||
cd firewalletbrowser || exit 1
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install python dependencies
|
||||
python3 -m pip install -r requirements.txt
|
||||
|
||||
# Write .env file
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "Creating .env file..."
|
||||
echo "INTERNAL_HSD=true" > .env
|
||||
echo "Created .env file with INTERNAL Node enabled."
|
||||
fi
|
||||
|
||||
echo "Installation complete. You can start the application by running ./start.sh"
|
||||
431
main.py
431
main.py
@@ -1,6 +1,7 @@
|
||||
import io
|
||||
import json
|
||||
import random
|
||||
import sqlite3
|
||||
import sys
|
||||
from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory,send_file
|
||||
import os
|
||||
@@ -12,12 +13,12 @@ import re
|
||||
from flask_qrcode import QRcode
|
||||
import domainLookup
|
||||
import urllib.parse
|
||||
import importlib
|
||||
import plugin as plugins_module
|
||||
import gitinfo
|
||||
import datetime
|
||||
import functools
|
||||
import time
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
@@ -30,36 +31,25 @@ fees = 0.02
|
||||
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"
|
||||
# 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)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
|
||||
# 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('/')
|
||||
def index():
|
||||
@@ -81,15 +71,16 @@ def index():
|
||||
if not os.path.exists(".git"):
|
||||
return render_template("index.html", account=account, plugins=plugins)
|
||||
|
||||
info = gitinfo.get_git_info()
|
||||
branch = info['refs']
|
||||
commit = info['commit']
|
||||
if commit != latestVersion(branch):
|
||||
print("New version available",flush=True)
|
||||
plugins += render_template('components/dashboard-alert.html', name='Update', output='A new version of FireWallet is available')
|
||||
alerts = get_alerts(account)
|
||||
for alert in alerts:
|
||||
output_html = alert['output']
|
||||
if 'id' in alert:
|
||||
# Add a dismiss button
|
||||
output_html += f" <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)
|
||||
|
||||
|
||||
def reverseDirection(direction: str):
|
||||
if direction == "⬆":
|
||||
return "⬇"
|
||||
@@ -116,7 +107,7 @@ def transactions():
|
||||
page = request.args.get('page', 1)
|
||||
try:
|
||||
page = int(page)
|
||||
except:
|
||||
except ValueError:
|
||||
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"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}"
|
||||
|
||||
|
||||
@@ -213,7 +204,7 @@ def sendConfirmed():
|
||||
address = request.args.get("address")
|
||||
amount = float(request.args.get("amount","0"))
|
||||
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 isinstance(response['error'], dict):
|
||||
if 'message' in response['error']:
|
||||
@@ -284,7 +275,7 @@ def auctions():
|
||||
|
||||
# Sort
|
||||
sort = request.args.get("sort")
|
||||
if sort == None:
|
||||
if sort is None:
|
||||
sort = "time"
|
||||
sort = sort.lower()
|
||||
sort_price = ""
|
||||
@@ -298,7 +289,7 @@ def auctions():
|
||||
reverse = False
|
||||
|
||||
direction = request.args.get("direction")
|
||||
if direction == None:
|
||||
if direction is None:
|
||||
if sort == "time":
|
||||
direction = "⬆"
|
||||
else:
|
||||
@@ -367,7 +358,7 @@ def revealAllBids():
|
||||
return redirect("/auctions?message=Failed to reveal bids")
|
||||
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
if response['error'] is not None:
|
||||
if response['error']['message'] == "Nothing to do.":
|
||||
return redirect("/auctions?message=No reveals pending")
|
||||
return redirect("/auctions?message=" + response['error']['message'])
|
||||
@@ -390,7 +381,7 @@ def redeemAllBids():
|
||||
return redirect("/auctions?message=Failed to redeem bids")
|
||||
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
if response['error'] is not None:
|
||||
if response['error']['message'] == "Nothing to do.":
|
||||
return redirect("/auctions?message=No redeems pending")
|
||||
return redirect("/auctions?message=" + response['error']['message'])
|
||||
@@ -412,7 +403,7 @@ def registerAllDomains():
|
||||
return redirect("/auctions?message=Failed to register domains")
|
||||
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
if response['error'] is not None:
|
||||
if response['error']['message'] == "Nothing to do.":
|
||||
return redirect("/auctions?message=No domains to register")
|
||||
return redirect("/auctions?message=" + response['error']['message'])
|
||||
@@ -431,7 +422,7 @@ def finalizeAllBids():
|
||||
|
||||
response = account_module.finalizeAll(request.cookies.get("account"))
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
if response['error'] is not None:
|
||||
if response['error']['message'] == "Nothing to do.":
|
||||
return redirect("/dashboard?message=No domains to finalize")
|
||||
return redirect("/dashboard?message=" + response['error']['message'])
|
||||
@@ -509,7 +500,6 @@ def search():
|
||||
domain_info = account_module.getDomain(search_term)
|
||||
owner = 'Unknown'
|
||||
dns = []
|
||||
txs = []
|
||||
|
||||
if domain_info:
|
||||
# Check if info and info.owner
|
||||
@@ -562,10 +552,10 @@ def manage(domain: str):
|
||||
dns = render.dns(dns)
|
||||
|
||||
errorMessage = request.args.get("error")
|
||||
if errorMessage == None:
|
||||
if errorMessage is None:
|
||||
errorMessage = ""
|
||||
address = request.args.get("address")
|
||||
if address == None:
|
||||
if address is None:
|
||||
address = ""
|
||||
|
||||
finalize_time = ""
|
||||
@@ -609,8 +599,8 @@ def finalize(domain: str):
|
||||
|
||||
domain = domain.lower()
|
||||
response = account_module.finalize(request.cookies.get("account"),domain)
|
||||
if response['error'] != None:
|
||||
print(response)
|
||||
if response['error'] is not None:
|
||||
logger.error(f"Error finalizing transfer for {domain}: {response['error']}")
|
||||
return redirect("/manage/" + domain + "?error=" + response['error']['message'])
|
||||
|
||||
return redirect("/success?tx=" + response['result']['hash'])
|
||||
@@ -628,8 +618,8 @@ def cancelTransfer(domain: str):
|
||||
domain = domain.lower()
|
||||
response = account_module.cancelTransfer(request.cookies.get("account"),domain)
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
print(response)
|
||||
if response['error'] is not None:
|
||||
logger.error(f"Error canceling transfer for {domain}: {response['error']}")
|
||||
return redirect("/manage/" + domain + "?error=" + response['error']['message'])
|
||||
|
||||
return redirect("/success?tx=" + response['result']['hash'])
|
||||
@@ -647,9 +637,9 @@ def revokeInit(domain: str):
|
||||
domain = domain.lower()
|
||||
|
||||
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 += f"This cannot be undone after the transaction is sent.<br><br>"
|
||||
content += f"Please enter your password to confirm."
|
||||
content += "This will return the domain to the auction pool and you will lose any funds spent on the domain.<br>"
|
||||
content += "This cannot be undone after the transaction is sent.<br><br>"
|
||||
content += "Please enter your password to confirm."
|
||||
|
||||
cancel = f"/manage/{domain}"
|
||||
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.")
|
||||
|
||||
response = account_module.check_password(request.cookies.get("account"),password)
|
||||
if response == False:
|
||||
if not response:
|
||||
return redirect("/manage/" + domain + "?error=Invalid password")
|
||||
|
||||
|
||||
response = account_module.revoke(request.cookies.get("account"),domain)
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
print(response)
|
||||
if response['error'] is not None:
|
||||
logger.error(f"Error revoking {domain}: {response['error']}")
|
||||
return redirect("/manage/" + domain + "?error=" + response['error']['message'])
|
||||
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
@@ -724,7 +714,7 @@ def editPage(domain: str):
|
||||
|
||||
|
||||
user_edits = request.args.get("dns")
|
||||
if user_edits != None:
|
||||
if user_edits is not None:
|
||||
dns = urllib.parse.unquote(user_edits)
|
||||
else:
|
||||
dns = account_module.getDNS(domain)
|
||||
@@ -737,7 +727,7 @@ def editPage(domain: str):
|
||||
# Check if new records have been added
|
||||
dnsType = request.args.get("type")
|
||||
dnsValue = request.args.get("value")
|
||||
if dnsType != None and dnsValue != None:
|
||||
if dnsType is not None and dnsValue is not None:
|
||||
if dnsType != "DS":
|
||||
dns.append({"type": dnsType, "value": dnsValue})
|
||||
else:
|
||||
@@ -751,7 +741,7 @@ def editPage(domain: str):
|
||||
key_tag = int(ds[0])
|
||||
algorithm = int(ds[1])
|
||||
digest_type = int(ds[2])
|
||||
except:
|
||||
except ValueError:
|
||||
raw_dns = str(dns).replace("'",'"')
|
||||
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("'",'"')
|
||||
dns = render.dns(dns,True)
|
||||
errorMessage = request.args.get("error")
|
||||
if errorMessage == None:
|
||||
if errorMessage is None:
|
||||
errorMessage = ""
|
||||
|
||||
|
||||
@@ -791,7 +781,7 @@ def editSave(domain: str):
|
||||
dns = urllib.parse.unquote(dns)
|
||||
response = account_module.setDNS(request.cookies.get("account"),domain,dns)
|
||||
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"/success?tx={response['hash']}")
|
||||
|
||||
@@ -822,7 +812,7 @@ def transfer(domain):
|
||||
|
||||
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"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}"
|
||||
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>"
|
||||
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"])
|
||||
content += f"Signature:<br><code>{signedMessage["result"]}</code><br><br>"
|
||||
content += f"Signature:<br><code>{signedMessage['result']}</code><br><br>"
|
||||
|
||||
data = {
|
||||
"domain": domain,
|
||||
@@ -889,7 +879,6 @@ def transferConfirm(domain):
|
||||
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
|
||||
|
||||
@app.route('/auction/<domain>')
|
||||
def auction(domain):
|
||||
# Check if the user is logged in
|
||||
@@ -908,7 +897,7 @@ def auction(domain):
|
||||
|
||||
domainInfo = account_module.getDomain(search_term)
|
||||
error = request.args.get("error")
|
||||
if error == None:
|
||||
if error is None:
|
||||
error = ""
|
||||
|
||||
if 'error' in domainInfo:
|
||||
@@ -918,9 +907,9 @@ def auction(domain):
|
||||
error=error)
|
||||
|
||||
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
|
||||
next_action = f'ERROR GETTING NEXT STATE'
|
||||
next_action = 'ERROR GETTING NEXT STATE'
|
||||
else:
|
||||
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
|
||||
return render_template("auction.html", account=account,
|
||||
@@ -938,13 +927,11 @@ def auction(domain):
|
||||
if state == 'CLOSED':
|
||||
if not domainInfo['info']['registered']:
|
||||
if account_module.isOwnDomain(account,domain):
|
||||
print("Waiting to be registered")
|
||||
state = 'PENDING REGISTER'
|
||||
next = "Pending Register"
|
||||
next_action = f'<a href="/auction/{domain}/register">Register Domain</a>'
|
||||
|
||||
else:
|
||||
print("Not registered")
|
||||
state = 'AVAILABLE'
|
||||
next = "Available Now"
|
||||
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
|
||||
@@ -967,7 +954,7 @@ def auction(domain):
|
||||
elif stats['blocksUntilReveal'] == 2:
|
||||
next += "<br>LAST CHANCE TO BID"
|
||||
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:
|
||||
next += f"<br>Last chance to bid in {stats['blocksUntilReveal']-2} blocks"
|
||||
|
||||
@@ -999,7 +986,7 @@ def rescan_auction(domain):
|
||||
|
||||
domain = domain.lower()
|
||||
|
||||
response = account_module.rescan_auction(account,domain)
|
||||
account_module.rescan_auction(account,domain)
|
||||
return redirect("/auction/" + domain)
|
||||
|
||||
@app.route('/auction/<domain>/bid')
|
||||
@@ -1095,7 +1082,7 @@ def open_auction(domain):
|
||||
response = account_module.openAuction(request.cookies.get("account"),domain)
|
||||
|
||||
if 'error' in response:
|
||||
if response['error'] != None:
|
||||
if response['error'] is not None:
|
||||
return redirect("/auction/" + domain + "?error=" + response['error']['message'])
|
||||
|
||||
return redirect(f"/success?tx={response['hash']}")
|
||||
@@ -1144,22 +1131,26 @@ def settings():
|
||||
return redirect("/logout")
|
||||
|
||||
error = request.args.get("error")
|
||||
if error == None:
|
||||
if error is None:
|
||||
error = ""
|
||||
success = request.args.get("success")
|
||||
if success == None:
|
||||
if success is None:
|
||||
success = ""
|
||||
|
||||
|
||||
if not os.path.exists(".git"):
|
||||
return render_template("settings.html", account=account,
|
||||
|
||||
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()
|
||||
if not info:
|
||||
return render_template("settings.html", account=account,
|
||||
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']
|
||||
if branch != "main":
|
||||
@@ -1174,7 +1165,8 @@ def settings():
|
||||
version += ' (New version available)'
|
||||
return render_template("settings.html", account=account,
|
||||
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>')
|
||||
def settings_action(action):
|
||||
@@ -1191,19 +1183,29 @@ def settings_action(action):
|
||||
if 'error' in resp:
|
||||
return redirect("/settings?error=" + str(resp['error']))
|
||||
return redirect("/settings?success=Rescan started")
|
||||
elif action == "resend":
|
||||
|
||||
if action == "resend":
|
||||
resp = account_module.resendTXs()
|
||||
if 'error' in resp:
|
||||
return redirect("/settings?error=" + str(resp['error']))
|
||||
return redirect("/settings?success=Resent transactions")
|
||||
|
||||
|
||||
elif action == "zap":
|
||||
resp = account_module.zapTXs(request.cookies.get("account"))
|
||||
if type(resp) == dict and 'error' in resp:
|
||||
if action == "zap":
|
||||
age = request.args.get("age", 1200)
|
||||
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?success=Zapped transactions")
|
||||
elif action == "xpub":
|
||||
|
||||
if action == "xpub":
|
||||
xpub = account_module.getxPub(request.cookies.get("account"))
|
||||
content = "<br><br>"
|
||||
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",
|
||||
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")
|
||||
|
||||
@app.route('/settings/upload', methods=['POST'])
|
||||
def upload_image():
|
||||
if not 'account' in request.cookies:
|
||||
if 'account' not in request.cookies:
|
||||
return redirect("/login?message=Not logged in")
|
||||
|
||||
account = request.cookies.get("account")
|
||||
@@ -1242,7 +1282,7 @@ def upload_image():
|
||||
return redirect("/settings?error=An error occurred")
|
||||
|
||||
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:
|
||||
return "Error"
|
||||
|
||||
@@ -1261,6 +1301,9 @@ def login():
|
||||
wallets = account_module.listWallets()
|
||||
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:
|
||||
return render_template("login.html",
|
||||
@@ -1276,7 +1319,7 @@ def login_post():
|
||||
account = request.form.get("account")
|
||||
password = request.form.get("password")
|
||||
|
||||
if account == None or password == None:
|
||||
if account is None or password is None:
|
||||
wallets = account_module.listWallets()
|
||||
wallets = render.wallets(wallets)
|
||||
return render_template("login.html",
|
||||
@@ -1315,7 +1358,7 @@ def register():
|
||||
password = request.form.get("password")
|
||||
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",
|
||||
error="Invalid account or password",
|
||||
name=account,password=password,password_repeat=repeatPassword)
|
||||
@@ -1362,7 +1405,7 @@ def import_wallet():
|
||||
repeatPassword = request.form.get("password_repeat")
|
||||
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",
|
||||
error="Invalid account, password or seed",
|
||||
name=account,password=password,password_repeat=repeatPassword,
|
||||
@@ -1399,7 +1442,7 @@ def import_wallet():
|
||||
name=account,password=password,password_repeat=repeatPassword,
|
||||
seed=seed)
|
||||
|
||||
|
||||
add_alert("Rescan needed", "Please rescan the wallet after importing to see all transactions", account)
|
||||
# Set the cookie
|
||||
response = make_response(redirect("/"))
|
||||
response.set_cookie("account", account+":"+password)
|
||||
@@ -1453,7 +1496,7 @@ def plugin(ptype,plugin):
|
||||
plugin = f"{ptype}/{plugin}"
|
||||
|
||||
if not plugins_module.pluginExists(plugin):
|
||||
print(f"Plugin {plugin} not found")
|
||||
logger.warning(f"Plugin not found: {plugin}")
|
||||
return redirect("/plugins")
|
||||
|
||||
data = plugins_module.getPluginData(plugin)
|
||||
@@ -1461,12 +1504,12 @@ def plugin(ptype,plugin):
|
||||
functions = plugins_module.getPluginFunctions(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
|
||||
|
||||
|
||||
error = request.args.get("error")
|
||||
if error == None:
|
||||
if error is None:
|
||||
error = ""
|
||||
|
||||
return render_template("plugin.html", account=account,
|
||||
@@ -1492,7 +1535,7 @@ def plugin_verify(ptype,plugin):
|
||||
|
||||
data = plugins_module.getPluginData(plugin)
|
||||
|
||||
if data['verified'] == False:
|
||||
if not data['verified']:
|
||||
plugins_module.verifyPlugin(plugin)
|
||||
|
||||
return redirect("/plugin/" + plugin)
|
||||
@@ -1579,7 +1622,7 @@ def api_hsd(function):
|
||||
if not domain:
|
||||
return jsonify({"error": "No domain specified"}), 400
|
||||
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
|
||||
stats = domainInfo['info']['stats'] if 'stats' in domainInfo['info'] else {}
|
||||
state = domainInfo['info']['state']
|
||||
@@ -1588,13 +1631,11 @@ def api_hsd(function):
|
||||
if state == 'CLOSED':
|
||||
if not domainInfo['info']['registered']:
|
||||
if account_module.isOwnDomain(account,domain):
|
||||
print("Waiting to be registered")
|
||||
state = 'PENDING REGISTER'
|
||||
next = "Pending Register"
|
||||
next_action = f'<a href="/auction/{domain}/register">Register Domain</a>'
|
||||
|
||||
else:
|
||||
print("Not registered")
|
||||
state = 'AVAILABLE'
|
||||
next = "Available Now"
|
||||
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
|
||||
@@ -1614,7 +1655,7 @@ def api_hsd(function):
|
||||
elif stats['blocksUntilReveal'] == 2:
|
||||
next += "<br>LAST CHANCE TO BID"
|
||||
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:
|
||||
next += f"<br>Last chance to bid in {stats['blocksUntilReveal']-2} blocks"
|
||||
|
||||
@@ -1700,7 +1741,7 @@ def api_wallet(function):
|
||||
|
||||
if function == "domains":
|
||||
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']})
|
||||
|
||||
# Add nameRender to each domain
|
||||
@@ -1714,7 +1755,7 @@ def api_wallet(function):
|
||||
page = request.args.get('page', 1)
|
||||
try:
|
||||
page = int(page)
|
||||
except:
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
if page < 1:
|
||||
@@ -1769,9 +1810,9 @@ def api_wallet(function):
|
||||
|
||||
if function == "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')
|
||||
files = os.listdir(f'user_data/images')
|
||||
files = os.listdir('user_data/images')
|
||||
for file in files:
|
||||
if file.startswith(account):
|
||||
return send_file(f'user_data/images/{file}')
|
||||
@@ -1787,7 +1828,6 @@ def api_wallet_mobile(function):
|
||||
return jsonify({"error": "Not logged in"})
|
||||
|
||||
account = account_module.check_account(request.cookies.get("account"))
|
||||
password = request.cookies.get("account","").split(":")[1]
|
||||
if not account:
|
||||
return jsonify({"error": "Invalid account"})
|
||||
|
||||
@@ -1806,9 +1846,9 @@ def api_wallet_mobile(function):
|
||||
|
||||
@app.route('/api/v1/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')
|
||||
files = os.listdir(f'user_data/images')
|
||||
files = os.listdir('user_data/images')
|
||||
for file in files:
|
||||
if file.startswith(account):
|
||||
return send_file(f'user_data/images/{file}')
|
||||
@@ -1827,6 +1867,29 @@ def api_status():
|
||||
#endregion
|
||||
|
||||
#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:
|
||||
"""
|
||||
@@ -1840,9 +1903,124 @@ def renderDomain(name: str) -> str:
|
||||
return f"{rendered}/ ({name})"
|
||||
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
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
|
||||
|
||||
|
||||
@@ -1880,15 +2058,44 @@ def try_path(path):
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
logger.warning(f"404 Not Found: {request.path}")
|
||||
account = account_module.check_account(request.cookies.get("account"))
|
||||
|
||||
return render_template('404.html',account=account), 404
|
||||
#endregion
|
||||
|
||||
if __name__ == '__main__':
|
||||
#TODO add parsing to allow for custom port and host
|
||||
# Check to see if --debug is in the command line arguments
|
||||
host = '127.0.0.1'
|
||||
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:
|
||||
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:
|
||||
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"
|
||||
12
plugin.py
12
plugin.py
@@ -62,7 +62,8 @@ def listPlugins(update=False):
|
||||
try:
|
||||
with open("user_data/plugin_signatures.json", "r") as f:
|
||||
signatures = json.load(f)
|
||||
except:
|
||||
except Exception as e:
|
||||
print(f"Error loading plugin signatures: {e}")
|
||||
# Write a new signatures file
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
@@ -87,7 +88,8 @@ def verifyPlugin(plugin: str):
|
||||
try:
|
||||
with open("user_data/plugin_signatures.json", "r") as f:
|
||||
signatures = json.load(f)
|
||||
except:
|
||||
except Exception as e:
|
||||
print(f"Error loading plugin signatures: {e}")
|
||||
# Write a new signatures file
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
@@ -120,7 +122,8 @@ def getPluginData(pluginStr: str):
|
||||
try:
|
||||
with open("user_data/plugin_signatures.json", "r") as f:
|
||||
signatures = json.load(f)
|
||||
except:
|
||||
except Exception as e:
|
||||
print(f"Error loading plugin signatures: {e}")
|
||||
# Write a new signatures file
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
@@ -171,7 +174,8 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
|
||||
try:
|
||||
with open("user_data/plugin_signatures.json", "r") as f:
|
||||
signatures = json.load(f)
|
||||
except:
|
||||
except Exception as e:
|
||||
print(f"Error loading plugin signatures: {e}")
|
||||
# Write a new signatures file
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import account
|
||||
import requests
|
||||
import threading
|
||||
@@ -127,7 +126,7 @@ def automations_background(authentication):
|
||||
account_name = account.check_account(authentication)
|
||||
password = ":".join(authentication.split(":")[1:])
|
||||
|
||||
if account_name == False:
|
||||
if not account_name:
|
||||
return {
|
||||
"error": {
|
||||
"message": "Invalid account"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import json
|
||||
import account
|
||||
import requests
|
||||
import os
|
||||
|
||||
|
||||
|
||||
@@ -384,7 +382,7 @@ def bid(params, authentication):
|
||||
bid = float(params["bid"])
|
||||
blind = float(params["blind"])
|
||||
blind+=bid
|
||||
except:
|
||||
except ValueError:
|
||||
return {
|
||||
"status":"Invalid bid amount",
|
||||
"transaction":None
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import json
|
||||
import account
|
||||
import requests
|
||||
import os
|
||||
|
||||
# Plugin Data
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import json
|
||||
import account
|
||||
import requests
|
||||
import os
|
||||
|
||||
# Plugin Data
|
||||
info = {
|
||||
@@ -90,7 +88,7 @@ def main(params, authentication):
|
||||
return {"status": f"Failed: {batch['error']['message']}", "transaction": "None"}
|
||||
|
||||
if 'result' in batch:
|
||||
if batch['result'] != None:
|
||||
if batch['result'] is not None:
|
||||
tx = batch['result']['hash']
|
||||
return {"status": "Success", "transaction": tx}
|
||||
# Note only one batch can be sent at a time
|
||||
|
||||
@@ -93,7 +93,7 @@ def status(params, authentication):
|
||||
response = requests.post(f"https://{instance}/api", json=data, headers=headers)
|
||||
if response.status_code != 200:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
if response.json()["success"] != True:
|
||||
if not response.json()["success"]:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
return {"status": f"Connected to {instance}"}
|
||||
|
||||
@@ -110,7 +110,7 @@ def login(params, authentication):
|
||||
if response.status_code != 200:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
|
||||
if response.json()["success"] != True:
|
||||
if not response.json()["success"]:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
|
||||
auth = {
|
||||
@@ -146,7 +146,7 @@ def addDomain(params, authentication):
|
||||
zones = requests.post(f"https://{instance}/api", json=data, headers=headers)
|
||||
if zones.status_code != 200:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
if zones.json()["success"] != True:
|
||||
if not zones.json()["success"]:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
|
||||
zones = zones.json()["data"]
|
||||
@@ -169,7 +169,7 @@ def addDomain(params, authentication):
|
||||
response = requests.post(f"https://{instance}/api", json=data, headers=headers)
|
||||
if response.status_code != 200:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
if response.json()["success"] != True:
|
||||
if not response.json()["success"]:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
zoneID = response.json()["data"]["zone"]
|
||||
data = {
|
||||
@@ -179,7 +179,7 @@ def addDomain(params, authentication):
|
||||
response = requests.post(f"https://{instance}/api", json=data, headers=headers)
|
||||
if response.status_code != 200:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
if response.json()["success"] != True:
|
||||
if not response.json()["success"]:
|
||||
return {"status": "Error connecting to Varo"}
|
||||
zone = response.json()["data"]
|
||||
|
||||
|
||||
83
render.py
83
render.py
@@ -2,34 +2,9 @@ import datetime
|
||||
import json
|
||||
import urllib.parse
|
||||
from flask import render_template
|
||||
from domainLookup import punycode_to_emoji
|
||||
import os
|
||||
from handywrapper import api
|
||||
import threading
|
||||
|
||||
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)
|
||||
import requests
|
||||
|
||||
# Get Explorer URL
|
||||
TX_EXPLORER_URL = os.getenv("EXPLORER_TX")
|
||||
@@ -78,7 +53,7 @@ def domains(domains, mobile=False):
|
||||
|
||||
link = f'/manage/{domain["name"]}'
|
||||
link_action = "Manage"
|
||||
if domain['registered'] == False:
|
||||
if not domain['registered']:
|
||||
link_action = "Register"
|
||||
link = f'/auction/{domain["name"]}/register'
|
||||
|
||||
@@ -205,7 +180,7 @@ def transactions(txs):
|
||||
elif amount > 0:
|
||||
amount = f"<span style='color: green;'>+{amount:,.2f}</span>"
|
||||
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>"
|
||||
@@ -276,7 +251,7 @@ def txs(data):
|
||||
amount = entry['amount']
|
||||
amount = amount / 1000000
|
||||
|
||||
if entry['blind'] == None:
|
||||
if entry['blind'] is None:
|
||||
html_output += f"<td>{amount:,.2f} HNS</td>\n"
|
||||
else:
|
||||
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>{timestamp_to_readable_time(entry['time'])}</td>\n"
|
||||
html_output += f"</tr>\n"
|
||||
html_output += "</tr>\n"
|
||||
|
||||
return html_output
|
||||
|
||||
@@ -339,13 +314,13 @@ def bids(bids,reveals):
|
||||
html += f"<td>{value:,.2f} HNS</td>"
|
||||
html += f"<td>{bidValue:,.2f} HNS</td>"
|
||||
else:
|
||||
html += f"<td>Hidden until reveal</td>"
|
||||
html += f"<td>Hidden until reveal</td>"
|
||||
html += "<td>Hidden until reveal</td>"
|
||||
html += "<td>Hidden until reveal</td>"
|
||||
|
||||
if bid['own']:
|
||||
html += "<td>You</td>"
|
||||
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 += "</tr>"
|
||||
@@ -433,22 +408,22 @@ def plugin_functions(functions, pluginName):
|
||||
functionType = functions[function]["type"]
|
||||
|
||||
|
||||
html += f'<div class="card" style="margin-top: 50px;">'
|
||||
html += f'<div class="card-body">'
|
||||
html += '<div class="card" style="margin-top: 50px;">'
|
||||
html += '<div class="card-body">'
|
||||
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">Function type: {functionType.capitalize()}</h6>'
|
||||
|
||||
if functionType != "default":
|
||||
html += f'<p class="card-text">Returns: {returns}</p>'
|
||||
html += f'</div>'
|
||||
html += f'</div>'
|
||||
html += '</div>'
|
||||
html += '</div>'
|
||||
continue
|
||||
|
||||
# Form
|
||||
html += f'<form method="post" style="padding: 20px;" action="/plugin/{pluginName}/{function}">'
|
||||
for param in params:
|
||||
html += f'<div style="margin-bottom: 20px;">'
|
||||
html += '<div style="margin-bottom: 20px;">'
|
||||
paramName = params[param]["name"]
|
||||
paramType = params[param]["type"]
|
||||
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 += f'</form>'
|
||||
html += '<button type="submit" class="btn btn-primary">Submit</button>'
|
||||
html += '</form>'
|
||||
# For debugging
|
||||
html += f'<p class="card-text">Returns: {returns}</p>'
|
||||
html += f'</div>'
|
||||
html += f'</div>'
|
||||
html += '</div>'
|
||||
html += '</div>'
|
||||
|
||||
|
||||
return html
|
||||
@@ -491,16 +466,16 @@ def plugin_output(outputs, returns):
|
||||
for returnOutput in returns:
|
||||
if returnOutput not in outputs:
|
||||
continue
|
||||
html += f'<div class="card" style="margin-top: 50px; margin-bottom: 50px;">'
|
||||
html += f'<div class="card-body">'
|
||||
html += '<div class="card" style="margin-top: 50px; margin-bottom: 50px;">'
|
||||
html += '<div class="card-body">'
|
||||
html += f'<h4 class="card-title">{returns[returnOutput]["name"]}</h4>'
|
||||
|
||||
output = outputs[returnOutput]
|
||||
if returns[returnOutput]["type"] == "list":
|
||||
html += f'<ul>'
|
||||
html += '<ul>'
|
||||
for item in output:
|
||||
html += f'<li>{item}</li>'
|
||||
html += f'</ul>'
|
||||
html += '</ul>'
|
||||
elif returns[returnOutput]["type"] == "text":
|
||||
html += f'<p>{output}</p>'
|
||||
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 += f'</div>'
|
||||
html += f'</div>'
|
||||
html += '</div>'
|
||||
html += '</div>'
|
||||
return html
|
||||
|
||||
def plugin_output_dash(outputs, returns):
|
||||
@@ -521,7 +496,7 @@ def plugin_output_dash(outputs, returns):
|
||||
for returnOutput in returns:
|
||||
if returnOutput not in outputs:
|
||||
continue
|
||||
if outputs[returnOutput] == None:
|
||||
if outputs[returnOutput] is None:
|
||||
continue
|
||||
html += render_template('components/dashboard-plugin.html', name=returns[returnOutput]["name"], output=outputs[returnOutput])
|
||||
return html
|
||||
@@ -540,7 +515,7 @@ def renderDomain(name: str) -> str:
|
||||
return f"{rendered}/ ({name})"
|
||||
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return f"{name}/"
|
||||
|
||||
def renderDomainAsync(namehash: str) -> None:
|
||||
@@ -558,8 +533,10 @@ def renderDomainAsync(namehash: str) -> None:
|
||||
|
||||
if namehash in cache:
|
||||
return
|
||||
# Fetch the name outside the lock (network call)
|
||||
name = hsd.rpc_getNameByHash(namehash)
|
||||
# Fetch the name outside the lock (network call) using hsd.hns.au
|
||||
# name = account.hsd.rpc_getNameByHash(namehash)
|
||||
name = requests.get(f"https://hsd.hns.au/api/v1/namehash/{namehash}").json()
|
||||
|
||||
if name["error"] is None:
|
||||
name = name["result"]
|
||||
rendered = renderDomain(name)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
from main import app
|
||||
@@ -32,13 +31,13 @@ def gunicornServer():
|
||||
gunicorn_app.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__':
|
||||
# Check if --gunicorn is in the command line arguments
|
||||
if "--gunicorn" in sys.argv:
|
||||
gunicornServer()
|
||||
sys.exit()
|
||||
|
||||
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(f'Serving on http://0.0.0.0:5000/', flush=True)
|
||||
print('Press Ctrl+C to stop the server', flush=True)
|
||||
print('Serving on http://0.0.0.0:5000/', flush=True)
|
||||
serve(app, host="0.0.0.0", port=5000, threads=threads)
|
||||
|
||||
9
start.sh
Executable file
9
start.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Find if .venv exists
|
||||
if [ -d ".venv" ]; then
|
||||
echo "Virtual environment found. Activating..."
|
||||
source .venv/bin/activate
|
||||
fi
|
||||
|
||||
python3 server.py
|
||||
@@ -68,25 +68,37 @@
|
||||
<h3 class="mb-1" style="text-align: center;color: rgb(0,255,0);">{{success}}</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Node Settings</h4><small>HSD Version: v{{hsd_version}}</small>
|
||||
<h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div><a class="btn btn-primary stick-right" role="button" href="/settings/resend">Resend</a>
|
||||
<h3>Resend unconfirmed transactions</h3><span>Resend any transactions that haven't been mined yet.</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div><a class="btn btn-primary stick-right" role="button" href="/settings/zap">Zap</a>
|
||||
<h3>Delete unconfirmed transactions</h3><span>This will only remove pending tx from the wallet older than 20 minutes (~ 2 blocks)</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 class="card-title">Node Settings</h4><small>HSD Version: v{{hsd_version}} Type: {% if internal %} Internal {% else %} Remote {% endif %} ({% if spv %}SPV{% else %}Full Node{% endif %})</small>
|
||||
<h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6><ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div><a class="btn btn-primary stick-right" role="button" href="/settings/resend">Resend</a>
|
||||
<h3>Resend unconfirmed transactions</h3><span>Resend any transactions that haven't been mined yet.</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div><a class="btn btn-primary stick-right" role="button" href="/settings/zap">Zap</a>
|
||||
<h3>Delete unconfirmed transactions</h3><span>This will only remove pending tx from the wallet older than 20 minutes (~ 2 blocks)</span>
|
||||
</div>
|
||||
</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'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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,7 +110,7 @@
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<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>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
@@ -121,7 +133,7 @@
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i> Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i> Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i> Donate to support development</a></div>
|
||||
<div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i> Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i> Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i> Donate to support development</a><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> Upload logs for debugging</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
47
templates/welcome.html
Normal file
47
templates/welcome.html
Normal 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&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("/assets/img/favicon.png") 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>
|
||||
Reference in New Issue
Block a user