Reviewed-on: #1
master
lza_menace 6 months ago
parent 6f6b971b9d
commit bda9bd7a31

@ -7,9 +7,44 @@ x-log-config: &log-config
max-file: "5"
volumes:
lws:
lwsadmin:
mymonero:
lwsadmin:
services:
lwsadmin:
container_name: lwsadmin
image: lalanza808/lwsadmin:latest
restart: unless-stopped
depends_on:
- monero-lws
environment:
LWS_URL: http://monero-lws:8080
LWS_ADMIN_URL: http://monero-lws:8081
QUART_ENV: production
HOST: 0.0.0.0
DEBUG: 0
SERVER_NAME: ${SERVER_NAME:-127.0.0.1:5000}
SECRET_KEY: ${SECRET_KEY:-thisisasecret}
volumes:
- lwsadmin:/srv/lwsadmin/data
user: "1000:1000"
command:
./.venv/bin/poetry run start
ports:
- 127.0.0.1:5000:5000
<<: *log-config
mymonero-web:
container_name: mymonero-web
image: lalanza808/mymonero-web-js:latest
restart: unless-stopped
environment:
MYMONERO_WEB_NETTYPE: 0
MYMONERO_WEB_SERVER_URL: http://localhost:8080/
MYMONERO_WEB_APP_NAME: LZAXMR
ports:
- 127.0.0.1:9110:80
volumes:
- mymonero:/app
<<: *log-config
monero-lws:
container_name: monero-lws
image: lalanza808/monero-lws:develop

@ -1,6 +1,6 @@
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get update -y
RUN apt-get install -y python3 python3-venv
WORKDIR /srv/lwsadmin
RUN adduser \

@ -0,0 +1 @@
from lws.factory import create_app

@ -18,4 +18,8 @@ QUART_AUTH_DURATION = int(env.get('QUART_AUTH_DURATION', 60 * 60)) # 1 hour
LWS_URL = env.get("LWS_URL", "http://127.0.0.1:8080")
LWS_ADMIN_URL = env.get("LWS_ADMIN_URL", "http://127.0.0.1:8081")
#
# Monerod
MONEROD_URL = env.get("MONEROD_URL", "http://singapore.node.xmr.pm:18089")
# MyMonero
MYMONERO_URL = env.get("MYMONERO_URL", "http://localhost:9110")

@ -20,16 +20,21 @@ def create_app():
@app.before_serving
async def startup():
from lws.routes import auth, wallet, meta
from lws.routes import auth, wallet, meta, htmx
from lws import filters
app.register_blueprint(filters.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(meta.bp)
app.register_blueprint(wallet.bp)
app.register_blueprint(htmx.bp)
@app.errorhandler(Unauthorized)
async def redirect_to_login(*_):
return redirect(f"/login?next={request.path}")
if request.path == "/":
return redirect(f"/login?next={request.path}")
else:
return f"<p>you need to authenticate first</p><a href=\"/login\">login</a>"
return app
bcrypt = Bcrypt(create_app())

@ -1,6 +1,8 @@
from monero.numbers import from_atomic
from quart import Blueprint
from lws.models import Wallet, get_random_words
bp = Blueprint('filters', 'filters')
@ -12,4 +14,12 @@ def atomic(amt):
@bp.app_template_filter('shorten')
def shorten(s):
return f"{s[:6]}...{s[-6:]}"
return f"{s[:6]}...{s[-6:]}"
@bp.app_template_filter('find_label')
def find_label(s):
w = Wallet.select().where(Wallet.address == s).first()
if w:
return w.label
else:
return get_random_words()

@ -18,7 +18,6 @@ from lws import config
# webhook_delete_uuid: {"event_ids": [...]}
# webhook_list: {}
class LWS:
def __init__(self):
pass
@ -26,6 +25,19 @@ class LWS:
def init(self, admin_key):
self.admin_key = admin_key
def _init(self):
self.admin_key = User.select().first().view_key
def get_address_info(self, address, view_key):
endpoint = f"{config.LWS_URL}/get_address_info"
data = {
"address": address,
"view_key": view_key
}
r = requests.post(endpoint, json=data, timeout=5)
r.raise_for_status()
return r.json()
def get_wallet(self, address: str) -> dict:
try:
res = self.list_accounts()
@ -108,9 +120,8 @@ class LWS:
print(f"Failed to add wallet {address}: {e}")
return {}
def modify_wallet(self, address: str, active: bool) -> dict:
def modify_wallet(self, address: str, status: str) -> dict:
endpoint = f"{config.LWS_ADMIN_URL}/modify_account_status"
status = "active" if active else "inactive"
data = {
"auth": self.admin_key,
"params": {

@ -1,9 +1,16 @@
from random import choice
from datetime import datetime
from peewee import *
from monero.wordlists import English
db = SqliteDatabase('data/lws.db')
db = SqliteDatabase("data/lws.db")
def get_random_words():
e = English().word_list
return f"{choice(e)}-{choice(e)}-{choice(e)}"
class User(Model):
@ -17,4 +24,13 @@ class User(Model):
database = db
db.create_tables([User])
class Wallet(Model):
date = DateTimeField(default=datetime.utcnow)
address = CharField()
label = CharField(default=get_random_words, null=False)
class Meta:
database = db
db.create_tables([User, Wallet])

@ -0,0 +1,80 @@
from quart import Blueprint, render_template, request
from monero.seed import Seed
from quart_auth import login_required
from lws.models import User, Wallet
from lws.helpers import lws
from lws import config
bp = Blueprint('htmx', 'htmx', url_prefix="/htmx")
@bp.route("/create_wallet")
async def create_wallet():
"""Creating a new wallet with newly generated seeds"""
seed = Seed()
return await render_template(
"htmx/create_wallet.html",
seed=seed.phrase,
address=seed.public_address(),
psk=seed.public_spend_key(),
pvk=seed.public_view_key(),
ssk=seed.secret_spend_key(),
svk=seed.secret_view_key()
)
@bp.route("/import_wallet")
async def import_wallet():
"""Importing an existing wallet"""
return await render_template("htmx/import_wallet.html")
@bp.route("/label_wallet")
async def label_wallet():
"""Changing the label on a stored wallet"""
address = request.args.get("address")
label = request.args.get("label")
return await render_template(
"htmx/label_wallet.html",
address=address,
label=label
)
@bp.route("/set_height")
async def set_height():
"""Setting a new height to scan from"""
address = request.args.get("address")
height = request.args.get("height")
return await render_template(
"htmx/set_height.html",
address=address,
height=height
)
@bp.route("/show_wallets")
@login_required
async def show_wallets():
"""Showing all wallets in the database in a table"""
admin = User.select().first()
lws.init(admin.view_key)
accounts = lws.list_accounts()
if 'hidden' in accounts:
del accounts["hidden"]
# save wallets if they don't exist in the db
for status in accounts:
for account in accounts[status]:
w = Wallet.select().where(Wallet.address == account["address"]).first()
if not w:
w = Wallet(
address=account["address"]
)
w.save()
requests = lws.list_requests()
return await render_template(
"htmx/show_wallets.html",
accounts=accounts,
requests=requests
)

@ -9,20 +9,15 @@ from lws import config
bp = Blueprint("meta", "meta")
@bp.route("/")
@login_required
async def index():
admin = User.select().first()
lws.init(admin.view_key)
accounts = lws.list_accounts()
if 'hidden' in accounts:
del accounts["hidden"]
requests = lws.list_requests()
return await render_template(
"index.html",
config=config,
accounts=accounts,
requests=requests
config=config
)

@ -1,11 +1,13 @@
import monero.address
from quart import Blueprint, render_template, request, flash, redirect
from quart_auth import login_required, current_user
from monero.seed import Seed
from quart import Blueprint, request, flash, redirect, url_for
from quart_auth import login_required
from lws.helpers import lws
from lws.models import Wallet, get_random_words
bp = Blueprint('wallet', 'wallet')
bp = Blueprint("wallet", "wallet")
@bp.route("/wallet/add", methods=["GET", "POST"])
@ -13,74 +15,61 @@ bp = Blueprint('wallet', 'wallet')
async def add():
form = await request.form
if form:
address = form.get("address", "")
view_key = form.get("view_key", "")
restore_height = form.get("restore_height", 0)
valid_view_key = False
if not address:
await flash("must provide an address")
return redirect("/wallet/add")
if not view_key:
await flash("must provide a view_key")
return redirect("/wallet/add")
label = form.get("label")
seed = form.get("seed")
restore_height = form.get("restore_height", None)
try:
_a = monero.address.Address(address)
valid_view_key = _a.check_private_view_key(view_key)
seed = Seed(seed)
except ValueError:
await flash("Invalid Monero address")
return redirect("/wallet/add")
if not valid_view_key:
await flash("Invalid view key provided for address")
return redirect("/wallet/add")
lws.add_wallet(address, view_key)
lws.rescan(address, int(restore_height))
await flash("wallet added")
return redirect(f"/")
return await render_template("wallet/add.html")
await flash("Invalid mnemonic seed")
return ""
lws._init()
address = str(seed.public_address())
svk = str(seed.secret_view_key())
lws.add_wallet(address, svk)
if restore_height != "-1":
lws.rescan(address, int(restore_height))
w = Wallet(
address=seed.public_address(),
label=label if label else get_random_words()
)
w.save()
return redirect(url_for("htmx.show_wallets"))
@bp.route("/wallet/rescan")
@bp.route("/wallet/<address>/rescan/<height>")
@login_required
async def rescan():
address = request.args.get('address')
height = request.args.get('height')
if not address or not height:
await flash("you need to provide both address and height")
return redirect("/")
if lws.rescan(address, int(height)):
await flash(f"rescanning {address} from block {height}")
else:
await flash("probz")
return redirect(f"/")
async def rescan(address, height):
lws.rescan(address, int(height))
return redirect(url_for("htmx.show_wallets"))
@bp.route("/wallet/<address>/disable")
@bp.route("/wallet/<address>/modify/<status>")
@login_required
async def disable(address):
lws.modify_wallet(address, False)
await flash(f"{address} disabled in LWS")
return redirect(f"/")
async def modify(address, status):
lws.modify_wallet(address, status)
return redirect(url_for("htmx.show_wallets"))
@bp.route("/wallet/<address>/enable")
@login_required
async def enable(address):
lws.modify_wallet(address, True)
await flash(f"{address} enabled in LWS")
return redirect(f"/")
@bp.route("/wallet/<address>/accept")
@login_required
async def accept(address):
lws.accept_request(address)
await flash(f"{address} accepted")
return redirect(f"/")
return redirect(url_for("htmx.show_wallets"))
@bp.route("/wallet/<address>/reject")
@login_required
async def reject(address):
lws.reject_request(address)
await flash(f"{address} rejected")
return redirect(f"/")
return redirect(url_for("htmx.show_wallets"))
@bp.route("/wallet/<address>/label/<label>")
@login_required
async def label(address, label):
w = Wallet.select().where(Wallet.address == address).first()
if w and label:
w.label = label
w.save()
return redirect(url_for("htmx.show_wallets"))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -23,4 +23,34 @@ input {
display: block;
padding-bottom: .5em;
margin-bottom: 1em;
}
.hidden {
display: none !important;
}
.indicator .htmx-request{
display:inline;
}
.indicator.htmx-request{
display:inline;
}
.underline {
text-decoration: underline dotted;
}
.showFade {
animation-name: render;
animation-duration: .5s;
}
@keyframes render {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@ -0,0 +1,26 @@
<div id="create_wallet">
<form hx-target="#show_wallets" hx-post="{{ url_for('wallet.add') }}" action="#">
<label for="label">Label</label>
<input type="text" name="label" />
<p>Seed: <span class="key">{{ seed }}</span></p>
<p>Public Address: <span class="key">{{ address }}</span></p>
<p>Public Spend Key: <span class="key">{{ psk }}</span></p>
<p>Public View Key: <span class="key">{{ pvk }}</span></p>
<p>Secret Spend Key: <span class="key">{{ ssk }}</span></p>
<p>Secret View Key: <span class="key">{{ svk }}</span></p>
<input type="text" name="address" value="{{ address }}" class="hidden" />
<input type="text" name="view_key" value="{{ svk }}" class="hidden" />
<input type="number" name="restore_height" value="-1" class="hidden" />
<button type="submit">Create</button>
<button onclick="cancelCreate()">Cancel</button>
</form>
<script>
function cancelCreate() {
document.getElementById("create_wallet").innerHTML = "";
}
document.getElementById("create_wallet").addEventListener("submit", function() {
cancelCreate();
})
</script>
</div>

@ -0,0 +1,22 @@
<div id="import_wallet">
<form hx-target="#show_wallets" hx-post="{{ url_for('wallet.add') }}" action="#" >
<label for="label">Label</label>
<input type="text" name="label" />
<label for="seed">Mnemonic Seed</label>
<p class="subtext">12, 13, or 25 word seeds</p>
<input type="password" name="seed" />
<label for="restore_height">Restore Height</label>
<input type="number" name="restore_height" value="-1" />
<button type="submit">Import</button>
<button onclick="cancelImport()">Cancel</button>
</form>
<script>
function cancelImport() {
document.getElementById("import_wallet").innerHTML = "";
}
document.getElementById("import_wallet").addEventListener("submit", function() {
cancelImport();
})
</script>
</div>

@ -0,0 +1,16 @@
<div>
<form action="#" onsubmit="updateLabel(event)">
<input type="text" name="label" onkeyup="updateLabel(event)" value="{{ label }}" id="label-input-{{ address }}" onfocusout="updateLabel(event)">
</form>
<script>
function updateLabel(e) {
e.preventDefault();
if (e.keyCode === 13) {
htmx.ajax('GET', `/wallet/{{ address }}/label/${e.target.value}`, '#show_wallets');
} else if (e.keyCode === 27 || e.type == "focusout") {
htmx.ajax('GET', '{{ url_for("htmx.show_wallets") }}', '#show_wallets');
}
}
document.getElementById('label-input-{{ address }}').focus();
</script>
</div>

@ -0,0 +1,16 @@
<div>
<form action="#" onsubmit="updateHeight(event)">
<input type="text" name="label" onkeyup="updateHeight(event)" value="{{ height }}" id="height-input-{{ address }}" onfocusout="updateHeight(event)">
</form>
<script>
function updateHeight(e) {
e.preventDefault();
if (e.keyCode === 13) {
htmx.ajax('GET', `/wallet/{{ address }}/rescan/${e.target.value}`, '#show_wallets');
} else if (e.keyCode === 27 || e.type == "focusout") {
htmx.ajax('GET', '{{ url_for("htmx.show_wallets") }}', '#show_wallets');
}
}
document.getElementById('height-input-{{ address }}').focus();
</script>
</div>

@ -0,0 +1,63 @@
<table class="striped">
<thead>
<tr>
<th>Label</th>
<th>Status</th>
<th>Action</th>
<th>Address</th>
<th>Height</th>
</tr>
</thead>
<tbody>
{% for status in requests %}
{% for request in requests[status] %}
<tr>
<td>?</td>
<td>
<span class="tag text-grey">
PENDING
</span>
</td>
<td>
<button class="button primary" hx-target="#show_wallets" hx-get="{{ url_for('wallet.accept', address=request['address']) }}" hx-swap="innerHTML">Accept</button>
<button class="button error" hx-target="#show_wallets" hx-get="{{ url_for('wallet.reject', address=request['address']) }}" hx-swap="innerHTML">Reject</button>
</td>
<td>{{ request['address'] | shorten }}</td>
<td>{{ request['start_height'] }}</td>
</tr>
{% endfor %}
{% endfor %}
{% for status in accounts %}
{% for account in accounts[status] %}
<tr>
<td>
<div hx-get="/htmx/label_wallet" hx-target="this" hx-swap="outerHTML" hx-vals='{"address": "{{ account['address'] }}", "label": "{{ account['address'] | find_label }}"}'>
<span class="underline">{{ account['address'] | find_label }}</span> <i class="fa-regular fa-pen-to-square"></i>
</div>
</td>
<td>
<span class="tag {% if status == 'active' %}text-primary{% else %}text-error{% endif %}">
{{ status | capitalize }}
</span>
</td>
<td>
{% if status == 'active' %}
<button class="button dark outline" hx-target="#show_wallets" hx-get="{{ url_for('wallet.modify', address=account['address'], status='inactive') }}" hx-swap="innerHTML">Deactivate</button>
{% else %}
<button class="button primary outline" hx-target="#show_wallets" hx-get="{{ url_for('wallet.modify', address=account['address'], status='active') }}" hx-swap="innerHTML" >Activate</button>
<button class="button error" hx-target="#show_wallets" hx-get="{{ url_for('wallet.modify', address=account['address'], status='hidden') }}" hx-swap="innerHTML" >Hide</button>
{% endif %}
</td>
<td>{{ account['address'] | shorten }}</td>
<td>
<div hx-get="/htmx/set_height" hx-target="this" hx-swap="outerHTML" hx-vals='{"address": "{{ account['address'] }}", "height": "{{ account['scan_height'] }}"}'>
<span class="underline">{{ account['scan_height'] }}</span>
<i class="fa-regular fa-pen-to-square"></i>
</div>
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>

@ -18,12 +18,10 @@
<meta name="twitter:description" content="Monero LWS web app">
<meta name="twitter:image" content="">
<meta name="keywords" content="Wownero, Monero, crypto, wallet, explorer">
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
<link rel="stylesheet" href="/static/chota.css">
<link rel="stylesheet" href="/static/main.css">
<!--
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/main.css">
-->
<link rel="stylesheet" href="/static/fa/all.min.css">
<script src="/static/htmx.js"></script>
</head>
<body>

@ -1,68 +1,43 @@
{% extends 'includes/base.html' %}
{% block content %}
<h1>LWS Web Admin</h1>
<h1>Monero Lightwallet Server</h1>
<p>LWS Admin: {{ config.LWS_ADMIN_URL }}</p>
<p>LWS RPC: {{ config.LWS_URL }}</p>
<h3>Accounts</h3>
<a href="/wallet/add" class="button outline primary">Add Wallet</a>
<table class="striped">
<thead>
<tr>
<th>Status</th>
<th>Action</th>
<th>Address</th>
<th>Height</th>
</tr>
</thead>
<tbody>
{% for status in accounts %}
{% for account in accounts[status] %}
<tr>
<td>
<span class="tag text-white {% if status == 'active' %}bg-success{% else %}bg-error{% endif %}">
{{ status | upper }}
</span>
</td>
<td>
{% if status == 'active' %}
<a href="/wallet/{{ account['address'] }}/disable" class="">disable</a>
{% else %}
<a href="/wallet/{{ account['address'] }}/enable" class="">enable</a>
{% endif %}
</td>
<td>{{ account['address'] | shorten }}</td>
<td>
<form method="get" action="{{ url_for('wallet.rescan') }}">
{{ account['scan_height'] }}
<input type="integer" name="height" style="width: 5em; display: inline;" />
<input type="hidden" name="address" value="{{ account['address'] }}" />
<button type="submit">Rescan</button>
</form>
</td>
</tr>
{% endfor %}
{% endfor %}
<p>Monero Web Wallet: <a href="{{ config.MYMONERO_URL }}" target="_blank">{{ config.MYMONERO_URL }}</a></p>
<div>
<a hx-get="/htmx/import_wallet" hx-target="#walletForm" class="button primary outline">Import Wallet</a>
<a hx-get="/htmx/create_wallet" hx-target="#walletForm" class="button primary">Create Wallet</a>
<p id="walletForm" style="margin: 2em 0 2em 0"></p>
</div>
<div>
<h3>Accounts</h3>
<p>
The below Monero accounts are stored in LWS;
only active accounts will be synced.
Accounts created in the web wallet must be approved here before use.
</p>
<div hx-get="/htmx/show_wallets" hx-target="#show_wallets" class="button outline" hx-indicator="#refresh_loader" onclick="handleRefresh()">
Refresh
<i class="fa-solid fa-rotate-right indicator" id="refresh_loader"></i>
</div>
<div hx-trigger="load" hx-get="/htmx/show_wallets" id="show_wallets" onload=""></div>
</div>
<script>
function handleRefresh() {
const show_wallets = document.getElementById('show_wallets');
const loader = document.getElementById('refresh_loader');
loader.classList.add('fa-spin');
show_wallets.classList.add('showFade');
setTimeout(function() {
show_wallets.classList.remove('showFade');
loader.classList.remove('fa-spin');
}, 2000)
}
</script>
{% for status in requests %}
{% for request in requests[status] %}
<tr>
<td>
<span class="tag text-white bg-dark">
PENDING
</span>
</td>
<td>
<a href="/wallet/{{ request['address'] }}/accept" class="">approve</a> |
<a href="/wallet/{{ request['address'] }}/reject" class="">reject</a>
</td>
<td>{{ request['address'] | shorten }}</td>
<td>{{ request['start_height'] }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% endblock %}

@ -4,7 +4,7 @@
<a href="/utils">Go Back</a>
<h1>Parse Mnemonic Seed</h1>
<form method="post">
<label for="seed">25 Word Seed</label>
<label for="seed">12, 13, or 25 Word Seed</label>
<input type="password" name="seed" />
<button type="submit">Send</button>
</form>

@ -0,0 +1,15 @@
{% extends 'includes/base.html' %}
{% block content %}
<a href="/">Go Back</a>
<h1>Add a Wallet</h1>
<form method="post">
<label for="address">Address</label>
<input type="text" name="address" value="{{ request.args.get('address', '') }}" />
<label for="view_key">View Key</label>
<input type="text" name="view_key" />
<label for="restore_height">Restore Height</label>
<input type="number" name="restore_height" />
<button type="submit">Send</button>
</form>
{% endblock %}

@ -21,9 +21,9 @@
<a href="{{ url_for('wallet.rescan', id=wallet.id) }}" class="button dark outline">rescan</a>
{% if wallet.is_active() %}
<a href="{{ url_for('wallet.disable', id=wallet.id) }}" class="button error">disable</a>
<a href="{{ url_for('wallet.modify', id=wallet.id, status) }}" class="button error">disable</a>
{% else %}
<a href="{{ url_for('wallet.enable', id=wallet.id) }}" class="button primary">enable</a>
<a href="{{ url_for('wallet.modify', id=wallet.id, status) }}" class="button primary">enable</a>
{% endif %}
{% else %}
<p>not connected to lws</p>

@ -22,4 +22,4 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
start = "lws.app:app.run"
start = "lws.app:run()"

Loading…
Cancel
Save