Source code for pystrike.charge

"""
Python library for interacting with ACINQ's Strike API for lightning
network payments.
"""

import json
import base64
import http.client
import urllib.parse
import ssl
import abc
import socket

from .exceptions import ConnectionException, ClientRequestException, \
        ChargeNotFoundException, UnexpectedResponseException, \
        ServerErrorException


[docs]class Charge(abc.ABC): """ The Charge class is your interface to the Strike web service. Use it to create, retrieve, and update lighting network charges. Each instance is a lazy mirror, reflecting a single charge on the Strike servers. The instance is lazy in that it will communicate with Strike implicitly, but only as needed. When you initialize a charge with an amount and description, the instance does not create an instance on Strike until the moment that you request an attribute such as `payment_request`. If you request the charge's `paid` attribute, then the charge will update itself from the Strike server if it has not yet seen its payment clear; but if `paid` is already set to `True` then the charge will simply report `True` without reaching out to the server. :ivar amount: The amount of the invoice, in self.currency. :ivar currency: The currency of the request. :ivar description: Narrative description of the invoice. :ivar customer_id: An optional customer identifier. :ivar id: The id of the charge on Strike's server. :ivar amount_satoshi: The amount of the request, in satoshi. :ivar payment_request: The payment request string for the charge. :ivar payment_hash: The hash of the payment for this charge. :ivar paid: Whether the request has been satisfied. :ivar created: When the charge was created, in epoch time. :ivar updated: When the charge was updated, in epoch time. """ CURRENCY_BTC = "btc" @property @abc.abstractmethod def api_key(self): """Concrete subclasses must define an api_key.""" pass @property @abc.abstractmethod def api_host(self): """Concrete subclasses must define an api_host.""" pass @property @abc.abstractmethod def api_base(self): """Concrete subclasses must define an api_base.""" pass
[docs] def __init__( self, amount, currency, description="", customer_id="", create=True, ): """ Initialize an instance of `Charge`. See the Strike API documentation for details on each of the arguments. Args: - amount (int): The amount of the charge, in Satoshi. - currenency (str): Must be `Charge.CURRENCY_BTC`. Kwargs: - description (str): Optional invoice description. - customer_id (str): Optional customer identifier. - create (bool): Whether to automatically create a corresponding charge on the Strike service. """ self.api_connection = http.client.HTTPSConnection( self.api_host, context=ssl.create_default_context(), ) self.amount = amount self.currency = currency self.description = description self.customer_id = customer_id self.id = None self.amount_satoshi = None self.payment_request = None self.payment_hash = None self.paid = False self.created = None self.updated = None if create: self.update()
def _make_request(self, method, path, body, headers, retry=True): try: self.api_connection.request( method, path, body=body, headers=headers, ) except socket.gaierror: raise ConnectionException("Unable to communicate with host.") try: response = self.api_connection.getresponse() except http.client.RemoteDisconnected: """ I found that the Strike server will prematurely close the connection the _first_ time I make a GET request after the invoice has been paid. This `except` clause represents a retry on that close condition. """ if method == 'GET' and retry: return self._make_request( method, path, body, headers, retry=False, ) else: raise ConnectionException( "Remote host disconnected without sending " + "a response" ) except: raise ConnectionException("Unable to communicate with host.") return json.loads(response.read().decode()) def _fill_from_data_dict(self, data): self.id = data['id'] self.amount = data['amount'] self.currency = data['currency'] self.amount_satoshi = data['amount_satoshi'] self.payment_hash = data['payment_hash'] self.payment_request = data['payment_request'] self.description = data['description'] self.paid = data['paid'] self.created = data['created'] self.updated = data['updated']
[docs] def update(self): """ Update the charge from the server. If this charge has an `id`, then the method will _retrieve_ the charge from the server. If this charge does not have an `id`, then this method will _create_ the charge on the server and then fill the local charge from the attributes created and returned by the Strike server. """ auth = base64.b64encode(self.api_key.encode() + b':').decode('ascii') must_create = super().__getattribute__('id') is None if must_create: method = 'POST' path = self.api_base + 'charges' body = urllib.parse.urlencode({ 'amount': self.amount, 'currency': self.currency, 'description': self.description, 'customer_id': self.customer_id, }) headers = { 'Authorization': 'Basic ' + auth, 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': '*/*', 'User-Agent': 'pystrikev0.5.1', } else: method = 'GET' path = self.api_base + 'charges/' + self.id body = None headers = { 'Authorization': 'Basic ' + auth, 'Accept': '*/*', 'User-Agent': 'pystrikev0.5.1', } data = self._make_request(method, path, body, headers) try: self._fill_from_data_dict(data) except KeyError: if 'code' in data: if data['code'] == 404: raise ChargeNotFoundException(data['message']) elif data['code'] >= 400 and data['code'] <= 499: raise ClientRequestException(data['message']) elif data['code'] >= 500 and data['code'] <= 599: raise ServerErrorException(data['message']) raise UnexpectedResponseException( "The strike server returned an unexpected response: " + json.dumps(data) )
[docs] @classmethod def from_charge_id(cls, charge_id): """ Class method to create and an instance of `Charge` and fill it from the Strike server. Args: - charge_id (str): The id of a charge on Strike's server. Returns: - An instance of `Charge`, filled from the attributes of the charge with the given `charge_id`. """ charge = cls(0, cls.CURRENCY_BTC, create=False) charge.id = charge_id charge.update() return charge
[docs]def make_charge_class(api_key, api_host, api_base): """ Generates a Charge class with the given parameters Args: - api_key (str): An API key associated with your Strike account. - api_host (str): The host name of the Strike server you'd like to connect to. Probably one of: - "api.strike.acinq.co" - "api.dev.strike.acinq.co" - api_base (str): The base path of the Strike API on the host server. Probably: "/api/v1/" Returns: A parameterized Charge class object. """ parameters = { 'api_key': api_key, 'api_host': api_host, 'api_base': api_base, } class MyCharge(Charge): """ This concrete subclass of `Charge` is defined and returned by the `make_charge_class` function. """ api_key = parameters['api_key'] api_host = parameters['api_host'] api_base = parameters['api_base'] return MyCharge