Merge pull request #43 from fiatjaf/lnurlpayserver
description_hash support, spark backend and lnurlp extension.
This commit is contained in:
commit
479760c5a5
|
@ -21,6 +21,11 @@ Using this wallet requires the installation of the `pylightning` Python package.
|
|||
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
|
||||
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
|
||||
|
||||
### Spark (c-lightning)
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **SparkWallet**
|
||||
- `SPARK_URL`: http://10.147.17.230:9737/rpc
|
||||
- `SPARK_TOKEN`: secret_access_key
|
||||
|
||||
### LND (gRPC)
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import bitstring
|
||||
import re
|
||||
from binascii import hexlify
|
||||
from bech32 import bech32_decode, CHARSET
|
||||
|
||||
|
||||
|
@ -51,9 +50,9 @@ def decode(pr: str) -> Invoice:
|
|||
if tag == "d":
|
||||
invoice.description = trim_to_bytes(tagdata).decode("utf-8")
|
||||
elif tag == "h" and data_length == 52:
|
||||
invoice.description = hexlify(trim_to_bytes(tagdata)).decode("ascii")
|
||||
invoice.description = trim_to_bytes(tagdata).hex()
|
||||
elif tag == "p" and data_length == 52:
|
||||
invoice.payment_hash = hexlify(trim_to_bytes(tagdata)).decode("ascii")
|
||||
invoice.payment_hash = trim_to_bytes(tagdata).hex()
|
||||
|
||||
return invoice
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ def get_wallet(wallet_id: str) -> Optional[Wallet]:
|
|||
def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
|
||||
with open_db() as db:
|
||||
row = db.fetchone(
|
||||
f"""
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE adminkey = ? OR inkey = ?
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
from typing import Optional, Tuple
|
||||
|
||||
from lnbits.bolt11 import decode as bolt11_decode # type: ignore
|
||||
from lnbits.bolt11 import decode as bolt11_decode # type: ignore
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from .crud import get_wallet, create_payment, delete_payment
|
||||
|
||||
|
||||
def create_invoice(*, wallet_id: str, amount: int, memo: str) -> Tuple[str, str]:
|
||||
def create_invoice(*, wallet_id: str, amount: int, memo: str, description_hash: bytes) -> Tuple[str, str]:
|
||||
|
||||
try:
|
||||
ok, checking_id, payment_request, error_message = WALLET.create_invoice(amount=amount, memo=memo)
|
||||
ok, checking_id, payment_request, error_message = WALLET.create_invoice(
|
||||
amount=amount, memo=memo, description_hash=description_hash
|
||||
)
|
||||
except Exception as e:
|
||||
ok, error_message = False, str(e)
|
||||
|
||||
|
@ -35,11 +38,7 @@ def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) -
|
|||
|
||||
fee_reserve = max(1000, int(invoice.amount_msat * 0.01))
|
||||
create_payment(
|
||||
wallet_id=wallet_id,
|
||||
checking_id=temp_id,
|
||||
amount=-invoice.amount_msat,
|
||||
fee=-fee_reserve,
|
||||
memo=temp_id,
|
||||
wallet_id=wallet_id, checking_id=temp_id, amount=-invoice.amount_msat, fee=-fee_reserve, memo=temp_id,
|
||||
)
|
||||
|
||||
wallet = get_wallet(wallet_id)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
from binascii import unhexlify
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
@ -27,13 +28,21 @@ def api_payments():
|
|||
@api_validate_post_request(
|
||||
schema={
|
||||
"amount": {"type": "integer", "min": 1, "required": True},
|
||||
"memo": {"type": "string", "empty": False, "required": True},
|
||||
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
|
||||
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
||||
}
|
||||
)
|
||||
def api_payments_create_invoice():
|
||||
if "description_hash" in g.data:
|
||||
description_hash = unhexlify(g.data["description_hash"])
|
||||
memo = ""
|
||||
else:
|
||||
description_hash = b""
|
||||
memo = g.data["memo"]
|
||||
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=g.wallet.id, amount=g.data["amount"], memo=g.data["memo"]
|
||||
wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
|
|
@ -36,7 +36,7 @@ def api_validate_post_request(*, schema: dict):
|
|||
return jsonify({"message": "Content-Type must be `application/json`."}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
v = Validator(schema)
|
||||
g.data = {key: (request.json[key] if key in request.json else None) for key in schema.keys()}
|
||||
g.data = {key: request.json[key] for key in schema.keys() if key in request.json}
|
||||
|
||||
if not v.validate(g.data):
|
||||
return jsonify({"message": f"Errors in request data: {v.errors}"}), HTTPStatus.BAD_REQUEST
|
||||
|
@ -56,7 +56,7 @@ def check_user_exists(param: str = "usr"):
|
|||
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
|
||||
|
||||
if allowed_users != "all" and g.user.id not in allowed_users.split(","):
|
||||
abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.")
|
||||
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
|
|
1
lnbits/extensions/lnurlp/README.md
Normal file
1
lnbits/extensions/lnurlp/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# LNURLp
|
8
lnbits/extensions/lnurlp/__init__.py
Normal file
8
lnbits/extensions/lnurlp/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from flask import Blueprint
|
||||
|
||||
|
||||
lnurlp_ext: Blueprint = Blueprint("lnurlp", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
10
lnbits/extensions/lnurlp/config.json
Normal file
10
lnbits/extensions/lnurlp/config.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "LNURLp",
|
||||
"short_description": "Make reusable LNURL pay links",
|
||||
"icon": "receipt",
|
||||
"contributors": [
|
||||
"arcbtc",
|
||||
"eillarra",
|
||||
"fiatjaf"
|
||||
]
|
||||
}
|
74
lnbits/extensions/lnurlp/crud.py
Normal file
74
lnbits/extensions/lnurlp/crud.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
from .models import PayLink
|
||||
|
||||
|
||||
def create_pay_link(*, wallet_id: str, description: str, amount: int) -> PayLink:
|
||||
with open_ext_db("lnurlp") as db:
|
||||
with db.cursor() as c:
|
||||
c.execute(
|
||||
"""
|
||||
INSERT INTO pay_links (
|
||||
wallet,
|
||||
description,
|
||||
amount,
|
||||
served_meta,
|
||||
served_pr
|
||||
)
|
||||
VALUES (?, ?, ?, 0, 0)
|
||||
""",
|
||||
(wallet_id, description, amount),
|
||||
)
|
||||
return get_pay_link(c.lastrowid)
|
||||
|
||||
|
||||
def get_pay_link(link_id: str) -> Optional[PayLink]:
|
||||
with open_ext_db("lnurlp") as db:
|
||||
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
|
||||
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
def get_pay_link_by_hash(unique_hash: str) -> Optional[PayLink]:
|
||||
with open_ext_db("lnurlp") as db:
|
||||
row = db.fetchone("SELECT * FROM pay_links WHERE unique_hash = ?", (unique_hash,))
|
||||
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("lnurlp") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM pay_links WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [PayLink.from_row(row) for row in rows]
|
||||
|
||||
|
||||
def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
with open_ext_db("lnurlp") as db:
|
||||
db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
|
||||
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
|
||||
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
def increment_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
|
||||
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
|
||||
|
||||
with open_ext_db("lnurlp") as db:
|
||||
db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
|
||||
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
|
||||
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
def delete_pay_link(link_id: str) -> None:
|
||||
with open_ext_db("lnurlp") as db:
|
||||
db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))
|
24
lnbits/extensions/lnurlp/migrations.py
Normal file
24
lnbits/extensions/lnurlp/migrations.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
def m001_initial(db):
|
||||
"""
|
||||
Initial pay table.
|
||||
"""
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pay_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
wallet TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
served_meta INTEGER NOT NULL,
|
||||
served_pr INTEGER NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("lnurlp") as db:
|
||||
m001_initial(db)
|
32
lnbits/extensions/lnurlp/models.py
Normal file
32
lnbits/extensions/lnurlp/models.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
import json
|
||||
from flask import url_for
|
||||
from lnurl import Lnurl, encode as lnurl_encode
|
||||
from lnurl.types import LnurlPayMetadata
|
||||
from sqlite3 import Row
|
||||
from typing import NamedTuple
|
||||
|
||||
from lnbits.settings import FORCE_HTTPS
|
||||
|
||||
|
||||
class PayLink(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
description: str
|
||||
amount: int
|
||||
served_meta: int
|
||||
served_pr: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "PayLink":
|
||||
data = dict(row)
|
||||
return cls(**data)
|
||||
|
||||
@property
|
||||
def lnurl(self) -> Lnurl:
|
||||
scheme = "https" if FORCE_HTTPS else None
|
||||
url = url_for("lnurlp.api_lnurl_response", link_id=self.id, _external=True, _scheme=scheme)
|
||||
return lnurl_encode(url)
|
||||
|
||||
@property
|
||||
def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
return LnurlPayMetadata(json.dumps([["text/plain", self.description]]))
|
130
lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html
Normal file
130
lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html
Normal file
|
@ -0,0 +1,130 @@
|
|||
<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>
|
28
lnbits/extensions/lnurlp/templates/lnurlp/_lnurl.html
Normal file
28
lnbits/extensions/lnurlp/templates/lnurlp/_lnurl.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<q-expansion-item group="extras" icon="info" label="Powered by LNURL">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>
|
||||
<b>WARNING: LNURL must be used over https or TOR</b><br />
|
||||
LNURL is a range of lightning-network standards that allow us to use
|
||||
lightning-network differently. An LNURL-pay is a link that wallets use
|
||||
to fetch an invoice from a server on-demand. The link or QR code is
|
||||
fixed, but each time it is read by a compatible wallet a new QR code is
|
||||
issued by the service. It can be used to activate machines without them
|
||||
having to maintain an electronic screen to generate and show invoices
|
||||
locally, or to sell any predefined good or service automatically.
|
||||
</p>
|
||||
<p>
|
||||
Exploring LNURL and finding use cases, is really helping inform
|
||||
lightning protocol development, rather than the protocol dictating how
|
||||
lightning-network should be engaged with.
|
||||
</p>
|
||||
<small
|
||||
>Check
|
||||
<a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank"
|
||||
>Awesome LNURL</a
|
||||
>
|
||||
for further information.</small
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
54
lnbits/extensions/lnurlp/templates/lnurlp/display.html
Normal file
54
lnbits/extensions/lnurlp/templates/lnurlp/display.html
Normal file
|
@ -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 %}
|
401
lnbits/extensions/lnurlp/templates/lnurlp/index.html
Normal file
401
lnbits/extensions/lnurlp/templates/lnurlp/index.html
Normal file
|
@ -0,0 +1,401 @@
|
|||
{% 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>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
>New pay link</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">Pay links</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="payLinks"
|
||||
row-key="id"
|
||||
:columns="payLinksTable.columns"
|
||||
:pagination.sync="payLinksTable.pagination"
|
||||
>
|
||||
{% 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">
|
||||
{{ col.label }}
|
||||
</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="launch"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.pay_url"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="visibility"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.id)"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<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="deletePayLink(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</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 LNURL-pay extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "lnurlp/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
{% include "lnurlp/_lnurl.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-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.description"
|
||||
type="text"
|
||||
label="Item description *"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.amount"
|
||||
type="number"
|
||||
label="Amount (sat) *"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
>Update pay link</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="
|
||||
formDialog.data.wallet == null ||
|
||||
formDialog.data.description == null ||
|
||||
(
|
||||
formDialog.data.amount == null ||
|
||||
formDialog.data.amount < 1
|
||||
)"
|
||||
type="submit"
|
||||
>Create pay link</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="qrCodeDialog.show" position="top">
|
||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
{% raw %}
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="qrCodeDialog.data.lnurl"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<p style="word-break: break-all;">
|
||||
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
||||
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }} sat<br />
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')"
|
||||
class="q-ml-sm"
|
||||
>Copy LNURL</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
|
||||
>Shareable link</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-if="!qrCodeDialog.data.is_unique"
|
||||
outline
|
||||
color="grey"
|
||||
icon="print"
|
||||
type="a"
|
||||
:href="qrCodeDialog.data.print_url"
|
||||
target="_blank"
|
||||
></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>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
var locationPath = [
|
||||
window.location.protocol,
|
||||
'//',
|
||||
window.location.hostname,
|
||||
window.location.pathname
|
||||
].join('')
|
||||
|
||||
var mapPayLink = function (obj) {
|
||||
obj._data = _.clone(obj)
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.print_url = [locationPath, 'print/', obj.id].join('')
|
||||
obj.pay_url = [locationPath, obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
checker: null,
|
||||
payLinks: [],
|
||||
payLinksTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
label: 'Description',
|
||||
field: 'description'
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'right',
|
||||
label: 'Amount (sat)',
|
||||
field: 'amount'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
secondMultiplier: 'seconds',
|
||||
secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
|
||||
data: {
|
||||
is_unique: false
|
||||
}
|
||||
},
|
||||
qrCodeDialog: {
|
||||
show: false,
|
||||
data: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPayLinks: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/lnurlp/api/v1/links?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.payLinks = response.data.map(function (obj) {
|
||||
return mapPayLink(obj)
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
clearInterval(self.checker)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {
|
||||
is_unique: false
|
||||
}
|
||||
},
|
||||
openQrCodeDialog: function (linkId) {
|
||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
||||
this.qrCodeDialog.data = _.clone(link)
|
||||
this.qrCodeDialog.show = true
|
||||
},
|
||||
openUpdateDialog: function (linkId) {
|
||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
||||
this.formDialog.data = _.clone(link._data)
|
||||
this.formDialog.show = true
|
||||
},
|
||||
sendFormData: function () {
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
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.updatePayLink(wallet, data)
|
||||
} else {
|
||||
this.createPayLink(wallet, data)
|
||||
}
|
||||
},
|
||||
updatePayLink: function (wallet, data) {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/lnurlp/api/v1/links/' + data.id,
|
||||
wallet.adminkey,
|
||||
_.pick(data, 'description', 'amount')
|
||||
)
|
||||
.then(function (response) {
|
||||
self.payLinks = _.reject(self.payLinks, function (obj) {
|
||||
return obj.id === data.id
|
||||
})
|
||||
self.payLinks.push(mapPayLink(response.data))
|
||||
self.formDialog.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
createPayLink: function (wallet, data) {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request('POST', '/lnurlp/api/v1/links', wallet.adminkey, data)
|
||||
.then(function (response) {
|
||||
self.payLinks.push(mapPayLink(response.data))
|
||||
self.formDialog.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deletePayLink: function (linkId) {
|
||||
var self = this
|
||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/lnurlp/api/v1/links/' + linkId,
|
||||
_.findWhere(self.g.user.wallets, {id: link.wallet}).adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.payLinks = _.reject(self.payLinks, 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 getPayLinks = this.getPayLinks
|
||||
getPayLinks()
|
||||
this.checker = setInterval(function () {
|
||||
getPayLinks()
|
||||
}, 20000)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
28
lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html
Normal file
28
lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends "print.html" %} {% block page %}
|
||||
<div class="row justify-center">
|
||||
<div class="qr">
|
||||
<qrcode value="{{ link.lnurl }}" :options="{width}"></qrcode>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block styles %}
|
||||
<style>
|
||||
.qr {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
{% 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',
|
||||
created: function () {
|
||||
window.print()
|
||||
},
|
||||
data: function () {
|
||||
return {width: window.innerWidth * 0.5}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
28
lnbits/extensions/lnurlp/views.py
Normal file
28
lnbits/extensions/lnurlp/views.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from flask import g, abort, render_template
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
|
||||
from lnbits.extensions.lnurlp import lnurlp_ext
|
||||
from .crud import get_pay_link
|
||||
|
||||
|
||||
@lnurlp_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
return render_template("lnurlp/index.html", user=g.user)
|
||||
|
||||
|
||||
@lnurlp_ext.route("/<link_id>")
|
||||
def display(link_id):
|
||||
link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
|
||||
|
||||
return render_template("lnurlp/display.html", link=link)
|
||||
|
||||
|
||||
@lnurlp_ext.route("/print/<link_id>")
|
||||
def print_qr(link_id):
|
||||
link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
|
||||
|
||||
return render_template("lnurlp/print_qr.html", link=link)
|
129
lnbits/extensions/lnurlp/views_api.py
Normal file
129
lnbits/extensions/lnurlp/views_api.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
import hashlib
|
||||
from flask import g, jsonify, request, url_for
|
||||
from http import HTTPStatus
|
||||
from lnurl import LnurlPayResponse, LnurlPayActionResponse
|
||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.settings import FORCE_HTTPS
|
||||
|
||||
from lnbits.extensions.lnurlp import lnurlp_ext
|
||||
from .crud import (
|
||||
create_pay_link,
|
||||
get_pay_link,
|
||||
get_pay_links,
|
||||
update_pay_link,
|
||||
increment_pay_link,
|
||||
delete_pay_link,
|
||||
)
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/links", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_links():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
try:
|
||||
return (
|
||||
jsonify([{**link._asdict(), **{"lnurl": link.lnurl}} for link in get_pay_links(wallet_ids)]),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
except LnurlInvalidUrl:
|
||||
return (
|
||||
jsonify({"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."}),
|
||||
HTTPStatus.UPGRADE_REQUIRED,
|
||||
)
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_link_retrieve(link_id):
|
||||
link = get_pay_link(link_id)
|
||||
|
||||
if not link:
|
||||
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if link.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/links", methods=["POST"])
|
||||
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"description": {"type": "string", "empty": False, "required": True},
|
||||
"amount": {"type": "integer", "min": 1, "required": True},
|
||||
}
|
||||
)
|
||||
def api_link_create_or_update(link_id=None):
|
||||
if link_id:
|
||||
link = get_pay_link(link_id)
|
||||
|
||||
if not link:
|
||||
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if link.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
link = update_pay_link(link_id, **g.data)
|
||||
else:
|
||||
link = create_pay_link(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK if link_id else HTTPStatus.CREATED
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_link_delete(link_id):
|
||||
link = get_pay_link(link_id)
|
||||
|
||||
if not link:
|
||||
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if link.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_pay_link(link_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"])
|
||||
def api_lnurl_response(link_id):
|
||||
link = increment_pay_link(link_id, served_meta=1)
|
||||
if not link:
|
||||
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
||||
|
||||
scheme = "https" if FORCE_HTTPS else None
|
||||
url = url_for("lnurlp.api_lnurl_callback", link_id=link.id, _external=True, _scheme=scheme)
|
||||
|
||||
resp = LnurlPayResponse(
|
||||
callback=url, min_sendable=link.amount * 1000, max_sendable=link.amount * 1000, metadata=link.lnurlpay_metadata,
|
||||
)
|
||||
|
||||
return jsonify(resp.dict()), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"])
|
||||
def api_lnurl_callback(link_id):
|
||||
link = increment_pay_link(link_id, served_pr=1)
|
||||
if not link:
|
||||
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
||||
|
||||
_, payment_request = create_invoice(
|
||||
wallet_id=link.wallet,
|
||||
amount=link.amount,
|
||||
memo=link.description,
|
||||
description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(),
|
||||
)
|
||||
resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[])
|
||||
|
||||
return jsonify(resp.dict()), HTTPStatus.OK
|
|
@ -7,3 +7,4 @@ from .opennode import OpenNodeWallet
|
|||
from .lnpay import LNPayWallet
|
||||
from .lnbits import LNbitsWallet
|
||||
from .lndrest import LndRestWallet
|
||||
from .spark import SparkWallet
|
||||
|
|
|
@ -26,7 +26,7 @@ class PaymentStatus(NamedTuple):
|
|||
|
||||
class Wallet(ABC):
|
||||
@abstractmethod
|
||||
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
@ -40,3 +40,7 @@ class Wallet(ABC):
|
|||
@abstractmethod
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
pass
|
||||
|
||||
|
||||
class Unsupported(Exception):
|
||||
pass
|
||||
|
|
|
@ -7,39 +7,47 @@ import random
|
|||
|
||||
from os import getenv
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
|
||||
|
||||
|
||||
class CLightningWallet(Wallet):
|
||||
|
||||
def __init__(self):
|
||||
if LightningRpc is None: # pragma: nocover
|
||||
raise ImportError("The `pylightning` library must be installed to use `CLightningWallet`.")
|
||||
|
||||
self.l1 = LightningRpc(getenv("CLIGHTNING_RPC"))
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
if description_hash:
|
||||
raise Unsupported("description_hash")
|
||||
|
||||
label = "lbl{}".format(random.random())
|
||||
r = self.l1.invoice(amount*1000, label, memo, exposeprivatechannels=True)
|
||||
r = self.l1.invoice(amount * 1000, label, memo, exposeprivatechannels=True)
|
||||
ok, checking_id, payment_request, error_message = True, r["payment_hash"], r["bolt11"], None
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
r = self.l1.pay(bolt11)
|
||||
ok, checking_id, fee_msat, error_message = True, None, 0, None
|
||||
ok, checking_id, fee_msat, error_message = True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None
|
||||
return PaymentResponse(ok, checking_id, fee_msat, error_message)
|
||||
|
||||
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = self.l1.listinvoices(checking_id)
|
||||
if r['invoices'][0]['status'] == 'unpaid':
|
||||
if not r["invoices"]:
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(True)
|
||||
if r["invoices"][0]["label"] == checking_id:
|
||||
return PaymentStatus(r["pays"][0]["status"] == "paid")
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = self.l1.listsendpays(checking_id)
|
||||
if not r.ok:
|
||||
r = self.l1.listpays(payment_hash=checking_id)
|
||||
if not r["pays"]:
|
||||
return PaymentStatus(False)
|
||||
if r["pays"][0]["payment_hash"] == checking_id:
|
||||
status = r["pays"][0]["status"]
|
||||
if status == "complete":
|
||||
return PaymentStatus(True)
|
||||
elif status == "failed":
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(None)
|
||||
payments = [p for p in r.json()["payments"] if p["payment_hash"] == checking_id]
|
||||
payment = payments[0] if payments else None
|
||||
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False}
|
||||
return PaymentStatus(statuses[payment["status"]] if payment else None)
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
|
|
|
@ -12,11 +12,11 @@ class LNbitsWallet(Wallet):
|
|||
self.auth_admin = {"X-Api-Key": getenv("LNBITS_ADMIN_KEY")}
|
||||
self.auth_invoice = {"X-Api-Key": getenv("LNBITS_INVOICE_KEY")}
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
r = post(
|
||||
url=f"{self.endpoint}/api/v1/payments",
|
||||
headers=self.auth_invoice,
|
||||
json={"out": False, "amount": amount, "memo": memo}
|
||||
json={"out": False, "amount": amount, "memo": memo, "description_hash": description_hash.hex(),},
|
||||
)
|
||||
ok, checking_id, payment_request, error_message = r.ok, None, None, None
|
||||
|
||||
|
@ -29,11 +29,7 @@ class LNbitsWallet(Wallet):
|
|||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
r = post(
|
||||
url=f"{self.endpoint}/api/v1/payments",
|
||||
headers=self.auth_admin,
|
||||
json={"out": True, "bolt11": bolt11}
|
||||
)
|
||||
r = post(url=f"{self.endpoint}/api/v1/payments", headers=self.auth_admin, json={"out": True, "bolt11": bolt11})
|
||||
ok, checking_id, fee_msat, error_message = True, None, 0, None
|
||||
|
||||
if r.ok:
|
||||
|
@ -50,7 +46,7 @@ class LNbitsWallet(Wallet):
|
|||
if not r.ok:
|
||||
return PaymentStatus(None)
|
||||
|
||||
return PaymentStatus(r.json()['paid'])
|
||||
return PaymentStatus(r.json()["paid"])
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = get(url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.auth_invoice)
|
||||
|
@ -58,4 +54,4 @@ class LNbitsWallet(Wallet):
|
|||
if not r.ok:
|
||||
return PaymentStatus(None)
|
||||
|
||||
return PaymentStatus(r.json()['paid'])
|
||||
return PaymentStatus(r.json()["paid"])
|
||||
|
|
|
@ -23,7 +23,7 @@ class LndWallet(Wallet):
|
|||
self.auth_read = getenv("LND_READ_MACAROON")
|
||||
self.auth_cert = getenv("LND_CERT")
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
lnd_rpc = lnd_grpc.Client(
|
||||
lnd_dir=None,
|
||||
macaroon_path=self.auth_invoice,
|
||||
|
@ -33,7 +33,13 @@ class LndWallet(Wallet):
|
|||
grpc_port=self.port,
|
||||
)
|
||||
|
||||
lndResponse = lnd_rpc.add_invoice(memo=memo, value=amount, expiry=600, private=True)
|
||||
lndResponse = lnd_rpc.add_invoice(
|
||||
memo=memo,
|
||||
description_hash=base64.b64encode(description_hash).decode("ascii"),
|
||||
value=amount,
|
||||
expiry=600,
|
||||
private=True,
|
||||
)
|
||||
decoded_hash = base64.b64encode(lndResponse.r_hash).decode("utf-8").replace("/", "_")
|
||||
print(lndResponse.r_hash)
|
||||
ok, checking_id, payment_request, error_message = True, decoded_hash, str(lndResponse.payment_request), None
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from os import getenv
|
||||
import os
|
||||
import base64
|
||||
from requests import get, post
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
|
@ -18,13 +17,17 @@ class LndRestWallet(Wallet):
|
|||
self.auth_read = {"Grpc-Metadata-macaroon": getenv("LND_REST_READ_MACAROON")}
|
||||
self.auth_cert = getenv("LND_REST_CERT")
|
||||
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
r = post(
|
||||
url=f"{self.endpoint}/v1/invoices",
|
||||
headers=self.auth_invoice, verify=self.auth_cert,
|
||||
json={"value": amount, "memo": memo, "private": True},
|
||||
headers=self.auth_invoice,
|
||||
verify=self.auth_cert,
|
||||
json={
|
||||
"value": amount,
|
||||
"memo": memo,
|
||||
"description_hash": base64.b64encode(description_hash).decode("ascii"),
|
||||
"private": True,
|
||||
},
|
||||
)
|
||||
print(self.auth_invoice)
|
||||
|
||||
|
@ -37,17 +40,19 @@ class LndRestWallet(Wallet):
|
|||
r = get(url=f"{self.endpoint}/v1/payreq/{payment_request}", headers=self.auth_read, verify=self.auth_cert,)
|
||||
print(r)
|
||||
if r.ok:
|
||||
checking_id = r.json()["payment_hash"].replace("/","_")
|
||||
checking_id = r.json()["payment_hash"].replace("/", "_")
|
||||
print(checking_id)
|
||||
error_message = None
|
||||
ok = True
|
||||
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
r = post(
|
||||
url=f"{self.endpoint}/v1/channels/transactions", headers=self.auth_admin, verify=self.auth_cert, json={"payment_request": bolt11}
|
||||
url=f"{self.endpoint}/v1/channels/transactions",
|
||||
headers=self.auth_admin,
|
||||
verify=self.auth_cert,
|
||||
json={"payment_request": bolt11},
|
||||
)
|
||||
ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
|
||||
r = get(url=f"{self.endpoint}/v1/payreq/{bolt11}", headers=self.auth_admin, verify=self.auth_cert,)
|
||||
|
@ -59,9 +64,8 @@ class LndRestWallet(Wallet):
|
|||
|
||||
return PaymentResponse(ok, checking_id, fee_msat, error_message)
|
||||
|
||||
|
||||
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
checking_id = checking_id.replace("_","/")
|
||||
checking_id = checking_id.replace("_", "/")
|
||||
print(checking_id)
|
||||
r = get(url=f"{self.endpoint}/v1/invoice/{checking_id}", headers=self.auth_invoice, verify=self.auth_cert,)
|
||||
print(r.json()["settled"])
|
||||
|
@ -71,7 +75,12 @@ class LndRestWallet(Wallet):
|
|||
return PaymentStatus(r.json()["settled"])
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = get(url=f"{self.endpoint}/v1/payments", headers=self.auth_admin, verify=self.auth_cert, params={"include_incomplete": "True", "max_payments": "20"})
|
||||
r = get(
|
||||
url=f"{self.endpoint}/v1/payments",
|
||||
headers=self.auth_admin,
|
||||
verify=self.auth_cert,
|
||||
params={"include_incomplete": "True", "max_payments": "20"},
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
return PaymentStatus(None)
|
||||
|
|
|
@ -15,13 +15,13 @@ class LNPayWallet(Wallet):
|
|||
self.auth_read = getenv("LNPAY_READ_KEY")
|
||||
self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")}
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
r = post(
|
||||
url=f"{self.endpoint}/user/wallet/{self.auth_invoice}/invoice",
|
||||
headers=self.auth_api,
|
||||
json={"num_satoshis": f"{amount}", "memo": memo},
|
||||
json={"num_satoshis": f"{amount}", "memo": memo, "description_hash": description_hash.hex(),},
|
||||
)
|
||||
ok, checking_id, payment_request, error_message = r.status_code == 201, None, None, None
|
||||
ok, checking_id, payment_request, error_message = r.status_code == 201, None, None, r.text
|
||||
|
||||
if ok:
|
||||
data = r.json()
|
||||
|
|
|
@ -13,8 +13,12 @@ class LntxbotWallet(Wallet):
|
|||
self.auth_admin = {"Authorization": f"Basic {getenv('LNTXBOT_ADMIN_KEY')}"}
|
||||
self.auth_invoice = {"Authorization": f"Basic {getenv('LNTXBOT_INVOICE_KEY')}"}
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
|
||||
r = post(url=f"{self.endpoint}/addinvoice", headers=self.auth_invoice, json={"amt": str(amount), "memo": memo})
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
r = post(
|
||||
url=f"{self.endpoint}/addinvoice",
|
||||
headers=self.auth_invoice,
|
||||
json={"amt": str(amount), "memo": memo, "description_hash": description_hash.hex()},
|
||||
)
|
||||
ok, checking_id, payment_request, error_message = r.ok, None, None, None
|
||||
|
||||
if r.ok:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from os import getenv
|
||||
from requests import get, post
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
|
||||
|
||||
|
||||
class OpenNodeWallet(Wallet):
|
||||
|
@ -13,7 +13,10 @@ class OpenNodeWallet(Wallet):
|
|||
self.auth_admin = {"Authorization": getenv("OPENNODE_ADMIN_KEY")}
|
||||
self.auth_invoice = {"Authorization": getenv("OPENNODE_INVOICE_KEY")}
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
if description_hash:
|
||||
raise Unsupported("description_hash")
|
||||
|
||||
r = post(
|
||||
url=f"{self.endpoint}/v1/charges",
|
||||
headers=self.auth_invoice,
|
||||
|
|
86
lnbits/wallets/spark.py
Normal file
86
lnbits/wallets/spark.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
import random
|
||||
import requests
|
||||
from os import getenv
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
|
||||
|
||||
class SparkError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SparkWallet(Wallet):
|
||||
def __init__(self):
|
||||
self.url = getenv("SPARK_URL")
|
||||
self.token = getenv("SPARK_TOKEN")
|
||||
|
||||
def __getattr__(self, key):
|
||||
def call(*args, **kwargs):
|
||||
if args and kwargs:
|
||||
raise TypeError(f"must supply either named arguments or a list of arguments, not both: {args} {kwargs}")
|
||||
elif args:
|
||||
params = args
|
||||
elif kwargs:
|
||||
params = kwargs
|
||||
|
||||
r = requests.post(self.url, headers={"X-Access": self.token}, json={"method": key, "params": params})
|
||||
try:
|
||||
data = r.json()
|
||||
except:
|
||||
raise UnknownError(r.text)
|
||||
if not r.ok:
|
||||
raise SparkError(data["message"])
|
||||
return data
|
||||
|
||||
return call
|
||||
|
||||
def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse:
|
||||
label = "lbs{}".format(random.random())
|
||||
checking_id = label
|
||||
|
||||
try:
|
||||
if description_hash:
|
||||
r = self.invoicewithdescriptionhash(
|
||||
msatoshi=amount * 1000, label=label, description_hash=description_hash.hex(),
|
||||
)
|
||||
else:
|
||||
r = self.invoice(msatoshi=amount * 1000, label=label, description=memo, exposeprivatechannels=True)
|
||||
ok, payment_request, error_message = True, r["bolt11"], ""
|
||||
except (SparkError, UnknownError) as e:
|
||||
ok, payment_request, error_message = False, None, str(e)
|
||||
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
try:
|
||||
r = self.pay(bolt11)
|
||||
ok, checking_id, fee_msat, error_message = True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None
|
||||
except (SparkError, UnknownError) as e:
|
||||
ok, checking_id, fee_msat, error_message = False, None, None, str(e)
|
||||
|
||||
return PaymentResponse(ok, checking_id, fee_msat, error_message)
|
||||
|
||||
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = self.listinvoices(label=checking_id)
|
||||
if not r or not r.get("invoices"):
|
||||
return PaymentStatus(None)
|
||||
if r["invoices"][0]["status"] == "unpaid":
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(True)
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = self.listpays(payment_hash=checking_id)
|
||||
if not r["pays"]:
|
||||
return PaymentStatus(False)
|
||||
if r["pays"][0]["payment_hash"] == checking_id:
|
||||
status = r["pays"][0]["status"]
|
||||
if status == "complete":
|
||||
return PaymentStatus(True)
|
||||
elif status == "failed":
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(None)
|
||||
raise KeyError("supplied an invalid checking_id")
|
Loading…
Reference in New Issue
Block a user