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

@ -7,9 +7,44 @@ x-log-config: &log-config
max-file: "5" max-file: "5"
volumes: volumes:
lws: lws:
lwsadmin:
mymonero: mymonero:
lwsadmin:
services: 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: monero-lws:
container_name: monero-lws container_name: monero-lws
image: lalanza808/monero-lws:develop image: lalanza808/monero-lws:develop

@ -1,6 +1,6 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
RUN apt-get update RUN apt-get update -y
RUN apt-get install -y python3 python3-venv RUN apt-get install -y python3 python3-venv
WORKDIR /srv/lwsadmin WORKDIR /srv/lwsadmin
RUN adduser \ 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_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") 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 @app.before_serving
async def startup(): async def startup():
from lws.routes import auth, wallet, meta from lws.routes import auth, wallet, meta, htmx
from lws import filters from lws import filters
app.register_blueprint(filters.bp) app.register_blueprint(filters.bp)
app.register_blueprint(auth.bp) app.register_blueprint(auth.bp)
app.register_blueprint(meta.bp) app.register_blueprint(meta.bp)
app.register_blueprint(wallet.bp) app.register_blueprint(wallet.bp)
app.register_blueprint(htmx.bp)
@app.errorhandler(Unauthorized) @app.errorhandler(Unauthorized)
async def redirect_to_login(*_): 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 return app
bcrypt = Bcrypt(create_app()) bcrypt = Bcrypt(create_app())

@ -1,6 +1,8 @@
from monero.numbers import from_atomic from monero.numbers import from_atomic
from quart import Blueprint from quart import Blueprint
from lws.models import Wallet, get_random_words
bp = Blueprint('filters', 'filters') bp = Blueprint('filters', 'filters')
@ -12,4 +14,12 @@ def atomic(amt):
@bp.app_template_filter('shorten') @bp.app_template_filter('shorten')
def shorten(s): 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_delete_uuid: {"event_ids": [...]}
# webhook_list: {} # webhook_list: {}
class LWS: class LWS:
def __init__(self): def __init__(self):
pass pass
@ -26,6 +25,19 @@ class LWS:
def init(self, admin_key): def init(self, admin_key):
self.admin_key = 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: def get_wallet(self, address: str) -> dict:
try: try:
res = self.list_accounts() res = self.list_accounts()
@ -108,9 +120,8 @@ class LWS:
print(f"Failed to add wallet {address}: {e}") print(f"Failed to add wallet {address}: {e}")
return {} 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" endpoint = f"{config.LWS_ADMIN_URL}/modify_account_status"
status = "active" if active else "inactive"
data = { data = {
"auth": self.admin_key, "auth": self.admin_key,
"params": { "params": {

@ -1,9 +1,16 @@
from random import choice
from datetime import datetime from datetime import datetime
from peewee import * 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): class User(Model):
@ -17,4 +24,13 @@ class User(Model):
database = db 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 = Blueprint("meta", "meta")
@bp.route("/") @bp.route("/")
@login_required @login_required
async def index(): async def index():
admin = User.select().first() admin = User.select().first()
lws.init(admin.view_key) lws.init(admin.view_key)
accounts = lws.list_accounts()
if 'hidden' in accounts:
del accounts["hidden"]
requests = lws.list_requests()
return await render_template( return await render_template(
"index.html", "index.html",
config=config, config=config
accounts=accounts,
requests=requests
) )

@ -1,11 +1,13 @@
import monero.address import monero.address
from quart import Blueprint, render_template, request, flash, redirect from monero.seed import Seed
from quart_auth import login_required, current_user from quart import Blueprint, request, flash, redirect, url_for
from quart_auth import login_required
from lws.helpers import lws 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"]) @bp.route("/wallet/add", methods=["GET", "POST"])
@ -13,74 +15,61 @@ bp = Blueprint('wallet', 'wallet')
async def add(): async def add():
form = await request.form form = await request.form
if form: if form:
address = form.get("address", "") label = form.get("label")
view_key = form.get("view_key", "") seed = form.get("seed")
restore_height = form.get("restore_height", 0) restore_height = form.get("restore_height", None)
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")
try: try:
_a = monero.address.Address(address) seed = Seed(seed)
valid_view_key = _a.check_private_view_key(view_key)
except ValueError: except ValueError:
await flash("Invalid Monero address") await flash("Invalid mnemonic seed")
return redirect("/wallet/add") return ""
if not valid_view_key: lws._init()
await flash("Invalid view key provided for address") address = str(seed.public_address())
return redirect("/wallet/add") svk = str(seed.secret_view_key())
lws.add_wallet(address, view_key) lws.add_wallet(address, svk)
lws.rescan(address, int(restore_height)) if restore_height != "-1":
await flash("wallet added") lws.rescan(address, int(restore_height))
return redirect(f"/") w = Wallet(
return await render_template("wallet/add.html") 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 @login_required
async def rescan(): async def rescan(address, height):
address = request.args.get('address') lws.rescan(address, int(height))
height = request.args.get('height') return redirect(url_for("htmx.show_wallets"))
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"/")
@bp.route("/wallet/<address>/disable") @bp.route("/wallet/<address>/modify/<status>")
@login_required @login_required
async def disable(address): async def modify(address, status):
lws.modify_wallet(address, False) lws.modify_wallet(address, status)
await flash(f"{address} disabled in LWS") return redirect(url_for("htmx.show_wallets"))
return redirect(f"/")
@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") @bp.route("/wallet/<address>/accept")
@login_required @login_required
async def accept(address): async def accept(address):
lws.accept_request(address) lws.accept_request(address)
await flash(f"{address} accepted") return redirect(url_for("htmx.show_wallets"))
return redirect(f"/")
@bp.route("/wallet/<address>/reject") @bp.route("/wallet/<address>/reject")
@login_required @login_required
async def reject(address): async def reject(address):
lws.reject_request(address) lws.reject_request(address)
await flash(f"{address} rejected") return redirect(url_for("htmx.show_wallets"))
return redirect(f"/")
@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; display: block;
padding-bottom: .5em; padding-bottom: .5em;
margin-bottom: 1em; 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:description" content="Monero LWS web app">
<meta name="twitter:image" content=""> <meta name="twitter:image" content="">
<meta name="keywords" content="Wownero, Monero, crypto, wallet, explorer"> <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/main.css">
<!-- <link rel="stylesheet" href="/static/fa/all.min.css">
<link rel="stylesheet" href="/static/css/skeleton.css"> <script src="/static/htmx.js"></script>
<link rel="stylesheet" href="/static/css/main.css">
-->
</head> </head>
<body> <body>

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

@ -4,7 +4,7 @@
<a href="/utils">Go Back</a> <a href="/utils">Go Back</a>
<h1>Parse Mnemonic Seed</h1> <h1>Parse Mnemonic Seed</h1>
<form method="post"> <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" /> <input type="password" name="seed" />
<button type="submit">Send</button> <button type="submit">Send</button>
</form> </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> <a href="{{ url_for('wallet.rescan', id=wallet.id) }}" class="button dark outline">rescan</a>
{% if wallet.is_active() %} {% 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 %} {% 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 %} {% endif %}
{% else %} {% else %}
<p>not connected to lws</p> <p>not connected to lws</p>

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

Loading…
Cancel
Save