mirror of
https://github.com/lnbits/lnbits.git
synced 2025-03-28 18:52:00 +01:00
back
This commit is contained in:
parent
d6f145eee9
commit
228717195b
@ -48,10 +48,7 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
|||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"]
|
||||||
allow_origins=origins,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
g().config = lnbits.settings
|
g().config = lnbits.settings
|
||||||
|
107
lnbits/bolt11.py
107
lnbits/bolt11.py
@ -116,7 +116,6 @@ def decode(pr: str) -> Invoice:
|
|||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def encode(options):
|
def encode(options):
|
||||||
""" Convert options into LnAddr and pass it to the encoder
|
""" Convert options into LnAddr and pass it to the encoder
|
||||||
"""
|
"""
|
||||||
@ -131,27 +130,31 @@ def encode(options):
|
|||||||
addr.paymenthash = unhexlify(options.paymenthash)
|
addr.paymenthash = unhexlify(options.paymenthash)
|
||||||
|
|
||||||
if options.description:
|
if options.description:
|
||||||
addr.tags.append(('d', options.description))
|
addr.tags.append(("d", options.description))
|
||||||
if options.description_hashed:
|
if options.description_hashed:
|
||||||
addr.tags.append(('h', options.description_hashed))
|
addr.tags.append(("h", options.description_hashed))
|
||||||
if options.expires:
|
if options.expires:
|
||||||
addr.tags.append(('x', options.expires))
|
addr.tags.append(("x", options.expires))
|
||||||
|
|
||||||
if options.fallback:
|
if options.fallback:
|
||||||
addr.tags.append(('f', options.fallback))
|
addr.tags.append(("f", options.fallback))
|
||||||
|
|
||||||
for r in options.route:
|
for r in options.route:
|
||||||
splits = r.split('/')
|
splits = r.split("/")
|
||||||
route=[]
|
route = []
|
||||||
while len(splits) >= 5:
|
while len(splits) >= 5:
|
||||||
route.append((unhexlify(splits[0]),
|
route.append(
|
||||||
unhexlify(splits[1]),
|
(
|
||||||
int(splits[2]),
|
unhexlify(splits[0]),
|
||||||
int(splits[3]),
|
unhexlify(splits[1]),
|
||||||
int(splits[4])))
|
int(splits[2]),
|
||||||
|
int(splits[3]),
|
||||||
|
int(splits[4]),
|
||||||
|
)
|
||||||
|
)
|
||||||
splits = splits[5:]
|
splits = splits[5:]
|
||||||
assert(len(splits) == 0)
|
assert len(splits) == 0
|
||||||
addr.tags.append(('r', route))
|
addr.tags.append(("r", route))
|
||||||
return lnencode(addr, options.privkey)
|
return lnencode(addr, options.privkey)
|
||||||
|
|
||||||
|
|
||||||
@ -159,21 +162,22 @@ def lnencode(addr, privkey):
|
|||||||
if addr.amount:
|
if addr.amount:
|
||||||
amount = Decimal(str(addr.amount))
|
amount = Decimal(str(addr.amount))
|
||||||
# We can only send down to millisatoshi.
|
# We can only send down to millisatoshi.
|
||||||
if amount * 10**12 % 10:
|
if amount * 10 ** 12 % 10:
|
||||||
raise ValueError("Cannot encode {}: too many decimal places".format(
|
raise ValueError(
|
||||||
addr.amount))
|
"Cannot encode {}: too many decimal places".format(addr.amount)
|
||||||
|
)
|
||||||
|
|
||||||
amount = addr.currency + shorten_amount(amount)
|
amount = addr.currency + shorten_amount(amount)
|
||||||
else:
|
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
|
# Start with the timestamp
|
||||||
data = bitstring.pack('uint:35', addr.date)
|
data = bitstring.pack("uint:35", addr.date)
|
||||||
|
|
||||||
# Payment hash
|
# Payment hash
|
||||||
data += tagged_bytes('p', addr.paymenthash)
|
data += tagged_bytes("p", addr.paymenthash)
|
||||||
tags_set = set()
|
tags_set = set()
|
||||||
|
|
||||||
for k, v in addr.tags:
|
for k, v in addr.tags:
|
||||||
@ -181,30 +185,36 @@ def lnencode(addr, privkey):
|
|||||||
# BOLT #11:
|
# BOLT #11:
|
||||||
#
|
#
|
||||||
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
|
# 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:
|
if k in tags_set:
|
||||||
raise ValueError("Duplicate '{}' tag".format(k))
|
raise ValueError("Duplicate '{}' tag".format(k))
|
||||||
|
|
||||||
if k == 'r':
|
if k == "r":
|
||||||
route = bitstring.BitArray()
|
route = bitstring.BitArray()
|
||||||
for step in v:
|
for step in v:
|
||||||
pubkey, channel, feebase, feerate, cltv = step
|
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))
|
route.append(
|
||||||
data += tagged('r', route)
|
bitstring.BitArray(pubkey)
|
||||||
elif k == 'f':
|
+ 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)
|
data += encode_fallback(v, addr.currency)
|
||||||
elif k == 'd':
|
elif k == "d":
|
||||||
data += tagged_bytes('d', v.encode())
|
data += tagged_bytes("d", v.encode())
|
||||||
elif k == 'x':
|
elif k == "x":
|
||||||
# Get minimal length by trimming leading 5 bits at a time.
|
# Get minimal length by trimming leading 5 bits at a time.
|
||||||
expirybits = bitstring.pack('intbe:64', v)[4:64]
|
expirybits = bitstring.pack("intbe:64", v)[4:64]
|
||||||
while expirybits.startswith('0b00000'):
|
while expirybits.startswith("0b00000"):
|
||||||
expirybits = expirybits[5:]
|
expirybits = expirybits[5:]
|
||||||
data += tagged('x', expirybits)
|
data += tagged("x", expirybits)
|
||||||
elif k == 'h':
|
elif k == "h":
|
||||||
data += tagged_bytes('h', hashlib.sha256(v.encode('utf-8')).digest())
|
data += tagged_bytes("h", hashlib.sha256(v.encode("utf-8")).digest())
|
||||||
elif k == 'n':
|
elif k == "n":
|
||||||
data += tagged_bytes('n', v)
|
data += tagged_bytes("n", v)
|
||||||
else:
|
else:
|
||||||
# FIXME: Support unknown tags?
|
# FIXME: Support unknown tags?
|
||||||
raise ValueError("Unknown tag {}".format(k))
|
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
|
# A writer MUST include either a `d` or `h` field, and MUST NOT include
|
||||||
# both.
|
# 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'")
|
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'")
|
raise ValueError("Must include either 'd' or 'h'")
|
||||||
|
|
||||||
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
||||||
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
|
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 :(
|
# This doesn't actually serialize, but returns a pair of values :(
|
||||||
sig, recid = privkey.ecdsa_recoverable_serialize(sig)
|
sig, recid = privkey.ecdsa_recoverable_serialize(sig)
|
||||||
data += bytes(sig) + bytes([recid])
|
data += bytes(sig) + bytes([recid])
|
||||||
|
|
||||||
return bech32_encode(hrp, bitarray_to_u5(data))
|
return bech32_encode(hrp, bitarray_to_u5(data))
|
||||||
|
|
||||||
|
|
||||||
class LnAddr(object):
|
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.date = int(time.time()) if not date else int(date)
|
||||||
self.tags = [] if not tags else tags
|
self.tags = [] if not tags else tags
|
||||||
self.unknown_tags = []
|
self.unknown_tags = []
|
||||||
self.paymenthash=paymenthash
|
self.paymenthash = paymenthash
|
||||||
self.signature = None
|
self.signature = None
|
||||||
self.pubkey = None
|
self.pubkey = None
|
||||||
self.currency = currency
|
self.currency = currency
|
||||||
@ -242,13 +257,13 @@ class LnAddr(object):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
||||||
hexlify(self.pubkey.serialize()).decode('utf-8'),
|
hexlify(self.pubkey.serialize()).decode("utf-8"),
|
||||||
self.amount, self.currency,
|
self.amount,
|
||||||
", ".join([k + '=' + str(v) for k, v in self.tags])
|
self.currency,
|
||||||
|
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
|
@ -30,8 +30,7 @@ async def get_account(
|
|||||||
user_id: str, conn: Optional[Connection] = None
|
user_id: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[User]:
|
) -> Optional[User]:
|
||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
"SELECT id, email, pass as password FROM accounts WHERE id = ?", (
|
"SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,)
|
||||||
user_id,)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return User(**row) if row else None
|
return User(**row) if row else None
|
||||||
@ -305,8 +304,7 @@ async def delete_expired_invoices(conn: Optional[Connection] = None,) -> None:
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
expiration_date = datetime.datetime.fromtimestamp(
|
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
||||||
invoice.date + invoice.expiry)
|
|
||||||
if expiration_date > datetime.datetime.utcnow():
|
if expiration_date > datetime.datetime.utcnow():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ async def pay_invoice(
|
|||||||
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.")
|
||||||
|
|
||||||
wallet = await get_wallet(wallet_id, conn=conn)
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
|
||||||
# put all parameters that don't change here
|
# put all parameters that don't change here
|
||||||
PaymentKwargs = TypedDict(
|
PaymentKwargs = TypedDict(
|
||||||
@ -141,15 +141,19 @@ async def pay_invoice(
|
|||||||
|
|
||||||
# do the balance check if internal payment
|
# do the balance check if internal payment
|
||||||
if internal_checking_id:
|
if internal_checking_id:
|
||||||
wallet = await get_wallet(wallet_id, conn=conn)
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
assert wallet
|
assert wallet
|
||||||
if wallet.balance_msat < 0:
|
if wallet.balance_msat < 0:
|
||||||
raise PermissionError("Insufficient balance.")
|
raise PermissionError("Insufficient balance.")
|
||||||
|
|
||||||
# do the balance check if external payment
|
# do the balance check if external payment
|
||||||
else:
|
else:
|
||||||
if invoice.amount_msat > wallet.balance_msat - (wallet.balance_msat / 100 * 2):
|
if invoice.amount_msat > wallet.balance_msat - (
|
||||||
raise PermissionError("LNbits requires you keep at least 2% reserve to cover potential routing fees.")
|
wallet.balance_msat / 100 * 2
|
||||||
|
):
|
||||||
|
raise PermissionError(
|
||||||
|
"LNbits requires you keep at least 2% reserve to cover potential routing fees."
|
||||||
|
)
|
||||||
|
|
||||||
if internal_checking_id:
|
if internal_checking_id:
|
||||||
# mark the invoice from the other side as not pending anymore
|
# mark the invoice from the other side as not pending anymore
|
||||||
@ -326,8 +330,7 @@ async def check_invoice_status(
|
|||||||
if not payment.pending:
|
if not payment.pending:
|
||||||
return status
|
return status
|
||||||
if payment.is_out and status.failed:
|
if payment.is_out and status.failed:
|
||||||
print(
|
print(f" - deleting outgoing failed payment {payment.checking_id}: {status}")
|
||||||
f" - deleting outgoing failed payment {payment.checking_id}: {status}")
|
|
||||||
await payment.delete()
|
await payment.delete()
|
||||||
elif not status.pending:
|
elif not status.pending:
|
||||||
print(
|
print(
|
||||||
|
@ -77,7 +77,9 @@ async def api_update_wallet(
|
|||||||
@core_app.get("/api/v1/payments")
|
@core_app.get("/api/v1/payments")
|
||||||
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
|
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:
|
for payment in pendingPayments:
|
||||||
await check_invoice_status(
|
await check_invoice_status(
|
||||||
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
|
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
|
||||||
@ -198,8 +200,7 @@ async def api_payments_create(
|
|||||||
invoiceData: CreateInvoiceData = Body(...),
|
invoiceData: CreateInvoiceData = Body(...),
|
||||||
):
|
):
|
||||||
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
|
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
|
||||||
|
|
||||||
if invoiceData.out is True and wallet.wallet_type == 0:
|
if invoiceData.out is True and wallet.wallet_type == 0:
|
||||||
if not invoiceData.bolt11:
|
if not invoiceData.bolt11:
|
||||||
@ -379,16 +380,14 @@ async def api_lnurlscan(code: str):
|
|||||||
params.update(callback=url) # with k1 already in it
|
params.update(callback=url) # with k1 already in it
|
||||||
|
|
||||||
lnurlauth_key = g().wallet.lnurlauth_key(domain)
|
lnurlauth_key = g().wallet.lnurlauth_key(domain)
|
||||||
params.update(
|
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||||
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
|
||||||
else:
|
else:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.get(url, timeout=5)
|
r = await client.get(url, timeout=5)
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
detail={"domain": domain,
|
detail={"domain": domain, "message": "failed to get parameters"},
|
||||||
"message": "failed to get parameters"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -418,8 +417,7 @@ async def api_lnurlscan(code: str):
|
|||||||
|
|
||||||
if tag == "withdrawRequest":
|
if tag == "withdrawRequest":
|
||||||
params.update(kind="withdraw")
|
params.update(kind="withdraw")
|
||||||
params.update(fixed=data["minWithdrawable"]
|
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
||||||
== data["maxWithdrawable"])
|
|
||||||
|
|
||||||
# callback with k1 already in it
|
# callback with k1 already in it
|
||||||
parsed_callback: ParseResult = urlparse(data["callback"])
|
parsed_callback: ParseResult = urlparse(data["callback"])
|
||||||
@ -509,18 +507,21 @@ async def api_list_currencies_available():
|
|||||||
|
|
||||||
|
|
||||||
class ConversionData(BaseModel):
|
class ConversionData(BaseModel):
|
||||||
from_: str = Field('sat', alias="from")
|
from_: str = Field("sat", alias="from")
|
||||||
amount: float
|
amount: float
|
||||||
to: str = Query('usd')
|
to: str = Query("usd")
|
||||||
|
|
||||||
|
|
||||||
@core_app.post("/api/v1/conversion")
|
@core_app.post("/api/v1/conversion")
|
||||||
async def api_fiat_as_sats(data: ConversionData):
|
async def api_fiat_as_sats(data: ConversionData):
|
||||||
output = {}
|
output = {}
|
||||||
if data.from_ == 'sat':
|
if data.from_ == "sat":
|
||||||
output["sats"] = int(data.amount)
|
output["sats"] = int(data.amount)
|
||||||
output["BTC"] = data.amount / 100000000
|
output["BTC"] = data.amount / 100000000
|
||||||
for currency in data.to.split(','):
|
for currency in data.to.split(","):
|
||||||
output[currency.strip().upper()] = await satoshis_amount_as_fiat(data.amount, currency.strip())
|
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
|
||||||
|
data.amount, currency.strip()
|
||||||
|
)
|
||||||
return output
|
return output
|
||||||
else:
|
else:
|
||||||
output[data.from_.upper()] = data.amount
|
output[data.from_.upper()] = data.amount
|
||||||
|
@ -43,9 +43,7 @@ async def home(request: Request, lightning: str = None):
|
|||||||
|
|
||||||
|
|
||||||
@core_html_routes.get(
|
@core_html_routes.get(
|
||||||
"/extensions",
|
"/extensions", name="core.extensions", response_class=HTMLResponse
|
||||||
name="core.extensions",
|
|
||||||
response_class=HTMLResponse,
|
|
||||||
)
|
)
|
||||||
async def extensions(
|
async def extensions(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
@ -171,6 +171,7 @@ async def require_admin_key(
|
|||||||
else:
|
else:
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
async def require_invoice_key(
|
async def require_invoice_key(
|
||||||
r: Request,
|
r: Request,
|
||||||
api_key_header: str = Security(api_key_header),
|
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
|
# If wallet type is not invoice then return the unauthorized status
|
||||||
# This also covers when the user passes an invalid key type
|
# This also covers when the user passes an invalid key type
|
||||||
raise HTTPException(
|
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:
|
else:
|
||||||
return wallet
|
return wallet
|
||||||
|
@ -52,7 +52,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
|
|||||||
amount_received = amount
|
amount_received = amount
|
||||||
|
|
||||||
domain = await get_domain(address.domain)
|
domain = await get_domain(address.domain)
|
||||||
|
|
||||||
base_url = (
|
base_url = (
|
||||||
address.wallet_endpoint[:-1]
|
address.wallet_endpoint[:-1]
|
||||||
if address.wallet_endpoint.endswith("/")
|
if address.wallet_endpoint.endswith("/")
|
||||||
@ -71,7 +71,9 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
|
|||||||
"out": False,
|
"out": False,
|
||||||
"amount": int(amount_received / 1000),
|
"amount": int(amount_received / 1000),
|
||||||
"description_hash": hashlib.sha256(
|
"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(),
|
).hexdigest(),
|
||||||
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
|
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
|
||||||
},
|
},
|
||||||
|
@ -53,5 +53,5 @@ class Addresses(BaseModel):
|
|||||||
text = f"Payment to {self.username}"
|
text = f"Payment to {self.username}"
|
||||||
identifier = f"{self.username}@{domain}"
|
identifier = f"{self.username}@{domain}"
|
||||||
metadata = [["text/plain", text], ["text/identifier", identifier]]
|
metadata = [["text/plain", text], ["text/identifier", identifier]]
|
||||||
|
|
||||||
return LnurlPayMetadata(json.dumps(metadata))
|
return LnurlPayMetadata(json.dumps(metadata))
|
||||||
|
@ -36,7 +36,7 @@ async def display(domain_id, request: Request):
|
|||||||
|
|
||||||
wallet = await get_wallet(domain.wallet)
|
wallet = await get_wallet(domain.wallet)
|
||||||
url = urlparse(str(request.url))
|
url = urlparse(str(request.url))
|
||||||
|
|
||||||
return lnaddress_renderer().TemplateResponse(
|
return lnaddress_renderer().TemplateResponse(
|
||||||
"lnaddress/display.html",
|
"lnaddress/display.html",
|
||||||
{
|
{
|
||||||
@ -45,6 +45,6 @@ async def display(domain_id, request: Request):
|
|||||||
"domain_domain": domain.domain,
|
"domain_domain": domain.domain,
|
||||||
"domain_cost": domain.cost,
|
"domain_cost": domain.cost,
|
||||||
"domain_wallet_inkey": wallet.inkey,
|
"domain_wallet_inkey": wallet.inkey,
|
||||||
"root_url": f"{url.scheme}://{url.netloc}"
|
"root_url": f"{url.scheme}://{url.netloc}",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -24,7 +24,15 @@ async def create_lnurldevice(data: createLnurldevice,) -> lnurldevices:
|
|||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
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)
|
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:
|
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###########################
|
########################lnuldevice payments###########################
|
||||||
|
|
||||||
@ -102,19 +112,23 @@ async def update_lnurldevicepayment(
|
|||||||
(*kwargs.values(), lnurldevicepayment_id),
|
(*kwargs.values(), lnurldevicepayment_id),
|
||||||
)
|
)
|
||||||
row = await db.fetchone(
|
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
|
return lnurldevicepayment(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
|
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
|
||||||
row = await db.fetchone(
|
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
|
return lnurldevicepayment(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
|
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
|
||||||
row = await db.fetchone(
|
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
|
||||||
|
@ -105,10 +105,7 @@ async def lnurl_v1_params(
|
|||||||
paymentcheck = await get_lnurlpayload(p)
|
paymentcheck = await get_lnurlpayload(p)
|
||||||
if device.device == "atm":
|
if device.device == "atm":
|
||||||
if paymentcheck:
|
if paymentcheck:
|
||||||
return {
|
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||||
"status": "ERROR",
|
|
||||||
"reason": f"Payment already claimed",
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p) % 4 > 0:
|
if len(p) % 4 > 0:
|
||||||
p += "=" * (4 - (len(p) % 4))
|
p += "=" * (4 - (len(p) % 4))
|
||||||
@ -174,27 +171,28 @@ async def lnurl_v1_params(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@lnurldevice_ext.get(
|
@lnurldevice_ext.get(
|
||||||
"/api/v1/lnurl/cb/{paymentid}",
|
"/api/v1/lnurl/cb/{paymentid}",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
name="lnurldevice.lnurl_callback",
|
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)
|
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
|
||||||
device = await get_lnurldevice(lnurldevicepayment.deviceid)
|
device = await get_lnurldevice(lnurldevicepayment.deviceid)
|
||||||
if not device:
|
if not device:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found."
|
status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found."
|
||||||
)
|
)
|
||||||
if pr:
|
if pr:
|
||||||
if lnurldevicepayment.id != k1:
|
if lnurldevicepayment.id != k1:
|
||||||
return {"status": "ERROR", "reason": "Bad K1"}
|
return {"status": "ERROR", "reason": "Bad K1"}
|
||||||
if lnurldevicepayment.payhash != "payment_hash":
|
if lnurldevicepayment.payhash != "payment_hash":
|
||||||
return {
|
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||||
"status": "ERROR",
|
|
||||||
"reason": f"Payment already claimed",
|
|
||||||
}
|
|
||||||
lnurldevicepayment = await update_lnurldevicepayment(
|
lnurldevicepayment = await update_lnurldevicepayment(
|
||||||
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
|
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,7 @@ from lnbits.db import Database
|
|||||||
|
|
||||||
db2 = Database("ext_lnurlpos")
|
db2 = Database("ext_lnurlpos")
|
||||||
|
|
||||||
|
|
||||||
async def m001_initial(db):
|
async def m001_initial(db):
|
||||||
"""
|
"""
|
||||||
Initial lnurldevice table.
|
Initial lnurldevice table.
|
||||||
@ -59,7 +60,8 @@ async def m002_redux(db):
|
|||||||
(row[0], row[1], row[2], row[3], row[4], "pos", 0),
|
(row[0], row[1], row[2], row[3], row[4], "pos", 0),
|
||||||
)
|
)
|
||||||
for row in [
|
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(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
@ -76,4 +78,4 @@ async def m002_redux(db):
|
|||||||
(row[0], row[1], row[3], row[4], row[5], row[6]),
|
(row[0], row[1], row[3], row[4], row[5], row[6]),
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
return
|
return
|
||||||
|
@ -33,12 +33,15 @@ class lnurldevices(BaseModel):
|
|||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
def lnurl(self, req: Request) -> Lnurl:
|
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)
|
return lnurl_encode(url)
|
||||||
|
|
||||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||||
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
||||||
|
|
||||||
|
|
||||||
class lnurldevicepayment(BaseModel):
|
class lnurldevicepayment(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
deviceid: str
|
deviceid: str
|
||||||
|
@ -41,10 +41,13 @@ async def displaypin(request: Request, paymentid: str = Query(None)):
|
|||||||
)
|
)
|
||||||
status = await api_payment(lnurldevicepayment.payhash)
|
status = await api_payment(lnurldevicepayment.payhash)
|
||||||
if status["paid"]:
|
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(
|
return lnurldevice_renderer().TemplateResponse(
|
||||||
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
|
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
|
||||||
)
|
)
|
||||||
return lnurldevice_renderer().TemplateResponse(
|
return lnurldevice_renderer().TemplateResponse(
|
||||||
"lnurldevice/error.html", {"request": request, "pin": "filler", "not_paid": True}
|
"lnurldevice/error.html",
|
||||||
|
{"request": request, "pin": "filler", "not_paid": True},
|
||||||
)
|
)
|
||||||
|
@ -48,7 +48,9 @@ async def api_lnurldevice_create_or_update(
|
|||||||
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||||
try:
|
try:
|
||||||
return [{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)]
|
return [
|
||||||
|
{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)
|
||||||
|
]
|
||||||
except:
|
except:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@ -71,7 +73,8 @@ async def api_lnurldevice_retrieve(
|
|||||||
|
|
||||||
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
|
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
|
||||||
async def api_lnurldevice_delete(
|
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)
|
lnurldevice = await get_lnurldevice(lnurldevice_id)
|
||||||
|
|
||||||
|
@ -13,10 +13,12 @@ lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout
|
|||||||
def lnurlpayout_renderer():
|
def lnurlpayout_renderer():
|
||||||
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
|
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
|
||||||
|
|
||||||
|
|
||||||
from .tasks import wait_for_paid_invoices
|
from .tasks import wait_for_paid_invoices
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
|
|
||||||
|
|
||||||
def lnurlpayout_start():
|
def lnurlpayout_start():
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||||
|
@ -6,14 +6,23 @@ from . import db
|
|||||||
from .models import lnurlpayout, CreateLnurlPayoutData
|
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()
|
lnurlpayout_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
|
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
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)
|
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]:
|
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
|
return lnurlpayout(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]:
|
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
|
return lnurlpayout(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
|
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
|
||||||
if isinstance(wallet_ids, str):
|
if isinstance(wallet_ids, str):
|
||||||
wallet_ids = [wallet_ids]
|
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:
|
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,)
|
||||||
|
)
|
||||||
|
@ -2,11 +2,13 @@ from sqlite3 import Row
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurlPayoutData(BaseModel):
|
class CreateLnurlPayoutData(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
lnurlpay: str
|
lnurlpay: str
|
||||||
threshold: int
|
threshold: int
|
||||||
|
|
||||||
|
|
||||||
class lnurlpayout(BaseModel):
|
class lnurlpayout(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
|
@ -26,28 +26,33 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
# Check its got a payout associated with it
|
# Check its got a payout associated with it
|
||||||
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
|
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
|
||||||
if lnurlpayout_link:
|
if lnurlpayout_link:
|
||||||
|
|
||||||
# Check the wallet balance is more than the threshold
|
# Check the wallet balance is more than the threshold
|
||||||
|
|
||||||
wallet = await get_wallet(lnurlpayout_link.wallet)
|
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
|
return
|
||||||
|
|
||||||
# Get the invoice from the LNURL to pay
|
# Get the invoice from the LNURL to pay
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
|
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
|
||||||
if str(url["domain"])[0:4] != "http":
|
if str(url["domain"])[0:4] != "http":
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken")
|
raise HTTPException(
|
||||||
try:
|
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
|
||||||
r = await client.get(
|
|
||||||
str(url["domain"]),
|
|
||||||
timeout=40,
|
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
r = await client.get(str(url["domain"]), timeout=40)
|
||||||
res = r.json()
|
res = r.json()
|
||||||
try:
|
try:
|
||||||
r = await client.get(
|
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,
|
timeout=40,
|
||||||
)
|
)
|
||||||
res = r.json()
|
res = r.json()
|
||||||
@ -65,6 +70,9 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
return
|
return
|
||||||
except Exception:
|
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:
|
except:
|
||||||
return
|
return
|
||||||
|
@ -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 lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
|
||||||
from . import lnurlpayout_ext
|
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 .models import lnurlpayout, CreateLnurlPayoutData
|
||||||
from .tasks import on_invoice_paid
|
from .tasks import on_invoice_paid
|
||||||
|
|
||||||
|
|
||||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
|
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
|
||||||
async def api_lnurlpayouts(
|
async def api_lnurlpayouts(
|
||||||
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
|
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)
|
@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED)
|
||||||
async def api_lnurlpayout_create(
|
async def api_lnurlpayout_create(
|
||||||
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
|
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
if await get_lnurlpayout_from_wallet(wallet.wallet.id):
|
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
|
return
|
||||||
url = await api_payments_decode({"data": data.lnurlpay})
|
url = await api_payments_decode({"data": data.lnurlpay})
|
||||||
if "domain" not in url:
|
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
|
return
|
||||||
if str(url["domain"])[0:4] != "http":
|
if str(url["domain"])[0:4] != "http":
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
|
||||||
return
|
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:
|
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
|
||||||
return lnurlpayout.dict()
|
return lnurlpayout.dict()
|
||||||
|
|
||||||
|
|
||||||
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
|
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
|
||||||
async def api_lnurlpayout_delete(
|
async def api_lnurlpayout_delete(
|
||||||
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
@ -57,26 +74,34 @@ async def api_lnurlpayout_delete(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if lnurlpayout.wallet != wallet.wallet.id:
|
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)
|
await delete_lnurlpayout(lnurlpayout_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_lnurlpayout_check(
|
async def api_lnurlpayout_check(
|
||||||
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
|
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
|
||||||
payments = await get_payments(
|
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])
|
result = await on_invoice_paid(payments[0])
|
||||||
return
|
return
|
||||||
|
|
||||||
# get payouts func
|
|
||||||
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
|
# get payouts func
|
||||||
# for lnurlpayout in lnurlpayouts:
|
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
|
||||||
# payments = await get_payments(
|
# for lnurlpayout in lnurlpayouts:
|
||||||
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
|
# payments = await get_payments(
|
||||||
# )
|
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
|
||||||
# await on_invoice_paid(payments[0])
|
# )
|
||||||
|
# await on_invoice_paid(payments[0])
|
||||||
|
@ -15,6 +15,7 @@ satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"])
|
|||||||
def satspay_renderer():
|
def satspay_renderer():
|
||||||
return template_renderer(["lnbits/extensions/satspay/templates"])
|
return template_renderer(["lnbits/extensions/satspay/templates"])
|
||||||
|
|
||||||
|
|
||||||
from .tasks import wait_for_paid_invoices
|
from .tasks import wait_for_paid_invoices
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
|
@ -115,7 +115,7 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
|
|||||||
pass
|
pass
|
||||||
if charge.lnbitswallet:
|
if charge.lnbitswallet:
|
||||||
invoice_status = await api_payment(charge.payment_hash)
|
invoice_status = await api_payment(charge.payment_hash)
|
||||||
|
|
||||||
if invoice_status["paid"]:
|
if invoice_status["paid"]:
|
||||||
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
||||||
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
||||||
|
@ -28,4 +28,3 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
|
|
||||||
await payment.set_pending(False)
|
await payment.set_pending(False)
|
||||||
await check_address_balance(charge_id=charge.id)
|
await check_address_balance(charge_id=charge.id)
|
||||||
|
|
||||||
|
@ -32,5 +32,6 @@ async def display(request: Request, charge_id):
|
|||||||
)
|
)
|
||||||
wallet = await get_wallet(charge.lnbitswallet)
|
wallet = await get_wallet(charge.lnbitswallet)
|
||||||
return satspay_renderer().TemplateResponse(
|
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},
|
||||||
)
|
)
|
||||||
|
@ -25,14 +25,15 @@ from .models import CreateCharge
|
|||||||
|
|
||||||
#############################CHARGES##########################
|
#############################CHARGES##########################
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.post("/api/v1/charge")
|
@satspay_ext.post("/api/v1/charge")
|
||||||
async def api_charge_create(
|
async def api_charge_create(
|
||||||
data: CreateCharge,
|
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key)
|
|
||||||
):
|
):
|
||||||
charge = await create_charge(user=wallet.wallet.user, data=data)
|
charge = await create_charge(user=wallet.wallet.user, data=data)
|
||||||
return charge.dict()
|
return charge.dict()
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.put("/api/v1/charge/{charge_id}")
|
@satspay_ext.put("/api/v1/charge/{charge_id}")
|
||||||
async def api_charge_update(
|
async def api_charge_update(
|
||||||
data: CreateCharge,
|
data: CreateCharge,
|
||||||
|
@ -285,5 +285,6 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
|
|||||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||||
return int(amount * (await get_fiat_rate_satoshis(currency)))
|
return int(amount * (await get_fiat_rate_satoshis(currency)))
|
||||||
|
|
||||||
|
|
||||||
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
||||||
return float(amount / (await get_fiat_rate_satoshis(currency)))
|
return float(amount / (await get_fiat_rate_satoshis(currency)))
|
||||||
|
@ -2,6 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional, Dict, AsyncGenerator
|
from typing import Optional, Dict, AsyncGenerator
|
||||||
import hashlib
|
import hashlib
|
||||||
from ..bolt11 import encode
|
from ..bolt11 import encode
|
||||||
@ -13,60 +14,65 @@ from .base import (
|
|||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeWallet(Wallet):
|
class FakeWallet(Wallet):
|
||||||
"""https://github.com/lnbits/lnbits"""
|
"""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.")
|
print(
|
||||||
return StatusResponse(
|
"The FakeWallet backend is for using LNbits as a centralised, stand-alone payment system."
|
||||||
None,
|
|
||||||
21000000000,
|
|
||||||
)
|
)
|
||||||
|
return StatusResponse(None, 21000000000)
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
self,
|
self,
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
|
class options:
|
||||||
options.amount = amount
|
def __init__(self, amount, timestamp, payments_hash, privkey, memo, ):
|
||||||
options.timestamp = datetime.now().timestamp()
|
self.name = name
|
||||||
|
self.age = age
|
||||||
|
async def status(self) -> StatusResponse:
|
||||||
|
|
||||||
randomHash = hashlib.sha256(b"some random data").hexdigest()
|
randomHash = hashlib.sha256(b"some random data").hexdigest()
|
||||||
options.payments_hash = hex(randomHash)
|
options = {
|
||||||
options.privkey = "v3qrevqrevm39qin0vq3r0ivmrewvmq3rimq03ig"
|
"amount": amount,
|
||||||
|
"timestamp": datetime.now().timestamp(),
|
||||||
|
"payments_hash": randomHash,
|
||||||
|
"privkey": "v3qrevqrevm39qin0vq3r0ivmrewvmq3rimq03ig",
|
||||||
|
"memo": "",
|
||||||
|
"description_hashed": "",
|
||||||
|
}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
options.description_hashed = description_hash
|
options.description_hashed = description_hash
|
||||||
else:
|
else:
|
||||||
options.memo = memo
|
options.memo = memo
|
||||||
payment_request = encode(options)
|
payment_request = encode(options)
|
||||||
|
print(payment_request)
|
||||||
checking_id = randomHash
|
checking_id = randomHash
|
||||||
|
|
||||||
return InvoiceResponse(ok, checking_id, payment_request)
|
return InvoiceResponse(ok, checking_id, payment_request)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||||
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
url = f"{self.endpoint}/api/v1/payments/sse"
|
yield ""
|
||||||
print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
|
|
||||||
|
|
||||||
#invoice = "lnbc"
|
# invoice = "lnbc"
|
||||||
#invoice += str(data.amount) + "m1"
|
# invoice += str(data.amount) + "m1"
|
||||||
#invoice += str(datetime.now().timestamp()).to_bytes(35, byteorder='big'))
|
# 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 += 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 += "dpl" # d then pl (p = 1, l = 31; 1 * 32 + 31 == 63)
|
||||||
#invoice += "2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq" #description, how do I encode this?
|
# invoice += "2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq" #description, how do I encode this?
|
||||||
#invoice += str(hashlib.sha224("lnbc" + str(data.amount) + "m1").hexdigest())
|
# invoice += str(hashlib.sha224("lnbc" + str(data.amount) + "m1").hexdigest())
|
||||||
|
@ -23,8 +23,7 @@ class LndRestWallet(Wallet):
|
|||||||
endpoint = getenv("LND_REST_ENDPOINT")
|
endpoint = getenv("LND_REST_ENDPOINT")
|
||||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||||
endpoint = (
|
endpoint = (
|
||||||
"https://" +
|
"https://" + endpoint if not endpoint.startswith("http") else endpoint
|
||||||
endpoint if not endpoint.startswith("http") else endpoint
|
|
||||||
)
|
)
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
|
|
||||||
@ -103,10 +102,7 @@ class LndRestWallet(Wallet):
|
|||||||
r = await client.post(
|
r = await client.post(
|
||||||
url=f"{self.endpoint}/v1/channels/transactions",
|
url=f"{self.endpoint}/v1/channels/transactions",
|
||||||
headers=self.auth,
|
headers=self.auth,
|
||||||
json={
|
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
|
||||||
"payment_request": bolt11,
|
|
||||||
"fee_limit": lnrpcFeeLimit,
|
|
||||||
},
|
|
||||||
timeout=180,
|
timeout=180,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -183,8 +179,7 @@ class LndRestWallet(Wallet):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
payment_hash = base64.b64decode(
|
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||||
inv["r_hash"]).hex()
|
|
||||||
yield payment_hash
|
yield payment_hash
|
||||||
except (OSError, httpx.ConnectError, httpx.ReadError):
|
except (OSError, httpx.ConnectError, httpx.ReadError):
|
||||||
pass
|
pass
|
||||||
|
@ -20,11 +20,10 @@ class VoidWallet(Wallet):
|
|||||||
raise Unsupported("")
|
raise Unsupported("")
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
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.")
|
print(
|
||||||
return StatusResponse(
|
"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."
|
||||||
None,
|
|
||||||
0,
|
|
||||||
)
|
)
|
||||||
|
return StatusResponse(None, 0)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||||
raise Unsupported("")
|
raise Unsupported("")
|
||||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "lnbits",
|
"name": "lnbits-legend",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user