feat: Add initial watchonly server files
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
This commit is contained in:
parent
353dc8319f
commit
f9af1b2606
41
.gitea/workflows/build.yml
Normal file
41
.gitea/workflows/build.yml
Normal file
@ -0,0 +1,41 @@
|
||||
name: Build Docker
|
||||
run-name: Build Docker Images
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
BuildImage:
|
||||
runs-on: [ubuntu-latest, amd]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Docker
|
||||
run : |
|
||||
apt-get install ca-certificates curl gnupg
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
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
|
||||
- name: Build Docker image
|
||||
run : |
|
||||
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
|
||||
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
|
||||
tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
tag=${tag//\//-}
|
||||
tag_num=${GITHUB_RUN_NUMBER}
|
||||
echo "tag_num=$tag_num"
|
||||
|
||||
if [[ "$tag" == "main" ]]; then
|
||||
tag="latest"
|
||||
else
|
||||
tag_num="${tag}-${tag_num}"
|
||||
fi
|
||||
|
||||
|
||||
docker build -t firewallet-api:$tag_num .
|
||||
docker tag firewallet-api:$tag_num git.woodburn.au/nathanwoodburn/firewallet-api:$tag_num
|
||||
docker push git.woodburn.au/nathanwoodburn/firewallet-api:$tag_num
|
||||
docker tag firewallet-api:$tag_num git.woodburn.au/nathanwoodburn/firewallet-api:$tag
|
||||
docker push git.woodburn.au/nathanwoodburn/firewallet-api:$tag
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.env
|
||||
__pycache__/
|
||||
|
||||
.local
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt /app
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
COPY . /app
|
||||
|
||||
# Add mount point for data volume
|
||||
VOLUME /data
|
||||
|
||||
ENTRYPOINT ["python3"]
|
||||
CMD ["main.py"]
|
||||
|
||||
FROM builder as dev-envs
|
5
hns.py
Normal file
5
hns.py
Normal file
@ -0,0 +1,5 @@
|
||||
def from_small(amount:int) -> float:
|
||||
return amount/10**6
|
||||
|
||||
def to_small(amount:float) -> int:
|
||||
return int(amount*(10**6))
|
346
hsd.py
Normal file
346
hsd.py
Normal file
@ -0,0 +1,346 @@
|
||||
from functools import cache
|
||||
import requests
|
||||
import dotenv
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import hns
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
HSD_API_KEY = os.getenv('HSD_API_KEY')
|
||||
HSD_API_IP = os.getenv('HSD_API_IP')
|
||||
if not HSD_API_IP:
|
||||
HSD_API_IP = '127.0.0.1'
|
||||
|
||||
HSD_API_NETWORK = os.getenv('HSD_API_NETWORK')
|
||||
if not HSD_API_NETWORK:
|
||||
HSD_API_NETWORK = 'main'
|
||||
|
||||
network_port_int = {
|
||||
'main': 1203,
|
||||
'testnet': 1303,
|
||||
'regtest': 1403,
|
||||
'simnet': 1503
|
||||
}
|
||||
|
||||
|
||||
account_file = 'accounts.json'
|
||||
wallet_file = 'wallets.json'
|
||||
|
||||
if not os.path.exists('.local'):
|
||||
account_file = '/data/accounts.json'
|
||||
wallet_file = '/data/wallets.json'
|
||||
|
||||
# Create any missing files
|
||||
if not os.path.exists('accounts.json'):
|
||||
with open('accounts.json', 'w') as f:
|
||||
json.dump({}, f, indent=4)
|
||||
|
||||
if not os.path.exists('wallets.json'):
|
||||
with open('wallets.json', 'w') as f:
|
||||
json.dump([], f, indent=4)
|
||||
|
||||
|
||||
def url(walletURL:bool) -> str:
|
||||
if walletURL:
|
||||
return f'http://x:{HSD_API_KEY}@{HSD_API_IP}:{network_port_int[HSD_API_NETWORK]}9/'
|
||||
else:
|
||||
return f'http://x:{HSD_API_KEY}@{HSD_API_IP}:{network_port_int[HSD_API_NETWORK]}7/'
|
||||
|
||||
|
||||
def rescan() -> dict:
|
||||
return requests.post(url(True)+'rescan', json={'height':0}).json()
|
||||
|
||||
def get_master() -> dict:
|
||||
return requests.get(url(True)+'a61ba47a-54de-43b9-9d22-fcf6827cd5c2/master').json()
|
||||
|
||||
def get_status() -> dict:
|
||||
return requests.get(url(False)).json()
|
||||
|
||||
def get_wallets() -> dict:
|
||||
return requests.get(url(True)+'wallet').json()
|
||||
|
||||
def create_account() -> dict:
|
||||
# Generate a UUID
|
||||
userID = str(uuid.uuid4())
|
||||
|
||||
with open('accounts.json', 'r') as f:
|
||||
accounts = json.load(f)
|
||||
|
||||
# Check if the user already exists
|
||||
if userID in accounts:
|
||||
return {
|
||||
'error': 'Please try again'
|
||||
}
|
||||
|
||||
accounts[userID] = {
|
||||
'wallets': []
|
||||
}
|
||||
|
||||
with open('accounts.json', 'w') as f:
|
||||
json.dump(accounts, f, indent=4)
|
||||
|
||||
return {
|
||||
'userID': userID
|
||||
}
|
||||
|
||||
def get_account(userID:str) -> dict:
|
||||
accounts = get_accounts()
|
||||
if userID in accounts:
|
||||
return accounts[userID]
|
||||
else:
|
||||
return {
|
||||
'error': 'User not found'
|
||||
}
|
||||
|
||||
def get_accounts() -> dict:
|
||||
# Read from accounts
|
||||
with open('accounts.json', 'r') as f:
|
||||
accounts = json.load(f)
|
||||
return accounts
|
||||
|
||||
|
||||
def import_wallet(name:str,userID:str,xpub:str) -> dict:
|
||||
accounts = get_accounts()
|
||||
|
||||
if userID not in accounts:
|
||||
return {
|
||||
'error': 'User not found'
|
||||
}
|
||||
|
||||
# Check if the wallet already exists
|
||||
for wallet in accounts[userID]['wallets']:
|
||||
if wallet['name'] == name:
|
||||
return {
|
||||
'error': 'Wallet already exists'
|
||||
}
|
||||
|
||||
# Create the wallet using a UUID
|
||||
walletID = str(uuid.uuid4())
|
||||
with open('wallets.json', 'r') as f:
|
||||
wallets = json.load(f)
|
||||
|
||||
if walletID in wallets:
|
||||
return {
|
||||
'error': 'Please try again'
|
||||
}
|
||||
|
||||
wallet = {
|
||||
'name': name,
|
||||
'xpub': xpub,
|
||||
'walletID': walletID
|
||||
}
|
||||
|
||||
wallet_data = {
|
||||
"watchOnly": True,
|
||||
"accountKey": xpub,
|
||||
}
|
||||
response = requests.put(url(True)+'wallet/'+walletID, json=wallet_data)
|
||||
if response.status_code != 200:
|
||||
print(response.text)
|
||||
return {
|
||||
'error': 'Error creating wallet'
|
||||
}
|
||||
|
||||
accounts[userID]['wallets'].append(wallet)
|
||||
with open('accounts.json', 'w') as f:
|
||||
json.dump(accounts, f, indent=4)
|
||||
|
||||
wallets.append(walletID)
|
||||
with open('wallets.json', 'w') as f:
|
||||
json.dump(wallets, f, indent=4)
|
||||
|
||||
# Rescan the wallet to get the balance
|
||||
rescan()
|
||||
|
||||
return {
|
||||
'walletID': walletID
|
||||
}
|
||||
|
||||
|
||||
@cache
|
||||
def get_wallet_UUID(userID:str, name:str) -> str|None:
|
||||
accounts = get_accounts()
|
||||
|
||||
if userID not in accounts:
|
||||
return None
|
||||
|
||||
walletID = None
|
||||
for wallet in accounts[userID]['wallets']:
|
||||
if wallet['name'] == name:
|
||||
walletID = wallet['walletID']
|
||||
break
|
||||
|
||||
return walletID
|
||||
|
||||
def get_wallet(userID:str, name:str) -> dict:
|
||||
walletID = get_wallet_UUID(userID, name)
|
||||
if not walletID:
|
||||
return {
|
||||
'error': 'Wallet not found'
|
||||
}
|
||||
|
||||
return requests.get(url(True)+'wallet/'+walletID).json()
|
||||
|
||||
def get_address(userID:str, name:str) -> dict:
|
||||
walletID = get_wallet_UUID(userID, name)
|
||||
if not walletID:
|
||||
return {
|
||||
'error': 'Wallet not found'
|
||||
}
|
||||
|
||||
request = requests.get(url(True)+'wallet/'+walletID+'/account/default').json()
|
||||
return {
|
||||
'address': request['receiveAddress']
|
||||
}
|
||||
|
||||
def get_balance(userID:str, name:str) -> dict:
|
||||
walletID = get_wallet_UUID(userID, name)
|
||||
if not walletID:
|
||||
return {
|
||||
'error': 'Wallet not found'
|
||||
}
|
||||
|
||||
balance = requests.get(url(True)+'wallet/'+walletID+'/balance?account=default').json()
|
||||
|
||||
domains_value = get_domains_value(userID, name)
|
||||
|
||||
# Convert to human readable
|
||||
return_json = {
|
||||
'available': balance['confirmed']/10**6,
|
||||
'available_small': balance['confirmed'],
|
||||
'locked': (balance['lockedConfirmed']-domains_value)/10**6,
|
||||
'locked_small': balance['lockedConfirmed']-domains_value,
|
||||
}
|
||||
|
||||
return return_json
|
||||
|
||||
|
||||
@cache
|
||||
def get_domains(userID:str, name:str) -> dict:
|
||||
walletID = get_wallet_UUID(userID, name)
|
||||
if not walletID:
|
||||
return {
|
||||
'error': 'Wallet not found'
|
||||
}
|
||||
|
||||
request = requests.get(url(True)+'wallet/'+walletID+'/name?own=true').json()
|
||||
|
||||
names = []
|
||||
names_value = 0
|
||||
for name in request:
|
||||
names_value += name['value']
|
||||
names.append({
|
||||
'name': name['name'],
|
||||
'state': name['state'],
|
||||
'highest': name['highest']/10**6,
|
||||
'value': name['value']/10**6,
|
||||
'height': name['height'],
|
||||
'stats': name['stats']
|
||||
})
|
||||
|
||||
# Save name value to accounts
|
||||
with open('accounts.json', 'r') as f:
|
||||
accounts = json.load(f)
|
||||
|
||||
|
||||
for account in accounts:
|
||||
if account == userID:
|
||||
wallets = accounts[account]['wallets']
|
||||
for wallet in wallets:
|
||||
if wallet['walletID'] == walletID:
|
||||
wallet['names_value'] = names_value
|
||||
break
|
||||
|
||||
with open('accounts.json', 'w') as f:
|
||||
json.dump(accounts, f, indent=4)
|
||||
|
||||
return names
|
||||
|
||||
|
||||
def get_domains_value(userID:str, name:str) -> int:
|
||||
walletID = get_wallet_UUID(userID, name)
|
||||
if not walletID:
|
||||
return {
|
||||
'error': 'Wallet not found'
|
||||
}
|
||||
|
||||
with open('accounts.json', 'r') as f:
|
||||
accounts = json.load(f)
|
||||
|
||||
for account in accounts:
|
||||
if account == userID:
|
||||
wallets = accounts[account]['wallets']
|
||||
for wallet in wallets:
|
||||
if wallet['walletID'] == walletID:
|
||||
if 'names_value' in wallet:
|
||||
return wallet['names_value']
|
||||
|
||||
|
||||
names = get_domains(userID, name)
|
||||
names_value = 0
|
||||
for name in names:
|
||||
names_value += name['value']
|
||||
|
||||
return names_value
|
||||
|
||||
|
||||
def send_to_address(userID:str, name:str, address:str, amount:str) -> dict:
|
||||
walletID = get_wallet_UUID(userID, name)
|
||||
if not walletID:
|
||||
return {
|
||||
'error': 'Wallet not found'
|
||||
}
|
||||
|
||||
# Verify amount
|
||||
amount = float(amount)
|
||||
if amount <= 0:
|
||||
return {
|
||||
'error': 'Amount must be greater than 0'
|
||||
}
|
||||
|
||||
balance = get_balance(userID, name)
|
||||
if balance['available'] < amount:
|
||||
return {
|
||||
'error': 'Insufficient funds'
|
||||
}
|
||||
|
||||
|
||||
data = {
|
||||
'outputs': [
|
||||
{
|
||||
'address': address,
|
||||
'value': hns.to_small(amount)
|
||||
}
|
||||
],
|
||||
'sign': False
|
||||
}
|
||||
|
||||
response = requests.post(url(True)+'wallet/'+walletID+'/create', json=data)
|
||||
if response.status_code != 200:
|
||||
print(response.text)
|
||||
return {
|
||||
'error': 'Error sending'
|
||||
}
|
||||
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
|
||||
# region Auctions
|
||||
|
||||
@cache
|
||||
def get_auctions(userID:str, name:str) -> dict:
|
||||
walletID = get_wallet_UUID(userID, name)
|
||||
if not walletID:
|
||||
return {
|
||||
'error': 'Wallet not found'
|
||||
}
|
||||
|
||||
request = requests.get(url(True)+'wallet/'+walletID+'/auction')
|
||||
return request.json()
|
||||
|
||||
|
||||
# endregion
|
101
main.py
Normal file
101
main.py
Normal file
@ -0,0 +1,101 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import requests
|
||||
import json
|
||||
import hsd
|
||||
import hns
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/status", methods=["GET"])
|
||||
def status():
|
||||
status_json = {"status": "ok", "hsd": hsd.get_status()}
|
||||
return jsonify(status_json)
|
||||
|
||||
|
||||
@app.route("/rescan", methods=["POST"])
|
||||
def rescan():
|
||||
# Only allow rescan if app is in debug mode
|
||||
if app.debug == False:
|
||||
return jsonify({"error": "Cannot rescan manually in production mode"})
|
||||
|
||||
return jsonify(hsd.rescan())
|
||||
|
||||
|
||||
@app.route("/account", methods=["POST"])
|
||||
def account():
|
||||
account_json = hsd.create_account()
|
||||
return jsonify(account_json)
|
||||
|
||||
|
||||
@app.route("/account", methods=["GET"])
|
||||
def accounts():
|
||||
userID = request.args.get("uuid")
|
||||
account_json = hsd.get_account(userID)
|
||||
return jsonify(account_json)
|
||||
|
||||
|
||||
@app.route("/wallet", methods=["POST"])
|
||||
def create_wallet():
|
||||
userID = request.args.get("uuid")
|
||||
name = request.args.get("name")
|
||||
xpub = request.args.get("xpub")
|
||||
wallet_json = hsd.import_wallet(name, userID, xpub)
|
||||
return jsonify(wallet_json)
|
||||
|
||||
|
||||
@app.route("/wallet", methods=["GET"])
|
||||
def get_wallet():
|
||||
userID = request.args.get("uuid")
|
||||
name = request.args.get("name")
|
||||
wallets_json = hsd.get_wallet(userID, name)
|
||||
return jsonify(wallets_json)
|
||||
|
||||
|
||||
@app.route("/wallet/address", methods=["GET"])
|
||||
def get_address():
|
||||
userID = request.args.get("uuid")
|
||||
name = request.args.get("name")
|
||||
address_json = hsd.get_address(userID, name)
|
||||
return jsonify(address_json)
|
||||
|
||||
|
||||
@app.route("/wallet/balance", methods=["GET"])
|
||||
def get_balance():
|
||||
userID = request.args.get("uuid")
|
||||
name = request.args.get("name")
|
||||
balance_json = hsd.get_balance(userID, name)
|
||||
return jsonify(balance_json)
|
||||
|
||||
@app.route('/wallet/domains', methods=['GET'])
|
||||
def get_domains():
|
||||
userID = request.args.get('uuid')
|
||||
name = request.args.get('name')
|
||||
domain_json = hsd.get_domains(userID, name)
|
||||
return jsonify(domain_json)
|
||||
|
||||
@app.route('/wallet/send', methods=['POST'])
|
||||
def send():
|
||||
userID = request.args.get('uuid')
|
||||
name = request.args.get('name')
|
||||
address = request.args.get('address')
|
||||
amount = request.args.get('amount')
|
||||
send_json = hsd.send_to_address(userID, name, address, amount)
|
||||
return jsonify(send_json)
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route('/wallet/auctions', methods=['GET'])
|
||||
def get_auctions():
|
||||
userID = request.args.get('uuid')
|
||||
name = request.args.get('name')
|
||||
auction_json = hsd.get_auctions(userID, name)
|
||||
return jsonify(auction_json)
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=8080, debug=True)
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
flask
|
||||
python-dotenv
|
Loading…
Reference in New Issue
Block a user