Compare commits

...

3 Commits
htmx ... master

Author SHA1 Message Date
lza_menace 7154ea81a8 repo and config adjustments 6 months ago
lza_menace e83b9d964b update readme 1 year ago
lza_menace bda9bd7a31 htmx (#1)
Reviewed-on: #1
1 year ago

@ -1,7 +1,7 @@
init: init:
git clone --recursive --branch develop https://github.com/vtnerd/monero-lws git clone --recursive --branch release-v0.3_0.18 https://github.com/vtnerd/monero-lws
git clone https://github.com/lalanza808/docker-monero-node git clone https://github.com/lalanza808/docker-monero-node
git clone https://github.com/CryptoGrampy/mymonero-web-js git clone https://github.com/lalanza808/mymonero-web-js
release: release:
docker-compose -f release.compose.yaml build docker-compose -f release.compose.yaml build

@ -8,9 +8,21 @@ Monero lightwallet project. Packages the following services in one package:
* `mymonero-web` by [MyMonero](https://mymonero.com) but forked and cleaned up for personal use by [CryptoGrampy](https://github.com/CryptoGrampy/mymonero-web-js) - the web wallet client * `mymonero-web` by [MyMonero](https://mymonero.com) but forked and cleaned up for personal use by [CryptoGrampy](https://github.com/CryptoGrampy/mymonero-web-js) - the web wallet client
## Setup ## Running
Works on Linux, built on Ubuntu 22. The default compose stack pulls in images which were pre-built for ease of use. See the `development` section to build everything locally.
Otherwise, clone the repo and run: `docker-compose up -d`
- `lwsadmin` will be available at http://127.0.0.1:5000
- `mymonero-web` will be available at http://127.0.0.1:9110
- `monero-lws` will be available at http://127.0.0.1:8080 (rpc) and http://127.0.0.1:8081 (admin)
- `monerod` will be available at :18080 (p2p), :18081 (unrestricted rpc), :18082 (zmq), and :18089 (restricted rpc)
## Development
Built on Ubuntu 22.04
1. Install packages 1. Install packages
2. Clone the repo 2. Clone the repo
@ -32,7 +44,7 @@ git clone https://github.com/lalanza808/docker-monero-node
git clone https://github.com/CryptoGrampy/mymonero-web-js git clone https://github.com/CryptoGrampy/mymonero-web-js
# 4 # 4
docker-compose build docker-compose build # builds images from the nested repos
# 5 # 5
docker-compose up -d docker-compose up -d

@ -7,17 +7,56 @@ 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
build:
context: lwsadmin
dockerfile: Dockerfile
# 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
build:
context: mymonero-web-js
dockerfile: Dockerfile
restart: unless-stopped
environment:
MYMONERO_WEB_NETTYPE: 0
MYMONERO_WEB_SERVER_URL: http://127.0.0.1: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:989b8cd8574797efa66b3b06bc077b6e36ebc074
# build: build:
# context: monero-lws context: monero-lws
# dockerfile: Dockerfile dockerfile: Dockerfile
# args:
# NPROC: 16
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- monerod - monerod
@ -29,15 +68,18 @@ services:
- 8081/tcp - 8081/tcp
volumes: volumes:
- lws:/home/monero-lws/.bitmonero/light_wallet_server - lws:/home/monero-lws/.bitmonero/light_wallet_server
environment:
SCAN_THREADS: ${SCAN_THREADS:-4}
REST_THREADS: ${REST_THREADS:-2}
command: command:
--scan-threads 4 --rest-threads 4 --rest-server http://0.0.0.0:8080 --admin-rest-server http://0.0.0.0:8081 --log-level 1 --daemon=tcp://monerod:18082 --sub=tcp://monerod:18083 --confirm-external-bind --access-control-origin "*" --scan-threads ${SCAN_THREADS} --rest-threads ${REST_THREADS} --rest-server http://0.0.0.0:8080 --admin-rest-server http://0.0.0.0:8081 --log-level 1 --daemon=tcp://monerod:18082 --sub=tcp://monerod:18083 --confirm-external-bind --access-control-origin "*"
<<: *log-config <<: *log-config
monerod: monerod:
container_name: monerod container_name: monerod
image: lalanza808/monerod:v0.18.3.1 # image: lalanza808/monerod:latest
# build: build:
# context: docker-monero-node/dockerfiles context: docker-monero-node/dockerfiles
# dockerfile: monero dockerfile: monero
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ~/.bitmonero:/srv/monerod - ~/.bitmonero:/srv/monerod
@ -47,6 +89,8 @@ services:
- 18082/tcp - 18082/tcp
- 18083/tcp - 18083/tcp
- 18089/tcp - 18089/tcp
ports:
- 127.0.0.1:18081:18081
command: command:
monerod --data-dir /srv/monerod/ --p2p-bind-ip=0.0.0.0 --p2p-bind-port=18080 --rpc-bind-ip=0.0.0.0 --rpc-bind-port=18081 --zmq-rpc-bind-ip=0.0.0.0 --zmq-rpc-bind-port=18082 --zmq-pub tcp://0.0.0.0:18083 --rpc-restricted-bind-ip=0.0.0.0 --rpc-restricted-bind-port=18089 --non-interactive --confirm-external-bind --public-node monerod --data-dir /srv/monerod/ --p2p-bind-ip=0.0.0.0 --p2p-bind-port=18080 --rpc-bind-ip=0.0.0.0 --rpc-bind-port=18081 --zmq-rpc-bind-ip=0.0.0.0 --zmq-rpc-bind-port=18082 --zmq-pub tcp://0.0.0.0:18083 --rpc-restricted-bind-ip=0.0.0.0 --rpc-restricted-bind-port=18089 --non-interactive --confirm-external-bind --public-node
<<: *log-config <<: *log-config

@ -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(*_):
if request.path == "/":
return redirect(f"/login?next={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')
@ -13,3 +15,11 @@ 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)
if restore_height != "-1":
lws.rescan(address, int(restore_height)) lws.rescan(address, int(restore_height))
await flash("wallet added") w = Wallet(
return redirect(f"/") address=seed.public_address(),
return await render_template("wallet/add.html") 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

@ -24,3 +24,33 @@ input {
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>
</tr>
</thead>
<tbody>
{% for status in accounts %} <div>
{% for account in accounts[status] %} <h3>Accounts</h3>
<tr> <p>
<td> The below Monero accounts are stored in LWS;
<span class="tag text-white {% if status == 'active' %}bg-success{% else %}bg-error{% endif %}"> only active accounts will be synced.
{{ status | upper }} Accounts created in the web wallet must be approved here before use.
</span> </p>
</td> <div hx-get="/htmx/show_wallets" hx-target="#show_wallets" class="button outline" hx-indicator="#refresh_loader" onclick="handleRefresh()">
<td> Refresh
{% if status == 'active' %} <i class="fa-solid fa-rotate-right indicator" id="refresh_loader"></i>
<a href="/wallet/{{ account['address'] }}/disable" class="">disable</a> </div>
{% else %} <div hx-trigger="load" hx-get="/htmx/show_wallets" id="show_wallets" onload=""></div>
<a href="/wallet/{{ account['address'] }}/enable" class="">enable</a> </div>
{% endif %}
</td>
<td>{{ account['address'] | shorten }}</td>
<td>
<form method="get" action="{{ url_for('wallet.rescan') }}"> <script>
{{ account['scan_height'] }} function handleRefresh() {
<input type="integer" name="height" style="width: 5em; display: inline;" /> const show_wallets = document.getElementById('show_wallets');
<input type="hidden" name="address" value="{{ account['address'] }}" /> const loader = document.getElementById('refresh_loader');
<button type="submit">Rescan</button> loader.classList.add('fa-spin');
</form> show_wallets.classList.add('showFade');
</td> setTimeout(function() {
</tr> show_wallets.classList.remove('showFade');
{% endfor %} loader.classList.remove('fa-spin');
{% endfor %} }, 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 %} {% 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