watchonly done

This commit is contained in:
Tiago vasconcelos 2021-10-14 22:30:47 +01:00
parent ec4117a5f4
commit 6935589dad
7 changed files with 119 additions and 98 deletions

View File

@ -1,13 +1,25 @@
from quart import Blueprint import asyncio
from fastapi import APIRouter
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_watchonly") db = Database("ext_watchonly")
watchonly_ext: Blueprint = Blueprint( watchonly_ext: APIRouter = APIRouter(
"watchonly", __name__, static_folder="static", template_folder="templates" prefix="/watchonly",
tags=["watchonly"]
) )
def watchonly_renderer():
return template_renderer(
[
"lnbits/extensions/watchonly/templates",
]
)
from .views_api import * # noqa from .views_api import * # noqa
from .views import * # noqa from .views import * # noqa

View File

@ -74,8 +74,9 @@ def parse_key(masterpub: str):
return desc, network return desc, network
async def create_watch_wallet(*, user: str, masterpub: str, title: str) -> Wallets: async def create_watch_wallet(user: str, masterpub: str, title: str) -> Wallets:
# check the masterpub is fine, it will raise an exception if not # check the masterpub is fine, it will raise an exception if not
print("PARSE", parse_key(masterpub))
parse_key(masterpub) parse_key(masterpub)
wallet_id = urlsafe_short_hash() wallet_id = urlsafe_short_hash()
await db.execute( await db.execute(
@ -131,19 +132,20 @@ async def delete_watch_wallet(wallet_id: str) -> None:
async def get_derive_address(wallet_id: str, num: int): async def get_derive_address(wallet_id: str, num: int):
wallet = await get_watch_wallet(wallet_id) wallet = await get_watch_wallet(wallet_id)
key = wallet[2] key = wallet.masterpub
desc, network = parse_key(key) desc, network = parse_key(key)
return desc.derive(num).address(network=network) return desc.derive(num).address(network=network)
async def get_fresh_address(wallet_id: str) -> Optional[Addresses]: async def get_fresh_address(wallet_id: str) -> Optional[Addresses]:
wallet = await get_watch_wallet(wallet_id) wallet = await get_watch_wallet(wallet_id)
if not wallet: if not wallet:
return None return None
address = await get_derive_address(wallet_id, wallet[4] + 1) address = await get_derive_address(wallet_id, wallet.address_no + 1)
await update_watch_wallet(wallet_id=wallet_id, address_no=wallet[4] + 1) await update_watch_wallet(wallet_id=wallet_id, address_no=wallet.address_no + 1)
masterpub_id = urlsafe_short_hash() masterpub_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
@ -181,7 +183,7 @@ async def get_addresses(wallet_id: str) -> List[Addresses]:
async def create_mempool(user: str) -> Optional[Mempool]: async def create_mempool(user: str) -> Optional[Mempool]:
await db.execute( await db.execute(
""" """
INSERT INTO watchonly.mempool ("user",endpoint) INSERT INTO watchonly.mempool ("user",endpoint)
VALUES (?, ?) VALUES (?, ?)
""", """,
(user, "https://mempool.space"), (user, "https://mempool.space"),

View File

@ -1,8 +1,12 @@
from sqlite3 import Row from sqlite3 import Row
from typing import NamedTuple from fastapi.param_functions import Query
from pydantic import BaseModel
class CreateWallet(BaseModel):
masterpub: str = Query("")
title: str = Query("")
class Wallets(NamedTuple): class Wallets(BaseModel):
id: str id: str
user: str user: str
masterpub: str masterpub: str
@ -15,7 +19,7 @@ class Wallets(NamedTuple):
return cls(**dict(row)) return cls(**dict(row))
class Mempool(NamedTuple): class Mempool(BaseModel):
user: str user: str
endpoint: str endpoint: str
@ -24,7 +28,7 @@ class Mempool(NamedTuple):
return cls(**dict(row)) return cls(**dict(row))
class Addresses(NamedTuple): class Addresses(BaseModel):
id: str id: str
address: str address: str
wallet: str wallet: str

View File

@ -38,7 +38,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}api/v1/wallet -H "X-Api-Key: {{ >curl -X GET {{ request.url_root }}api/v1/wallet -H "X-Api-Key: {{
g.user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -67,7 +67,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}api/v1/wallet/&lt;wallet_id&gt; >curl -X GET {{ request.url_root }}api/v1/wallet/&lt;wallet_id&gt;
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -91,7 +91,7 @@
<code <code
>curl -X POST {{ request.url_root }}api/v1/wallet -d '{"title": >curl -X POST {{ request.url_root }}api/v1/wallet -d '{"title":
&lt;string&gt;, "masterpub": &lt;string&gt;}' -H "Content-type: &lt;string&gt;, "masterpub": &lt;string&gt;}' -H "Content-type:
application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" application/json" -H "X-Api-Key: {{ user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -117,7 +117,7 @@
<code <code
>curl -X DELETE {{ request.url_root >curl -X DELETE {{ request.url_root
}}api/v1/wallet/&lt;wallet_id&gt; -H "X-Api-Key: {{ }}api/v1/wallet/&lt;wallet_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}" user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -143,7 +143,7 @@
<code <code
>curl -X GET {{ request.url_root >curl -X GET {{ request.url_root
}}api/v1/addresses/&lt;wallet_id&gt; -H "X-Api-Key: {{ }}api/v1/addresses/&lt;wallet_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -174,7 +174,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}api/v1/address/&lt;wallet_id&gt; >curl -X GET {{ request.url_root }}api/v1/address/&lt;wallet_id&gt;
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -203,7 +203,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}api/v1/mempool -H "X-Api-Key: {{ >curl -X GET {{ request.url_root }}api/v1/mempool -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}" user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -235,7 +235,7 @@
<code <code
>curl -X PUT {{ request.url_root }}api/v1/mempool -d '{"endpoint": >curl -X PUT {{ request.url_root }}api/v1/mempool -d '{"endpoint":
&lt;string&gt;}' -H "Content-type: application/json" -H "X-Api-Key: &lt;string&gt;}' -H "Content-type: application/json" -H "X-Api-Key:
{{ g.user.wallets[0].adminkey }}" {{ user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>

View File

@ -287,7 +287,6 @@
{% endraw %} {% endraw %}
</div> </div>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script type="text/javascript" src="https://mempool.space/mempool.js"></script> <script type="text/javascript" src="https://mempool.space/mempool.js"></script>
<style></style> <style></style>
<script> <script>

View File

@ -1,22 +1,32 @@
from quart import g, abort, render_template
from http import HTTPStatus from http import HTTPStatus
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from starlette.requests import Request
from fastapi.params import Depends
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import watchonly_ext from . import watchonly_ext, watchonly_renderer
# from .crud import get_payment
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
@watchonly_ext.route("/") @watchonly_ext.get("/", response_class=HTMLResponse)
@validate_uuids(["usr"], required=True) async def index(request: Request, user: User = Depends(check_user_exists)):
@check_user_exists() return watchonly_renderer().TemplateResponse("watchonly/index.html", {"request": request,"user": user.dict()})
async def index():
return await render_template("watchonly/index.html", user=g.user)
@watchonly_ext.route("/<charge_id>") # @watchonly_ext.get("/{charge_id}", response_class=HTMLResponse)
async def display(charge_id): # async def display(request: Request, charge_id):
link = get_payment(charge_id) or abort( # link = get_payment(charge_id)
HTTPStatus.NOT_FOUND, "Charge link does not exist." # if not link:
) # raise HTTPException(
# status_code=HTTPStatus.NOT_FOUND,
return await render_template("watchonly/display.html", link=link) # detail="Charge link does not exist."
# )
#
# return watchonly_renderer().TemplateResponse("watchonly/display.html", {"request": request,"link": link.dict()})

View File

@ -1,11 +1,15 @@
import hashlib import hashlib
from quart import g, jsonify, url_for, request
from http import HTTPStatus from http import HTTPStatus
import httpx import httpx
import json import json
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import WalletTypeInfo, get_key_type
from fastapi import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from .models import CreateWallet
from lnbits.extensions.watchonly import watchonly_ext from lnbits.extensions.watchonly import watchonly_ext
from .crud import ( from .crud import (
@ -24,86 +28,83 @@ from .crud import (
###################WALLETS############################# ###################WALLETS#############################
@watchonly_ext.route("/api/v1/wallet", methods=["GET"]) @watchonly_ext.get("/api/v1/wallet")
@api_check_wallet_key("invoice") async def api_wallets_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_wallets_retrieve():
try: try:
return ( return [wallet.dict() for wallet in await get_watch_wallets(wallet.wallet.user)]
jsonify(
[wallet._asdict() for wallet in await get_watch_wallets(g.wallet.user)]
),
HTTPStatus.OK,
)
except: except:
return "" return ""
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["GET"]) @watchonly_ext.get("/api/v1/wallet/{wallet_id}")
@api_check_wallet_key("invoice") async def api_wallet_retrieve(wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_wallet_retrieve(wallet_id): w_wallet = await get_watch_wallet(wallet_id)
wallet = await get_watch_wallet(wallet_id)
if not wallet: if not w_wallet:
return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet does not exist."
)
return jsonify(wallet._asdict()), HTTPStatus.OK return w_wallet.dict()
@watchonly_ext.route("/api/v1/wallet", methods=["POST"]) @watchonly_ext.post("/api/v1/wallet")
@api_check_wallet_key("admin") async def api_wallet_create_or_update(data: CreateWallet, wallet_id=None, w: WalletTypeInfo = Depends(get_key_type)):
@api_validate_post_request(
schema={
"masterpub": {"type": "string", "empty": False, "required": True},
"title": {"type": "string", "empty": False, "required": True},
}
)
async def api_wallet_create_or_update(wallet_id=None):
try: try:
wallet = await create_watch_wallet( wallet = await create_watch_wallet(
user=g.wallet.user, masterpub=g.data["masterpub"], title=g.data["title"] user=w.wallet.user, masterpub=data.masterpub, title=data.title
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST raise HTTPException(
mempool = await get_mempool(g.wallet.user) status_code=HTTPStatus.BAD_REQUEST,
detail=str(e)
)
mempool = await get_mempool(w.wallet.user)
if not mempool: if not mempool:
create_mempool(user=g.wallet.user) create_mempool(user=w.wallet.user)
return jsonify(wallet._asdict()), HTTPStatus.CREATED return wallet.dict()
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["DELETE"]) @watchonly_ext.delete("/api/v1/wallet/{wallet_id}")
@api_check_wallet_key("admin") async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
async def api_wallet_delete(wallet_id):
wallet = await get_watch_wallet(wallet_id) wallet = await get_watch_wallet(wallet_id)
if not wallet: if not wallet:
return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet does not exist."
)
await delete_watch_wallet(wallet_id) await delete_watch_wallet(wallet_id)
return jsonify({"deleted": "true"}), HTTPStatus.NO_CONTENT raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
#############################ADDRESSES########################## #############################ADDRESSES##########################
@watchonly_ext.route("/api/v1/address/<wallet_id>", methods=["GET"]) @watchonly_ext.get("/api/v1/address/{wallet_id}")
@api_check_wallet_key("invoice") async def api_fresh_address(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
async def api_fresh_address(wallet_id):
await get_fresh_address(wallet_id) await get_fresh_address(wallet_id)
addresses = await get_addresses(wallet_id) addresses = await get_addresses(wallet_id)
return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK return [address.dict() for address in addresses]
@watchonly_ext.route("/api/v1/addresses/<wallet_id>", methods=["GET"]) @watchonly_ext.get("/api/v1/addresses/{wallet_id}")
@api_check_wallet_key("invoice")
async def api_get_addresses(wallet_id): async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
wallet = await get_watch_wallet(wallet_id) wallet = await get_watch_wallet(wallet_id)
if not wallet: if not wallet:
return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet does not exist."
)
addresses = await get_addresses(wallet_id) addresses = await get_addresses(wallet_id)
@ -111,28 +112,21 @@ async def api_get_addresses(wallet_id):
await get_fresh_address(wallet_id) await get_fresh_address(wallet_id)
addresses = await get_addresses(wallet_id) addresses = await get_addresses(wallet_id)
return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK return [address.dict() for address in addresses]
#############################MEMPOOL########################## #############################MEMPOOL##########################
@watchonly_ext.route("/api/v1/mempool", methods=["PUT"]) @watchonly_ext.put("/api/v1/mempool")
@api_check_wallet_key("admin") async def api_update_mempool(endpoint: str = Query(...), w: WalletTypeInfo = Depends(get_key_type)):
@api_validate_post_request( mempool = await update_mempool(endpoint, user=w.wallet.user)
schema={ return mempool.dict()
"endpoint": {"type": "string", "empty": False, "required": True},
}
)
async def api_update_mempool():
mempool = await update_mempool(user=g.wallet.user, **g.data)
return jsonify(mempool._asdict()), HTTPStatus.OK
@watchonly_ext.route("/api/v1/mempool", methods=["GET"]) @watchonly_ext.get("/api/v1/mempool")
@api_check_wallet_key("admin") async def api_get_mempool(w: WalletTypeInfo = Depends(get_key_type)):
async def api_get_mempool(): mempool = await get_mempool(w.wallet.user)
mempool = await get_mempool(g.wallet.user)
if not mempool: if not mempool:
mempool = await create_mempool(user=g.wallet.user) mempool = await create_mempool(user=w.wallet.user)
return jsonify(mempool._asdict()), HTTPStatus.OK return mempool.dict()