Browse Source

Merge pull request #81 from lnbits/internalpaymentsmegachanges

aiosqlite
fiatjaf 5 years ago
committed by GitHub
parent
commit
6513908a8d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 42
      .github/workflows/linting.yml
  2. 2
      .github/workflows/tests.yml
  3. 18
      Makefile
  4. 2
      README.md
  5. 66
      lnbits/bolt11.py
  6. 90
      lnbits/core/crud.py
  7. 35
      lnbits/core/migrations.py
  8. 40
      lnbits/core/models.py
  9. 108
      lnbits/core/services.py
  10. 24
      lnbits/core/static/js/wallet.js
  11. 12
      lnbits/core/templates/core/_api_docs.html
  12. 84
      lnbits/core/templates/core/wallet.html
  13. 53
      lnbits/core/views/api.py
  14. 2
      lnbits/core/views/generic.py
  15. 10
      lnbits/db.py
  16. 36
      lnbits/extensions/amilk/views_api.py
  17. 24
      lnbits/extensions/events/crud.py
  18. 2
      lnbits/extensions/events/templates/events/display.html
  19. 31
      lnbits/extensions/events/views_api.py
  20. 16
      lnbits/extensions/lnticket/crud.py
  21. 24
      lnbits/extensions/lnticket/templates/lnticket/display.html
  22. 46
      lnbits/extensions/lnticket/views_api.py
  23. 1
      lnbits/extensions/lnurlp/views_api.py
  24. 6
      lnbits/extensions/paywall/templates/paywall/_api_docs.html
  25. 2
      lnbits/extensions/paywall/templates/paywall/display.html
  26. 15
      lnbits/extensions/paywall/views_api.py
  27. 2
      lnbits/extensions/tpos/templates/tpos/tpos.html
  28. 20
      lnbits/extensions/tpos/views_api.py
  29. 8
      lnbits/extensions/usermanager/templates/usermanager/_api_docs.html
  30. 2
      lnbits/extensions/withdraw/models.py
  31. 8
      lnbits/extensions/withdraw/views_api.py
  32. 9
      lnbits/static/css/base.css
  33. 32
      lnbits/static/js/base.js
  34. 3
      package.json

42
.github/workflows/linting.yml

@ -1,25 +1,33 @@
name: Run Linters name: Linters
on: [push, pull_request] on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs: jobs:
mypy: black:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - uses: actions/checkout@v2
uses: actions/checkout@v1 - run: sudo apt-get install python3-venv
- name: Run MyPy python type checker - run: python3 -m venv venv
uses: jpetrucciani/mypy-check@master - run: ./venv/bin/pip install black
with: - run: make checkblack
path: 'lnbits'
prettier: prettier:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - uses: actions/checkout@v2
uses: actions/checkout@v1 - run: npm install
- name: Check JS code formatting convention - run: make checkprettier
uses: creyD/prettier_action@v2.2 mypy:
with: runs-on: ubuntu-latest
dry: True steps:
prettier_options: --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/** - uses: actions/checkout@v2
- run: sudo apt-get install python3-venv
- run: sudo apt-get install libev-dev
- run: python3 -m venv venv
- run: ./venv/bin/pip install -r requirements.txt
- run: ./venv/bin/pip install mypy
- run: make mypy

2
.github/workflows/tests.yml

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.6, 3.7, 3.8] python-version: [3.8]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

18
Makefile

@ -1,10 +1,20 @@
all: prettier mypy black all: format check
format: prettier black
check: mypy checkprettier checkblack
prettier: $(shell find lnbits -name "*.js" -name ".html") prettier: $(shell find lnbits -name "*.js" -name ".html")
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js ./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js
black: $(shell find lnbits -name "*.py")
./venv/bin/black --line-length 120 lnbits
mypy: $(shell find lnbits -name "*.py") mypy: $(shell find lnbits -name "*.py")
mypy lnbits ./venv/bin/mypy lnbits
black: $(shell find lnbits -name "*.py") checkprettier: $(shell find lnbits -name "*.js" -name ".html")
black lnbits ./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js
checkblack: $(shell find lnbits -name "*.py")
./venv/bin/black --check --line-length 120 lnbits

2
README.md

@ -25,7 +25,7 @@ See [lnbits.org](https://lnbits.org) for more detailed documentation.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series. Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits! LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits.
## LNbits as an account system ## LNbits as an account system

66
lnbits/bolt11.py

@ -1,12 +1,10 @@
# type: ignore import bitstring # type: ignore
import bitstring
import re import re
import hashlib import hashlib
from typing import List, NamedTuple from typing import List, NamedTuple, Optional
from bech32 import bech32_decode, CHARSET from bech32 import bech32_decode, CHARSET
from ecdsa import SECP256k1, VerifyingKey from ecdsa import SECP256k1, VerifyingKey # type: ignore
from ecdsa.util import sigdecode_string from ecdsa.util import sigdecode_string # type: ignore
from binascii import unhexlify from binascii import unhexlify
@ -19,40 +17,40 @@ class Route(NamedTuple):
class Invoice(object): class Invoice(object):
payment_hash: str = None payment_hash: str
amount_msat: int = 0 amount_msat: int = 0
description: str = None description: Optional[str] = None
payee: str = None description_hash: Optional[str] = None
date: int = None payee: str
date: int
expiry: int = 3600 expiry: int = 3600
secret: str = None secret: Optional[str] = None
route_hints: List[Route] = [] route_hints: List[Route] = []
min_final_cltv_expiry: int = 18 min_final_cltv_expiry: int = 18
def decode(pr: str) -> Invoice: def decode(pr: str) -> Invoice:
""" Super naïve bolt11 decoder, """bolt11 decoder,
only gets payment_hash, description/description_hash and amount in msatoshi.
based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py
""" """
hrp, data = bech32_decode(pr)
if not hrp:
raise ValueError("Bad bech32 checksum")
hrp, decoded_data = bech32_decode(pr)
if hrp is None or decoded_data is None:
raise ValueError("Bad bech32 checksum")
if not hrp.startswith("ln"): if not hrp.startswith("ln"):
raise ValueError("Does not start with ln") raise ValueError("Does not start with ln")
data = u5_to_bitarray(data) bitarray = _u5_to_bitarray(decoded_data)
# final signature 65 bytes, split it off. # final signature 65 bytes, split it off.
if len(data) < 65 * 8: if len(bitarray) < 65 * 8:
raise ValueError("Too short to contain signature") raise ValueError("Too short to contain signature")
# extract the signature # extract the signature
signature = data[-65 * 8 :].tobytes() signature = bitarray[-65 * 8 :].tobytes()
# the tagged fields as a bitstream # the tagged fields as a bitstream
data = bitstring.ConstBitStream(data[: -65 * 8]) data = bitstring.ConstBitStream(bitarray[: -65 * 8])
# build the invoice object # build the invoice object
invoice = Invoice() invoice = Invoice()
@ -62,35 +60,35 @@ def decode(pr: str) -> Invoice:
if m: if m:
amountstr = hrp[2 + m.end() :] amountstr = hrp[2 + m.end() :]
if amountstr != "": if amountstr != "":
invoice.amount_msat = unshorten_amount(amountstr) invoice.amount_msat = _unshorten_amount(amountstr)
# pull out date # pull out date
invoice.date = data.read(35).uint invoice.date = data.read(35).uint
while data.pos != data.len: while data.pos != data.len:
tag, tagdata, data = pull_tagged(data) tag, tagdata, data = _pull_tagged(data)
data_length = len(tagdata) / 5 data_length = len(tagdata) / 5
if tag == "d": if tag == "d":
invoice.description = trim_to_bytes(tagdata).decode("utf-8") invoice.description = _trim_to_bytes(tagdata).decode("utf-8")
elif tag == "h" and data_length == 52: elif tag == "h" and data_length == 52:
invoice.description = trim_to_bytes(tagdata).hex() invoice.description_hash = _trim_to_bytes(tagdata).hex()
elif tag == "p" and data_length == 52: elif tag == "p" and data_length == 52:
invoice.payment_hash = trim_to_bytes(tagdata).hex() invoice.payment_hash = _trim_to_bytes(tagdata).hex()
elif tag == "x": elif tag == "x":
invoice.expiry = tagdata.uint invoice.expiry = tagdata.uint
elif tag == "n": elif tag == "n":
invoice.payee = trim_to_bytes(tagdata).hex() invoice.payee = _trim_to_bytes(tagdata).hex()
# this won't work in most cases, we must extract the payee # this won't work in most cases, we must extract the payee
# from the signature # from the signature
elif tag == "s": elif tag == "s":
invoice.secret = trim_to_bytes(tagdata).hex() invoice.secret = _trim_to_bytes(tagdata).hex()
elif tag == "r": elif tag == "r":
s = bitstring.ConstBitStream(tagdata) s = bitstring.ConstBitStream(tagdata)
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len: while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
route = Route( route = Route(
pubkey=s.read(264).tobytes().hex(), pubkey=s.read(264).tobytes().hex(),
short_channel_id=readable_scid(s.read(64).intbe), short_channel_id=_readable_scid(s.read(64).intbe),
base_fee_msat=s.read(32).intbe, base_fee_msat=s.read(32).intbe,
ppm_fee=s.read(32).intbe, ppm_fee=s.read(32).intbe,
cltv=s.read(16).intbe, cltv=s.read(16).intbe,
@ -116,7 +114,7 @@ def decode(pr: str) -> Invoice:
return invoice return invoice
def unshorten_amount(amount: str) -> int: def _unshorten_amount(amount: str) -> int:
""" Given a shortened amount, return millisatoshis """ Given a shortened amount, return millisatoshis
""" """
# BOLT #11: # BOLT #11:
@ -141,18 +139,18 @@ def unshorten_amount(amount: str) -> int:
raise ValueError("Invalid amount '{}'".format(amount)) raise ValueError("Invalid amount '{}'".format(amount))
if unit in units: if unit in units:
return int(amount[:-1]) * 100_000_000_000 / units[unit] return int(int(amount[:-1]) * 100_000_000_000 / units[unit])
else: else:
return int(amount) * 100_000_000_000 return int(amount) * 100_000_000_000
def pull_tagged(stream): def _pull_tagged(stream):
tag = stream.read(5).uint tag = stream.read(5).uint
length = stream.read(5).uint * 32 + stream.read(5).uint length = stream.read(5).uint * 32 + stream.read(5).uint
return (CHARSET[tag], stream.read(length * 5), stream) return (CHARSET[tag], stream.read(length * 5), stream)
def trim_to_bytes(barr): def _trim_to_bytes(barr):
# Adds a byte if necessary. # Adds a byte if necessary.
b = barr.tobytes() b = barr.tobytes()
if barr.len % 8 != 0: if barr.len % 8 != 0:
@ -160,7 +158,7 @@ def trim_to_bytes(barr):
return b return b
def readable_scid(short_channel_id: int) -> str: def _readable_scid(short_channel_id: int) -> str:
return "{blockheight}x{transactionindex}x{outputindex}".format( return "{blockheight}x{transactionindex}x{outputindex}".format(
blockheight=((short_channel_id >> 40) & 0xFFFFFF), blockheight=((short_channel_id >> 40) & 0xFFFFFF),
transactionindex=((short_channel_id >> 16) & 0xFFFFFF), transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
@ -168,7 +166,7 @@ def readable_scid(short_channel_id: int) -> str:
) )
def u5_to_bitarray(arr): def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
ret = bitstring.BitArray() ret = bitstring.BitArray()
for a in arr: for a in arr:
ret += bitstring.pack("uint:5", a) ret += bitstring.pack("uint:5", a)

90
lnbits/core/crud.py

@ -1,7 +1,10 @@
from typing import List, Optional import json
import datetime
from uuid import uuid4 from uuid import uuid4
from typing import List, Optional, Dict
from lnbits.db import open_db from lnbits.db import open_db
from lnbits import bolt11
from lnbits.settings import DEFAULT_WALLET_NAME from lnbits.settings import DEFAULT_WALLET_NAME
from .models import User, Wallet, Payment from .models import User, Wallet, Payment
@ -136,18 +139,18 @@ def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
# --------------- # ---------------
def get_wallet_payment(wallet_id: str, checking_id: str) -> Optional[Payment]: def get_wallet_payment(wallet_id: str, payment_hash: str) -> Optional[Payment]:
with open_db() as db: with open_db() as db:
row = db.fetchone( row = db.fetchone(
""" """
SELECT payhash as checking_id, amount, fee, pending, memo, time SELECT *
FROM apipayments FROM apipayments
WHERE wallet = ? AND payhash = ? WHERE wallet = ? AND hash = ?
""", """,
(wallet_id, checking_id), (wallet_id, payment_hash),
) )
return Payment(**row) if row else None return Payment.from_row(row) if row else None
def get_wallet_payments( def get_wallet_payments(
@ -179,7 +182,7 @@ def get_wallet_payments(
with open_db() as db: with open_db() as db:
rows = db.fetchall( rows = db.fetchall(
f""" f"""
SELECT payhash as checking_id, amount, fee, pending, memo, time SELECT *
FROM apipayments FROM apipayments
WHERE wallet = ? {clause} WHERE wallet = ? {clause}
ORDER BY time DESC ORDER BY time DESC
@ -187,17 +190,34 @@ def get_wallet_payments(
(wallet_id,), (wallet_id,),
) )
return [Payment(**row) for row in rows] return [Payment.from_row(row) for row in rows]
def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> None: def delete_expired_invoices() -> None:
with open_db() as db: with open_db() as db:
rows = db.fetchall(
"""
SELECT bolt11
FROM apipayments
WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 86400
"""
)
for (payment_request,) in rows:
try:
invoice = bolt11.decode(payment_request)
except:
continue
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
if expiration_date > datetime.datetime.utcnow():
continue
db.execute( db.execute(
""" """
DELETE DELETE FROM apipayments
FROM apipayments WHERE wallet = ? AND pending = 1 AND time < strftime('%s', 'now') - ? WHERE pending = 1 AND payment_hash = ?
""", """,
(wallet_id, seconds), (invoice.payment_hash,),
) )
@ -206,18 +226,41 @@ def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> N
def create_payment( def create_payment(
*, wallet_id: str, checking_id: str, amount: int, memo: str, fee: int = 0, pending: bool = True *,
wallet_id: str,
checking_id: str,
payment_request: str,
payment_hash: str,
amount: int,
memo: str,
fee: int = 0,
preimage: Optional[str] = None,
pending: bool = True,
extra: Optional[Dict] = None,
) -> Payment: ) -> Payment:
with open_db() as db: with open_db() as db:
db.execute( db.execute(
""" """
INSERT INTO apipayments (wallet, payhash, amount, pending, memo, fee) INSERT INTO apipayments
VALUES (?, ?, ?, ?, ?, ?) (wallet, checking_id, bolt11, hash, preimage,
amount, pending, memo, fee, extra)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(wallet_id, checking_id, amount, int(pending), memo, fee), (
wallet_id,
checking_id,
payment_request,
payment_hash,
preimage,
amount,
int(pending),
memo,
fee,
json.dumps(extra) if extra and extra != {} and type(extra) is dict else None,
),
) )
new_payment = get_wallet_payment(wallet_id, checking_id) new_payment = get_wallet_payment(wallet_id, payment_hash)
assert new_payment, "Newly created payment couldn't be retrieved" assert new_payment, "Newly created payment couldn't be retrieved"
return new_payment return new_payment
@ -225,9 +268,18 @@ def create_payment(
def update_payment_status(checking_id: str, pending: bool) -> None: def update_payment_status(checking_id: str, pending: bool) -> None:
with open_db() as db: with open_db() as db:
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), checking_id,)) db.execute("UPDATE apipayments SET pending = ? WHERE checking_id = ?", (int(pending), checking_id,))
def delete_payment(checking_id: str) -> None: def delete_payment(checking_id: str) -> None:
with open_db() as db: with open_db() as db:
db.execute("DELETE FROM apipayments WHERE payhash = ?", (checking_id,)) db.execute("DELETE FROM apipayments WHERE checking_id = ?", (checking_id,))
def check_internal(payment_hash: str) -> Optional[str]:
with open_db() as db:
row = db.fetchone("SELECT checking_id FROM apipayments WHERE hash = ?", (payment_hash,))
if not row:
return None
else:
return row["checking_id"]

35
lnbits/core/migrations.py

@ -51,6 +51,7 @@ def m001_initial(db):
); );
""" """
) )
db.execute( db.execute(
""" """
CREATE VIEW IF NOT EXISTS balances AS CREATE VIEW IF NOT EXISTS balances AS
@ -70,6 +71,40 @@ def m001_initial(db):
) )
def m002_add_fields_to_apipayments(db):
"""
Adding fields to apipayments for better accounting,
and renaming payhash to checking_id since that is what it really is.
"""
db.execute("ALTER TABLE apipayments RENAME COLUMN payhash TO checking_id")
db.execute("ALTER TABLE apipayments ADD COLUMN hash TEXT")
db.execute("CREATE INDEX by_hash ON apipayments (hash)")
db.execute("ALTER TABLE apipayments ADD COLUMN preimage TEXT")
db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT")
db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT")
import json
rows = db.fetchall("SELECT * FROM apipayments")
for row in rows:
if not row["memo"] or not row["memo"].startswith("#"):
continue
for ext in ["withdraw", "events", "lnticket", "paywall", "tpos"]:
prefix = "#" + ext + " "
if row["memo"].startswith(prefix):
new = row["memo"][len(prefix) :]
db.execute(
"""
UPDATE apipayments SET extra = ?, memo = ?
WHERE checking_id = ? AND memo = ?
""",
(json.dumps({"tag": ext}), new, row["checking_id"], row["memo"]),
)
break
def migrate(): def migrate():
with open_db() as db: with open_db() as db:
m001_initial(db) m001_initial(db)
m002_add_fields_to_apipayments(db)

40
lnbits/core/models.py

@ -1,4 +1,6 @@
from typing import List, NamedTuple, Optional import json
from typing import List, NamedTuple, Optional, Dict
from sqlite3 import Row
class User(NamedTuple): class User(NamedTuple):
@ -29,10 +31,10 @@ class Wallet(NamedTuple):
def balance(self) -> int: def balance(self) -> int:
return self.balance_msat // 1000 return self.balance_msat // 1000
def get_payment(self, checking_id: str) -> Optional["Payment"]: def get_payment(self, payment_hash: str) -> Optional["Payment"]:
from .crud import get_wallet_payment from .crud import get_wallet_payment
return get_wallet_payment(self.id, checking_id) return get_wallet_payment(self.id, payment_hash)
def get_payments( def get_payments(
self, *, complete: bool = True, pending: bool = False, outgoing: bool = True, incoming: bool = True self, *, complete: bool = True, pending: bool = False, outgoing: bool = True, incoming: bool = True
@ -41,11 +43,6 @@ class Wallet(NamedTuple):
return get_wallet_payments(self.id, complete=complete, pending=pending, outgoing=outgoing, incoming=incoming) return get_wallet_payments(self.id, complete=complete, pending=pending, outgoing=outgoing, incoming=incoming)
def delete_expired_payments(self, seconds: int = 86400) -> None:
from .crud import delete_wallet_payments_expired
delete_wallet_payments_expired(self.id, seconds=seconds)
class Payment(NamedTuple): class Payment(NamedTuple):
checking_id: str checking_id: str
@ -54,6 +51,29 @@ class Payment(NamedTuple):
fee: int fee: int
memo: str memo: str
time: int time: int
bolt11: str
preimage: str
payment_hash: str
extra: Dict
@classmethod
def from_row(cls, row: Row):
return cls(
checking_id=row["checking_id"],
payment_hash=row["hash"],
bolt11=row["bolt11"],
preimage=row["preimage"],
extra=json.loads(row["extra"] or "{}"),
pending=row["pending"],
amount=row["amount"],
fee=row["fee"],
memo=row["memo"],
time=row["time"],
)
@property
def tag(self) -> Optional[str]:
return self.extra.get("tag")
@property @property
def msat(self) -> int: def msat(self) -> int:
@ -71,6 +91,10 @@ class Payment(NamedTuple):
def is_out(self) -> bool: def is_out(self) -> bool:
return self.amount < 0 return self.amount < 0
@property
def is_uncheckable(self) -> bool:
return self.checking_id.startswith("temp_") or self.checking_id.startswith("internal_")
def set_pending(self, pending: bool) -> None: def set_pending(self, pending: bool) -> None:
from .crud import update_payment_status from .crud import update_payment_status

108
lnbits/core/services.py

@ -1,72 +1,112 @@
from typing import Optional, Tuple from typing import Optional, Tuple, Dict, TypedDict
from lnbits.bolt11 import decode as bolt11_decode # type: ignore from lnbits import bolt11
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from lnbits.settings import WALLET from lnbits.settings import WALLET
from lnbits.wallets.base import PaymentStatus
from .crud import get_wallet, create_payment, delete_payment from .crud import get_wallet, create_payment, delete_payment, check_internal, update_payment_status, get_wallet_payment
def create_invoice(*, wallet_id: str, amount: int, memo: str, description_hash: bytes = None) -> Tuple[str, str]: def create_invoice(
*, wallet_id: str, amount: int, memo: str, description_hash: Optional[bytes] = None, extra: Optional[Dict] = None,
) -> Tuple[str, str]:
invoice_memo = None if description_hash else memo
storeable_memo = memo
try:
ok, checking_id, payment_request, error_message = WALLET.create_invoice( ok, checking_id, payment_request, error_message = WALLET.create_invoice(
amount=amount, memo=memo, description_hash=description_hash amount=amount, memo=invoice_memo, description_hash=description_hash
) )
except Exception as e:
ok, error_message = False, str(e)
if not ok: if not ok:
raise Exception(error_message or "Unexpected backend error.") raise Exception(error_message or "Unexpected backend error.")
invoice = bolt11.decode(payment_request)
amount_msat = amount * 1000 amount_msat = amount * 1000
create_payment(wallet_id=wallet_id, checking_id=checking_id, amount=amount_msat, memo=memo) create_payment(
wallet_id=wallet_id,
checking_id=checking_id,
payment_request=payment_request,
payment_hash=invoice.payment_hash,
amount=amount_msat,
memo=storeable_memo,
extra=extra,
)
return checking_id, payment_request return invoice.payment_hash, payment_request
def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) -> str: def pay_invoice(
*, wallet_id: str, payment_request: str, max_sat: Optional[int] = None, extra: Optional[Dict] = None
) -> str:
temp_id = f"temp_{urlsafe_short_hash()}" temp_id = f"temp_{urlsafe_short_hash()}"
try: internal_id = f"internal_{urlsafe_short_hash()}"
invoice = bolt11_decode(bolt11)
invoice = bolt11.decode(payment_request)
if invoice.amount_msat == 0: if invoice.amount_msat == 0:
raise ValueError("Amountless invoices not supported.") raise ValueError("Amountless invoices not supported.")
if max_sat and invoice.amount_msat > max_sat * 1000: if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.") raise ValueError("Amount in invoice is too high.")
fee_reserve = max(1000, int(invoice.amount_msat * 0.01)) # put all parameters that don't change here
create_payment( PaymentKwargs = TypedDict(
wallet_id=wallet_id, checking_id=temp_id, amount=-invoice.amount_msat, fee=-fee_reserve, memo=temp_id, "PaymentKwargs",
{
"wallet_id": str,
"payment_request": str,
"payment_hash": str,
"amount": int,
"memo": str,
"extra": Optional[Dict],
},
)
payment_kwargs: PaymentKwargs = dict(
wallet_id=wallet_id,
payment_request=payment_request,
payment_hash=invoice.payment_hash,
amount=-invoice.amount_msat,
memo=invoice.description or "",
extra=extra,
) )
# check_internal() returns the checking_id of the invoice we're waiting for
internal = check_internal(invoice.payment_hash)
if internal:
# create a new payment from this wallet
create_payment(checking_id=internal_id, fee=0, pending=False, **payment_kwargs)
else:
# create a temporary payment here so we can check if
# the balance is enough in the next step
fee_reserve = max(1000, int(invoice.amount_msat * 0.01))
create_payment(checking_id=temp_id, fee=-fee_reserve, **payment_kwargs)
# do the balance check
wallet = get_wallet(wallet_id) wallet = get_wallet(wallet_id)
assert wallet, "invalid wallet id" assert wallet, "invalid wallet id"
if wallet.balance_msat < 0: if wallet.balance_msat < 0:
raise PermissionError("Insufficient balance.") raise PermissionError("Insufficient balance.")
ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(bolt11) if internal:
# mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure
# the payer has enough to deduct from
update_payment_status(checking_id=internal, pending=False)
else:
# actually pay the external invoice
ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(payment_request)
if ok: if ok:
create_payment( create_payment(checking_id=checking_id, fee=fee_msat, **payment_kwargs)
wallet_id=wallet_id,
checking_id=checking_id,
amount=-invoice.amount_msat,
fee=fee_msat,
memo=invoice.description,
)
except Exception as e:
ok, error_message = False, str(e)
delete_payment(temp_id) delete_payment(temp_id)
if not ok: if not ok:
raise Exception(error_message or "Unexpected backend error.") raise Exception(error_message or "Unexpected backend error.")
return checking_id return invoice.payment_hash
def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus:
payment = get_wallet_payment(wallet_id, payment_hash)
if not payment:
return PaymentStatus(None)
def check_payment(*, checking_id: str) -> str: return WALLET.get_invoice_status(payment.checking_id)
pass

24
lnbits/core/static/js/wallet.js

@ -1,3 +1,5 @@
/* globals decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader) Vue.use(VueQrcodeReader)
@ -115,6 +117,7 @@ new Vue({
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
user: LNbits.map.user(window.user),
receive: { receive: {
show: false, show: false,
status: 'pending', status: 'pending',
@ -138,7 +141,12 @@ new Vue({
payments: [], payments: [],
paymentsTable: { paymentsTable: {
columns: [ columns: [
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'}, {
name: 'memo',
align: 'left',
label: 'Memo',
field: 'memo'
},
{ {
name: 'date', name: 'date',
align: 'left', align: 'left',
@ -171,7 +179,7 @@ new Vue({
computed: { computed: {
filteredPayments: function () { filteredPayments: function () {
var q = this.paymentsTable.filter var q = this.paymentsTable.filter
if (!q || q == '') return this.payments if (!q || q === '') return this.payments
return LNbits.utils.search(this.payments, q) return LNbits.utils.search(this.payments, q)
}, },
@ -261,7 +269,7 @@ new Vue({
self.receive.paymentChecker = setInterval(function () { self.receive.paymentChecker = setInterval(function () {
LNbits.api LNbits.api
.getPayment(self.g.wallet, response.data.checking_id) .getPayment(self.g.wallet, response.data.payment_hash)
.then(function (response) { .then(function (response) {
if (response.data.paid) { if (response.data.paid) {
self.fetchPayments() self.fetchPayments()
@ -308,11 +316,11 @@ new Vue({
_.each(invoice.data.tags, function (tag) { _.each(invoice.data.tags, function (tag) {
if (_.isObject(tag) && _.has(tag, 'description')) { if (_.isObject(tag) && _.has(tag, 'description')) {
if (tag.description == 'payment_hash') { if (tag.description === 'payment_hash') {
cleanInvoice.hash = tag.value cleanInvoice.hash = tag.value
} else if (tag.description == 'description') { } else if (tag.description === 'description') {
cleanInvoice.description = tag.value cleanInvoice.description = tag.value
} else if (tag.description == 'expiry') { } else if (tag.description === 'expiry') {
var expireDate = new Date( var expireDate = new Date(
(invoice.data.time_stamp + tag.value) * 1000 (invoice.data.time_stamp + tag.value) * 1000
) )
@ -330,7 +338,7 @@ new Vue({
payInvoice: function () { payInvoice: function () {
var self = this var self = this
dismissPaymentMsg = this.$q.notify({ let dismissPaymentMsg = this.$q.notify({
timeout: 0, timeout: 0,
message: 'Processing payment...', message: 'Processing payment...',
icon: null icon: null
@ -341,7 +349,7 @@ new Vue({
.then(function (response) { .then(function (response) {
self.send.paymentChecker = setInterval(function () { self.send.paymentChecker = setInterval(function () {
LNbits.api LNbits.api
.getPayment(self.g.wallet, response.data.checking_id) .getPayment(self.g.wallet, response.data.payment_hash)
.then(function (res) { .then(function (res) {
if (res.data.paid) { if (res.data.paid) {
self.send.show = false self.send.show = false

12
lnbits/core/templates/core/_api_docs.html

@ -23,7 +23,7 @@
Returns 201 CREATED (application/json) Returns 201 CREATED (application/json)
</h5> </h5>
<code <code
>{"checking_id": &lt;string&gt;, "payment_request": >{"payment_hash": &lt;string&gt;, "payment_request":
&lt;string&gt;}</code &lt;string&gt;}</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
@ -51,7 +51,7 @@
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json) Returns 201 CREATED (application/json)
</h5> </h5>
<code>{"checking_id": &lt;string&gt;}</code> <code>{"payment_hash": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": true, >curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": true,
@ -73,7 +73,7 @@
<q-card-section> <q-card-section>
<code <code
><span class="text-light-blue">GET</span> ><span class="text-light-blue">GET</span>
/api/v1/payments/&lt;checking_id&gt;</code /api/v1/payments/&lt;payment_hash&gt;</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": "{{ wallet.inkey }}"}</code> <code>{"X-Api-Key": "{{ wallet.inkey }}"}</code>
@ -83,9 +83,9 @@
<code>{"paid": &lt;bool&gt;}</code> <code>{"paid": &lt;bool&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}api/v1/payments/&lt;checking_id&gt; >curl -X GET {{ request.url_root
-H "X-Api-Key: <i>{{ wallet.inkey }}"</i> -H "Content-type: }}api/v1/payments/&lt;payment_hash&gt; -H "X-Api-Key:
application/json"</code <i>{{ wallet.inkey }}"</i> -H "Content-type: application/json"</code
> >
</q-card-section> </q-card-section>
</q-card> </q-card>

84
lnbits/core/templates/core/wallet.html

@ -8,7 +8,7 @@
{% endblock %} {% block scripts %} {{ window_vars(user, wallet) }} {% endblock %} {% block scripts %} {{ window_vars(user, wallet) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> <script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
{% assets filters='rjsmin', output='__bundle__/core/chart.js', {% assets filters='rjsmin', output='__bundle__/core/chart.js',
'vendor/moment@2.25.1/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %} 'vendor/moment@2.27.0/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script> <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js', {% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js',
'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js', 'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js',
@ -76,7 +76,7 @@
clearable clearable
v-model="paymentsTable.filter" v-model="paymentsTable.filter"
debounce="300" debounce="300"
placeholder="Search by memo, amount" placeholder="Search by tag, memo, amount"
class="q-mb-md" class="q-mb-md"
> >
</q-input> </q-input>
@ -84,7 +84,7 @@
dense dense
flat flat
:data="filteredPayments" :data="filteredPayments"
row-key="payhash" row-key="payment_hash"
:columns="paymentsTable.columns" :columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination" :pagination.sync="paymentsTable.pagination"
> >
@ -103,14 +103,28 @@
<q-icon <q-icon
v-if="props.row.isPaid" v-if="props.row.isPaid"
size="14px" size="14px"
:name="(props.row.sat < 0) ? 'call_made' : 'call_received'" :name="props.row.isOut ? 'call_made' : 'call_received'"
:color="(props.row.sat < 0) ? 'pink' : 'green'" :color="props.row.isOut ? 'pink' : 'green'"
@click="props.expand = !props.expand"
></q-icon> ></q-icon>
<q-icon v-else name="settings_ethernet" color="grey"> <q-icon
v-else
name="settings_ethernet"
color="grey"
@click="props.expand = !props.expand"
>
<q-tooltip>Pending</q-tooltip> <q-tooltip>Pending</q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
<q-td key="memo" :props="props"> <q-td key="memo" :props="props">
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
<a
class="inherit"
:href="['/', props.row.tag, '?usr=', user.id].join('')"
>
#{{ props.row.tag }}
</a>
</q-badge>
{{ props.row.memo }} {{ props.row.memo }}
</q-td> </q-td>
<q-td auto-width key="date" :props="props"> <q-td auto-width key="date" :props="props">
@ -120,6 +134,64 @@
{{ props.row.fsat }} {{ props.row.fsat }}
</q-td> </q-td>
</q-tr> </q-tr>
<q-dialog v-model="props.expand" :props="props">
<q-card
v-if="props.row.amount > 0 && props.row.pending"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="receive.paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Close</q-btn
>
</div>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<div v-if="props.row.isPaid && props.row.isIn">
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
Payment Received
</div>
<div v-else-if="props.row.isPaid && props.row.isOut">
<q-icon
size="18px"
:name="'call_made'"
:color="'pink'"
></q-icon>
Payment Sent
</div>
<div v-else>
<q-icon name="settings_ethernet" color="grey"></q-icon>
Outgoing payment pending
</div>
<q-tooltip>Payment Hash</q-tooltip>
<div class="text-wrap mono q-pa-md">
{{ props.row.payment_hash }}
</div>
</div>
</q-card>
</q-dialog>
</template> </template>
{% endraw %} {% endraw %}
</q-table> </q-table>

53
lnbits/core/views/api.py

@ -2,21 +2,24 @@ from flask import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from binascii import unhexlify from binascii import unhexlify
from lnbits import bolt11
from lnbits.core import core_app from lnbits.core import core_app
from lnbits.core.services import create_invoice, pay_invoice
from lnbits.core.crud import delete_expired_invoices
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.settings import WALLET from lnbits.settings import WALLET
from ..services import create_invoice, pay_invoice
@core_app.route("/api/v1/payments", methods=["GET"]) @core_app.route("/api/v1/payments", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_payments(): def api_payments():
if "check_pending" in request.args: if "check_pending" in request.args:
g.wallet.delete_expired_payments() delete_expired_invoices()
for payment in g.wallet.get_payments(complete=False, pending=True): for payment in g.wallet.get_payments(complete=False, pending=True):
if payment.is_out: if payment.is_uncheckable:
pass
elif payment.is_out:
payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending) payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending)
else: else:
payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending) payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending)
@ -41,20 +44,31 @@ def api_payments_create_invoice():
memo = g.data["memo"] memo = g.data["memo"]
try: try:
checking_id, payment_request = create_invoice( payment_hash, payment_request = create_invoice(
wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED invoice = bolt11.decode(payment_request)
return (
jsonify(
{
"payment_hash": invoice.payment_hash,
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
"checking_id": invoice.payment_hash,
}
),
HTTPStatus.CREATED,
)
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
@api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}}) @api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}})
def api_payments_pay_invoice(): def api_payments_pay_invoice():
try: try:
checking_id = pay_invoice(wallet_id=g.wallet.id, bolt11=g.data["bolt11"]) payment_hash = pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["bolt11"])
except ValueError as e: except ValueError as e:
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
except PermissionError as e: except PermissionError as e:
@ -62,7 +76,16 @@ def api_payments_pay_invoice():
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id}), HTTPStatus.CREATED return (
jsonify(
{
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
),
HTTPStatus.CREATED,
)
@core_app.route("/api/v1/payments", methods=["POST"]) @core_app.route("/api/v1/payments", methods=["POST"])
@ -73,10 +96,10 @@ def api_payments_create():
return api_payments_create_invoice() return api_payments_create_invoice()
@core_app.route("/api/v1/payments/<checking_id>", methods=["GET"]) @core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_payment(checking_id): def api_payment(payment_hash):
payment = g.wallet.get_payment(checking_id) payment = g.wallet.get_payment(payment_hash)
if not payment: if not payment:
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
@ -84,10 +107,12 @@ def api_payment(checking_id):
return jsonify({"paid": True}), HTTPStatus.OK return jsonify({"paid": True}), HTTPStatus.OK
try: try:
if payment.is_out: if payment.is_uncheckable:
is_paid = not WALLET.get_payment_status(checking_id).pending pass
elif payment.is_out:
is_paid = not WALLET.get_payment_status(payment.checking_id).pending
elif payment.is_in: elif payment.is_in:
is_paid = not WALLET.get_invoice_status(checking_id).pending is_paid = not WALLET.get_invoice_status(payment.checking_id).pending
except Exception: except Exception:
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK

2
lnbits/core/views/generic.py

@ -64,7 +64,7 @@ def wallet():
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all") allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
if allowed_users != "all" and user_id not in allowed_users.split(","): if allowed_users != "all" and user_id not in allowed_users.split(","):
abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.") abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
if not wallet_id: if not wallet_id:
if user.wallets and not wallet_name: if user.wallets and not wallet_name:

10
lnbits/db.py

@ -15,22 +15,26 @@ class Database:
return self return self
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
self.connection.commit()
self.cursor.close() self.cursor.close()
self.connection.close() self.connection.close()
def fetchall(self, query: str, values: tuple = ()) -> list: def fetchall(self, query: str, values: tuple = ()) -> list:
"""Given a query, return cursor.fetchall() rows.""" """Given a query, return cursor.fetchall() rows."""
self.cursor.execute(query, values) self.execute(query, values)
return self.cursor.fetchall() return self.cursor.fetchall()
def fetchone(self, query: str, values: tuple = ()): def fetchone(self, query: str, values: tuple = ()):
self.cursor.execute(query, values) self.execute(query, values)
return self.cursor.fetchone() return self.cursor.fetchone()
def execute(self, query: str, values: tuple = ()) -> None: def execute(self, query: str, values: tuple = ()) -> None:
"""Given a query, cursor.execute() it.""" """Given a query, cursor.execute() it."""
try:
self.cursor.execute(query, values) self.cursor.execute(query, values)
self.connection.commit() except sqlite3.Error as exc:
self.connection.rollback()
raise exc
def open_db(db_name: str = "database") -> Database: def open_db(db_name: str = "database") -> Database:

36
lnbits/extensions/amilk/views_api.py

@ -1,19 +1,16 @@
from flask import g, jsonify, request import requests
from flask import g, jsonify, request, abort
from http import HTTPStatus from http import HTTPStatus
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl
from lnurl.exceptions import LnurlException
from time import sleep
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.extensions.amilk import amilk_ext from lnbits.extensions.amilk import amilk_ext
from .crud import create_amilk, get_amilk, get_amilks, delete_amilk from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
from lnbits.core.services import create_invoice
from flask import abort, redirect, request, url_for
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl
from lnurl.exceptions import LnurlException
from time import sleep
import requests
from lnbits.settings import WALLET
@amilk_ext.route("/api/v1/amilk", methods=["GET"]) @amilk_ext.route("/api/v1/amilk", methods=["GET"])
@ -36,13 +33,10 @@ def api_amilkit(amilk_id):
withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse) withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse)
except LnurlException: except LnurlException:
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
print(withdraw_res.max_sats)
try: payment_hash, payment_request = create_invoice(
checking_id, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo) wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo, extra={"tag": "amilk"}
# print(payment_request) )
except Exception as e:
error_message = False, str(e)
r = requests.get( r = requests.get(
withdraw_res.callback.base, withdraw_res.callback.base,
@ -50,19 +44,17 @@ def api_amilkit(amilk_id):
) )
if not r.ok: if not r.ok:
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
for i in range(10): for i in range(10):
invoice_status = WALLET.get_invoice_status(checking_id)
sleep(i) sleep(i)
if not invoice_status.paid: invoice_status = check_invoice_status(milk.wallet, payment_hash)
continue if invoice_status.paid:
return jsonify({"paid": True}), HTTPStatus.OK
else: else:
return jsonify({"paid": False}), HTTPStatus.OK continue
break
return jsonify({"paid": True}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK
@amilk_ext.route("/api/v1/amilk", methods=["POST"]) @amilk_ext.route("/api/v1/amilk", methods=["POST"])

24
lnbits/extensions/events/crud.py

@ -9,31 +9,31 @@ from .models import Tickets, Events
#######TICKETS######## #######TICKETS########
def create_ticket(checking_id: str, wallet: str, event: str, name: str, email: str) -> Tickets: def create_ticket(payment_hash: str, wallet: str, event: str, name: str, email: str) -> Tickets:
with open_ext_db("events") as db: with open_ext_db("events") as db:
db.execute( db.execute(
""" """
INSERT INTO ticket (id, wallet, event, name, email, registered, paid) INSERT INTO ticket (id, wallet, event, name, email, registered, paid)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
(checking_id, wallet, event, name, email, False, False), (payment_hash, wallet, event, name, email, False, False),
) )
return get_ticket(checking_id) return get_ticket(payment_hash)
def update_ticket(paid: bool, checking_id: str) -> Tickets: def update_ticket(paid: bool, payment_hash: str) -> Tickets:
with open_ext_db("events") as db: with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,)) row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
if row[6] == True: if row[6] == True:
return get_ticket(checking_id) return get_ticket(payment_hash)
db.execute( db.execute(
""" """
UPDATE ticket UPDATE ticket
SET paid = ? SET paid = ?
WHERE id = ? WHERE id = ?
""", """,
(paid, checking_id), (paid, payment_hash),
) )
eventdata = get_event(row[2]) eventdata = get_event(row[2])
@ -47,12 +47,12 @@ def update_ticket(paid: bool, checking_id: str) -> Tickets:
""", """,
(sold, amount_tickets, row[2]), (sold, amount_tickets, row[2]),
) )
return get_ticket(checking_id) return get_ticket(payment_hash)
def get_ticket(checking_id: str) -> Optional[Tickets]: def get_ticket(payment_hash: str) -> Optional[Tickets]:
with open_ext_db("events") as db: with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,)) row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
return Tickets(**row) if row else None return Tickets(**row) if row else None
@ -68,9 +68,9 @@ def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
return [Tickets(**row) for row in rows] return [Tickets(**row) for row in rows]
def delete_ticket(checking_id: str) -> None: def delete_ticket(payment_hash: str) -> None:
with open_ext_db("events") as db: with open_ext_db("events") as db:
db.execute("DELETE FROM ticket WHERE id = ?", (checking_id,)) db.execute("DELETE FROM ticket WHERE id = ?", (payment_hash,))
########EVENTS######### ########EVENTS#########

2
lnbits/extensions/events/templates/events/display.html

@ -144,7 +144,7 @@
) )
.then(function (response) { .then(function (response) {
self.paymentReq = response.data.payment_request self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.checking_id self.paymentCheck = response.data.payment_hash
dismissMsg = self.$q.notify({ dismissMsg = self.$q.notify({
timeout: 0, timeout: 0,

31
lnbits/extensions/events/views_api.py

@ -2,9 +2,8 @@ from flask import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.settings import WALLET
from lnbits.extensions.events import events_ext from lnbits.extensions.events import events_ext
from .crud import ( from .crud import (
@ -108,39 +107,37 @@ def api_tickets():
} }
) )
def api_ticket_make_ticket(event_id, sats): def api_ticket_make_ticket(event_id, sats):
event = get_event(event_id) event = get_event(event_id)
if not event: if not event:
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
try: try:
checking_id, payment_request = create_invoice( payment_hash, payment_request = create_invoice(
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}" wallet_id=event.wallet, amount=int(sats), memo=f"{event_id}", extra={"tag": "events"}
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, event=event_id, **g.data) ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, event=event_id, **g.data)
if not ticket: if not ticket:
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Event could not be fetched."}), HTTPStatus.NOT_FOUND
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK
@events_ext.route("/api/v1/tickets/<checking_id>", methods=["GET"]) @events_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])
def api_ticket_send_ticket(checking_id): def api_ticket_send_ticket(payment_hash):
theticket = get_ticket(checking_id) ticket = get_ticket(payment_hash)
try: try:
is_paid = not WALLET.get_invoice_status(checking_id).pending is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending
except Exception: except Exception:
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
if is_paid: if is_paid:
wallet = get_wallet(theticket.wallet) wallet = get_wallet(ticket.wallet)
payment = wallet.get_payment(checking_id) payment = wallet.get_payment(payment_hash)
payment.set_pending(False) payment.set_pending(False)
ticket = update_ticket(paid=True, checking_id=checking_id) ticket = update_ticket(paid=True, payment_hash=payment_hash)
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK

16
lnbits/extensions/lnticket/crud.py

@ -9,31 +9,31 @@ from .models import Tickets, Forms
#######TICKETS######## #######TICKETS########
def create_ticket(checking_id: str, wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets: def create_ticket(payment_hash: str, wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets:
with open_ext_db("lnticket") as db: with open_ext_db("lnticket") as db:
db.execute( db.execute(
""" """
INSERT INTO ticket (id, form, email, ltext, name, wallet, sats, paid) INSERT INTO ticket (id, form, email, ltext, name, wallet, sats, paid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(checking_id, form, email, ltext, name, wallet, sats, False), (payment_hash, form, email, ltext, name, wallet, sats, False),
) )
return get_ticket(checking_id) return get_ticket(payment_hash)
def update_ticket(paid: bool, checking_id: str) -> Tickets: def update_ticket(paid: bool, payment_hash: str) -> Tickets:
with open_ext_db("lnticket") as db: with open_ext_db("lnticket") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,)) row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
if row[7] == True: if row[7] == True:
return get_ticket(checking_id) return get_ticket(payment_hash)
db.execute( db.execute(
""" """
UPDATE ticket UPDATE ticket
SET paid = ? SET paid = ?
WHERE id = ? WHERE id = ?
""", """,
(paid, checking_id), (paid, payment_hash),
) )
formdata = get_form(row[1]) formdata = get_form(row[1])
@ -46,7 +46,7 @@ def update_ticket(paid: bool, checking_id: str) -> Tickets:
""", """,
(amount, row[1]), (amount, row[1]),
) )
return get_ticket(checking_id) return get_ticket(payment_hash)
def get_ticket(ticket_id: str) -> Optional[Tickets]: def get_ticket(ticket_id: str) -> Optional[Tickets]:

24
lnbits/extensions/lnticket/templates/lnticket/display.html

@ -106,15 +106,15 @@
computed: { computed: {
amountWords() { amountWords() {
var regex = /\s+/gi var regex = /\s+/gi
var char = this.formDialog.data.text var nwords = this.formDialog.data.text
.trim() .trim()
.replace(regex, ' ') .replace(regex, ' ')
.split(' ').length .split(' ').length
this.formDialog.data.sats = char * parseInt('{{ form_costpword }}') var sats = nwords * parseInt('{{ form_costpword }}')
if (this.formDialog.data.sats == parseInt('{{ form_costpword }}')) { if (sats === parseInt('{{ form_costpword }}')) {
return '0 Sats to pay' return '0 Sats to pay'
} else { } else {
return this.formDialog.data.sats + ' Sats to pay' return sats + ' Sats to pay'
} }
} }
}, },
@ -125,7 +125,6 @@
this.formDialog.data.name = '' this.formDialog.data.name = ''
this.formDialog.data.email = '' this.formDialog.data.email = ''
this.formDialog.data.text = '' this.formDialog.data.text = ''
this.formDialog.data.sats = 0
}, },
closeReceiveDialog: function () { closeReceiveDialog: function () {
@ -138,21 +137,15 @@
Invoice: function () { Invoice: function () {
var self = this var self = this
axios axios
.post( .post('/lnticket/api/v1/tickets/{{ form_id }}', {
'/lnticket/api/v1/tickets/' +
'{{ form_id }}/' +
self.formDialog.data.sats,
{
form: '{{ form_id }}', form: '{{ form_id }}',
name: self.formDialog.data.name, name: self.formDialog.data.name,
email: self.formDialog.data.email, email: self.formDialog.data.email,
ltext: self.formDialog.data.text, ltext: self.formDialog.data.text
sats: self.formDialog.data.sats })
}
)
.then(function (response) { .then(function (response) {
self.paymentReq = response.data.payment_request self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.checking_id self.paymentCheck = response.data.payment_hash
dismissMsg = self.$q.notify({ dismissMsg = self.$q.notify({
timeout: 0, timeout: 0,
@ -175,7 +168,6 @@
self.formDialog.data.name = '' self.formDialog.data.name = ''
self.formDialog.data.email = '' self.formDialog.data.email = ''
self.formDialog.data.text = '' self.formDialog.data.text = ''
self.formDialog.data.sats = 0
self.$q.notify({ self.$q.notify({
type: 'positive', type: 'positive',

46
lnbits/extensions/lnticket/views_api.py

@ -1,10 +1,10 @@
import re
from flask import g, jsonify, request from flask import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.settings import WALLET
from lnbits.extensions.lnticket import lnticket_ext from lnbits.extensions.lnticket import lnticket_ext
from .crud import ( from .crud import (
@ -49,7 +49,6 @@ def api_forms():
def api_form_create(form_id=None): def api_form_create(form_id=None):
if form_id: if form_id:
form = get_form(form_id) form = get_form(form_id)
print(g.data)
if not form: if not form:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
@ -93,51 +92,52 @@ def api_tickets():
return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK
@lnticket_ext.route("/api/v1/tickets/<form_id>/<sats>", methods=["POST"]) @lnticket_ext.route("/api/v1/tickets/<form_id>", methods=["POST"])
@api_validate_post_request( @api_validate_post_request(
schema={ schema={
"form": {"type": "string", "empty": False, "required": True}, "form": {"type": "string", "empty": False, "required": True},
"name": {"type": "string", "empty": False, "required": True}, "name": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True}, "email": {"type": "string", "empty": True, "required": True},
"ltext": {"type": "string", "empty": False, "required": True}, "ltext": {"type": "string", "empty": False, "required": True},
"sats": {"type": "integer", "min": 0, "required": True},
} }
) )
def api_ticket_make_ticket(form_id, sats): def api_ticket_make_ticket(form_id):
form = get_form(form_id)
event = get_form(form_id) if not form:
if not event:
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
try: try:
checking_id, payment_request = create_invoice( nwords = len(re.split(r"\s+", g.data["ltext"]))
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {form_id}" sats = nwords * form.costpword
payment_hash, payment_request = create_invoice(
wallet_id=form.wallet,
amount=sats,
memo=f"ticket with {nwords} words on {form_id}",
extra={"tag": "lnticket"},
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, **g.data) ticket = create_ticket(payment_hash=payment_hash, wallet=form.wallet, sats=sats, **g.data)
if not ticket: if not ticket:
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK
@lnticket_ext.route("/api/v1/tickets/<checking_id>", methods=["GET"]) @lnticket_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])
def api_ticket_send_ticket(checking_id): def api_ticket_send_ticket(payment_hash):
theticket = get_ticket(checking_id) ticket = get_ticket(payment_hash)
try: try:
is_paid = not WALLET.get_invoice_status(checking_id).pending is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending
except Exception: except Exception:
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
if is_paid: if is_paid:
wallet = get_wallet(theticket.wallet) wallet = get_wallet(ticket.wallet)
payment = wallet.get_payment(checking_id) payment = wallet.get_payment(payment_hash)
payment.set_pending(False) payment.set_pending(False)
ticket = update_ticket(paid=True, checking_id=checking_id) ticket = update_ticket(paid=True, payment_hash=payment_hash)
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK

1
lnbits/extensions/lnurlp/views_api.py

@ -123,6 +123,7 @@ def api_lnurl_callback(link_id):
amount=link.amount, amount=link.amount,
memo=link.description, memo=link.description,
description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(), description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(),
extra={"tag": "lnurlp"},
) )
resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[]) resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[])

6
lnbits/extensions/paywall/templates/paywall/_api_docs.html

@ -75,7 +75,7 @@
Returns 201 CREATED (application/json) Returns 201 CREATED (application/json)
</h5> </h5>
<code <code
>{"checking_id": &lt;string&gt;, "payment_request": >{"payment_hash": &lt;string&gt;, "payment_request":
&lt;string&gt;}</code &lt;string&gt;}</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
@ -100,7 +100,7 @@
/paywall/api/v1/paywalls/&lt;paywall_id&gt;/check_invoice</code /paywall/api/v1/paywalls/&lt;paywall_id&gt;/check_invoice</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"checking_id": &lt;string&gt;}</code> <code>{"payment_hash": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json) Returns 200 OK (application/json)
</h5> </h5>
@ -113,7 +113,7 @@
<code <code
>curl -X POST {{ request.url_root >curl -X POST {{ request.url_root
}}paywall/api/v1/paywalls/&lt;paywall_id&gt;/check_invoice -d }}paywall/api/v1/paywalls/&lt;paywall_id&gt;/check_invoice -d
'{"checking_id": &lt;string&gt;}' -H "Content-type: application/json" '{"payment_hash": &lt;string&gt;}' -H "Content-type: application/json"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>

2
lnbits/extensions/paywall/templates/paywall/display.html

@ -121,7 +121,7 @@
axios axios
.post( .post(
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice', '/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
{checking_id: response.data.checking_id} {payment_hash: response.data.payment_hash}
) )
.then(function (res) { .then(function (res) {
if (res.data.paid) { if (res.data.paid) {

15
lnbits/extensions/paywall/views_api.py

@ -2,9 +2,8 @@ from flask import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.settings import WALLET
from lnbits.extensions.paywall import paywall_ext from lnbits.extensions.paywall import paywall_ext
from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall
@ -64,17 +63,17 @@ def api_paywall_create_invoice(paywall_id):
try: try:
amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount
checking_id, payment_request = create_invoice( payment_hash, payment_request = create_invoice(
wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}" wallet_id=paywall.wallet, amount=amount, memo=f"{paywall.memo}", extra={"tag": "paywall"}
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.CREATED
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"]) @paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
@api_validate_post_request(schema={"checking_id": {"type": "string", "empty": False, "required": True}}) @api_validate_post_request(schema={"payment_hash": {"type": "string", "empty": False, "required": True}})
def api_paywal_check_invoice(paywall_id): def api_paywal_check_invoice(paywall_id):
paywall = get_paywall(paywall_id) paywall = get_paywall(paywall_id)
@ -82,13 +81,13 @@ def api_paywal_check_invoice(paywall_id):
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
try: try:
is_paid = not WALLET.get_invoice_status(g.data["checking_id"]).pending is_paid = not check_invoice_status(paywall.wallet, g.data["payment_hash"]).pending
except Exception: except Exception:
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK
if is_paid: if is_paid:
wallet = get_wallet(paywall.wallet) wallet = get_wallet(paywall.wallet)
payment = wallet.get_payment(g.data["checking_id"]) payment = wallet.get_payment(g.data["payment_hash"])
payment.set_pending(False) payment.set_pending(False)
return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK

2
lnbits/extensions/tpos/templates/tpos/tpos.html

@ -224,7 +224,7 @@
'/tpos/api/v1/tposs/' + '/tpos/api/v1/tposs/' +
self.tposId + self.tposId +
'/invoices/' + '/invoices/' +
response.data.checking_id response.data.payment_hash
) )
.then(function (res) { .then(function (res) {
if (res.data.paid) { if (res.data.paid) {

20
lnbits/extensions/tpos/views_api.py

@ -2,9 +2,8 @@ from flask import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.settings import WALLET
from lnbits.extensions.tpos import tpos_ext from lnbits.extensions.tpos import tpos_ext
from .crud import create_tpos, get_tpos, get_tposs, delete_tpos from .crud import create_tpos, get_tpos, get_tposs, delete_tpos
@ -60,30 +59,31 @@ def api_tpos_create_invoice(tpos_id):
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
try: try:
checking_id, payment_request = create_invoice( payment_hash, payment_request = create_invoice(
wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"#tpos {tpos.name}" wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"{tpos.name}", extra={"tag": "tpos"}
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.CREATED
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<checking_id>", methods=["GET"]) @tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<payment_hash>", methods=["GET"])
def api_tpos_check_invoice(tpos_id, checking_id): def api_tpos_check_invoice(tpos_id, payment_hash):
tpos = get_tpos(tpos_id) tpos = get_tpos(tpos_id)
if not tpos: if not tpos:
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
try: try:
is_paid = not WALLET.get_invoice_status(checking_id).pending is_paid = not check_invoice_status(tpos.wallet, payment_hash).pending
except Exception: except Exception as exc:
print(exc)
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK
if is_paid: if is_paid:
wallet = get_wallet(tpos.wallet) wallet = get_wallet(tpos.wallet)
payment = wallet.get_payment(checking_id) payment = wallet.get_payment(payment_hash)
payment.set_pending(False) payment.set_pending(False)
return jsonify({"paid": True}), HTTPStatus.OK return jsonify({"paid": True}), HTTPStatus.OK

8
lnbits/extensions/usermanager/templates/usermanager/_api_docs.html

@ -122,7 +122,8 @@
Returns 201 CREATED (application/json) Returns 201 CREATED (application/json)
</h5> </h5>
<code <code
>{"checking_id": &lt;string&gt;,"payment_request": >{"id": &lt;string&gt;, "name": &lt;string&gt;, "admin":
&lt;string&gt;, "email": &lt;string&gt;, "password":
&lt;string&gt;}</code &lt;string&gt;}</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
@ -158,8 +159,9 @@
Returns 201 CREATED (application/json) Returns 201 CREATED (application/json)
</h5> </h5>
<code <code
>{"checking_id": &lt;string&gt;,"payment_request": >{"id": &lt;string&gt;, "admin": &lt;string&gt;, "name":
&lt;string&gt;}</code &lt;string&gt;, "user": &lt;string&gt;, "adminkey": &lt;string&gt;,
"inkey": &lt;string&gt;}</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code

2
lnbits/extensions/withdraw/models.py

@ -62,5 +62,5 @@ class WithdrawLink(NamedTuple):
k1=self.k1, k1=self.k1,
min_withdrawable=self.min_withdrawable * 1000, min_withdrawable=self.min_withdrawable * 1000,
max_withdrawable=self.max_withdrawable * 1000, max_withdrawable=self.max_withdrawable * 1000,
default_description="#withdraw LNbits LNURL", default_description="LNbits voucher",
) )

8
lnbits/extensions/withdraw/views_api.py

@ -182,14 +182,18 @@ def api_lnurl_callback(unique_hash):
return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK
try: try:
pay_invoice(wallet_id=link.wallet, bolt11=payment_request, max_sat=link.max_withdrawable) pay_invoice(
wallet_id=link.wallet,
payment_request=payment_request,
max_sat=link.max_withdrawable,
extra={"tag": "withdraw"},
)
changes = { changes = {
"open_time": link.wait_time + now, "open_time": link.wait_time + now,
} }
update_withdraw_link(link.id, **changes) update_withdraw_link(link.id, **changes)
except ValueError as e: except ValueError as e:
return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK
except PermissionError: except PermissionError:

9
lnbits/static/css/base.css

@ -66,3 +66,12 @@ a.inherit {
direction: ltr; direction: ltr;
-moz-font-feature-settings: 'liga'; -moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale; } -moz-osx-font-smoothing: grayscale; }
.text-wrap {
word-wrap: break-word;
word-break: break-all;
}
.mono {
font-family: monospace;
}

32
lnbits/static/js/base.js

@ -1,3 +1,5 @@
/* globals Vue, EventHub, axios, Quasar, _ */
var LOCALE = 'en' var LOCALE = 'en'
var EventHub = new Vue() var EventHub = new Vue()
@ -35,8 +37,12 @@ var LNbits = {
wallet.inkey wallet.inkey
) )
}, },
getPayment: function (wallet, payhash) { getPayment: function (wallet, paymentHash) {
return this.request('get', '/api/v1/payments/' + payhash, wallet.inkey) return this.request(
'get',
'/api/v1/payments/' + paymentHash,
wallet.inkey
)
} }
}, },
href: { href: {
@ -88,7 +94,18 @@ var LNbits = {
}, },
payment: function (data) { payment: function (data) {
var obj = _.object( var obj = _.object(
['payhash', 'pending', 'amount', 'fee', 'memo', 'time'], [
'checking_id',
'pending',
'amount',
'fee',
'memo',
'time',
'bolt11',
'preimage',
'payment_hash',
'extra'
],
data data
) )
obj.date = Quasar.utils.date.formatDate( obj.date = Quasar.utils.date.formatDate(
@ -97,10 +114,11 @@ var LNbits = {
) )
obj.msat = obj.amount obj.msat = obj.amount
obj.sat = obj.msat / 1000 obj.sat = obj.msat / 1000
obj.tag = obj.extra.tag
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat) obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat)
obj.isIn = obj.amount > 0 obj.isIn = obj.amount > 0
obj.isOut = obj.amount < 0 obj.isOut = obj.amount < 0
obj.isPaid = obj.pending == 0 obj.isPaid = obj.pending === 0
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase() obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
return obj return obj
} }
@ -146,8 +164,6 @@ var LNbits = {
}) })
}, },
search: function (data, q, field, separator) { search: function (data, q, field, separator) {
var field = field || '_q'
try { try {
var queries = q.toLowerCase().split(separator || ' ') var queries = q.toLowerCase().split(separator || ' ')
return data.filter(function (obj) { return data.filter(function (obj) {
@ -155,7 +171,7 @@ var LNbits = {
_.each(queries, function (q) { _.each(queries, function (q) {
if (obj[field].indexOf(q) !== -1) matches++ if (obj[field].indexOf(q) !== -1) matches++
}) })
return matches == queries.length return matches === queries.length
}) })
} catch (err) { } catch (err) {
return data return data
@ -255,7 +271,7 @@ var windowMixin = {
}) })
.map(function (obj) { .map(function (obj) {
if (user) { if (user) {
obj.isEnabled = user.extensions.indexOf(obj.code) != -1 obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
} else { } else {
obj.isEnabled = false obj.isEnabled = false
} }

3
package.json

@ -1,8 +1,5 @@
{ {
"devDependencies": { "devDependencies": {
"prettier": "^2.0.5" "prettier": "^2.0.5"
},
"scripts": {
"lint": "prettier --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**"
} }
} }

Loading…
Cancel
Save