mirror of https://github.com/lukechilds/lnbits.git
12 changed files with 1419 additions and 0 deletions
@ -0,0 +1,4 @@ |
|||
# Watch Only wallet |
|||
|
|||
Monitor an extended public key and generate deterministic fresh public keys with this simple watch only wallet. Invoice payments can also be generated, both through a publically shareable page and API. |
|||
|
@ -0,0 +1,8 @@ |
|||
from quart import Blueprint |
|||
|
|||
|
|||
watchonly_ext: Blueprint = Blueprint("watchonly", __name__, static_folder="static", template_folder="templates") |
|||
|
|||
|
|||
from .views_api import * # noqa |
|||
from .views import * # noqa |
@ -0,0 +1,8 @@ |
|||
{ |
|||
"name": "Watch Only", |
|||
"short_description": "Onchain watch only wallets", |
|||
"icon": "visibility", |
|||
"contributors": [ |
|||
"arcbtc" |
|||
] |
|||
} |
@ -0,0 +1,186 @@ |
|||
from typing import List, Optional, Union |
|||
|
|||
from lnbits.db import open_ext_db |
|||
|
|||
from .models import Wallets, Payments, Addresses, Mempool |
|||
from lnbits.helpers import urlsafe_short_hash |
|||
|
|||
from embit import bip32 |
|||
from embit import ec |
|||
from embit.networks import NETWORKS |
|||
from embit import base58 |
|||
from embit.util import hashlib |
|||
import io |
|||
from embit.util import secp256k1 |
|||
from embit import hashes |
|||
from binascii import hexlify |
|||
from quart import jsonify |
|||
from embit import script |
|||
from embit import ec |
|||
from embit.networks import NETWORKS |
|||
from binascii import unhexlify, hexlify, a2b_base64, b2a_base64 |
|||
|
|||
########################ADDRESSES####################### |
|||
|
|||
def get_derive_address(wallet_id: str, num: int): |
|||
|
|||
wallet = get_watch_wallet(wallet_id) |
|||
k = bip32.HDKey.from_base58(str(wallet[2])) |
|||
child = k.derive([0, num]) |
|||
address = script.p2wpkh(child).address() |
|||
|
|||
return address |
|||
|
|||
def get_fresh_address(wallet_id: str) -> Addresses: |
|||
wallet = get_watch_wallet(wallet_id) |
|||
|
|||
address = get_derive_address(wallet_id, wallet[4] + 1) |
|||
|
|||
update_watch_wallet(wallet_id = wallet_id, address_no = wallet[4] + 1) |
|||
with open_ext_db("watchonly") as db: |
|||
db.execute( |
|||
""" |
|||
INSERT INTO addresses ( |
|||
address, |
|||
wallet, |
|||
amount |
|||
) |
|||
VALUES (?, ?, ?) |
|||
""", |
|||
(address, wallet_id, 0), |
|||
) |
|||
|
|||
return get_address(address) |
|||
|
|||
|
|||
def get_address(address: str) -> Addresses: |
|||
with open_ext_db("watchonly") as db: |
|||
row = db.fetchone("SELECT * FROM addresses WHERE address = ?", (address,)) |
|||
return Addresses.from_row(row) if row else None |
|||
|
|||
|
|||
def get_addresses(wallet_id: str) -> List[Addresses]: |
|||
with open_ext_db("watchonly") as db: |
|||
rows = db.fetchall("SELECT * FROM addresses WHERE wallet = ?", (wallet_id,)) |
|||
return [Addresses(**row) for row in rows] |
|||
|
|||
|
|||
##########################WALLETS#################### |
|||
|
|||
def create_watch_wallet(*, user: str, masterpub: str, title: str) -> Wallets: |
|||
wallet_id = urlsafe_short_hash() |
|||
with open_ext_db("watchonly") as db: |
|||
db.execute( |
|||
""" |
|||
INSERT INTO wallets ( |
|||
id, |
|||
user, |
|||
masterpub, |
|||
title, |
|||
address_no, |
|||
amount |
|||
) |
|||
VALUES (?, ?, ?, ?, ?, ?) |
|||
""", |
|||
(wallet_id, user, masterpub, title, 0, 0), |
|||
) |
|||
# weallet_id = db.cursor.lastrowid |
|||
address = get_fresh_address(wallet_id) |
|||
return get_watch_wallet(wallet_id) |
|||
|
|||
|
|||
def get_watch_wallet(wallet_id: str) -> Wallets: |
|||
with open_ext_db("watchonly") as db: |
|||
row = db.fetchone("SELECT * FROM wallets WHERE id = ?", (wallet_id,)) |
|||
return Wallets.from_row(row) if row else None |
|||
|
|||
def get_watch_wallets(user: str) -> List[Wallets]: |
|||
with open_ext_db("watchonly") as db: |
|||
rows = db.fetchall("SELECT * FROM wallets WHERE user = ?", (user,)) |
|||
return [Wallets(**row) for row in rows] |
|||
|
|||
def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[Wallets]: |
|||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) |
|||
|
|||
with open_ext_db("watchonly") as db: |
|||
db.execute(f"UPDATE wallets SET {q} WHERE id = ?", (*kwargs.values(), wallet_id)) |
|||
row = db.fetchone("SELECT * FROM wallets WHERE id = ?", (wallet_id,)) |
|||
return Wallets.from_row(row) if row else None |
|||
|
|||
|
|||
def delete_watch_wallet(wallet_id: str) -> None: |
|||
with open_ext_db("watchonly") as db: |
|||
db.execute("DELETE FROM wallets WHERE id = ?", (wallet_id,)) |
|||
|
|||
|
|||
###############PAYMENTS########################## |
|||
|
|||
def create_payment(*, user: str, ex_key: str, description: str, amount: int) -> Payments: |
|||
|
|||
address = get_fresh_address(ex_key) |
|||
payment_id = urlsafe_short_hash() |
|||
with open_ext_db("watchonly") as db: |
|||
db.execute( |
|||
""" |
|||
INSERT INTO payments ( |
|||
payment_id, |
|||
user, |
|||
ex_key, |
|||
address, |
|||
amount |
|||
) |
|||
VALUES (?, ?, ?, ?, ?) |
|||
""", |
|||
(payment_id, user, ex_key, address, amount), |
|||
) |
|||
payment_id = db.cursor.lastrowid |
|||
return get_payment(payment_id) |
|||
|
|||
|
|||
def get_payment(payment_id: str) -> Payments: |
|||
with open_ext_db("watchonly") as db: |
|||
row = db.fetchone("SELECT * FROM payments WHERE id = ?", (payment_id,)) |
|||
return Payments.from_row(row) if row else None |
|||
|
|||
|
|||
def get_payments(user: str) -> List[Payments]: |
|||
with open_ext_db("watchonly") as db: |
|||
rows = db.fetchall("SELECT * FROM payments WHERE user IN ?", (user,)) |
|||
return [Payments.from_row(row) for row in rows] |
|||
|
|||
|
|||
def delete_payment(payment_id: str) -> None: |
|||
with open_ext_db("watchonly") as db: |
|||
db.execute("DELETE FROM payments WHERE id = ?", (payment_id,)) |
|||
|
|||
|
|||
######################MEMPOOL####################### |
|||
|
|||
def create_mempool(user: str) -> Mempool: |
|||
with open_ext_db("watchonly") as db: |
|||
db.execute( |
|||
""" |
|||
INSERT INTO mempool ( |
|||
user, |
|||
endpoint |
|||
) |
|||
VALUES (?, ?) |
|||
""", |
|||
(user, 'https://mempool.space'), |
|||
) |
|||
row = db.fetchone("SELECT * FROM mempool WHERE user = ?", (user,)) |
|||
return Mempool.from_row(row) if row else None |
|||
|
|||
def update_mempool(user: str, **kwargs) -> Optional[Mempool]: |
|||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) |
|||
|
|||
with open_ext_db("watchonly") as db: |
|||
db.execute(f"UPDATE mempool SET {q} WHERE user = ?", (*kwargs.values(), user)) |
|||
row = db.fetchone("SELECT * FROM mempool WHERE user = ?", (user,)) |
|||
return Mempool.from_row(row) if row else None |
|||
|
|||
|
|||
def get_mempool(user: str) -> Mempool: |
|||
with open_ext_db("watchonly") as db: |
|||
row = db.fetchone("SELECT * FROM mempool WHERE user = ?", (user,)) |
|||
return Mempool.from_row(row) if row else None |
@ -0,0 +1,48 @@ |
|||
def m001_initial(db): |
|||
""" |
|||
Initial wallet table. |
|||
""" |
|||
db.execute( |
|||
""" |
|||
CREATE TABLE IF NOT EXISTS wallets ( |
|||
id TEXT NOT NULL PRIMARY KEY, |
|||
user TEXT, |
|||
masterpub TEXT NOT NULL, |
|||
title TEXT NOT NULL, |
|||
address_no INTEGER NOT NULL DEFAULT 0, |
|||
amount INTEGER NOT NULL |
|||
); |
|||
""" |
|||
) |
|||
|
|||
db.execute( |
|||
""" |
|||
CREATE TABLE IF NOT EXISTS addresses ( |
|||
address TEXT NOT NULL PRIMARY KEY, |
|||
wallet TEXT NOT NULL, |
|||
amount INTEGER NOT NULL |
|||
); |
|||
""" |
|||
) |
|||
|
|||
db.execute( |
|||
""" |
|||
CREATE TABLE IF NOT EXISTS payments ( |
|||
id TEXT NOT NULL PRIMARY KEY, |
|||
user TEXT, |
|||
masterpub TEXT NOT NULL, |
|||
address TEXT NOT NULL, |
|||
time_to_pay INTEGER NOT NULL, |
|||
amount INTEGER NOT NULL, |
|||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) |
|||
); |
|||
""" |
|||
) |
|||
db.execute( |
|||
""" |
|||
CREATE TABLE IF NOT EXISTS mempool ( |
|||
user TEXT NOT NULL, |
|||
endpoint TEXT NOT NULL |
|||
); |
|||
""" |
|||
) |
@ -0,0 +1,44 @@ |
|||
from sqlite3 import Row |
|||
from typing import NamedTuple |
|||
|
|||
class Wallets(NamedTuple): |
|||
id: str |
|||
user: str |
|||
masterpub: str |
|||
title: str |
|||
address_no: int |
|||
amount: int |
|||
|
|||
@classmethod |
|||
def from_row(cls, row: Row) -> "Wallets": |
|||
return cls(**dict(row)) |
|||
|
|||
class Payments(NamedTuple): |
|||
id: str |
|||
user: str |
|||
ex_key: str |
|||
address: str |
|||
time_to_pay: str |
|||
amount: int |
|||
time: int |
|||
|
|||
@classmethod |
|||
def from_row(cls, row: Row) -> "Payments": |
|||
return cls(**dict(row)) |
|||
|
|||
class Addresses(NamedTuple): |
|||
address: str |
|||
wallet: str |
|||
amount: int |
|||
|
|||
@classmethod |
|||
def from_row(cls, row: Row) -> "Addresses": |
|||
return cls(**dict(row)) |
|||
|
|||
class Mempool(NamedTuple): |
|||
user: str |
|||
endpoint: str |
|||
|
|||
@classmethod |
|||
def from_row(cls, row: Row) -> "Mempool": |
|||
return cls(**dict(row)) |
@ -0,0 +1,141 @@ |
|||
<q-card> |
|||
<q-card-section> |
|||
<p>The WatchOnly extension uses https://mempool.block for blockchain data.<br /> |
|||
<small> |
|||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small |
|||
> |
|||
</p> |
|||
</q-card-section> |
|||
|
|||
|
|||
|
|||
<q-expansion-item |
|||
group="extras" |
|||
icon="swap_vertical_circle" |
|||
label="API info" |
|||
:content-inset-level="0.5" |
|||
> |
|||
<q-expansion-item group="api" dense expand-separator label="List pay links"> |
|||
<q-card> |
|||
<q-card-section> |
|||
<code><span class="text-blue">GET</span> /pay/api/v1/links</code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> |
|||
<code>{"X-Api-Key": <invoice_key>}</code><br /> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> |
|||
<h5 class="text-caption q-mt-sm q-mb-none"> |
|||
Returns 200 OK (application/json) |
|||
</h5> |
|||
<code>[<pay_link_object>, ...]</code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> |
|||
<code |
|||
>curl -X GET {{ request.url_root }}pay/api/v1/links -H "X-Api-Key: {{ |
|||
g.user.wallets[0].inkey }}" |
|||
</code> |
|||
</q-card-section> |
|||
</q-card> |
|||
</q-expansion-item> |
|||
<q-expansion-item group="api" dense expand-separator label="Get a pay link"> |
|||
<q-card> |
|||
<q-card-section> |
|||
<code |
|||
><span class="text-blue">GET</span> |
|||
/pay/api/v1/links/<pay_id></code |
|||
> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> |
|||
<code>{"X-Api-Key": <invoice_key>}</code><br /> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> |
|||
<h5 class="text-caption q-mt-sm q-mb-none"> |
|||
Returns 201 CREATED (application/json) |
|||
</h5> |
|||
<code>{"lnurl": <string>}</code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> |
|||
<code |
|||
>curl -X GET {{ request.url_root }}pay/api/v1/links/<pay_id> -H |
|||
"X-Api-Key: {{ g.user.wallets[0].inkey }}" |
|||
</code> |
|||
</q-card-section> |
|||
</q-card> |
|||
</q-expansion-item> |
|||
<q-expansion-item |
|||
group="api" |
|||
dense |
|||
expand-separator |
|||
label="Create a pay link" |
|||
> |
|||
<q-card> |
|||
<q-card-section> |
|||
<code><span class="text-green">POST</span> /pay/api/v1/links</code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> |
|||
<code>{"X-Api-Key": <admin_key>}</code><br /> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> |
|||
<code>{"description": <string> "amount": <integer>}</code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none"> |
|||
Returns 201 CREATED (application/json) |
|||
</h5> |
|||
<code>{"lnurl": <string>}</code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> |
|||
<code |
|||
>curl -X POST {{ request.url_root }}pay/api/v1/links -d |
|||
'{"description": <string>, "amount": <integer>}' -H |
|||
"Content-type: application/json" -H "X-Api-Key: {{ |
|||
g.user.wallets[0].adminkey }}" |
|||
</code> |
|||
</q-card-section> |
|||
</q-card> |
|||
</q-expansion-item> |
|||
<q-expansion-item |
|||
group="api" |
|||
dense |
|||
expand-separator |
|||
label="Update a pay link" |
|||
> |
|||
<q-card> |
|||
<q-card-section> |
|||
<code |
|||
><span class="text-green">PUT</span> |
|||
/pay/api/v1/links/<pay_id></code |
|||
> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> |
|||
<code>{"X-Api-Key": <admin_key>}</code><br /> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> |
|||
<code>{"description": <string>, "amount": <integer>}</code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none"> |
|||
Returns 200 OK (application/json) |
|||
</h5> |
|||
<code>{"lnurl": <string>}</code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> |
|||
<code |
|||
>curl -X PUT {{ request.url_root }}pay/api/v1/links/<pay_id> -d |
|||
'{"description": <string>, "amount": <integer>}' -H |
|||
"Content-type: application/json" -H "X-Api-Key: {{ |
|||
g.user.wallets[0].adminkey }}" |
|||
</code> |
|||
</q-card-section> |
|||
</q-card> |
|||
</q-expansion-item> |
|||
<q-expansion-item |
|||
group="api" |
|||
dense |
|||
expand-separator |
|||
label="Delete a pay link" |
|||
class="q-pb-md" |
|||
> |
|||
<q-card> |
|||
<q-card-section> |
|||
<code |
|||
><span class="text-pink">DELETE</span> |
|||
/pay/api/v1/links/<pay_id></code |
|||
> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> |
|||
<code>{"X-Api-Key": <admin_key>}</code><br /> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5> |
|||
<code></code> |
|||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> |
|||
<code |
|||
>curl -X DELETE {{ request.url_root }}pay/api/v1/links/<pay_id> |
|||
-H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" |
|||
</code> |
|||
</q-card-section> |
|||
</q-card> |
|||
</q-expansion-item> |
|||
</q-expansion-item> |
@ -0,0 +1,54 @@ |
|||
{% extends "public.html" %} {% block page %} |
|||
<div class="row q-col-gutter-md justify-center"> |
|||
<div class="col-12 col-sm-6 col-md-5 col-lg-4"> |
|||
<q-card class="q-pa-lg"> |
|||
<q-card-section class="q-pa-none"> |
|||
<div class="text-center"> |
|||
<a href="lightning:{{ link.lnurl }}"> |
|||
<q-responsive :ratio="1" class="q-mx-md"> |
|||
<qrcode |
|||
value="{{ link.lnurl }}" |
|||
:options="{width: 800}" |
|||
class="rounded-borders" |
|||
></qrcode> |
|||
</q-responsive> |
|||
</a> |
|||
</div> |
|||
<div class="row q-mt-lg"> |
|||
<q-btn outline color="grey" @click="copyText('{{ link.lnurl }}')" |
|||
>Copy LNURL</q-btn |
|||
> |
|||
</div> |
|||
</q-card-section> |
|||
</q-card> |
|||
</div> |
|||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md"> |
|||
<q-card> |
|||
<q-card-section> |
|||
<h6 class="text-subtitle1 q-mb-sm q-mt-none"> |
|||
LNbits LNURL-pay link |
|||
</h6> |
|||
<p class="q-my-none"> |
|||
Use a LNURL compatible bitcoin wallet to claim the sats. |
|||
</p> |
|||
</q-card-section> |
|||
<q-card-section class="q-pa-none"> |
|||
<q-separator></q-separator> |
|||
<q-list> |
|||
{% include "lnurlp/_lnurl.html" %} |
|||
</q-list> |
|||
</q-card-section> |
|||
</q-card> |
|||
</div> |
|||
</div> |
|||
{% endblock %} {% block scripts %} |
|||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> |
|||
<script> |
|||
Vue.component(VueQrcode.name, VueQrcode) |
|||
|
|||
new Vue({ |
|||
el: '#vue', |
|||
mixins: [windowMixin] |
|||
}) |
|||
</script> |
|||
{% endblock %} |
@ -0,0 +1,710 @@ |
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context |
|||
%} {% block page %} |
|||
<div class="row q-col-gutter-md"> |
|||
<div class="col-12 col-md-7 q-gutter-y-md"> |
|||
<q-card> |
|||
<q-card-section> |
|||
{% raw %} |
|||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true" |
|||
>New wallet</q-btn |
|||
> |
|||
<q-btn unelevated color="deep-purple" |
|||
icon="edit"> |
|||
<div class="cursor-pointer"> |
|||
<q-tooltip> |
|||
Point to another Mempool |
|||
</q-tooltip> |
|||
{{ this.mempool.endpoint }} |
|||
<q-popup-edit v-model="mempool.endpoint"> |
|||
<q-input color="accent" v-model="mempool.endpoint"> |
|||
</q-input> |
|||
<center><q-btn |
|||
flat |
|||
dense |
|||
@click="updateMempool()" |
|||
v-close-popup |
|||
>set</q-btn> |
|||
<q-btn |
|||
flat |
|||
dense |
|||
v-close-popup |
|||
>cancel</q-btn> |
|||
</center> |
|||
|
|||
</q-popup-edit> |
|||
</div> |
|||
|
|||
</q-btn |
|||
> |
|||
</q-card-section> |
|||
</q-card> |
|||
|
|||
<q-card> |
|||
<q-card-section> |
|||
<div class="row items-center no-wrap q-mb-md"> |
|||
<div class="col"> |
|||
<h5 class="text-subtitle1 q-my-none">Wallets</h5> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<q-input borderless dense debounce="300" v-model="filter" placeholder="Search"> |
|||
<template v-slot:append> |
|||
<q-icon name="search"></q-icon> |
|||
</template> |
|||
</q-input> |
|||
|
|||
</div> |
|||
</div> |
|||
<q-table |
|||
flat |
|||
dense |
|||
:data="walletLinks" |
|||
row-key="id" |
|||
:columns="WalletsTable.columns" |
|||
:pagination.sync="WalletsTable.pagination" |
|||
:filter="filter" |
|||
> |
|||
<template v-slot:header="props"> |
|||
<q-tr :props="props"> |
|||
<q-th auto-width></q-th> |
|||
|
|||
|
|||
|
|||
<q-th v-for="col in props.cols" :key="col.name" :props="props" auto-width> |
|||
<div v-if="col.name == 'id'"></div> |
|||
<div v-else> |
|||
{{ col.label }} |
|||
</div> |
|||
</q-th> |
|||
<q-th auto-width></q-th> |
|||
</q-tr> |
|||
</template> |
|||
<template v-slot:body="props"> |
|||
<q-tr :props="props"> |
|||
|
|||
<q-td auto-width> |
|||
<q-btn |
|||
unelevated |
|||
dense |
|||
size="xs" |
|||
icon="toll" |
|||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" |
|||
@click="formDialogPayLink.show = true" |
|||
> |
|||
|
|||
<q-tooltip> |
|||
Payment link |
|||
</q-tooltip> |
|||
</q-btn> |
|||
<q-btn |
|||
unelevated |
|||
dense |
|||
size="xs" |
|||
icon="dns" |
|||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" |
|||
@click="openQrCodeDialog(props.row.id)" |
|||
> |
|||
|
|||
<q-tooltip> |
|||
Adresses |
|||
</q-tooltip> |
|||
</q-btn> |
|||
<q-btn |
|||
flat |
|||
dense |
|||
size="xs" |
|||
@click="openUpdateDialog(props.row.id)" |
|||
icon="edit" |
|||
color="light-blue" |
|||
></q-btn> |
|||
<q-btn |
|||
flat |
|||
dense |
|||
size="xs" |
|||
@click="deleteWalletLink(props.row.id)" |
|||
icon="cancel" |
|||
color="pink" |
|||
></q-btn> |
|||
|
|||
|
|||
</q-td> |
|||
</q-td> |
|||
<q-td v-for="col in props.cols" :key="col.name" :props="props" auto-width> |
|||
<div v-if="col.name == 'id'"></div> |
|||
<div v-else> |
|||
{{ col.value }} |
|||
</div> |
|||
</q-td> |
|||
|
|||
|
|||
</q-tr> |
|||
</template> |
|||
</q-table> |
|||
</q-card-section> |
|||
</q-card> |
|||
|
|||
|
|||
|
|||
<q-card> |
|||
<q-card-section> |
|||
<div class="row items-center no-wrap q-mb-md"> |
|||
<div class="col"> |
|||
<h5 class="text-subtitle1 q-my-none">Paylinks</h5> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<q-input borderless dense debounce="300" v-model="filter" placeholder="Search"> |
|||
<template v-slot:append> |
|||
<q-icon name="search"></q-icon> |
|||
</template> |
|||
</q-input> |
|||
{% endraw %} |
|||
</div> |
|||
</div> |
|||
<q-table |
|||
flat |
|||
dense |
|||
:data="payLinks" |
|||
row-key="id" |
|||
:columns="PaylinksTable.columns" |
|||
:pagination.sync="PaylinksTable.pagination" |
|||
:filter="filter" |
|||
> |
|||
|
|||
{% raw %} |
|||
<template v-slot:header="props"> |
|||
<q-tr :props="props"> |
|||
<q-th auto-width></q-th> |
|||
|
|||
|
|||
|
|||
<q-th v-for="col in props.cols" :key="col.name" :props="props" auto-width> |
|||
<div v-if="col.name == 'id'"></div> |
|||
<div v-else> |
|||
{{ col.label }} |
|||
</div> |
|||
</q-th> |
|||
<q-th auto-width></q-th> |
|||
</q-tr> |
|||
</template> |
|||
<template v-slot:body="props"> |
|||
<q-tr :props="props"> |
|||
|
|||
<q-td auto-width> |
|||
<q-btn |
|||
flat |
|||
dense |
|||
size="xs" |
|||
@click="openUpdateDialog(props.row.id)" |
|||
icon="edit" |
|||
color="light-blue" |
|||
></q-btn> |
|||
<q-btn |
|||
flat |
|||
dense |
|||
size="xs" |
|||
@click="deleteWalletLink(props.row.id)" |
|||
icon="cancel" |
|||
color="pink" |
|||
></q-btn> |
|||
|
|||
|
|||
</q-td> |
|||
</q-td> |
|||
<q-td v-for="col in props.cols" :key="col.name" :props="props" auto-width> |
|||
<div v-if="col.name == 'id'"></div> |
|||
<div v-else> |
|||
{{ col.value }} |
|||
</div> |
|||
</q-td> |
|||
|
|||
|
|||
</q-tr> |
|||
</template> |
|||
{% endraw %} |
|||
|
|||
|
|||
|
|||
|
|||
</q-table> |
|||
</q-card-section> |
|||
</q-card> |
|||
|
|||
|
|||
|
|||
|
|||
</div> |
|||
|
|||
|
|||
|
|||
<div class="col-12 col-md-5 q-gutter-y-md"> |
|||
<q-card> |
|||
<q-card-section> |
|||
<h6 class="text-subtitle1 q-my-none"> |
|||
LNbits WatchOnly Extension |
|||
</h6> |
|||
</q-card-section> |
|||
<q-card-section class="q-pa-none"> |
|||
<q-separator></q-separator> |
|||
<q-list> |
|||
{% include "watchonly/_api_docs.html" %} |
|||
</q-list> |
|||
</q-card-section> |
|||
</q-card> |
|||
</div> |
|||
|
|||
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog"> |
|||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> |
|||
<q-form @submit="sendFormData" class="q-gutter-md"> |
|||
<q-input |
|||
filled |
|||
dense |
|||
v-model.trim="formDialog.data.title" |
|||
type="text" |
|||
label="Title" |
|||
></q-input> |
|||
<q-input |
|||
filled |
|||
type="textarea" |
|||
v-model="formDialog.data.masterpub" |
|||
height="50px" |
|||
autogrow |
|||
label="Master Public Key, either xpub, ypub, zpub" |
|||
></q-input> |
|||
|
|||
|
|||
<div class="row q-mt-lg"> |
|||
<q-btn |
|||
v-if="formDialog.data.id" |
|||
unelevated |
|||
color="deep-purple" |
|||
type="submit" |
|||
>Update Watch-only Wallet</q-btn |
|||
> |
|||
<q-btn |
|||
v-else |
|||
unelevated |
|||
color="deep-purple" |
|||
:disable=" |
|||
formDialog.data.masterpub == null || |
|||
formDialog.data.title == null" |
|||
type="submit" |
|||
>Create Watch-only Wallet</q-btn |
|||
> |
|||
<q-btn v-close-popup flat color="grey" class="q-ml-auto" |
|||
>Cancel</q-btn |
|||
> |
|||
</div> |
|||
</q-form> |
|||
</q-card> |
|||
</q-dialog> |
|||
|
|||
|
|||
<q-dialog v-model="formDialogPayLink.show" position="top" @hide="closeFormDialog"> |
|||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> |
|||
<q-form @submit="sendFormData" class="q-gutter-md"> |
|||
|
|||
<q-input |
|||
filled |
|||
dense |
|||
v-model.trim="formDialogPayLink.data.title" |
|||
type="text" |
|||
label="Title" |
|||
></q-input> |
|||
|
|||
<q-input |
|||
filled |
|||
dense |
|||
v-model.trim="formDialogPayLink.data.amount" |
|||
type="number" |
|||
label="Amount (sats)" |
|||
></q-input> |
|||
|
|||
<q-input |
|||
filled |
|||
dense |
|||
v-model.trim="formDialogPayLink.data.time" |
|||
type="number" |
|||
label="Time (mins)" |
|||
> </q-input> |
|||
|
|||
<div class="row q-mt-lg"> |
|||
<q-btn |
|||
v-if="formDialogPayLink.data.id" |
|||
unelevated |
|||
color="deep-purple" |
|||
type="submit" |
|||
>Update Paylink</q-btn |
|||
> |
|||
<q-btn |
|||
v-else |
|||
unelevated |
|||
color="deep-purple" |
|||
:disable=" |
|||
formDialogPayLink.data.time == null || |
|||
formDialogPayLink.data.amount == null" |
|||
type="submit" |
|||
>Create Paylink</q-btn |
|||
> |
|||
<q-btn v-close-popup flat color="grey" class="q-ml-auto" |
|||
>Cancel</q-btn |
|||
> |
|||
</div> |
|||
</q-form> |
|||
</q-card> |
|||
</q-dialog> |
|||
|
|||
|
|||
<q-dialog v-model="Addresses.show" position="top"> |
|||
<q-card v-if="Addresses.data" class="q-pa-lg lnbits__dialog-card"> |
|||
{% raw %} |
|||
<h5 class="text-subtitle1 q-my-none">Addresses</h5> |
|||
<q-separator></q-separator><br/> |
|||
<p><strong>Current:</strong> |
|||
{{ Addresses.data[0].address }} |
|||
<q-btn |
|||
flat |
|||
dense |
|||
size="ms" |
|||
icon="visibility" |
|||
type="a" |
|||
:href="mempool.endpoint + '/address/' + Addresses.data[0].address" |
|||
target="_blank" |
|||
></q-btn> |
|||
|
|||
</p> |
|||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md"> |
|||
<qrcode |
|||
:value="Addresses.data[0].address" |
|||
:options="{width: 800}" |
|||
class="rounded-borders" |
|||
></qrcode> |
|||
</q-responsive> |
|||
<p style="word-break: break-all;"> |
|||
<br /><br /> |
|||
Table of addresses and amount will go here... |
|||
|
|||
</p> |
|||
{% endraw %} |
|||
<div class="row q-mt-lg q-gutter-sm"> |
|||
<q-btn |
|||
outline |
|||
color="grey" |
|||
@click="copyText(Addresses.show, 'LNURL copied to clipboard!')" |
|||
class="q-ml-sm" |
|||
>Get fresh address</q-btn |
|||
> |
|||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> |
|||
</div> |
|||
</q-card> |
|||
</q-dialog> |
|||
|
|||
|
|||
</div> |
|||
{% endblock %} {% block scripts %} {{ window_vars(user) }} |
|||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> |
|||
<style> |
|||
|
|||
</style> |
|||
<script> |
|||
Vue.component(VueQrcode.name, VueQrcode) |
|||
|
|||
var locationPath = [ |
|||
window.location.protocol, |
|||
'//', |
|||
window.location.hostname, |
|||
window.location.pathname |
|||
].join('') |
|||
|
|||
var mapWalletLink = function (obj) { |
|||
obj._data = _.clone(obj) |
|||
obj.date = Quasar.utils.date.formatDate( |
|||
new Date(obj.time * 1000), |
|||
'YYYY-MM-DD HH:mm' |
|||
) |
|||
return obj |
|||
} |
|||
|
|||
new Vue({ |
|||
el: '#vue', |
|||
mixins: [windowMixin], |
|||
data: function () { |
|||
return { |
|||
filter: '', |
|||
checker: null, |
|||
walletLinks: [], |
|||
Addresses: { |
|||
show: false, |
|||
data: null |
|||
}, |
|||
mempool:{ |
|||
endpoint:"" |
|||
}, |
|||
WalletsTable: { |
|||
columns: [ |
|||
{name: 'id', align: 'left', label: 'ID', field: 'id'}, |
|||
{ |
|||
name: 'title', |
|||
align: 'left', |
|||
label: 'Title', |
|||
field: 'title' |
|||
}, |
|||
{ |
|||
name: 'amount', |
|||
align: 'left', |
|||
label: 'Amount', |
|||
field: 'amount' |
|||
}, |
|||
{ |
|||
name: 'masterpub', |
|||
align: 'left', |
|||
label: 'MasterPub', |
|||
field: 'masterpub' |
|||
}, |
|||
], |
|||
pagination: { |
|||
rowsPerPage: 10 |
|||
} |
|||
}, |
|||
PaylinksTable: { |
|||
columns: [ |
|||
{name: 'id', align: 'left', label: 'ID', field: 'id'}, |
|||
{ |
|||
name: 'title', |
|||
align: 'left', |
|||
label: 'Title', |
|||
field: 'title' |
|||
}, |
|||
{ |
|||
name: 'amount', |
|||
align: 'left', |
|||
label: 'Amount', |
|||
field: 'amount' |
|||
}, |
|||
{ |
|||
name: 'time', |
|||
align: 'left', |
|||
label: 'time', |
|||
field: 'time' |
|||
}, |
|||
], |
|||
pagination: { |
|||
rowsPerPage: 10 |
|||
} |
|||
}, |
|||
AddressTable: { |
|||
columns: [ |
|||
{ |
|||
name: 'address', |
|||
align: 'left', |
|||
label: 'Address', |
|||
field: 'address' |
|||
}, |
|||
{ |
|||
name: 'amount', |
|||
align: 'left', |
|||
label: 'Amount', |
|||
field: 'amount' |
|||
}, |
|||
], |
|||
pagination: { |
|||
rowsPerPage: 10 |
|||
} |
|||
}, |
|||
formDialog: { |
|||
show: false, |
|||
data: {} |
|||
}, |
|||
formDialogPayLink: { |
|||
show: false, |
|||
data: {} |
|||
}, |
|||
qrCodeDialog: { |
|||
show: false, |
|||
data: null |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
|
|||
getAddresses: function (walletID) { |
|||
var self = this |
|||
|
|||
LNbits.api |
|||
.request( |
|||
'GET', |
|||
'/watchonly/api/v1/addresses/' + walletID, |
|||
this.g.user.wallets[0].inkey |
|||
) |
|||
.then(function (response) { |
|||
|
|||
self.walletLinks = response.data.map(function (obj) { |
|||
self.Addresses.data = response.data |
|||
}) |
|||
}) |
|||
.catch(function (error) { |
|||
LNbits.utils.notifyApiError(error) |
|||
}) |
|||
}, |
|||
addressRedirect: function (address){ |
|||
window.location.href = this.mempool.endpoint + "/address/" + address; |
|||
}, |
|||
getMempool: function () { |
|||
var self = this |
|||
|
|||
LNbits.api |
|||
.request( |
|||
'GET', |
|||
'/watchonly/api/v1/mempool', |
|||
this.g.user.wallets[0].inkey |
|||
) |
|||
.then(function (response) { |
|||
console.log(response.data.endpoint) |
|||
self.mempool.endpoint = response.data.endpoint |
|||
console.log(this.mempool.endpoint) |
|||
}) |
|||
.catch(function (error) { |
|||
LNbits.utils.notifyApiError(error) |
|||
}) |
|||
}, |
|||
|
|||
updateMempool: function () { |
|||
var self = this |
|||
var wallet = this.g.user.wallets[0] |
|||
LNbits.api |
|||
.request( |
|||
'PUT', |
|||
'/watchonly/api/v1/mempool', |
|||
wallet.inkey, self.mempool) |
|||
.then(function (response) { |
|||
self.mempool.endpoint = response.data.endpoint |
|||
self.walletLinks.push(mapwalletLink(response.data)) |
|||
}) |
|||
.catch(function (error) { |
|||
LNbits.utils.notifyApiError(error) |
|||
}) |
|||
}, |
|||
|
|||
|
|||
getWalletLinks: function () { |
|||
var self = this |
|||
|
|||
LNbits.api |
|||
.request( |
|||
'GET', |
|||
'/watchonly/api/v1/wallet', |
|||
this.g.user.wallets[0].inkey |
|||
) |
|||
.then(function (response) { |
|||
self.walletLinks = response.data.map(function (obj) { |
|||
return mapWalletLink(obj) |
|||
}) |
|||
}) |
|||
.catch(function (error) { |
|||
LNbits.utils.notifyApiError(error) |
|||
}) |
|||
}, |
|||
closeFormDialog: function () { |
|||
this.formDialog.data = { |
|||
is_unique: false |
|||
} |
|||
}, |
|||
openQrCodeDialog: function (linkId) { |
|||
var getAddresses = this.getAddresses |
|||
getAddresses(linkId) |
|||
this.Addresses.show = true |
|||
}, |
|||
openUpdateDialog: function (linkId) { |
|||
var link = _.findWhere(this.walletLinks, {id: linkId}) |
|||
this.formDialog.data = _.clone(link._data) |
|||
this.formDialog.show = true |
|||
}, |
|||
sendFormData: function () { |
|||
var wallet = this.g.user.wallets[0] |
|||
var data = _.omit(this.formDialog.data, 'wallet') |
|||
|
|||
data.wait_time = |
|||
data.wait_time * |
|||
{ |
|||
seconds: 1, |
|||
minutes: 60, |
|||
hours: 3600 |
|||
}[this.formDialog.secondMultiplier] |
|||
|
|||
if (data.id) { |
|||
this.updateWalletLink(wallet, data) |
|||
} else { |
|||
this.createWalletLink(wallet, data) |
|||
} |
|||
}, |
|||
updateWalletLink: function (wallet, data) { |
|||
var self = this |
|||
|
|||
LNbits.api |
|||
.request( |
|||
'PUT', |
|||
'/watchonly/api/v1/wallet/' + data.id, |
|||
wallet.inkey, data) |
|||
.then(function (response) { |
|||
self.walletLinks = _.reject(self.walletLinks, function (obj) { |
|||
return obj.id === data.id |
|||
}) |
|||
self.walletLinks.push(mapWalletLink(response.data)) |
|||
self.formDialog.show = false |
|||
}) |
|||
.catch(function (error) { |
|||
LNbits.utils.notifyApiError(error) |
|||
}) |
|||
}, |
|||
createWalletLink: function (wallet, data) { |
|||
var self = this |
|||
|
|||
LNbits.api |
|||
.request('POST', '/watchonly/api/v1/wallet', wallet.inkey, data) |
|||
.then(function (response) { |
|||
self.walletLinks.push(mapWalletLink(response.data)) |
|||
self.formDialog.show = false |
|||
console.log(response.data[1][1]) |
|||
}) |
|||
.catch(function (error) { |
|||
LNbits.utils.notifyApiError(error) |
|||
}) |
|||
}, |
|||
deleteWalletLink: function (linkId) { |
|||
var self = this |
|||
var link = _.findWhere(this.walletLinks, {id: linkId}) |
|||
console.log(self.g.user.wallets[0].adminkey) |
|||
LNbits.utils |
|||
.confirmDialog('Are you sure you want to delete this pay link?') |
|||
.onOk(function () { |
|||
LNbits.api |
|||
.request( |
|||
'DELETE', |
|||
'/watchonly/api/v1/wallet/' + linkId, |
|||
self.g.user.wallets[0].inkey |
|||
) |
|||
.then(function (response) { |
|||
self.walletLinks = _.reject(self.walletLinks, function (obj) { |
|||
return obj.id === linkId |
|||
})}) |
|||
.catch(function (error) { |
|||
LNbits.utils.notifyApiError(error) |
|||
}) |
|||
}) |
|||
}, |
|||
exportCSV: function () { |
|||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls) |
|||
}, |
|||
|
|||
}, |
|||
created: function () { |
|||
if (this.g.user.wallets.length) { |
|||
var getWalletLinks = this.getWalletLinks |
|||
getWalletLinks() |
|||
var getMempool = this.getMempool |
|||
getMempool() |
|||
|
|||
} |
|||
|
|||
} |
|||
}) |
|||
</script> |
|||
{% endblock %} |
@ -0,0 +1,21 @@ |
|||
from quart import g, abort, render_template |
|||
from http import HTTPStatus |
|||
|
|||
from lnbits.decorators import check_user_exists, validate_uuids |
|||
|
|||
from lnbits.extensions.watchonly import watchonly_ext |
|||
from .crud import get_payment |
|||
|
|||
|
|||
@watchonly_ext.route("/") |
|||
@validate_uuids(["usr"], required=True) |
|||
@check_user_exists() |
|||
async def index(): |
|||
return await render_template("watchonly/index.html", user=g.user) |
|||
|
|||
|
|||
@watchonly_ext.route("/<payment_id>") |
|||
async def display(payment_id): |
|||
link = get_payment(payment_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.") |
|||
|
|||
return await render_template("watchonly/display.html", link=link) |
@ -0,0 +1,194 @@ |
|||
import hashlib |
|||
from quart import g, jsonify, request, url_for |
|||
from http import HTTPStatus |
|||
|
|||
from lnbits.core.crud import get_user |
|||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request |
|||
|
|||
from lnbits.extensions.watchonly import watchonly_ext |
|||
from .crud import ( |
|||
create_watch_wallet, |
|||
get_watch_wallet, |
|||
get_watch_wallets, |
|||
update_watch_wallet, |
|||
delete_watch_wallet, |
|||
create_payment, |
|||
get_payment, |
|||
get_payments, |
|||
delete_payment, |
|||
create_mempool, |
|||
update_mempool, |
|||
get_mempool, |
|||
get_addresses, |
|||
get_fresh_address, |
|||
get_address |
|||
) |
|||
|
|||
###################WALLETS############################# |
|||
|
|||
@watchonly_ext.route("/api/v1/wallet", methods=["GET"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_wallets_retrieve(): |
|||
|
|||
try: |
|||
return ( |
|||
jsonify([wallet._asdict() for wallet in get_watch_wallets(g.wallet.user)]), HTTPStatus.OK |
|||
) |
|||
except: |
|||
return ( |
|||
jsonify({"message": "Cant fetch."}), |
|||
HTTPStatus.UPGRADE_REQUIRED, |
|||
) |
|||
|
|||
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["GET"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_wallet_retrieve(wallet_id): |
|||
wallet = get_watch_wallet(wallet_id) |
|||
|
|||
if not wallet: |
|||
return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND |
|||
|
|||
return jsonify({wallet}), HTTPStatus.OK |
|||
|
|||
|
|||
@watchonly_ext.route("/api/v1/wallet", methods=["POST"]) |
|||
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["PUT"]) |
|||
@api_check_wallet_key("invoice") |
|||
@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): |
|||
print("g.data") |
|||
if not wallet_id: |
|||
wallet = create_watch_wallet(user=g.wallet.user, masterpub=g.data["masterpub"], title=g.data["title"]) |
|||
mempool = get_mempool(g.wallet.user) |
|||
if not mempool: |
|||
create_mempool(user=g.wallet.user) |
|||
return jsonify(wallet._asdict()), HTTPStatus.CREATED |
|||
|
|||
else: |
|||
wallet = update_watch_wallet(wallet_id=wallet_id, **g.data) |
|||
return jsonify(wallet._asdict()), HTTPStatus.OK |
|||
|
|||
|
|||
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["DELETE"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_wallet_delete(wallet_id): |
|||
wallet = get_watch_wallet(wallet_id) |
|||
|
|||
if not wallet: |
|||
return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND |
|||
|
|||
delete_watch_wallet(wallet_id) |
|||
|
|||
return jsonify({"deleted": "true"}), HTTPStatus.NO_CONTENT |
|||
|
|||
|
|||
#############################ADDRESSES########################## |
|||
|
|||
@watchonly_ext.route("/api/v1/address/<wallet_id>", methods=["GET"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_fresh_address(wallet_id): |
|||
address = get_fresh_address(wallet_id) |
|||
|
|||
if not address: |
|||
return jsonify({"message": "something went wrong"}), HTTPStatus.NOT_FOUND |
|||
|
|||
return jsonify({address}), HTTPStatus.OK |
|||
|
|||
|
|||
@watchonly_ext.route("/api/v1/addresses/<wallet_id>", methods=["GET"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_get_addresses(wallet_id): |
|||
addresses = get_addresses(wallet_id) |
|||
print(addresses) |
|||
if not addresses: |
|||
return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND |
|||
|
|||
return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK |
|||
|
|||
#############################PAYEMENTS########################## |
|||
|
|||
@watchonly_ext.route("/api/v1/payment", methods=["GET"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_payments_retrieve(): |
|||
|
|||
try: |
|||
return ( |
|||
jsonify(get_payments(g.wallet.user)), |
|||
HTTPStatus.OK, |
|||
) |
|||
except: |
|||
return ( |
|||
jsonify({"message": "Cant fetch."}), |
|||
HTTPStatus.UPGRADE_REQUIRED, |
|||
) |
|||
|
|||
@watchonly_ext.route("/api/v1/payment/<payment_id>", methods=["GET"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_payment_retrieve(payment_id): |
|||
payment = get_payment(payment_id) |
|||
|
|||
if not payment: |
|||
return jsonify({"message": "payment does not exist"}), HTTPStatus.NOT_FOUND |
|||
|
|||
return jsonify({payment}), HTTPStatus.OK |
|||
|
|||
|
|||
@watchonly_ext.route("/api/v1/payment", methods=["POST"]) |
|||
@watchonly_ext.route("/api/v1/payment/<payment_id>", methods=["PUT"]) |
|||
@api_check_wallet_key("invoice") |
|||
@api_validate_post_request( |
|||
schema={ |
|||
"ex_key": {"type": "string", "empty": False, "required": True}, |
|||
"pub_key": {"type": "string", "empty": False, "required": True}, |
|||
"time_to_pay": {"type": "integer", "min": 1, "required": True}, |
|||
"amount": {"type": "integer", "min": 1, "required": True}, |
|||
} |
|||
) |
|||
async def api_payment_create_or_update(payment_id=None): |
|||
|
|||
if not payment_id: |
|||
payment = create_payment(g.wallet.user, g.data.ex_key, g.data.pub_key, g.data.amount) |
|||
return jsonify(get_payment(payment)), HTTPStatus.CREATED |
|||
|
|||
else: |
|||
payment = update_payment(payment_id, g.data) |
|||
return jsonify({payment}), HTTPStatus.OK |
|||
|
|||
|
|||
@watchonly_ext.route("/api/v1/payment/<payment_id>", methods=["DELETE"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_payment_delete(payment_id): |
|||
payment = get_watch_wallet(payment_id) |
|||
|
|||
if not payment: |
|||
return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND |
|||
|
|||
delete_watch_wallet(payment_id) |
|||
|
|||
return "", HTTPStatus.NO_CONTENT |
|||
|
|||
#############################MEMPOOL########################## |
|||
|
|||
@watchonly_ext.route("/api/v1/mempool", methods=["PUT"]) |
|||
@api_check_wallet_key("invoice") |
|||
@api_validate_post_request( |
|||
schema={ |
|||
"endpoint": {"type": "string", "empty": False, "required": True}, |
|||
} |
|||
) |
|||
async def api_update_mempool(): |
|||
mempool = update_mempool(user=g.wallet.user, **g.data) |
|||
return jsonify(mempool._asdict()), HTTPStatus.OK |
|||
|
|||
@watchonly_ext.route("/api/v1/mempool", methods=["GET"]) |
|||
@api_check_wallet_key("invoice") |
|||
async def api_get_mempool(): |
|||
mempool = get_mempool(g.wallet.user) |
|||
if not mempool: |
|||
mempool = create_mempool(user=g.wallet.user) |
|||
return jsonify(mempool._asdict()), HTTPStatus.OK |
Loading…
Reference in new issue