feat: Initial concept code
All checks were successful
Build Docker / BuildImage (push) Successful in 54s

This commit is contained in:
Nathan Woodburn 2025-01-30 16:22:58 +11:00
parent a51d2e89c4
commit 15bf3f0b01
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
15 changed files with 441 additions and 12 deletions

View File

@ -9,7 +9,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY . /app
# Optionally mount /data to store the data
# VOLUME /data
VOLUME /app/data
ENTRYPOINT ["python3"]
CMD ["main.py"]

BIN
FireSales.bsdesign Normal file

Binary file not shown.

9
data/listings.json Normal file
View File

@ -0,0 +1,9 @@
[
{
"domain": "woodburn43",
"description": "This is a test listing update",
"price": 100.2,
"tx": "somelonghexstring",
"updated": 1738214534
}
]

2
example.env Normal file
View File

@ -0,0 +1,2 @@
HSD_IP=127.0.0.1
HSD_API=hsd-api-key

5
requests/buy.hurl Normal file
View File

@ -0,0 +1,5 @@
POST http://127.0.0.1:5000/api/v1/delete
{
"domain":"woodburn43",
"tx":"somelonghexstring"
}

5
requests/cancel.hurl Normal file
View File

@ -0,0 +1,5 @@
POST http://127.0.0.1:5000/api/v1/delete
{
"domain":"woodburn43",
"signature":"zodqz5r8ibyGeoC9yZZJPmJXMFSGhno3M0lVSdZXblI1iBIIaVYzMsSaYrWiAaXZoh2FBnFv8fAI+m/U3BhiaA=="
}

7
requests/list.hurl Normal file
View File

@ -0,0 +1,7 @@
POST http://127.0.0.1:5000/api/v1/list
{
"domain":"woodburn43",
"price":100.2,
"description":"This is a test listing update",
"tx":"somelonghexstring"
}

136
sales.py Normal file
View File

@ -0,0 +1,136 @@
import os
import json
import dotenv
import requests
import time
dotenv.load_dotenv()
HSD_IP = os.getenv("HSD_IP")
HSD_API = os.getenv("HSD_API")
if not os.path.exists("data/listings.json"):
with open("data/listings.json", "w") as f:
f.write("[]")
# region Classes
class Listing:
def __init__(self, **kwargs):
self.domain = ''
self.price = 0
self.description = ''
self.tx = ''
self.updated = -1
for key, value in kwargs.items():
setattr(self, key, value)
self.updated = int(time.time())
def __str__(self):
return f"Listing of {self.domain} for {self.price}, Description: {self.description}, Contact: {self.contact}, Signed: {self.signed}, Signature: {self.signature}, Updated: {self.updated}"
def toHTML(self):
return f"""
<div class="card">
<div class="card-body">
<h4 class="card-title">{self.domain}/</h4>
<h6 class="text-muted card-subtitle mb-2">{self.price} HNS</h6>
<p class="card-text">{self.description}</p>
<p class="card-text">TX: <code>{self.tx}</code></p>
</div>
</div>
"""
def toJSON(self):
return {
'domain': self.domain,
'description': self.description,
'price': self.price,
'tx': self.tx,
'updated': self.updated
}
def to_dict(self):
return self.toJSON()
def txValid(self):
# TODO Validate tx is valid
return True
# endregion
def get_listings() -> list[Listing]:
with open("data/listings.json", "r") as f:
data = json.loads(f.read())
listings = []
for listing in data:
listings.append(Listing(**listing))
return listings
def search_listings(domain) -> Listing:
listings = get_listings()
for listing in listings:
if listing.domain == domain:
return listing
return None
def remove_listing(domain) -> bool:
listings = get_listings()
for listing in listings:
if listing.domain == domain:
listings.remove(listing)
saveListings(listings)
return True
return False
def get_listings_rendered() -> str:
listings = get_listings()
html = ""
for listing in listings:
html += listing.toHTML()
return html
def saveListings(listings:list[Listing]):
with open("data/listings.json", "w") as f:
f.write(json.dumps(listings,default=Listing.toJSON,indent=4))
def add_listing(listing:Listing):
if not listing.txValid():
return "Invalid tx"
# Remove any listings with the same domain
remove_listing(listing.domain)
listings = get_listings()
listings.append(listing)
saveListings(listings)
return True
def validate_signature(domain,signature,message) -> bool:
response = requests.post(f"http://x:{HSD_API}@{HSD_IP}:12037", json={
"method":"verifymessagewithname",
"params":[
domain,
signature,
message
]
})
if response.status_code != 200:
return False
response = response.json()
if response['result'] != True:
return False
return True
def validate_buy_tx(domain,tx) -> bool:
return False
def validate_cancel_signature(domain,signature) -> bool:
message = f"FS: {domain}"
if (not validate_signature(domain,signature,message)):
return False
return True

View File

@ -15,6 +15,7 @@ import json
import requests
from datetime import datetime
import dotenv
import sales
dotenv.load_dotenv()
@ -74,8 +75,83 @@ def wellknown(path):
# region Main routes
@app.route("/")
def index():
return render_template("index.html")
listings = sales.get_listings_rendered()
return render_template("index.html", listings=listings)
@app.route("/view/<path:domain>")
def view(domain: str):
listing = sales.search_listings(domain)
if not listing:
return render_template("404.html"), 404
return render_template("view.html", listing=listing)
@app.route("/view/<path:domain>", methods=["POST"])
def post_offer(domain: str):
data = request.form
# Convert to JSON
data = json.loads(json.dumps(data))
offer = sales.send_offer(domain, data)
listing = sales.search_listings(domain)
return render_template("view.html", listing=listing,message=offer['message'])
@app.route("/report/<path:domain>")
def report(domain: str):
listing = sales.search_listings(domain)
if not listing:
return render_template("404.html"), 404
return redirect("https://l.woodburn.au/contact")
#region API Routes
def validate_data(data,required):
for key in required:
if key not in data:
return jsonify({'error': f'Missing {key}','success': False})
return True
@app.route("/api/v1/list", methods=["POST"])
def list():
data = request.get_json()
# Validate data has domain,description,price,contact
valid = validate_data(data,['domain','description','price','tx'])
if valid != True:
return valid
listing = sales.Listing(**data)
status = sales.add_listing(listing)
if status != True:
return jsonify({'error': 'Failed to add listing','success': False,'message': status})
return jsonify({'success': True,'error': None})
@app.route("/api/v1/delete", methods=["POST"])
def delete():
data = request.get_json()
# Validate data has domain
if not 'domain' in data:
return jsonify({'error': 'Domain is required','success': False})
if 'tx' in data:
if not sales.validate_buy_tx(data['domain'],data['tx']):
return jsonify({'error': 'Invalid tx','success': False})
sales.remove_listing(data['domain'])
return jsonify({'success': True,'error': None})
if 'signature' in data:
if not sales.validate_cancel_signature(data['domain'],data['signature']):
return jsonify({'error': 'Invalid signature','success': False,'message': f"FS: {data['domain']}"})
sales.remove_listing(data['domain'])
return jsonify({'success': True,'error': None})
return jsonify({'error': 'Signature or tx is required','success': False})
#endregion
@app.route("/<path:path>")
def catch_all(path: str):

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,57 @@
.bs-icon {
--bs-icon-size: .75rem;
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
font-size: var(--bs-icon-size);
width: calc(var(--bs-icon-size) * 2);
height: calc(var(--bs-icon-size) * 2);
color: var(--bs-primary);
}
.bs-icon-xs {
--bs-icon-size: 1rem;
width: calc(var(--bs-icon-size) * 1.5);
height: calc(var(--bs-icon-size) * 1.5);
}
.bs-icon-sm {
--bs-icon-size: 1rem;
}
.bs-icon-md {
--bs-icon-size: 1.5rem;
}
.bs-icon-lg {
--bs-icon-size: 2rem;
}
.bs-icon-xl {
--bs-icon-size: 2.5rem;
}
.bs-icon.bs-icon-primary {
color: var(--bs-white);
background: var(--bs-primary);
}
.bs-icon.bs-icon-primary-light {
color: var(--bs-primary);
background: rgba(var(--bs-primary-rgb), .2);
}
.bs-icon.bs-icon-semi-white {
color: var(--bs-primary);
background: rgba(255, 255, 255, .5);
}
.bs-icon.bs-icon-rounded {
border-radius: .5rem;
}
.bs-icon.bs-icon-circle {
border-radius: 50%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,19 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<html data-bs-theme="dark" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nathan.Woodburn/</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="stylesheet" href="/assets/css/index.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>FireSales</title>
<meta name="twitter:description" content="Self Custodial Domain listings">
<meta name="twitter:card" content="summary">
<meta property="og:type" content="website">
<meta name="twitter:image" content="/assets/img/favicon.png">
<meta name="twitter:title" content="FireSales">
<meta name="description" content="Self Custodial Domain listings">
<meta property="og:image" content="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&amp;display=swap">
<link rel="stylesheet" href="/assets/css/Navbar-Right-Links-Dark-icons.css">
</head>
<body>
<div class="spacer"></div>
<div class="centre">
<h1>Nathan.Woodburn/</h1>
</div>
<nav class="navbar navbar-expand-md bg-dark py-3" data-bs-theme="dark">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><img src="/assets/img/favicon.png" width="64px"><span style="margin: 10px;">FireSales</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-5"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-5">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link active" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link" href="/docs">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="/plugin">FireWallet Plugin</a></li>
</ul>
</div>
</div>
</nav>
<section class="py-4 py-xl-5">
<div class="container h-100">
<div class="row h-100">
<div class="col-md-10 col-xl-8 text-center d-flex d-sm-flex d-md-flex justify-content-center align-items-center mx-auto justify-content-md-start align-items-md-center justify-content-xl-center">
<div>
<h2 class="text-uppercase fw-bold mb-3">FireSales</h2>
<p class="mb-4">Self custodial domain sales</p>
</div>
</div>
</div>
</div>
</section>
<section style="margin: 10px;">{{listings|safe}}</section>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

84
templates/view.html Normal file
View File

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html data-bs-theme="dark" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>FireSales</title>
<meta name="twitter:description" content="Self Custodial Domain listings">
<meta name="twitter:card" content="summary">
<meta property="og:type" content="website">
<meta name="twitter:image" content="/assets/img/favicon.png">
<meta name="twitter:title" content="FireSales">
<meta name="description" content="Self Custodial Domain listings">
<meta property="og:image" content="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&amp;display=swap">
<link rel="stylesheet" href="/assets/css/Navbar-Right-Links-Dark-icons.css">
</head>
<body>
<nav class="navbar navbar-expand-md bg-dark py-3" data-bs-theme="dark">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><img src="/assets/img/favicon.png" width="64px"><span style="margin: 10px;">FireSales</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-5"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-5">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link" href="/docs">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="/plugin">FireWallet Plugin</a></li>
</ul>
</div>
</div>
</nav>
<section class="py-4 py-xl-5">
<div class="container h-100">
<div class="row h-100">
<div class="col-md-10 col-xl-8 text-center d-flex d-sm-flex d-md-flex justify-content-center align-items-center mx-auto justify-content-md-start align-items-md-center justify-content-xl-center">
<div>
<h2 class="text-uppercase fw-bold mb-3">FireSales</h2>
<p class="mb-4">Self custodial domain sales</p>
</div>
</div>
</div>
</div>
<h1 class="text-center">{{message}}</h1>
</section>
<section class="d-xl-flex justify-content-xl-center" style="margin: 10px;">
<div class="card" style="width: fit-content;padding-right: 2em;padding-left: 2em;margin: 10px;">
<div class="card-body">
<h4 class="card-title">{{listing['domain']}}/</h4>
<h6 class="text-muted card-subtitle mb-2">{{listing['price']}} HNS</h6>
<p class="card-text">{{listing['description']}}</p>
<p class="card-text">Signature:&nbsp;<code>{{listing['signature']}}</code></p>
<div class="card mb-5">
<div class="card-body p-sm-5">
<h2 class="text-center mb-4">Send Offer</h2>
<form method="post">
<div class="mb-3"><input class="form-control" type="text" id="name-2" name="address" placeholder="Receiving HNS address"><label class="form-label">Where you would like the domain sent to</label></div>
<div class="mb-3"><input class="form-control" type="text" id="name-1" name="price" placeholder="HNS Price of offer"></div>
<div class="mb-3"><textarea class="form-control" id="message-2" name="message" rows="6" placeholder="Message"></textarea></div>
<div><button class="btn btn-primary d-block w-100" type="submit">Send </button></div>
</form>
</div>
</div>
</div>
</div>
<div class="card" style="width: fit-content;padding-right: 2em;padding-left: 2em;margin: 10px;">
<div class="card-body">{% for offer in listing['offers'] %}
<div class="card" style="margin: 10px;">
<div class="card-body">
<h4 class="card-title">{{offer['address']}} - {{offer['status']}}</h4>
<h6 class="text-muted card-subtitle mb-2">Price: {{offer['price']}}</h6>
<h6 class="text-muted card-subtitle mb-2">Submitted: {{offer['date']}}</h6>
<p class="card-text">{{offer['message']}}</p>
<p class="card-text">{% if offer['response'] %}Owner Response: {{offer['response']}}{% endif %}{% if offer['tx'] %}<br>TX: {{offer['tx']}}{% endif %}</p>
</div>
</div>{% endfor %}
</div>
</div>
</section>
<section class="d-xl-flex justify-content-xl-center" style="margin: 10px;"></section>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>