mirror of
https://github.com/Blockstream/satellite-api.git
synced 2025-02-22 21:45:19 +01:00
- Add new error: INVOICE_ALREADY_EXPIRED. - Rename ORDER_ALREADY_PAID to INVOICE_ALREADY_PAID.
151 lines
5.6 KiB
Python
151 lines
5.6 KiB
Python
from http import HTTPStatus
|
|
import datetime
|
|
import requests
|
|
|
|
from sqlalchemy import and_, func
|
|
|
|
from constants import InvoiceStatus
|
|
import constants
|
|
from database import db
|
|
from error import get_http_error_resp
|
|
from models import Invoice
|
|
from order_helpers import maybe_mark_order_as_expired, maybe_mark_order_as_paid
|
|
from utils import hmac_sha256_digest
|
|
|
|
|
|
def new_invoice(order, bid):
|
|
"""Generate a lightning invoice
|
|
|
|
Args:
|
|
order: The order for which this invoice is being generated.
|
|
bid: Bid amount.
|
|
Returns:
|
|
A pair whose first element is a boolean indicating whether
|
|
the invoice generation was successful or not. If False, then
|
|
the second element is the error key. If True, then the second
|
|
element is the newly generated invoice.
|
|
"""
|
|
try:
|
|
bid = int(bid)
|
|
# generate Lightning invoice
|
|
charged_response = requests.post(f"{constants.CHARGE_ROOT}/invoice",
|
|
json={
|
|
'msatoshi': bid,
|
|
'description':
|
|
constants.LN_INVOICE_DESCRIPTION,
|
|
'expiry':
|
|
constants.LN_INVOICE_EXPIRY,
|
|
'metadata': {
|
|
'uuid':
|
|
order.uuid,
|
|
'sha256_message_digest':
|
|
order.message_digest
|
|
}
|
|
},
|
|
timeout=(constants.CONNECTION_TIMEOUT,
|
|
constants.RESPONSE_TIMEOUT))
|
|
except requests.exceptions.RequestException:
|
|
return False, get_http_error_resp('LIGHTNING_CHARGE_INVOICE_ERROR')
|
|
except ValueError:
|
|
return False, get_http_error_resp('PARAM_COERCION', 'bid')
|
|
|
|
if charged_response.status_code != HTTPStatus.CREATED:
|
|
return False, get_http_error_resp('LIGHTNING_CHARGE_INVOICE_ERROR')
|
|
|
|
lightning_invoice = charged_response.json()
|
|
if 'id' not in lightning_invoice:
|
|
return False, get_http_error_resp('LIGHTNING_CHARGE_INVOICE_ERROR')
|
|
|
|
invoice = Invoice(order_id=order.id,
|
|
amount=bid,
|
|
lid=lightning_invoice['id'],
|
|
invoice=charged_response.content,
|
|
status=InvoiceStatus.pending.value,
|
|
expires_at=datetime.datetime.utcnow() +
|
|
datetime.timedelta(seconds=constants.LN_INVOICE_EXPIRY))
|
|
|
|
try:
|
|
# register the webhook
|
|
charged_auth_token = hmac_sha256_digest(
|
|
constants.LIGHTNING_WEBHOOK_KEY, invoice.lid)
|
|
callback_url = f"{constants.CALLBACK_URI_ROOT}/callback\
|
|
/{invoice.lid}/{charged_auth_token}"
|
|
|
|
webhook_registration_response = requests.post(
|
|
f"{constants.CHARGE_ROOT}/invoice/{invoice.lid}/webhook",
|
|
json={'url': callback_url},
|
|
timeout=(constants.CONNECTION_TIMEOUT, constants.RESPONSE_TIMEOUT))
|
|
|
|
if webhook_registration_response.status_code != HTTPStatus.CREATED:
|
|
return False, get_http_error_resp(
|
|
'LIGHTNING_CHARGE_WEBHOOK_REGISTRATION_ERROR')
|
|
|
|
except requests.exceptions.RequestException:
|
|
return False, get_http_error_resp(
|
|
'LIGHTNING_CHARGE_WEBHOOK_REGISTRATION_ERROR')
|
|
|
|
return True, invoice
|
|
|
|
|
|
def get_and_authenticate_invoice(lid, charged_auth_token):
|
|
invoice = Invoice.query.filter_by(lid=lid).first()
|
|
|
|
if invoice is None:
|
|
return False, get_http_error_resp('INVOICE_ID_NOT_FOUND_ERROR', lid)
|
|
|
|
db_invoice_charged_auth_token = hmac_sha256_digest(
|
|
constants.LIGHTNING_WEBHOOK_KEY, invoice.lid)
|
|
|
|
if db_invoice_charged_auth_token != charged_auth_token:
|
|
return False, get_http_error_resp('INVALID_AUTH_TOKEN')
|
|
|
|
return True, invoice
|
|
|
|
|
|
def pay_invoice(invoice):
|
|
if invoice.status == InvoiceStatus.paid.value:
|
|
return get_http_error_resp('INVOICE_ALREADY_PAID')
|
|
if invoice.status == InvoiceStatus.expired.value:
|
|
return get_http_error_resp('INVOICE_ALREADY_EXPIRED')
|
|
|
|
invoice.status = InvoiceStatus.paid.value
|
|
invoice.paid_at = datetime.datetime.utcnow()
|
|
db.session.commit()
|
|
maybe_mark_order_as_paid(invoice.order_id)
|
|
return None
|
|
|
|
|
|
def get_pending_invoices(order_id):
|
|
return Invoice.query.filter(
|
|
and_(Invoice.order_id == order_id,
|
|
Invoice.status == constants.InvoiceStatus.pending.value)).all()
|
|
|
|
|
|
def expire_invoice(invoice):
|
|
if invoice.status == InvoiceStatus.pending.value:
|
|
invoice.status = constants.InvoiceStatus.expired.value
|
|
db.session.commit()
|
|
|
|
|
|
def expire_unpaid_invoices():
|
|
"""Expire unpaid invoices
|
|
|
|
Expire any unpaid invoice that has reached its expiration time. The
|
|
corresponding order may be auto-expired as a result.
|
|
|
|
Returns:
|
|
Tuple with the list of invoices and the list of orders that got expired
|
|
by this function.
|
|
|
|
"""
|
|
invoices_to_expire = Invoice.query.filter(
|
|
and_(Invoice.status == constants.InvoiceStatus.pending.value,
|
|
func.datetime(Invoice.expires_at) <
|
|
datetime.datetime.utcnow())).all()
|
|
expired_orders = []
|
|
for invoice in invoices_to_expire:
|
|
expire_invoice(invoice)
|
|
expired_order = maybe_mark_order_as_expired(invoice.order_id)
|
|
if (expired_order is not None):
|
|
expired_orders.append(expired_order)
|
|
return invoices_to_expire, expired_orders
|