Compare commits
57 Commits
f2cda461ba
...
fix/balanc
| Author | SHA1 | Date | |
|---|---|---|---|
|
c4cd2bc443
|
|||
|
608933c228
|
|||
|
d9e847a995
|
|||
|
15d919ca97
|
|||
|
aa52911823
|
|||
|
2ee294cab8
|
|||
|
19771fe30d
|
|||
|
12d3958b9d
|
|||
|
d20fc1eb55
|
|||
|
148e5f325a
|
|||
|
6442aa4df6
|
|||
|
2e86e64dd0
|
|||
|
7fc19a7f19
|
|||
|
eb6306bb83
|
|||
|
9f8daa8b88
|
|||
|
63e0f0b804
|
|||
|
0c17c4ad9b
|
|||
|
938fff8791
|
|||
|
155662d2b1
|
|||
|
97569faf0e
|
|||
|
59000afa87
|
|||
|
699a74f093
|
|||
|
6096f82c4d
|
|||
|
4353eb8fa4
|
|||
|
344cde07d0
|
|||
|
2fb841aeaf
|
|||
|
60df317f78
|
|||
|
4c1ea9fb12
|
|||
|
58ed636ce3
|
|||
|
e537c323c2
|
|||
|
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
|
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:
|
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
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
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,7 @@ cache/
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
hsd/
|
hsd/
|
||||||
hsd-data/
|
hsd_data/
|
||||||
|
logs/
|
||||||
hsd.lock
|
hsd.lock
|
||||||
hsdconfig.json
|
hsdconfig.json
|
||||||
|
|||||||
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
|
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
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.
30
README.md
30
README.md
@@ -13,6 +13,17 @@ cp example.env .env
|
|||||||
Edit .env to have your HSD api key.
|
Edit .env to have your HSD api key.
|
||||||
If you have HSD runnning on a separate computer also add the IP here
|
If you have HSD runnning on a separate computer also add the IP here
|
||||||
|
|
||||||
|
For a quick and easy installation on ubuntu/debian you can run the install.sh script
|
||||||
|
```bash
|
||||||
|
curl https://firewallet.au/install.sh | bash
|
||||||
|
```
|
||||||
|
This will install all dependencies (including Node/NPM for an internal HSD node), create a python virtual environment and install the required python packages.
|
||||||
|
After the script has run you can start the wallet with
|
||||||
|
```bash
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Make sure HSD is running then run the following commands:
|
Make sure HSD is running then run the following commands:
|
||||||
@@ -143,6 +154,25 @@ If you set INTERNAL_HSD=true in the .env file the wallet will start and manage i
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
393
account.py
393
account.py
@@ -13,7 +13,8 @@ import signal
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from functools import wraps
|
import logging
|
||||||
|
logger = logging.getLogger("firewallet")
|
||||||
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
@@ -44,9 +45,7 @@ if HSD_INTERNAL_NODE:
|
|||||||
HSD_API = "firewallet-" + str(int(time.time()))
|
HSD_API = "firewallet-" + str(int(time.time()))
|
||||||
HSD_IP = "localhost"
|
HSD_IP = "localhost"
|
||||||
|
|
||||||
SHOW_EXPIRED = os.getenv("SHOW_EXPIRED")
|
SHOW_EXPIRED = os.getenv("SHOW_EXPIRED","false").lower() in ["1","true","yes"]
|
||||||
if SHOW_EXPIRED is None:
|
|
||||||
SHOW_EXPIRED = False
|
|
||||||
|
|
||||||
HSD_PROCESS = None
|
HSD_PROCESS = None
|
||||||
SPV_MODE = None
|
SPV_MODE = None
|
||||||
@@ -64,7 +63,8 @@ HSD_CONFIG = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
CACHE_TTL = int(os.getenv("CACHE_TTL",90))
|
TX_CACHE_TTL = 3600
|
||||||
|
DOMAIN_CACHE_TTL = int(os.getenv("CACHE_TTL",90))
|
||||||
|
|
||||||
if not os.path.exists('hsdconfig.json'):
|
if not os.path.exists('hsdconfig.json'):
|
||||||
with open('hsdconfig.json', 'w') as f:
|
with open('hsdconfig.json', 'w') as f:
|
||||||
@@ -78,8 +78,6 @@ else:
|
|||||||
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
|
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
|
||||||
hsw = api.hsw(HSD_API, HSD_IP, HSD_WALLET_PORT)
|
hsw = api.hsw(HSD_API, HSD_IP, HSD_WALLET_PORT)
|
||||||
|
|
||||||
cacheTime = 3600
|
|
||||||
|
|
||||||
# Verify the connection
|
# Verify the connection
|
||||||
response = hsd.getInfo()
|
response = hsd.getInfo()
|
||||||
|
|
||||||
@@ -95,6 +93,7 @@ def hsdConnected():
|
|||||||
def hsdVersion(format=True):
|
def hsdVersion(format=True):
|
||||||
info = hsd.getInfo()
|
info = hsd.getInfo()
|
||||||
if 'error' in info:
|
if 'error' in info:
|
||||||
|
logger.error(f"HSD connection error: {info.get('error', 'Unknown error')}")
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
# Check if SPV mode is enabled
|
# Check if SPV mode is enabled
|
||||||
@@ -118,9 +117,12 @@ def check_account(cookie: str | None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
account = cookie.split(":")[0]
|
account = cookie.split(":")[0]
|
||||||
|
if len(account) < 1:
|
||||||
|
return False
|
||||||
# Check if the account is valid
|
# Check if the account is valid
|
||||||
info = hsw.getAccountInfo(account, 'default')
|
info = hsw.getAccountInfo(account, 'default')
|
||||||
if 'error' in info:
|
if 'error' in info:
|
||||||
|
logger.error(f"HSW error checking account {account}: {info.get('error', 'Unknown error')}")
|
||||||
return False
|
return False
|
||||||
return account
|
return account
|
||||||
|
|
||||||
@@ -132,7 +134,7 @@ def check_password(cookie: str|None, password: str|None):
|
|||||||
password = ""
|
password = ""
|
||||||
|
|
||||||
account = check_account(cookie)
|
account = check_account(cookie)
|
||||||
if account == False:
|
if not account:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check if the password is valid
|
# Check if the password is valid
|
||||||
@@ -272,7 +274,7 @@ def getCachedDomains():
|
|||||||
domain_cache[row['name']] = json.loads(row['info'])
|
domain_cache[row['name']] = json.loads(row['info'])
|
||||||
domain_cache[row['name']]['last_updated'] = row['last_updated']
|
domain_cache[row['name']]['last_updated'] = row['last_updated']
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(f"Error parsing cached data for domain {row['name']}")
|
logger.error(f"Error parsing cached data for domain {row['name']}")
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
return domain_cache
|
return domain_cache
|
||||||
@@ -312,7 +314,7 @@ def update_domain_cache(domain_names: list):
|
|||||||
domain_info = getDomain(domain_name)
|
domain_info = getDomain(domain_name)
|
||||||
|
|
||||||
if 'error' in domain_info or not domain_info.get('info'):
|
if 'error' in domain_info or not domain_info.get('info'):
|
||||||
print(f"Failed to get info for domain {domain_name}: {domain_info.get('error', 'Unknown error')}", flush=True)
|
logger.error(f"Failed to get info for domain {domain_name}: {domain_info.get('error', 'Unknown error')}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Update or insert into database
|
# Update or insert into database
|
||||||
@@ -324,9 +326,9 @@ def update_domain_cache(domain_names: list):
|
|||||||
(domain_name, serialized_info, now)
|
(domain_name, serialized_info, now)
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Updated cache for domain {domain_name}")
|
logger.info(f"Updated cache for domain {domain_name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating cache for domain {domain_name}: {str(e)}")
|
logger.error(f"Error updating cache for domain {domain_name}: {str(e)}", exc_info=True)
|
||||||
finally:
|
finally:
|
||||||
# Always remove from active set, even if there was an error
|
# Always remove from active set, even if there was an error
|
||||||
with DOMAIN_UPDATE_LOCK:
|
with DOMAIN_UPDATE_LOCK:
|
||||||
@@ -338,20 +340,21 @@ def update_domain_cache(domain_names: list):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating domain cache: {str(e)}", flush=True)
|
logger.error(f"Error updating domain cache: {str(e)}", exc_info=True)
|
||||||
# Make sure to clean up the active set on any exception
|
# Make sure to clean up the active set on any exception
|
||||||
with DOMAIN_UPDATE_LOCK:
|
with DOMAIN_UPDATE_LOCK:
|
||||||
for domain in domains_to_update:
|
for domain in domains_to_update:
|
||||||
if domain in ACTIVE_DOMAIN_UPDATES:
|
if domain in ACTIVE_DOMAIN_UPDATES:
|
||||||
ACTIVE_DOMAIN_UPDATES.remove(domain)
|
ACTIVE_DOMAIN_UPDATES.remove(domain)
|
||||||
|
|
||||||
print("Updated cache for domains")
|
logger.info("Updated cache for domains")
|
||||||
|
|
||||||
|
|
||||||
def getBalance(account: str):
|
def getBalance(account: str):
|
||||||
# Get the total balance
|
# Get the total balance
|
||||||
info = hsw.getBalance('default', account)
|
info = hsw.getBalance('default', account)
|
||||||
if 'error' in info:
|
if 'error' in info:
|
||||||
|
logger.error(f"Error getting balance for account {account}: {info['error']}")
|
||||||
return {'available': 0, 'total': 0}
|
return {'available': 0, 'total': 0}
|
||||||
|
|
||||||
total = info['confirmed']
|
total = info['confirmed']
|
||||||
@@ -361,8 +364,9 @@ def getBalance(account: str):
|
|||||||
# Convert to HNS
|
# Convert to HNS
|
||||||
total = total / 1000000
|
total = total / 1000000
|
||||||
available = available / 1000000
|
available = available / 1000000
|
||||||
|
logger.debug(f"Initial balance for account {account}: total={total}, available={available}, locked={locked}")
|
||||||
|
|
||||||
domains = getDomains(account)
|
domains = getDomains(account,True,True)
|
||||||
domainValue = 0
|
domainValue = 0
|
||||||
domains_to_update = [] # Track domains that need cache updates
|
domains_to_update = [] # Track domains that need cache updates
|
||||||
|
|
||||||
@@ -377,7 +381,7 @@ def getBalance(account: str):
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
cache_cutoff = now - (CACHE_TTL * 86400) # Cache TTL in days
|
cache_cutoff = now - (DOMAIN_CACHE_TTL * 86400) # Cache TTL in days
|
||||||
|
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
domain_name = domain['name']
|
domain_name = domain['name']
|
||||||
@@ -404,6 +408,7 @@ def getBalance(account: str):
|
|||||||
if domain_info.get('info', {}).get('state', "") == "CLOSED":
|
if domain_info.get('info', {}).get('state', "") == "CLOSED":
|
||||||
domainValue += domain_info.get('info', {}).get('value', 0)
|
domainValue += domain_info.get('info', {}).get('value', 0)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
|
logger.warning(f"Error parsing cached data for domain {domain_name}")
|
||||||
# Only add for update if not already being updated
|
# Only add for update if not already being updated
|
||||||
with DOMAIN_UPDATE_LOCK:
|
with DOMAIN_UPDATE_LOCK:
|
||||||
if domain_name not in ACTIVE_DOMAIN_UPDATES:
|
if domain_name not in ACTIVE_DOMAIN_UPDATES:
|
||||||
@@ -426,6 +431,7 @@ def getBalance(account: str):
|
|||||||
|
|
||||||
total = total - (domainValue/1000000)
|
total = total - (domainValue/1000000)
|
||||||
locked = locked - (domainValue/1000000)
|
locked = locked - (domainValue/1000000)
|
||||||
|
logger.debug(f"Adjusted balance for account {account}: total={total}, available={available}, locked={locked}")
|
||||||
|
|
||||||
# Only keep 2 decimal places
|
# Only keep 2 decimal places
|
||||||
total = round(total, 2)
|
total = round(total, 2)
|
||||||
@@ -467,61 +473,100 @@ def getPendingTX(account: str):
|
|||||||
return pending
|
return pending
|
||||||
|
|
||||||
|
|
||||||
def getDomains(account, own=True):
|
def getDomains(account, own: bool = True, expired: bool = SHOW_EXPIRED):
|
||||||
if own:
|
if own:
|
||||||
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name?own=true"))
|
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name?own=true"))
|
||||||
else:
|
else:
|
||||||
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name"))
|
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name"))
|
||||||
info = response.json()
|
info = response.json()
|
||||||
|
|
||||||
if SHOW_EXPIRED:
|
if expired:
|
||||||
return info
|
return info
|
||||||
|
|
||||||
# Remove any expired domains
|
# Remove any expired domains
|
||||||
domains = []
|
domains = []
|
||||||
for domain in info:
|
for domain in info:
|
||||||
if 'stats' in domain:
|
if 'stats' in domain and domain['stats'] is not None:
|
||||||
if 'daysUntilExpire' in domain['stats']:
|
if 'daysUntilExpire' in domain['stats']:
|
||||||
if domain['stats']['daysUntilExpire'] < 0:
|
if domain['stats']['daysUntilExpire'] < 0:
|
||||||
|
logger.debug(f"Excluding expired domain: {domain['name']} due to daysUntilExpire")
|
||||||
|
continue
|
||||||
|
if 'blocksSinceExpired' in domain['stats']:
|
||||||
|
if domain['stats']['blocksSinceExpired'] > 0:
|
||||||
|
logger.debug(f"Excluding expired domain: {domain['name']} due to blocksSinceExpired")
|
||||||
continue
|
continue
|
||||||
domains.append(domain)
|
domains.append(domain)
|
||||||
|
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
def init_tx_page_db():
|
||||||
|
"""Initialize the SQLite database for transaction page cache."""
|
||||||
|
os.makedirs('cache', exist_ok=True)
|
||||||
|
db_path = os.path.join('cache', 'tx_pages.db')
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create the tx_pages table if it doesn't exist
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS tx_pages (
|
||||||
|
account TEXT,
|
||||||
|
page_key TEXT,
|
||||||
|
txid TEXT,
|
||||||
|
timestamp INTEGER,
|
||||||
|
PRIMARY KEY (account, page_key)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def getPageTXCache(account, page, size=100):
|
def getPageTXCache(account, page, size=100):
|
||||||
page = f"{page}-{size}"
|
"""Get cached transaction ID from SQLite database."""
|
||||||
if not os.path.exists(f'cache'):
|
account = getxPub(account)
|
||||||
os.mkdir(f'cache')
|
page_key = f"{page}-{size}"
|
||||||
|
|
||||||
if not os.path.exists(f'cache/{account}_page.json'):
|
# Initialize database if needed
|
||||||
with open(f'cache/{account}_page.json', 'w') as f:
|
init_tx_page_db()
|
||||||
f.write('{}')
|
|
||||||
with open(f'cache/{account}_page.json') as f:
|
|
||||||
pageCache = json.load(f)
|
|
||||||
|
|
||||||
if page in pageCache and pageCache[page]['time'] > int(time.time()) - cacheTime:
|
db_path = os.path.join('cache', 'tx_pages.db')
|
||||||
return pageCache[page]['txid']
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Query for the cached transaction ID
|
||||||
|
cursor.execute(
|
||||||
|
'SELECT txid, timestamp FROM tx_pages WHERE account = ? AND page_key = ?',
|
||||||
|
(account, page_key)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row and row[1] > int(time.time()) - TX_CACHE_TTL:
|
||||||
|
return row[0] # Return the cached txid
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def pushPageTXCache(account, page, txid, size=100):
|
def pushPageTXCache(account, page, txid, size=100):
|
||||||
page = f"{page}-{size}"
|
"""Store transaction ID in SQLite database."""
|
||||||
if not os.path.exists(f'cache/{account}_page.json'):
|
account = getxPub(account)
|
||||||
with open(f'cache/{account}_page.json', 'w') as f:
|
page_key = f"{page}-{size}"
|
||||||
f.write('{}')
|
|
||||||
with open(f'cache/{account}_page.json') as f:
|
|
||||||
pageCache = json.load(f)
|
|
||||||
|
|
||||||
pageCache[page] = {
|
# Initialize database if needed
|
||||||
'time': int(time.time()),
|
init_tx_page_db()
|
||||||
'txid': txid
|
|
||||||
}
|
|
||||||
with open(f'cache/{account}_page.json', 'w') as f:
|
|
||||||
json.dump(pageCache, f, indent=4)
|
|
||||||
|
|
||||||
return pageCache[page]['txid']
|
db_path = os.path.join('cache', 'tx_pages.db')
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Insert or replace the transaction ID
|
||||||
|
cursor.execute(
|
||||||
|
'INSERT OR REPLACE INTO tx_pages (account, page_key, txid, timestamp) VALUES (?, ?, ?, ?)',
|
||||||
|
(account, page_key, txid, int(time.time()))
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return txid
|
||||||
|
|
||||||
def getTXFromPage(account, page, size=100):
|
def getTXFromPage(account, page, size=100):
|
||||||
if page == 1:
|
if page == 1:
|
||||||
@@ -545,6 +590,7 @@ def getTransactions(account, page=1, limit=100):
|
|||||||
return []
|
return []
|
||||||
info = hsw.getWalletTxHistory(account)
|
info = hsw.getWalletTxHistory(account)
|
||||||
if 'error' in info:
|
if 'error' in info:
|
||||||
|
logger.error(f"Error getting transactions for account {account}: {info['error']}")
|
||||||
return []
|
return []
|
||||||
return info[::-1]
|
return info[::-1]
|
||||||
|
|
||||||
@@ -562,14 +608,14 @@ def getTransactions(account, page=1, limit=100):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print(response.text)
|
logger.error(f"Error fetching transactions: {response.status_code} - {response.text}")
|
||||||
return []
|
return []
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
# Refresh the cache if the next page is different
|
# Refresh the cache if the next page is different
|
||||||
nextPage = getPageTXCache(account, page, limit)
|
nextPage = getPageTXCache(account, page, limit)
|
||||||
if nextPage is not None and nextPage != data[-1]['hash']:
|
if nextPage is not None and nextPage != data[-1]['hash']:
|
||||||
print(f'Refreshing page {page}')
|
logger.info(f'Refreshing tx page {page}')
|
||||||
pushPageTXCache(account, page, data[-1]['hash'], limit)
|
pushPageTXCache(account, page, data[-1]['hash'], limit)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -606,7 +652,7 @@ def check_address(address: str, allow_name: bool = True, return_address: bool =
|
|||||||
return False
|
return False
|
||||||
return 'Invalid address'
|
return 'Invalid address'
|
||||||
|
|
||||||
if response['result']['isvalid'] == True:
|
if response['result']['isvalid']:
|
||||||
if return_address:
|
if return_address:
|
||||||
return address
|
return address
|
||||||
return 'Valid address'
|
return 'Valid address'
|
||||||
@@ -740,7 +786,6 @@ def getDomain(domain: str):
|
|||||||
"message": response['error']['message']
|
"message": response['error']['message']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response['result']
|
return response['result']
|
||||||
|
|
||||||
def isKnownDomain(domain: str) -> bool:
|
def isKnownDomain(domain: str) -> bool:
|
||||||
@@ -757,11 +802,12 @@ def getAddressFromCoin(coinhash: str, coinindex = 0):
|
|||||||
# Get the address from the hash
|
# Get the address from the hash
|
||||||
response = requests.get(get_node_api_url(f"coin/{coinhash}/{coinindex}"))
|
response = requests.get(get_node_api_url(f"coin/{coinhash}/{coinindex}"))
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print(f"Error getting address from coin")
|
logger.error("Error getting address from coin")
|
||||||
return "No Owner"
|
return "No Owner"
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if 'address' not in data:
|
if 'address' not in data:
|
||||||
print(json.dumps(data, indent=4))
|
logger.error("Error getting address from coin")
|
||||||
|
logger.error(json.dumps(data, indent=4))
|
||||||
return "No Owner"
|
return "No Owner"
|
||||||
return data['address']
|
return data['address']
|
||||||
|
|
||||||
@@ -770,7 +816,7 @@ def renewDomain(account, domain):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -783,6 +829,17 @@ def renewDomain(account, domain):
|
|||||||
|
|
||||||
def getDNS(domain: str):
|
def getDNS(domain: str):
|
||||||
# Get the DNS
|
# Get the DNS
|
||||||
|
|
||||||
|
if isSPV():
|
||||||
|
response = requests.get(f"https://hsd.hns.au/api/v1/nameresource/{domain}")
|
||||||
|
if response.status_code != 200:
|
||||||
|
return {
|
||||||
|
"error": f"Error fetching DNS records: {response.status_code}"
|
||||||
|
}
|
||||||
|
response = response.json()
|
||||||
|
return response.get('records', [])
|
||||||
|
|
||||||
|
|
||||||
response = hsd.rpc_getNameResource(domain)
|
response = hsd.rpc_getNameResource(domain)
|
||||||
if response['error'] is not None:
|
if response['error'] is not None:
|
||||||
return {
|
return {
|
||||||
@@ -792,7 +849,7 @@ def getDNS(domain: str):
|
|||||||
return {
|
return {
|
||||||
"error": "No DNS records"
|
"error": "No DNS records"
|
||||||
}
|
}
|
||||||
if response['result'] == None:
|
if response['result'] is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if 'records' not in response['result']:
|
if 'records' not in response['result']:
|
||||||
@@ -804,7 +861,7 @@ def setDNS(account, domain, records):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -867,6 +924,7 @@ def register(account, domain):
|
|||||||
def getNodeSync():
|
def getNodeSync():
|
||||||
response = hsd.getInfo()
|
response = hsd.getInfo()
|
||||||
if 'error' in response:
|
if 'error' in response:
|
||||||
|
logger.error(f"Error getting node sync status: {response['error']}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
sync = response['chain']['progress']*100
|
sync = response['chain']['progress']*100
|
||||||
@@ -874,11 +932,14 @@ def getNodeSync():
|
|||||||
return sync
|
return sync
|
||||||
|
|
||||||
|
|
||||||
def getWalletStatus():
|
def getWalletStatus(verbose: bool = False):
|
||||||
response = hsw.rpc_getWalletInfo()
|
response = hsw.rpc_getWalletInfo()
|
||||||
if 'error' in response and response['error'] != None:
|
|
||||||
|
if 'error' in response and response['error'] is not None:
|
||||||
return "Error"
|
return "Error"
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
return response.get('result', {})
|
||||||
# return response
|
# return response
|
||||||
walletHeight = response['result']['height']
|
walletHeight = response['result']['height']
|
||||||
# Get the current block height
|
# Get the current block height
|
||||||
@@ -925,7 +986,7 @@ def getPendingReveals(account):
|
|||||||
if bid['name'] == domain['name']:
|
if bid['name'] == domain['name']:
|
||||||
state_found = False
|
state_found = False
|
||||||
for reveal in reveals:
|
for reveal in reveals:
|
||||||
if reveal['own'] == True:
|
if reveal['own']:
|
||||||
if bid['value'] == reveal['value']:
|
if bid['value'] == reveal['value']:
|
||||||
state_found = True
|
state_found = True
|
||||||
|
|
||||||
@@ -955,8 +1016,8 @@ def getPendingRedeems(account, password):
|
|||||||
pending.append(nameHash)
|
pending.append(nameHash)
|
||||||
else:
|
else:
|
||||||
pending.append(name['result'])
|
pending.append(name['result'])
|
||||||
except:
|
except Exception as e:
|
||||||
print("Failed to parse redeems")
|
logger.error(f"Failed to parse redeems: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
return pending
|
return pending
|
||||||
|
|
||||||
@@ -966,7 +1027,7 @@ def getPendingRegisters(account):
|
|||||||
domains = getDomains(account, False)
|
domains = getDomains(account, False)
|
||||||
pending = []
|
pending = []
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
if domain['state'] == "CLOSED" and domain['registered'] == False:
|
if domain['state'] == "CLOSED" and not domain['registered']:
|
||||||
for bid in bids:
|
for bid in bids:
|
||||||
if bid['name'] == domain['name']:
|
if bid['name'] == domain['name']:
|
||||||
if bid['value'] == domain['highest']:
|
if bid['value'] == domain['highest']:
|
||||||
@@ -984,9 +1045,9 @@ def getPendingFinalizes(account, password):
|
|||||||
pending = []
|
pending = []
|
||||||
try:
|
try:
|
||||||
for output in tx['outputs']:
|
for output in tx['outputs']:
|
||||||
if type(output) != dict:
|
if type(output) is not dict:
|
||||||
continue
|
continue
|
||||||
if not 'covenant' in output:
|
if 'covenant' not in output:
|
||||||
continue
|
continue
|
||||||
if output['covenant'].get("type") != 10:
|
if output['covenant'].get("type") != 10:
|
||||||
continue
|
continue
|
||||||
@@ -999,8 +1060,8 @@ def getPendingFinalizes(account, password):
|
|||||||
pending.append(nameHash)
|
pending.append(nameHash)
|
||||||
else:
|
else:
|
||||||
pending.append(name['result'])
|
pending.append(name['result'])
|
||||||
except:
|
except Exception as e:
|
||||||
print("Failed to parse finalizes")
|
logger.error(f"Failed to parse finalizes: {str(e)}", exc_info=True)
|
||||||
return pending
|
return pending
|
||||||
|
|
||||||
|
|
||||||
@@ -1010,9 +1071,8 @@ def getRevealTX(reveal):
|
|||||||
index = prevout['index']
|
index = prevout['index']
|
||||||
tx = hsd.getTxByHash(hash)
|
tx = hsd.getTxByHash(hash)
|
||||||
if 'inputs' not in tx:
|
if 'inputs' not in tx:
|
||||||
print(f'Something is up with this tx: {hash}')
|
logger.error(f'Something is up with this tx: {hash}')
|
||||||
print(tx)
|
logger.error(tx)
|
||||||
print('---')
|
|
||||||
# No idea what happened here
|
# No idea what happened here
|
||||||
# Check if registered?
|
# Check if registered?
|
||||||
return None
|
return None
|
||||||
@@ -1023,7 +1083,7 @@ def revealAuction(account, domain):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1043,7 +1103,7 @@ def revealAll(account):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1077,7 +1137,7 @@ def redeemAll(account):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1109,9 +1169,8 @@ def redeemAll(account):
|
|||||||
|
|
||||||
def registerAll(account):
|
def registerAll(account):
|
||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1134,9 +1193,8 @@ def registerAll(account):
|
|||||||
|
|
||||||
def finalizeAll(account):
|
def finalizeAll(account):
|
||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1169,7 +1227,7 @@ def bid(account, domain, bid, blind):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1194,7 +1252,7 @@ def openAuction(account, domain):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1216,7 +1274,7 @@ def transfer(account, domain, address):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1238,7 +1296,7 @@ def finalize(account, domain):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1275,7 +1333,7 @@ def cancelTransfer(account, domain):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1312,7 +1370,7 @@ def revoke(account, domain):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1349,7 +1407,7 @@ def sendBatch(account, batch):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1398,7 +1456,7 @@ def createBatch(account, batch):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1458,10 +1516,10 @@ def getMempoolBids():
|
|||||||
for txid in mempoolTxs:
|
for txid in mempoolTxs:
|
||||||
tx = hsd.getTxByHash(txid)
|
tx = hsd.getTxByHash(txid)
|
||||||
if 'error' in tx and tx['error'] is not None:
|
if 'error' in tx and tx['error'] is not None:
|
||||||
print(f"Error getting tx {txid}: {tx['error']}")
|
logger.error(f"Error getting tx {txid}: {tx['error']}")
|
||||||
continue
|
continue
|
||||||
if 'outputs' not in tx:
|
if 'outputs' not in tx:
|
||||||
print(f"Error getting outputs for tx {txid}")
|
logger.error(f"Error getting outputs for tx {txid}")
|
||||||
continue
|
continue
|
||||||
for output in tx['outputs']:
|
for output in tx['outputs']:
|
||||||
if output['covenant']['action'] not in ["BID", "REVEAL"]:
|
if output['covenant']['action'] not in ["BID", "REVEAL"]:
|
||||||
@@ -1518,9 +1576,15 @@ def getMempoolBids():
|
|||||||
|
|
||||||
|
|
||||||
# region settingsAPIs
|
# region settingsAPIs
|
||||||
def rescan():
|
def rescan(height:int = 0):
|
||||||
try:
|
try:
|
||||||
response = hsw.walletRescan(0)
|
response = hsw.walletRescan(height)
|
||||||
|
if 'success' in response and response['success'] is False:
|
||||||
|
return {
|
||||||
|
"error": {
|
||||||
|
"message": "Rescan already in progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
@@ -1542,12 +1606,10 @@ def resendTXs():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def zapTXs(account):
|
def zapTXs(account, age=1200):
|
||||||
age = 60 * 20 # 20 minutes
|
|
||||||
|
|
||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1569,9 +1631,11 @@ def zapTXs(account):
|
|||||||
|
|
||||||
|
|
||||||
def getxPub(account):
|
def getxPub(account):
|
||||||
account_name = check_account(account)
|
account_name = account
|
||||||
|
if account.count(":") > 0:
|
||||||
|
account_name = check_account(account)
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1587,8 +1651,6 @@ def getxPub(account):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response['accountKey']
|
return response['accountKey']
|
||||||
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
@@ -1601,7 +1663,7 @@ def signMessage(account, domain, message):
|
|||||||
account_name = check_account(account)
|
account_name = check_account(account)
|
||||||
password = ":".join(account.split(":")[1:])
|
password = ":".join(account.split(":")[1:])
|
||||||
|
|
||||||
if account_name == False:
|
if not account_name:
|
||||||
return {
|
return {
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid account"
|
"message": "Invalid account"
|
||||||
@@ -1641,6 +1703,7 @@ def verifyMessageWithName(domain, signature, message):
|
|||||||
return response['result']
|
return response['result']
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Error verifying message with name: {str(e)}", exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -1651,6 +1714,7 @@ def verifyMessage(address, signature, message):
|
|||||||
return response['result']
|
return response['result']
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Error verifying message: {str(e)}", exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
@@ -1697,7 +1761,7 @@ def get_node_api_url(path=''):
|
|||||||
base_url = f"http://x:{HSD_API}@{HSD_IP}:{HSD_NODE_PORT}"
|
base_url = f"http://x:{HSD_API}@{HSD_IP}:{HSD_NODE_PORT}"
|
||||||
if isSPV() and any(path.startswith(route) for route in SPV_EXTERNAL_ROUTES):
|
if isSPV() and any(path.startswith(route) for route in SPV_EXTERNAL_ROUTES):
|
||||||
# If in SPV mode and the path is one of the external routes, use the external API
|
# If in SPV mode and the path is one of the external routes, use the external API
|
||||||
base_url = f"https://hsd.hns.au/api/v1"
|
base_url = "https://hsd.hns.au/api/v1"
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
# Ensure path starts with a slash if it's not empty
|
# Ensure path starts with a slash if it's not empty
|
||||||
@@ -1742,32 +1806,39 @@ def checkPreRequisites() -> dict[str, bool]:
|
|||||||
"hsd": False
|
"hsd": False
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if node is installed and get version
|
try:
|
||||||
nodeSubprocess = subprocess.run(["node", "-v"], capture_output=True, text=True)
|
# Check if node is installed and get version
|
||||||
if nodeSubprocess.returncode == 0:
|
nodeSubprocess = subprocess.run(["node", "-v"], capture_output=True, text=True,timeout=2)
|
||||||
major_version = int(nodeSubprocess.stdout.strip().lstrip('v').split('.')[0])
|
if nodeSubprocess.returncode == 0:
|
||||||
if major_version >= HSD_CONFIG.get("minNodeVersion", 20):
|
major_version = int(nodeSubprocess.stdout.strip().lstrip('v').split('.')[0])
|
||||||
prerequisites["node"] = True
|
if major_version >= HSD_CONFIG.get("minNodeVersion", 20):
|
||||||
|
prerequisites["node"] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Check if npm is installed
|
try:
|
||||||
npmSubprocess = subprocess.run(["npm", "-v"], capture_output=True, text=True)
|
# Check if npm is installed
|
||||||
if npmSubprocess.returncode == 0:
|
npmSubprocess = subprocess.run(["npm", "-v"], capture_output=True, text=True,timeout=2)
|
||||||
major_version = int(npmSubprocess.stdout.strip().split('.')[0])
|
if npmSubprocess.returncode == 0:
|
||||||
if major_version >= HSD_CONFIG.get("minNPMVersion", 8):
|
major_version = int(npmSubprocess.stdout.strip().split('.')[0])
|
||||||
prerequisites["npm"] = True
|
if major_version >= HSD_CONFIG.get("minNPMVersion", 8):
|
||||||
|
prerequisites["npm"] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if git is installed
|
||||||
|
gitSubprocess = subprocess.run(["git", "--version"], capture_output=True, text=True,timeout=2)
|
||||||
|
if gitSubprocess.returncode == 0:
|
||||||
|
prerequisites["git"] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Check if git is installed
|
|
||||||
gitSubprocess = subprocess.run(["git", "-v"], capture_output=True, text=True)
|
|
||||||
if gitSubprocess.returncode == 0:
|
|
||||||
prerequisites["git"] = True
|
|
||||||
|
|
||||||
# Check if hsd is installed
|
# Check if hsd is installed
|
||||||
if os.path.exists("./hsd/bin/hsd"):
|
if os.path.exists("./hsd/bin/hsd"):
|
||||||
prerequisites["hsd"] = True
|
prerequisites["hsd"] = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return prerequisites
|
return prerequisites
|
||||||
|
|
||||||
|
|
||||||
@@ -1775,26 +1846,43 @@ def checkPreRequisites() -> dict[str, bool]:
|
|||||||
def hsdInit():
|
def hsdInit():
|
||||||
if not HSD_INTERNAL_NODE:
|
if not HSD_INTERNAL_NODE:
|
||||||
return
|
return
|
||||||
prerequisites = checkPreRequisites()
|
|
||||||
|
|
||||||
|
# Don't check prerequisites if HSD is included in a docker container
|
||||||
|
if os.getenv("HSD_DOCKER_CONTAINER", "false").lower() == "true":
|
||||||
|
prerequisites = {
|
||||||
|
"node": True,
|
||||||
|
"npm": True,
|
||||||
|
"git": True,
|
||||||
|
"hsd": True
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
prerequisites = checkPreRequisites()
|
||||||
|
|
||||||
|
minNodeVersion = HSD_CONFIG.get("minNodeVersion", 20)
|
||||||
|
minNPMVersion = HSD_CONFIG.get("minNpmVersion", 8)
|
||||||
PREREQ_MESSAGES = {
|
PREREQ_MESSAGES = {
|
||||||
"node": "Install Node.js from https://nodejs.org/en/download (Version >= {minNodeVersion})",
|
"node": f"Install Node.js from https://nodejs.org/en/download (Version >= {minNodeVersion})",
|
||||||
"npm": "Install npm (version >= {minNPMVersion}) - usually comes with Node.js",
|
"npm": f"Install npm (version >= {minNPMVersion}) - usually comes with Node.js",
|
||||||
"git": "Install Git from https://git-scm.com/downloads"}
|
"git": "Install Git from https://git-scm.com/downloads"}
|
||||||
|
|
||||||
|
|
||||||
# Check if all prerequisites are met (except hsd)
|
# Check if all prerequisites are met (except hsd)
|
||||||
if not all(prerequisites[key] for key in prerequisites if key != "hsd"):
|
if not all(prerequisites[key] for key in prerequisites if key != "hsd"):
|
||||||
print("HSD Internal Node prerequisites not met:")
|
print("HSD Internal Node prerequisites not met:",flush=True)
|
||||||
|
logger.error("HSD Internal Node prerequisites not met:")
|
||||||
for key, value in prerequisites.items():
|
for key, value in prerequisites.items():
|
||||||
if not value:
|
if not value:
|
||||||
print(f" - {key} is missing or does not meet the version requirement.")
|
print(f" - {key} is missing or does not meet the version requirement.",flush=True)
|
||||||
exit(1)
|
logger.error(f" - {key} is missing or does not meet the version requirement.")
|
||||||
|
if key in PREREQ_MESSAGES:
|
||||||
|
print(PREREQ_MESSAGES[key],flush=True)
|
||||||
|
logger.error(PREREQ_MESSAGES[key])
|
||||||
|
exit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if hsd is installed
|
# Check if hsd is installed
|
||||||
if not prerequisites["hsd"]:
|
if not prerequisites["hsd"]:
|
||||||
print("HSD not found, installing...")
|
logger.info("HSD not found, installing...")
|
||||||
# If hsd folder exists, remove it
|
# If hsd folder exists, remove it
|
||||||
if os.path.exists("hsd"):
|
if os.path.exists("hsd"):
|
||||||
os.rmdir("hsd")
|
os.rmdir("hsd")
|
||||||
@@ -1802,19 +1890,22 @@ def hsdInit():
|
|||||||
# Clone hsd repo
|
# Clone hsd repo
|
||||||
gitClone = subprocess.run(["git", "clone", "--depth", "1", "--branch", HSD_CONFIG.get("version", "latest"), "https://github.com/handshake-org/hsd.git", "hsd"], capture_output=True, text=True)
|
gitClone = subprocess.run(["git", "clone", "--depth", "1", "--branch", HSD_CONFIG.get("version", "latest"), "https://github.com/handshake-org/hsd.git", "hsd"], capture_output=True, text=True)
|
||||||
if gitClone.returncode != 0:
|
if gitClone.returncode != 0:
|
||||||
print("Failed to clone hsd repository:")
|
print("Failed to clone hsd repository:",flush=True)
|
||||||
print(gitClone.stderr)
|
logger.error("Failed to clone hsd repository:")
|
||||||
|
print(gitClone.stderr,flush=True)
|
||||||
|
logger.error(gitClone.stderr)
|
||||||
exit(1)
|
exit(1)
|
||||||
print("Cloned hsd repository.")
|
logger.info("Cloned hsd repository.")
|
||||||
|
logger.info("Installing hsd dependencies...")
|
||||||
# Install hsd dependencies
|
# Install hsd dependencies
|
||||||
print("Installing hsd dependencies...")
|
|
||||||
npmInstall = subprocess.run(["npm", "install"], cwd="hsd", capture_output=True, text=True)
|
npmInstall = subprocess.run(["npm", "install"], cwd="hsd", capture_output=True, text=True)
|
||||||
if npmInstall.returncode != 0:
|
if npmInstall.returncode != 0:
|
||||||
print("Failed to install hsd dependencies:")
|
print("Failed to install hsd dependencies:",flush=True)
|
||||||
print(npmInstall.stderr)
|
logger.error("Failed to install hsd dependencies:")
|
||||||
|
print(npmInstall.stderr,flush=True)
|
||||||
|
logger.error(npmInstall.stderr)
|
||||||
exit(1)
|
exit(1)
|
||||||
print("Installed hsd dependencies.")
|
logger.info("Installed hsd dependencies.")
|
||||||
|
|
||||||
def hsdStart():
|
def hsdStart():
|
||||||
global HSD_PROCESS
|
global HSD_PROCESS
|
||||||
global SPV_MODE
|
global SPV_MODE
|
||||||
@@ -1825,12 +1916,12 @@ def hsdStart():
|
|||||||
if os.path.exists("hsd.lock"):
|
if os.path.exists("hsd.lock"):
|
||||||
lock_time = os.path.getmtime("hsd.lock")
|
lock_time = os.path.getmtime("hsd.lock")
|
||||||
if time.time() - lock_time < 30:
|
if time.time() - lock_time < 30:
|
||||||
print("HSD was started recently, skipping start.")
|
logger.info("HSD was started recently, skipping start.")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
os.remove("hsd.lock")
|
os.remove("hsd.lock")
|
||||||
|
|
||||||
print("Starting HSD...")
|
logger.info("Starting HSD...")
|
||||||
# Create a lock file
|
# Create a lock file
|
||||||
with open("hsd.lock", "w") as f:
|
with open("hsd.lock", "w") as f:
|
||||||
f.write(str(time.time()))
|
f.write(str(time.time()))
|
||||||
@@ -1839,7 +1930,7 @@ def hsdStart():
|
|||||||
chain_migrate = HSD_CONFIG.get("chainMigrate", False)
|
chain_migrate = HSD_CONFIG.get("chainMigrate", False)
|
||||||
wallet_migrate = HSD_CONFIG.get("walletMigrate", False)
|
wallet_migrate = HSD_CONFIG.get("walletMigrate", False)
|
||||||
spv = HSD_CONFIG.get("spv", False)
|
spv = HSD_CONFIG.get("spv", False)
|
||||||
prefix = HSD_CONFIG.get("prefix", os.path.join(os.getcwd(), "hsd-data"))
|
prefix = HSD_CONFIG.get("prefix", os.path.join(os.getcwd(), "hsd_data"))
|
||||||
|
|
||||||
|
|
||||||
# Base command
|
# Base command
|
||||||
@@ -1868,13 +1959,17 @@ def hsdStart():
|
|||||||
cmd.append(flag)
|
cmd.append(flag)
|
||||||
|
|
||||||
# Launch process
|
# Launch process
|
||||||
HSD_PROCESS = subprocess.Popen(
|
try:
|
||||||
cmd,
|
HSD_PROCESS = subprocess.Popen(
|
||||||
cwd=os.getcwd(),
|
cmd,
|
||||||
text=True
|
cwd=os.getcwd(),
|
||||||
)
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
print(f"HSD started with PID {HSD_PROCESS.pid}")
|
logger.info(f"HSD started with PID {HSD_PROCESS.pid}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start HSD: {str(e)}", exc_info=True)
|
||||||
|
return
|
||||||
|
|
||||||
atexit.register(hsdStop)
|
atexit.register(hsdStop)
|
||||||
|
|
||||||
@@ -1882,9 +1977,23 @@ def hsdStart():
|
|||||||
try:
|
try:
|
||||||
signal.signal(signal.SIGINT, lambda s, f: (hsdStop(), sys.exit(0)))
|
signal.signal(signal.SIGINT, lambda s, f: (hsdStop(), sys.exit(0)))
|
||||||
signal.signal(signal.SIGTERM, lambda s, f: (hsdStop(), sys.exit(0)))
|
signal.signal(signal.SIGTERM, lambda s, f: (hsdStop(), sys.exit(0)))
|
||||||
except:
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to set signal handlers: {str(e)}", exc_info=True)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def hsdRunning() -> bool:
|
||||||
|
global HSD_PROCESS
|
||||||
|
if not HSD_INTERNAL_NODE:
|
||||||
|
return False
|
||||||
|
if HSD_PROCESS is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if process has terminated
|
||||||
|
poll_result = HSD_PROCESS.poll()
|
||||||
|
if poll_result is not None:
|
||||||
|
logger.error(f"HSD process has terminated with exit code: {poll_result}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def hsdStop():
|
def hsdStop():
|
||||||
global HSD_PROCESS
|
global HSD_PROCESS
|
||||||
@@ -1892,16 +2001,16 @@ def hsdStop():
|
|||||||
if HSD_PROCESS is None:
|
if HSD_PROCESS is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
print("Stopping HSD...")
|
logger.info("Stopping HSD...")
|
||||||
|
|
||||||
# Send SIGINT (like Ctrl+C)
|
# Send SIGINT (like Ctrl+C)
|
||||||
HSD_PROCESS.send_signal(signal.SIGINT)
|
HSD_PROCESS.send_signal(signal.SIGINT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
HSD_PROCESS.wait(timeout=10) # wait for graceful exit
|
HSD_PROCESS.wait(timeout=10) # wait for graceful exit
|
||||||
print("HSD shut down cleanly.")
|
logger.info("HSD shut down cleanly.")
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
print("HSD did not exit yet, is it alright???")
|
logger.warning("HSD did not exit yet, is it alright???")
|
||||||
|
|
||||||
# Clean up lock file
|
# Clean up lock file
|
||||||
if os.path.exists("hsd.lock"):
|
if os.path.exists("hsd.lock"):
|
||||||
@@ -1914,8 +2023,6 @@ def hsdRestart():
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
hsdStart()
|
hsdStart()
|
||||||
|
|
||||||
|
|
||||||
checkPreRequisites()
|
|
||||||
hsdInit()
|
hsdInit()
|
||||||
hsdStart()
|
hsdStart()
|
||||||
# endregion
|
# endregion
|
||||||
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 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
|
||||||
@@ -5,3 +5,4 @@ SHOW_EXPIRED=false
|
|||||||
EXPLORER_TX=https://shakeshift.com/transaction/
|
EXPLORER_TX=https://shakeshift.com/transaction/
|
||||||
DISABLE_WALLETDNS=false
|
DISABLE_WALLETDNS=false
|
||||||
INTERNAL_HSD=false
|
INTERNAL_HSD=false
|
||||||
|
LOG_LEVEL=WARNING
|
||||||
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"
|
||||||
12
plugin.py
12
plugin.py
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import json
|
import json
|
||||||
import account
|
|
||||||
import requests
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Plugin Data
|
# Plugin Data
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
52
render.py
52
render.py
@@ -2,9 +2,7 @@ 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
|
import requests
|
||||||
|
|
||||||
@@ -55,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'
|
||||||
|
|
||||||
@@ -182,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>"
|
||||||
@@ -253,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']
|
||||||
@@ -261,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
|
||||||
|
|
||||||
@@ -316,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>"
|
||||||
@@ -410,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":
|
||||||
@@ -449,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
|
||||||
@@ -468,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":
|
||||||
@@ -487,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):
|
||||||
@@ -498,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
|
||||||
@@ -517,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:
|
||||||
|
|||||||
@@ -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
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,7 +68,7 @@
|
|||||||
<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}} 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">
|
<h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6><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>
|
||||||
@@ -85,6 +85,12 @@
|
|||||||
<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's API</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
{% if internal %}
|
{% if internal %}
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<div><a class="btn btn-primary stick-right" role="button" href="/settings/restart">Restart Node</a>
|
<div><a class="btn btn-primary stick-right" role="button" href="/settings/restart">Restart Node</a>
|
||||||
@@ -104,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">
|
||||||
@@ -127,7 +133,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">About</h4>
|
<h4 class="card-title">About</h4>
|
||||||
<h6 class="text-muted mb-2 card-subtitle">FireWallet is a UI to allow easy connection with HSD created by <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a> and freely available. Please contact him <a href="https://l.woodburn.au/contact" target="_blank">here</a> if you would like to request any features or report any bugs.<br>FireWallet version: <code>{{version}}</code></h6>
|
<h6 class="text-muted mb-2 card-subtitle">FireWallet is a UI to allow easy connection with HSD created by <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a> and freely available. Please contact him <a href="https://l.woodburn.au/contact" target="_blank">here</a> if you would like to request any features or report any bugs.<br>FireWallet version: <code>{{version}}</code></h6>
|
||||||
<div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i> Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i> Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i> Donate to support development</a></div>
|
<div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i> Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i> Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i> Donate to support development</a><a href="/settings/logs" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-help" style="color: var(--bs-emphasis-color);"></i> Upload logs for debugging</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user