diff --git a/src/lib/api/strike.py b/src/lib/api/strike.py deleted file mode 100644 index 9fb04fc..0000000 --- a/src/lib/api/strike.py +++ /dev/null @@ -1,68 +0,0 @@ -from lib.utils import try_get, http_request -import qrcode -from io import BytesIO - - -class API: - BASE_URL = "https://api.strike.me/v1" - INVOICES_URL = f"{BASE_URL}/invoices" - HEADERS = { - "Content-Type": "application/json", - "Accept": "application/json", - } - - -class Strike(API): - def __init__(self, api_key, correlation_id, description): - assert (api_key is not None, "Strike API key must be supplied") - self.api_key = api_key - self.correlation_id = correlation_id - self.description = description - self.invoice_id = None - self.STRIKE_HEADERS = { - "Authorization": f"Bearer {self.api_key}", - **Strike.HEADERS, - } - - def invoice(self): - response = http_request( - self.STRIKE_HEADERS, - "POST", - Strike.INVOICES_URL, - { - "correlationId": self.correlation_id, - "description": self.description, - "amount": {"amount": "1.00", "currency": "USD"}, - }, - ) - self.invoice_id = try_get(response, "invoiceId") - response = http_request( - self.STRIKE_HEADERS, - "POST", - f"{Strike.INVOICES_URL}/{self.invoice_id}/quote", - ) - return ( - try_get(response, "lnInvoice"), - try_get(response, "expirationInSec"), - ) - - def paid(self): - response = http_request( - self.STRIKE_HEADERS, "GET", f"{Strike.INVOICES_URL}/{self.invoice_id}" - ) - return try_get(response, "state") == "PAID" - - def expire_invoice(self): - response = http_request( - self.STRIKE_HEADERS, - "PATCH", - f"{Strike.INVOICES_URL}/{self.invoice_id}/cancel", - ) - return try_get(response, "state") == "CANCELLED" - - def qr_code(self, ln_invoice): - qr = qrcode.make(ln_invoice) - bio = BytesIO() - qr.save(bio, "PNG") - bio.seek(0) - return bio diff --git a/src/lib/payments.py b/src/lib/payments.py index 8d49cbb..b3c6014 100644 --- a/src/lib/payments.py +++ b/src/lib/payments.py @@ -1,5 +1,29 @@ from abc import ABC, abstractmethod -from .utils import http_request, try_get +from lib.utils import try_get +import httpx +from env import PAYMENT_PROCESSOR_KIND, PAYMENT_PROCESSOR_TOKEN, LNBITS_BASE_URL + + +def init_payment_processor(): + available_processors = ["strike", "lnbits", "opennode"] + + if ( + PAYMENT_PROCESSOR_KIND is None + or PAYMENT_PROCESSOR_KIND not in available_processors + ): + raise Exception( + f"PAYMENT_PROCESSOR_KIND must be one of {', '.join(available_processors)}" + ) + + if not PAYMENT_PROCESSOR_TOKEN.trim(): + raise Exception("PAYMENT_PROCESSOR_TOKEN must be a valid API token") + + if PAYMENT_PROCESSOR_KIND == "strike": + return Strike(PAYMENT_PROCESSOR_TOKEN) + elif PAYMENT_PROCESSOR_KIND == "lnbits": + return LNbits(LNBITS_BASE_URL, PAYMENT_PROCESSOR_TOKEN) + elif PAYMENT_PROCESSOR_KIND == "opennode": + return OpenNode(PAYMENT_PROCESSOR_TOKEN) class Processor(ABC): @@ -25,27 +49,29 @@ class Strike(Processor): A Strike payment processor """ - _base_url = "https://api.strike.me/v1" - def __init__(self, api_key): super().__init__() assert (api_key is not None, "a Strike API key must be supplied") - self.api_key = api_key + self._client = httpx.AsyncClient( + base_url="https://api.strike.me/v1", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer {api_key}", + }, + ) - def get_invoice(self, correlation_id, description): - invoice_resp = self.__make_request( - "POST", - f"{self._base_url}/invoices", - { + async def get_invoice(self, correlation_id, description): + invoice_resp = await self._client.post( + "/invoices", + json={ "correlationId": correlation_id, "description": description, "amount": {"amount": "1.00", "currency": "USD"}, }, ) invoice_id = try_get(invoice_resp, "invoiceId") - quote_resp = self.__make_request( - "POST", f"{self._base_url}/invoices/{invoice_id}/quote" - ) + quote_resp = await self._client.post(f"/invoices/{invoice_id}/quote") return ( invoice_id, @@ -53,24 +79,105 @@ def get_invoice(self, correlation_id, description): try_get(quote_resp, "expirationInSec"), ) - def invoice_is_paid(self, invoice_id): - resp = self.__make_request("GET", f"{self._base_url}/invoices/{invoice_id}") + async def invoice_is_paid(self, invoice_id): + resp = await self._client.get(f"/invoices/{invoice_id}") return try_get(resp, "state") == "PAID" - def expire_invoice(self, invoice_id): - resp = self.__make_request( - "PATCH", f"{self._base_url}/invoices/{invoice_id}/cancel" - ) + async def expire_invoice(self, invoice_id): + resp = await self._client.patch(f"/invoices/{invoice_id}/cancel") return try_get(resp, "state") == "CANCELLED" - def __make_request(self, method, path, body): - return http_request( - { + +class LNbits(Processor): + """ + An LNbits payment processor + """ + + def __init__(self, base_url, api_key): + super().__init__() + assert (base_url is not None, "an LNbits base URL must be supplied") + assert (api_key is not None, "an LNbits API key must be supplied") + self._client = httpx.AsyncClient( + base_url=f"{base_url}/api/v1", + headers={ + "Content-Type": "application/json", + "X-Api-Key": api_key, + }, + ) + + async def get_invoice(self, correlation_id, description): + create_resp = await self._client.post( + "/payments", + json={ + "out": False, + "amount": 1, + "unit": "USD", + "unhashed_description": description.encode("utf-8").hex(), + "extra": { + "correlationId": correlation_id, + }, + }, + ) + payment_request = try_get(create_resp, "payment_request") + payment_hash = try_get(create_resp, "payment_hash") + payment_resp = await self._client.get(f"/payments/{payment_hash}") + + return ( + payment_hash, + payment_request, + int(try_get(payment_resp, "details", "expiry")), + ) + + async def invoice_is_paid(self, payment_hash): + resp = await self._client.get(f"/payments/{payment_hash}") + return try_get(resp, "paid") + + async def expire_invoice(self, invoice_id): + """LNbits doesn't seem to have an explicit way to expire an invoice""" + pass + + +class OpenNode(Processor): + """ + An OpenNode payment processor + """ + + def __init__(self, api_key): + super().__init__() + assert ( + api_key is not None, + "an OpenNode API key with invoice permissions must be supplied", + ) + self._client = httpx.AsyncClient( + base_url="https://api.opennode.com", + headers={ "Content-Type": "application/json", "Accept": "application/json", - "Authorization": f"Bearer {self.api_key}", + "Authorization": api_key, + }, + ) + + async def get_invoice(self, correlation_id, description): + resp = await self._client.post( + "/v1/charges", + json={ + "amount": 1, + "currency": "USD", + "order_id": correlation_id, + "description": description, }, - method, - path, - body, ) + + return ( + try_get(resp, "data", "id"), + try_get(resp, "data", "lightning_invoice", "payreq"), + try_get(resp, "data", "ttl"), + ) + + async def invoice_is_paid(self, invoice_id): + resp = await self._client.get(f"/v2/charge/{invoice_id}") + return try_get(resp, "data", "status") == "paid" + + async def expire_invoice(self, invoice_id): + """OpenNode doesn't seem to have an explicit way to expire an invoice""" + pass