This commit is contained in:
benarc 2022-01-30 19:43:30 +00:00
parent d6f145eee9
commit 228717195b
31 changed files with 278 additions and 182 deletions

View File

@ -48,10 +48,7 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_methods=["*"],
allow_headers=["*"],
CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"]
)
g().config = lnbits.settings

View File

@ -116,7 +116,6 @@ def decode(pr: str) -> Invoice:
return invoice
def encode(options):
""" Convert options into LnAddr and pass it to the encoder
"""
@ -131,27 +130,31 @@ def encode(options):
addr.paymenthash = unhexlify(options.paymenthash)
if options.description:
addr.tags.append(('d', options.description))
addr.tags.append(("d", options.description))
if options.description_hashed:
addr.tags.append(('h', options.description_hashed))
addr.tags.append(("h", options.description_hashed))
if options.expires:
addr.tags.append(('x', options.expires))
addr.tags.append(("x", options.expires))
if options.fallback:
addr.tags.append(('f', options.fallback))
addr.tags.append(("f", options.fallback))
for r in options.route:
splits = r.split('/')
route=[]
splits = r.split("/")
route = []
while len(splits) >= 5:
route.append((unhexlify(splits[0]),
unhexlify(splits[1]),
int(splits[2]),
int(splits[3]),
int(splits[4])))
route.append(
(
unhexlify(splits[0]),
unhexlify(splits[1]),
int(splits[2]),
int(splits[3]),
int(splits[4]),
)
)
splits = splits[5:]
assert(len(splits) == 0)
addr.tags.append(('r', route))
assert len(splits) == 0
addr.tags.append(("r", route))
return lnencode(addr, options.privkey)
@ -159,21 +162,22 @@ def lnencode(addr, privkey):
if addr.amount:
amount = Decimal(str(addr.amount))
# We can only send down to millisatoshi.
if amount * 10**12 % 10:
raise ValueError("Cannot encode {}: too many decimal places".format(
addr.amount))
if amount * 10 ** 12 % 10:
raise ValueError(
"Cannot encode {}: too many decimal places".format(addr.amount)
)
amount = addr.currency + shorten_amount(amount)
else:
amount = addr.currency if addr.currency else ''
amount = addr.currency if addr.currency else ""
hrp = 'ln' + amount
hrp = "ln" + amount
# Start with the timestamp
data = bitstring.pack('uint:35', addr.date)
data = bitstring.pack("uint:35", addr.date)
# Payment hash
data += tagged_bytes('p', addr.paymenthash)
data += tagged_bytes("p", addr.paymenthash)
tags_set = set()
for k, v in addr.tags:
@ -181,30 +185,36 @@ def lnencode(addr, privkey):
# BOLT #11:
#
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
if k in ('d', 'h', 'n', 'x'):
if k in ("d", "h", "n", "x"):
if k in tags_set:
raise ValueError("Duplicate '{}' tag".format(k))
if k == 'r':
if k == "r":
route = bitstring.BitArray()
for step in v:
pubkey, channel, feebase, feerate, cltv = step
route.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv))
data += tagged('r', route)
elif k == 'f':
route.append(
bitstring.BitArray(pubkey)
+ bitstring.BitArray(channel)
+ bitstring.pack("intbe:32", feebase)
+ bitstring.pack("intbe:32", feerate)
+ bitstring.pack("intbe:16", cltv)
)
data += tagged("r", route)
elif k == "f":
data += encode_fallback(v, addr.currency)
elif k == 'd':
data += tagged_bytes('d', v.encode())
elif k == 'x':
elif k == "d":
data += tagged_bytes("d", v.encode())
elif k == "x":
# Get minimal length by trimming leading 5 bits at a time.
expirybits = bitstring.pack('intbe:64', v)[4:64]
while expirybits.startswith('0b00000'):
expirybits = bitstring.pack("intbe:64", v)[4:64]
while expirybits.startswith("0b00000"):
expirybits = expirybits[5:]
data += tagged('x', expirybits)
elif k == 'h':
data += tagged_bytes('h', hashlib.sha256(v.encode('utf-8')).digest())
elif k == 'n':
data += tagged_bytes('n', v)
data += tagged("x", expirybits)
elif k == "h":
data += tagged_bytes("h", hashlib.sha256(v.encode("utf-8")).digest())
elif k == "n":
data += tagged_bytes("n", v)
else:
# FIXME: Support unknown tags?
raise ValueError("Unknown tag {}".format(k))
@ -215,26 +225,31 @@ def lnencode(addr, privkey):
#
# A writer MUST include either a `d` or `h` field, and MUST NOT include
# both.
if 'd' in tags_set and 'h' in tags_set:
if "d" in tags_set and "h" in tags_set:
raise ValueError("Cannot include both 'd' and 'h'")
if not 'd' in tags_set and not 'h' in tags_set:
if not "d" in tags_set and not "h" in tags_set:
raise ValueError("Must include either 'd' or 'h'")
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
sig = privkey.ecdsa_sign_recoverable(bytearray([ord(c) for c in hrp]) + data.tobytes())
sig = privkey.ecdsa_sign_recoverable(
bytearray([ord(c) for c in hrp]) + data.tobytes()
)
# This doesn't actually serialize, but returns a pair of values :(
sig, recid = privkey.ecdsa_recoverable_serialize(sig)
data += bytes(sig) + bytes([recid])
return bech32_encode(hrp, bitarray_to_u5(data))
class LnAddr(object):
def __init__(self, paymenthash=None, amount=None, currency='bc', tags=None, date=None):
def __init__(
self, paymenthash=None, amount=None, currency="bc", tags=None, date=None
):
self.date = int(time.time()) if not date else int(date)
self.tags = [] if not tags else tags
self.unknown_tags = []
self.paymenthash=paymenthash
self.paymenthash = paymenthash
self.signature = None
self.pubkey = None
self.currency = currency
@ -242,13 +257,13 @@ class LnAddr(object):
def __str__(self):
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
hexlify(self.pubkey.serialize()).decode('utf-8'),
self.amount, self.currency,
", ".join([k + '=' + str(v) for k, v in self.tags])
hexlify(self.pubkey.serialize()).decode("utf-8"),
self.amount,
self.currency,
", ".join([k + "=" + str(v) for k, v in self.tags]),
)
def _unshorten_amount(amount: str) -> int:
"""Given a shortened amount, return millisatoshis"""
# BOLT #11:

View File

@ -30,8 +30,7 @@ async def get_account(
user_id: str, conn: Optional[Connection] = None
) -> Optional[User]:
row = await (conn or db).fetchone(
"SELECT id, email, pass as password FROM accounts WHERE id = ?", (
user_id,)
"SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,)
)
return User(**row) if row else None
@ -305,8 +304,7 @@ async def delete_expired_invoices(conn: Optional[Connection] = None,) -> None:
except:
continue
expiration_date = datetime.datetime.fromtimestamp(
invoice.date + invoice.expiry)
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
if expiration_date > datetime.datetime.utcnow():
continue

View File

@ -95,7 +95,7 @@ async def pay_invoice(
if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.")
wallet = await get_wallet(wallet_id, conn=conn)
wallet = await get_wallet(wallet_id, conn=conn)
# put all parameters that don't change here
PaymentKwargs = TypedDict(
@ -141,15 +141,19 @@ async def pay_invoice(
# do the balance check if internal payment
if internal_checking_id:
wallet = await get_wallet(wallet_id, conn=conn)
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
if wallet.balance_msat < 0:
raise PermissionError("Insufficient balance.")
# do the balance check if external payment
else:
if invoice.amount_msat > wallet.balance_msat - (wallet.balance_msat / 100 * 2):
raise PermissionError("LNbits requires you keep at least 2% reserve to cover potential routing fees.")
if invoice.amount_msat > wallet.balance_msat - (
wallet.balance_msat / 100 * 2
):
raise PermissionError(
"LNbits requires you keep at least 2% reserve to cover potential routing fees."
)
if internal_checking_id:
# mark the invoice from the other side as not pending anymore
@ -326,8 +330,7 @@ async def check_invoice_status(
if not payment.pending:
return status
if payment.is_out and status.failed:
print(
f" - deleting outgoing failed payment {payment.checking_id}: {status}")
print(f" - deleting outgoing failed payment {payment.checking_id}: {status}")
await payment.delete()
elif not status.pending:
print(

View File

@ -77,7 +77,9 @@ async def api_update_wallet(
@core_app.get("/api/v1/payments")
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
pendingPayments = await get_payments(wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True)
pendingPayments = await get_payments(
wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True
)
for payment in pendingPayments:
await check_invoice_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
@ -198,8 +200,7 @@ async def api_payments_create(
invoiceData: CreateInvoiceData = Body(...),
):
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11:
@ -379,16 +380,14 @@ async def api_lnurlscan(code: str):
params.update(callback=url) # with k1 already in it
lnurlauth_key = g().wallet.lnurlauth_key(domain)
params.update(
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
r = await client.get(url, timeout=5)
if r.is_error:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={"domain": domain,
"message": "failed to get parameters"},
detail={"domain": domain, "message": "failed to get parameters"},
)
try:
@ -418,8 +417,7 @@ async def api_lnurlscan(code: str):
if tag == "withdrawRequest":
params.update(kind="withdraw")
params.update(fixed=data["minWithdrawable"]
== data["maxWithdrawable"])
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
# callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"])
@ -509,18 +507,21 @@ async def api_list_currencies_available():
class ConversionData(BaseModel):
from_: str = Field('sat', alias="from")
from_: str = Field("sat", alias="from")
amount: float
to: str = Query('usd')
to: str = Query("usd")
@core_app.post("/api/v1/conversion")
async def api_fiat_as_sats(data: ConversionData):
output = {}
if data.from_ == 'sat':
if data.from_ == "sat":
output["sats"] = int(data.amount)
output["BTC"] = data.amount / 100000000
for currency in data.to.split(','):
output[currency.strip().upper()] = await satoshis_amount_as_fiat(data.amount, currency.strip())
for currency in data.to.split(","):
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
data.amount, currency.strip()
)
return output
else:
output[data.from_.upper()] = data.amount

View File

@ -43,9 +43,7 @@ async def home(request: Request, lightning: str = None):
@core_html_routes.get(
"/extensions",
name="core.extensions",
response_class=HTMLResponse,
"/extensions", name="core.extensions", response_class=HTMLResponse
)
async def extensions(
request: Request,

View File

@ -171,6 +171,7 @@ async def require_admin_key(
else:
return wallet
async def require_invoice_key(
r: Request,
api_key_header: str = Security(api_key_header),
@ -184,7 +185,8 @@ async def require_invoice_key(
# If wallet type is not invoice then return the unauthorized status
# This also covers when the user passes an invalid key type
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invoice (or Admin) key required."
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
else:
return wallet

View File

@ -52,7 +52,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
amount_received = amount
domain = await get_domain(address.domain)
base_url = (
address.wallet_endpoint[:-1]
if address.wallet_endpoint.endswith("/")
@ -71,7 +71,9 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
"out": False,
"amount": int(amount_received / 1000),
"description_hash": hashlib.sha256(
(await address.lnurlpay_metadata(domain=domain.domain)).encode("utf-8")
(await address.lnurlpay_metadata(domain=domain.domain)).encode(
"utf-8"
)
).hexdigest(),
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
},

View File

@ -53,5 +53,5 @@ class Addresses(BaseModel):
text = f"Payment to {self.username}"
identifier = f"{self.username}@{domain}"
metadata = [["text/plain", text], ["text/identifier", identifier]]
return LnurlPayMetadata(json.dumps(metadata))

View File

@ -36,7 +36,7 @@ async def display(domain_id, request: Request):
wallet = await get_wallet(domain.wallet)
url = urlparse(str(request.url))
return lnaddress_renderer().TemplateResponse(
"lnaddress/display.html",
{
@ -45,6 +45,6 @@ async def display(domain_id, request: Request):
"domain_domain": domain.domain,
"domain_cost": domain.cost,
"domain_wallet_inkey": wallet.inkey,
"root_url": f"{url.scheme}://{url.netloc}"
"root_url": f"{url.scheme}://{url.netloc}",
},
)

View File

@ -24,7 +24,15 @@ async def create_lnurldevice(data: createLnurldevice,) -> lnurldevices:
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(lnurldevice_id, lnurldevice_key, data.title, data.wallet, data.currency, data.device, data.profit,),
(
lnurldevice_id,
lnurldevice_key,
data.title,
data.wallet,
data.currency,
data.device,
data.profit,
),
)
return await get_lnurldevice(lnurldevice_id)
@ -63,7 +71,9 @@ async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevic
async def delete_lnurldevice(lnurldevice_id: str) -> None:
await db.execute("DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,))
await db.execute(
"DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
########################lnuldevice payments###########################
@ -102,19 +112,23 @@ async def update_lnurldevicepayment(
(*kwargs.values(), lnurldevicepayment_id),
)
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,)
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
(lnurldevicepayment_id,),
)
return lnurldevicepayment(**row) if row else None
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,)
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
(lnurldevicepayment_id,),
)
return lnurldevicepayment(**row) if row else None
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?", (lnurldevicepayment_payload,)
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?",
(lnurldevicepayment_payload,),
)
return lnurldevicepayment(**row) if row else None
return lnurldevicepayment(**row) if row else None

View File

@ -105,10 +105,7 @@ async def lnurl_v1_params(
paymentcheck = await get_lnurlpayload(p)
if device.device == "atm":
if paymentcheck:
return {
"status": "ERROR",
"reason": f"Payment already claimed",
}
return {"status": "ERROR", "reason": f"Payment already claimed"}
if len(p) % 4 > 0:
p += "=" * (4 - (len(p) % 4))
@ -174,27 +171,28 @@ async def lnurl_v1_params(
}
@lnurldevice_ext.get(
"/api/v1/lnurl/cb/{paymentid}",
status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_callback",
)
async def lnurl_callback(request: Request, paymentid: str = Query(None), pr: str = Query(None), k1: str = Query(None)):
async def lnurl_callback(
request: Request,
paymentid: str = Query(None),
pr: str = Query(None),
k1: str = Query(None),
):
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found."
)
if pr:
if pr:
if lnurldevicepayment.id != k1:
return {"status": "ERROR", "reason": "Bad K1"}
if lnurldevicepayment.payhash != "payment_hash":
return {
"status": "ERROR",
"reason": f"Payment already claimed",
}
return {"status": "ERROR", "reason": f"Payment already claimed"}
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
)

View File

@ -2,6 +2,7 @@ from lnbits.db import Database
db2 = Database("ext_lnurlpos")
async def m001_initial(db):
"""
Initial lnurldevice table.
@ -59,7 +60,8 @@ async def m002_redux(db):
(row[0], row[1], row[2], row[3], row[4], "pos", 0),
)
for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpospayment")
list(row)
for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpospayment")
]:
await db.execute(
"""
@ -76,4 +78,4 @@ async def m002_redux(db):
(row[0], row[1], row[3], row[4], row[5], row[6]),
)
except:
return
return

View File

@ -33,12 +33,15 @@ class lnurldevices(BaseModel):
return cls(**dict(row))
def lnurl(self, req: Request) -> Lnurl:
url = req.url_for("lnurldevice.lnurl_response", device_id=self.id, _external=True)
url = req.url_for(
"lnurldevice.lnurl_response", device_id=self.id, _external=True
)
return lnurl_encode(url)
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
class lnurldevicepayment(BaseModel):
id: str
deviceid: str

View File

@ -41,10 +41,13 @@ async def displaypin(request: Request, paymentid: str = Query(None)):
)
status = await api_payment(lnurldevicepayment.payhash)
if status["paid"]:
await update_payment_status(checking_id=lnurldevicepayment.payhash, pending=True)
await update_payment_status(
checking_id=lnurldevicepayment.payhash, pending=True
)
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
)
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/error.html", {"request": request, "pin": "filler", "not_paid": True}
"lnurldevice/error.html",
{"request": request, "pin": "filler", "not_paid": True},
)

View File

@ -48,7 +48,9 @@ async def api_lnurldevice_create_or_update(
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)]
return [
{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)
]
except:
return ""
@ -71,7 +73,8 @@ async def api_lnurldevice_retrieve(
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_delete(
wallet: WalletTypeInfo = Depends(require_admin_key), lnurldevice_id: str = Query(None)
wallet: WalletTypeInfo = Depends(require_admin_key),
lnurldevice_id: str = Query(None),
):
lnurldevice = await get_lnurldevice(lnurldevice_id)

View File

@ -13,10 +13,12 @@ lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout
def lnurlpayout_renderer():
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def lnurlpayout_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View File

@ -6,14 +6,23 @@ from . import db
from .models import lnurlpayout, CreateLnurlPayoutData
async def create_lnurlpayout(wallet_id: str, admin_key: str, data: CreateLnurlPayoutData) -> lnurlpayout:
async def create_lnurlpayout(
wallet_id: str, admin_key: str, data: CreateLnurlPayoutData
) -> lnurlpayout:
lnurlpayout_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
VALUES (?, ?, ?, ?, ?, ?)
""",
(lnurlpayout_id, data.title, wallet_id, admin_key, data.lnurlpay, data.threshold),
(
lnurlpayout_id,
data.title,
wallet_id,
admin_key,
data.lnurlpay,
data.threshold,
),
)
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
@ -22,13 +31,19 @@ async def create_lnurlpayout(wallet_id: str, admin_key: str, data: CreateLnurlPa
async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]:
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,))
row = await db.fetchone(
"SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
)
return lnurlpayout(**row) if row else None
async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]:
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,))
row = await db.fetchone(
"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,)
)
return lnurlpayout(**row) if row else None
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
@ -42,4 +57,6 @@ async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayou
async def delete_lnurlpayout(lnurlpayout_id: str) -> None:
await db.execute("DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,))
await db.execute(
"DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
)

View File

@ -2,11 +2,13 @@ from sqlite3 import Row
from pydantic import BaseModel
class CreateLnurlPayoutData(BaseModel):
title: str
lnurlpay: str
threshold: int
class lnurlpayout(BaseModel):
id: str
title: str

View File

@ -26,28 +26,33 @@ async def on_invoice_paid(payment: Payment) -> None:
# Check its got a payout associated with it
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
if lnurlpayout_link:
# Check the wallet balance is more than the threshold
wallet = await get_wallet(lnurlpayout_link.wallet)
if wallet.balance < lnurlpayout_link.threshold + (lnurlpayout_link.threshold*0.02):
if wallet.balance < lnurlpayout_link.threshold + (
lnurlpayout_link.threshold * 0.02
):
return
# Get the invoice from the LNURL to pay
async with httpx.AsyncClient() as client:
try:
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
if str(url["domain"])[0:4] != "http":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken")
try:
r = await client.get(
str(url["domain"]),
timeout=40,
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
)
try:
r = await client.get(str(url["domain"]), timeout=40)
res = r.json()
try:
r = await client.get(
res["callback"] + "?amount=" + str(int((wallet.balance - wallet.balance*0.02) * 1000)),
res["callback"]
+ "?amount="
+ str(
int((wallet.balance - wallet.balance * 0.02) * 1000)
),
timeout=40,
)
res = r.json()
@ -65,6 +70,9 @@ async def on_invoice_paid(payment: Payment) -> None:
except (httpx.ConnectError, httpx.RequestError):
return
except Exception:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Failed to save LNURLPayout",
)
except:
return
return

View File

@ -10,10 +10,17 @@ from lnbits.core.views.api import api_payment, api_payments_decode
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import lnurlpayout_ext
from .crud import create_lnurlpayout, delete_lnurlpayout, get_lnurlpayout, get_lnurlpayouts, get_lnurlpayout_from_wallet
from .crud import (
create_lnurlpayout,
delete_lnurlpayout,
get_lnurlpayout,
get_lnurlpayouts,
get_lnurlpayout_from_wallet,
)
from .models import lnurlpayout, CreateLnurlPayoutData
from .tasks import on_invoice_paid
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
async def api_lnurlpayouts(
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
@ -28,23 +35,33 @@ async def api_lnurlpayouts(
@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED)
async def api_lnurlpayout_create(
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
):
):
if await get_lnurlpayout_from_wallet(wallet.wallet.id):
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Wallet already has lnurlpayout set")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Wallet already has lnurlpayout set",
)
return
url = await api_payments_decode({"data": data.lnurlpay})
if "domain" not in url:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded"
)
return
if str(url["domain"])[0:4] != "http":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
return
lnurlpayout = await create_lnurlpayout(wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data)
lnurlpayout = await create_lnurlpayout(
wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data
)
if not lnurlpayout:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout"
)
return
return lnurlpayout.dict()
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
async def api_lnurlpayout_delete(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
@ -57,26 +74,34 @@ async def api_lnurlpayout_delete(
)
if lnurlpayout.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout.")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout."
)
await delete_lnurlpayout(lnurlpayout_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
async def api_lnurlpayout_check(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
):
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
payments = await get_payments(
wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
wallet_id=lnurlpayout.wallet,
complete=True,
pending=False,
outgoing=True,
incoming=True,
)
result = await on_invoice_paid(payments[0])
return
# get payouts func
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
# for lnurlpayout in lnurlpayouts:
# payments = await get_payments(
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
# )
# await on_invoice_paid(payments[0])
# get payouts func
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
# for lnurlpayout in lnurlpayouts:
# payments = await get_payments(
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
# )
# await on_invoice_paid(payments[0])

View File

@ -15,6 +15,7 @@ satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"])
def satspay_renderer():
return template_renderer(["lnbits/extensions/satspay/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa

View File

@ -115,7 +115,7 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
pass
if charge.lnbitswallet:
invoice_status = await api_payment(charge.payment_hash)
if invoice_status["paid"]:
return await update_charge(charge_id=charge_id, balance=charge.amount)
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))

View File

@ -28,4 +28,3 @@ async def on_invoice_paid(payment: Payment) -> None:
await payment.set_pending(False)
await check_address_balance(charge_id=charge.id)

View File

@ -32,5 +32,6 @@ async def display(request: Request, charge_id):
)
wallet = await get_wallet(charge.lnbitswallet)
return satspay_renderer().TemplateResponse(
"satspay/display.html", {"request": request, "charge": charge, "wallet_key": wallet.inkey}
"satspay/display.html",
{"request": request, "charge": charge, "wallet_key": wallet.inkey},
)

View File

@ -25,14 +25,15 @@ from .models import CreateCharge
#############################CHARGES##########################
@satspay_ext.post("/api/v1/charge")
async def api_charge_create(
data: CreateCharge,
wallet: WalletTypeInfo = Depends(require_invoice_key)
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
):
charge = await create_charge(user=wallet.wallet.user, data=data)
return charge.dict()
@satspay_ext.put("/api/v1/charge/{charge_id}")
async def api_charge_update(
data: CreateCharge,

View File

@ -285,5 +285,6 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
return int(amount * (await get_fiat_rate_satoshis(currency)))
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
return float(amount / (await get_fiat_rate_satoshis(currency)))

View File

@ -2,6 +2,7 @@ import asyncio
import json
import httpx
from os import getenv
from datetime import datetime, timedelta
from typing import Optional, Dict, AsyncGenerator
import hashlib
from ..bolt11 import encode
@ -13,60 +14,65 @@ from .base import (
Wallet,
)
class FakeWallet(Wallet):
"""https://github.com/lnbits/lnbits"""
async def status(self) -> StatusResponse:
print("This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits.")
return StatusResponse(
None,
21000000000,
print(
"The FakeWallet backend is for using LNbits as a centralised, stand-alone payment system."
)
return StatusResponse(None, 21000000000)
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse:
options.amount = amount
options.timestamp = datetime.now().timestamp()
class options:
def __init__(self, amount, timestamp, payments_hash, privkey, memo, ):
self.name = name
self.age = age
async def status(self) -> StatusResponse:
randomHash = hashlib.sha256(b"some random data").hexdigest()
options.payments_hash = hex(randomHash)
options.privkey = "v3qrevqrevm39qin0vq3r0ivmrewvmq3rimq03ig"
options = {
"amount": amount,
"timestamp": datetime.now().timestamp(),
"payments_hash": randomHash,
"privkey": "v3qrevqrevm39qin0vq3r0ivmrewvmq3rimq03ig",
"memo": "",
"description_hashed": "",
}
if description_hash:
options.description_hashed = description_hash
else:
options.memo = memo
payment_request = encode(options)
print(payment_request)
checking_id = randomHash
return InvoiceResponse(ok, checking_id, payment_request)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
return ""
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return ""
return ""
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
return ""
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
url = f"{self.endpoint}/api/v1/payments/sse"
print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
await asyncio.sleep(5)
yield ""
#invoice = "lnbc"
#invoice += str(data.amount) + "m1"
#invoice += str(datetime.now().timestamp()).to_bytes(35, byteorder='big'))
#invoice += str(hashlib.sha256(b"some random data").hexdigest()) # hash of preimage, can be fake as invoice handled internally
#invoice += "dpl" # d then pl (p = 1, l = 31; 1 * 32 + 31 == 63)
#invoice += "2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq" #description, how do I encode this?
#invoice += str(hashlib.sha224("lnbc" + str(data.amount) + "m1").hexdigest())
# invoice = "lnbc"
# invoice += str(data.amount) + "m1"
# invoice += str(datetime.now().timestamp()).to_bytes(35, byteorder='big'))
# invoice += str(hashlib.sha256(b"some random data").hexdigest()) # hash of preimage, can be fake as invoice handled internally
# invoice += "dpl" # d then pl (p = 1, l = 31; 1 * 32 + 31 == 63)
# invoice += "2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq" #description, how do I encode this?
# invoice += str(hashlib.sha224("lnbc" + str(data.amount) + "m1").hexdigest())

View File

@ -23,8 +23,7 @@ class LndRestWallet(Wallet):
endpoint = getenv("LND_REST_ENDPOINT")
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = (
"https://" +
endpoint if not endpoint.startswith("http") else endpoint
"https://" + endpoint if not endpoint.startswith("http") else endpoint
)
self.endpoint = endpoint
@ -103,10 +102,7 @@ class LndRestWallet(Wallet):
r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions",
headers=self.auth,
json={
"payment_request": bolt11,
"fee_limit": lnrpcFeeLimit,
},
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
timeout=180,
)
@ -183,8 +179,7 @@ class LndRestWallet(Wallet):
except:
continue
payment_hash = base64.b64decode(
inv["r_hash"]).hex()
payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash
except (OSError, httpx.ConnectError, httpx.ReadError):
pass

View File

@ -20,11 +20,10 @@ class VoidWallet(Wallet):
raise Unsupported("")
async def status(self) -> StatusResponse:
print("This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits.")
return StatusResponse(
None,
0,
print(
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits."
)
return StatusResponse(None, 0)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
raise Unsupported("")

2
package-lock.json generated
View File

@ -1,5 +1,5 @@
{
"name": "lnbits",
"name": "lnbits-legend",
"lockfileVersion": 2,
"requires": true,
"packages": {