├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── pystrike ├── __init__.py ├── charge.py └── exceptions.py ├── readthedocs.yml ├── setup.py └── test ├── __init__.py └── test_charge.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | 5 | # command to run tests 6 | script: 7 | - python -m unittest discover 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joseph Schilz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/JASchilz/pystrike.svg?branch=master 2 | :target: https://travis-ci.org/JASchilz/pystrike 3 | .. image:: https://api.codeclimate.com/v1/badges/3b5d31b0331c41501416/maintainability 4 | :target: https://codeclimate.com/github/JASchilz/pystrike/maintainability 5 | :alt: Maintainability 6 | .. image:: https://img.shields.io/pypi/v/pystrike.svg 7 | :target: https://pypi.org/project/pystrike/ 8 | :alt: PyPI 9 | .. image:: https://readthedocs.org/projects/pystrike/badge/?version=latest 10 | :target: https://pystrike.readthedocs.io/en/latest/?badge=latest 11 | :alt: Documentation Status 12 | 13 | 14 | pystrike 15 | ======== 16 | 17 | Python wrapper for `Acinq’s Strike lightning network payment service`_. 18 | 19 | The lightning network allows near-fee, near-instant transactions atop the Bitcoin chain layer. Acinq operates the Strike service, which allows you to create lightning invoices, receive lightning payments into your Strike account, and then receive consolidated payouts on-chain. This Python library allows you to invoice customers and check the payment-status of those invoices in just a few lines of code. 20 | 21 | This library does not require any third-party dependencies. 22 | 23 | Example 24 | ------- 25 | 26 | Initialize the ``Charge`` class: 27 | 28 | :: 29 | 30 | from pystrike.charge import make_charge_class 31 | 32 | Charge = make_charge_class( 33 | api_key="YOURSTRIKETESTNETAPIKEY", 34 | api_host="api.dev.strike.acinq.co", 35 | api_base="/api/v1/", 36 | ) 37 | 38 | Create a new ``charge``: 39 | 40 | :: 41 | 42 | charge = Charge( 43 | currency=Charge.CURRENCY_BTC, 44 | amount=4200, # Amount in Satoshi 45 | description="services rendered", 46 | ) 47 | 48 | Retrieve a payment request: 49 | 50 | :: 51 | 52 | payment_request = charge.payment_request 53 | 54 | # Now `payment_request` might be something like "lnbtb420u1pfoobarbaz..." 55 | 56 | At this point, you would present the ``payment_request`` to your 57 | customer. You can call ``charge.update()`` to poll the Strike server 58 | for the current status of the charge, and then retrieve whether or not 59 | the charge has been paid from the ``charge.paid`` attribute. 60 | 61 | For example, suppose that ``charge.payment_request`` has not yet been paid and then we run the following code: 62 | 63 | :: 64 | 65 | charge.update() # Reaches out the the Acinq server to retrieve the 66 | # status of the charge 67 | 68 | paid = charge.paid 69 | # Because the payment request has not yet been paid, charge.paid is False 70 | 71 | Then suppose that the client pays the ``charge.payment_request`` and then we run the following code: 72 | 73 | :: 74 | 75 | charge.update() 76 | paid = charge.paid 77 | # Because the client paid the request before we called `update`, charge.paid 78 | # evaluates to True. 79 | 80 | Acinq's Strike service also offers a web hook/callback service, which is a better way to update your charges than frequent polling if you are running a web service. 81 | 82 | The example above uses Strike's testnet web service at ``api.dev.strike.acinq.co``. When you're ready to issue mainnet lightning invoices, you'll need to use your Strike mainnet API key and make your requests to host ``api.strike.acinq.co``. 83 | 84 | Use 85 | --- 86 | 87 | Install pystrike 88 | ^^^^^^^^^^^^^^^^ 89 | 90 | :: 91 | 92 | $ pip install pystrike 93 | 94 | Creating an API Key 95 | ^^^^^^^^^^^^^^^^^^^ 96 | 97 | Begin by creating an account on `Acinq’s Strike lightning network payment service`_. Note that there is also a `testnet version of the service`_ that you may wish to use for your initial development. The two versions of this service are distinct, with separate accounts, separate API keys, and separate API hosts. 98 | 99 | Once you have created an account and logged into the dashboard, you can retrieve an API key from your dashboard settings. You will need this key to configure your connection to Strike. 100 | 101 | Configuring a Charge Class 102 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 103 | 104 | You'll begin by creating a Charge class from the provided ``make_charge_class`` function. 105 | 106 | :: 107 | 108 | from pystrike.charge import make_charge_class 109 | 110 | Charge = make_charge_class( 111 | api_key="YOURSTRIKETESTNETAPIKEY", 112 | api_host="api.dev.strike.acinq.co", 113 | api_base="/api/v1/", 114 | ) 115 | 116 | The host will probably be one of: 117 | 118 | - api.strike.acinq.co: the mainnet version of Strike 119 | - api.dev.strike.acinq.co: the testnet version of Strike 120 | 121 | If you're pointing your charge class to the mainnet version then be sure to use the API key from your mainnet Strike dashboard. 122 | 123 | Creating a Charge 124 | ^^^^^^^^^^^^^^^^^ 125 | 126 | You can create a new charge with the following code: 127 | 128 | :: 129 | 130 | charge = Charge( 131 | currency=Charge.CURRENCY_BTC, 132 | amount=4200, # Amount in Satoshi 133 | description="services rendered", 134 | ) 135 | 136 | This initialization will automatically reach out to the Strike web service and create a new charge on their servers. Once this call has returned, you can immediately access the details of that charge through ``charge.id``, ``charge.payment_request``, and so on. 137 | 138 | At this point, you might present the ``charge.payment_request`` to your customer for payment. 139 | 140 | Retrieving a Charge 141 | ^^^^^^^^^^^^^^^^^^^ 142 | 143 | Rather than creating a new charge, if you know the Strike id of an existing charge you can retrieve it with the following code: 144 | 145 | :: 146 | 147 | charge = Charge.from_charge_id('ch_LWafoobarbazjFFv8eurFJkerhgDA') 148 | 149 | Updating a Charge 150 | ^^^^^^^^^^^^^^^^^ 151 | 152 | You can poll the Strike server to update your local charge object: 153 | 154 | :: 155 | 156 | charge.update() 157 | 158 | This command reaches out to the Strike server and updates the attributes of the charge. For example, if you are waiting on payment for a charge, you might run ``charge.update()`` to retrieve the status of the charge from the Strike server and then access ``charge.paid`` to see if a payment has been recorded for the charge on the Strike server. 159 | 160 | If you're developing a web application, you could use web hooks instead of polling the server. See Strike's documentation on web hooks for more information. 161 | 162 | Testing 163 | ------- 164 | 165 | Running the library tests requires two environment variables: 166 | 167 | - ``STRIKE_TESTNET_API_KEY``: Your API key for the ``api.dev.strike.acinq.co`` 168 | web service. 169 | - ``RETRIEVE_CHARGE_ID``: The Strike id of a charge in your 170 | ``api.dev.strike.acinq.co``. For example: ``ch_LWafoobarbazjFFv8eufoobarbaz`` 171 | 172 | .. _Acinq’s Strike lightning network payment service: https://strike.acinq.co 173 | .. _testnet version of the service: https://dev.strike.acinq.co 174 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('..')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'pystrike' 23 | copyright = '2018, Joseph Schilz' 24 | author = 'Joseph Schilz' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.todo', 44 | 'sphinx.ext.viewcode', 45 | 'sphinx.ext.githubpages', 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = '.rst' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | # 63 | # This is also used if you do content translation via gettext catalogs. 64 | # Usually you set "language" from the command line for these cases. 65 | language = None 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | # This pattern also affects html_static_path and html_extra_path. 70 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 71 | 72 | # The name of the Pygments (syntax highlighting) style to use. 73 | pygments_style = None 74 | 75 | 76 | # -- Options for HTML output ------------------------------------------------- 77 | 78 | # The theme to use for HTML and HTML Help pages. See the documentation for 79 | # a list of builtin themes. 80 | # 81 | html_theme = 'sphinx_rtd_theme' 82 | 83 | # Theme options are theme-specific and customize the look and feel of a theme 84 | # further. For a list of options available for each theme, see the 85 | # documentation. 86 | # 87 | # html_theme_options = {} 88 | 89 | # Add any paths that contain custom static files (such as style sheets) here, 90 | # relative to this directory. They are copied after the builtin static files, 91 | # so a file named "default.css" will overwrite the builtin "default.css". 92 | html_static_path = ['_static'] 93 | 94 | # Custom sidebar templates, must be a dictionary that maps document names 95 | # to template names. 96 | # 97 | # The default sidebars (for documents that don't match any pattern) are 98 | # defined by theme itself. Builtin themes are using these templates by 99 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 100 | # 'searchbox.html']``. 101 | # 102 | # html_sidebars = {} 103 | 104 | 105 | # -- Options for HTMLHelp output --------------------------------------------- 106 | 107 | # Output file base name for HTML help builder. 108 | htmlhelp_basename = 'pystrikedoc' 109 | 110 | 111 | # -- Options for LaTeX output ------------------------------------------------ 112 | 113 | latex_elements = { 114 | # The paper size ('letterpaper' or 'a4paper'). 115 | # 116 | # 'papersize': 'letterpaper', 117 | 118 | # The font size ('10pt', '11pt' or '12pt'). 119 | # 120 | # 'pointsize': '10pt', 121 | 122 | # Additional stuff for the LaTeX preamble. 123 | # 124 | # 'preamble': '', 125 | 126 | # Latex figure (float) alignment 127 | # 128 | # 'figure_align': 'htbp', 129 | } 130 | 131 | # Grouping the document tree into LaTeX files. List of tuples 132 | # (source start file, target name, title, 133 | # author, documentclass [howto, manual, or own class]). 134 | latex_documents = [ 135 | (master_doc, 'pystrike.tex', 'pystrike Documentation', 136 | 'Joseph Schilz', 'manual'), 137 | ] 138 | 139 | 140 | # -- Options for manual page output ------------------------------------------ 141 | 142 | # One entry per manual page. List of tuples 143 | # (source start file, name, description, authors, manual section). 144 | man_pages = [ 145 | (master_doc, 'pystrike', 'pystrike Documentation', 146 | [author], 1) 147 | ] 148 | 149 | 150 | # -- Options for Texinfo output ---------------------------------------------- 151 | 152 | # Grouping the document tree into Texinfo files. List of tuples 153 | # (source start file, target name, title, author, 154 | # dir menu entry, description, category) 155 | texinfo_documents = [ 156 | (master_doc, 'pystrike', 'pystrike Documentation', 157 | author, 'pystrike', 'One line description of project.', 158 | 'Miscellaneous'), 159 | ] 160 | 161 | 162 | # -- Options for Epub output ------------------------------------------------- 163 | 164 | # Bibliographic Dublin Core info. 165 | epub_title = project 166 | 167 | # The unique identifier of the text. This can be a ISBN number 168 | # or the project homepage. 169 | # 170 | # epub_identifier = '' 171 | 172 | # A unique identification for the text. 173 | # 174 | # epub_uid = '' 175 | 176 | # A list of files that should not be packed into the epub file. 177 | epub_exclude_files = ['search.html'] 178 | 179 | 180 | # -- Extension configuration ------------------------------------------------- 181 | 182 | # -- Options for todo extension ---------------------------------------------- 183 | 184 | # If true, `todo` and `todoList` produce output, else they produce nothing. 185 | todo_include_todos = True 186 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pystrike documentation master file, created by 2 | sphinx-quickstart on Thu Sep 13 15:03:57 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | 8 | .. include:: ../README.rst 9 | 10 | API Guide 11 | --------- 12 | 13 | pystrike.charge.Charge 14 | ^^^^^^^^^^^^^^^^^^^^^^ 15 | 16 | .. autoclass:: pystrike.charge.Charge 17 | :members: 18 | 19 | .. automethod:: __init__ 20 | .. automethod:: __getattribute__ 21 | 22 | pystrike.charge.make_charge_class 23 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 24 | 25 | .. autofunction:: pystrike.charge.make_charge_class 26 | 27 | .. toctree:: 28 | :maxdepth: 4 29 | :caption: Contents: 30 | 31 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pystrike/__init__.py: -------------------------------------------------------------------------------- 1 | name = "pystrike" 2 | -------------------------------------------------------------------------------- /pystrike/charge.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python library for interacting with ACINQ's Strike API for lightning 3 | network payments. 4 | """ 5 | 6 | import json 7 | import base64 8 | import http.client 9 | import urllib.parse 10 | import ssl 11 | import abc 12 | import socket 13 | 14 | from .exceptions import ConnectionException, ClientRequestException, \ 15 | ChargeNotFoundException, UnexpectedResponseException, \ 16 | ServerErrorException 17 | 18 | 19 | class Charge(abc.ABC): 20 | """ 21 | The Charge class is your interface to the Strike web service. 22 | Use it to create, retrieve, and update lighting network 23 | charges. 24 | 25 | Each instance is a lazy mirror, reflecting a single charge on 26 | the Strike servers. The instance is lazy in that it will 27 | communicate with Strike implicitly, but only as needed. 28 | 29 | When you initialize a charge with an amount and description, 30 | the instance does not create an instance on Strike until the 31 | moment that you request an attribute such as `payment_request`. 32 | If you request the charge's `paid` attribute, then the charge 33 | will update itself from the Strike server if it has not yet 34 | seen its payment clear; but if `paid` is already set to `True` 35 | then the charge will simply report `True` without reaching out 36 | to the server. 37 | 38 | :ivar amount: The amount of the invoice, in self.currency. 39 | :ivar currency: The currency of the request. 40 | :ivar description: Narrative description of the invoice. 41 | :ivar customer_id: An optional customer identifier. 42 | :ivar id: The id of the charge on Strike's server. 43 | :ivar amount_satoshi: The amount of the request, in satoshi. 44 | :ivar payment_request: The payment request string for the charge. 45 | :ivar payment_hash: The hash of the payment for this charge. 46 | :ivar paid: Whether the request has been satisfied. 47 | :ivar created: When the charge was created, in epoch time. 48 | :ivar updated: When the charge was updated, in epoch time. 49 | 50 | """ 51 | 52 | CURRENCY_BTC = "btc" 53 | 54 | @property 55 | @abc.abstractmethod 56 | def api_key(self): 57 | """Concrete subclasses must define an api_key.""" 58 | 59 | pass 60 | 61 | @property 62 | @abc.abstractmethod 63 | def api_host(self): 64 | """Concrete subclasses must define an api_host.""" 65 | 66 | pass 67 | 68 | @property 69 | @abc.abstractmethod 70 | def api_base(self): 71 | """Concrete subclasses must define an api_base.""" 72 | 73 | pass 74 | 75 | def __init__( 76 | self, 77 | amount, 78 | currency, 79 | description="", 80 | customer_id="", 81 | create=True, 82 | ): 83 | """ 84 | Initialize an instance of `Charge`. See the Strike API 85 | documentation for details on each of the arguments. 86 | 87 | Args: 88 | - amount (int): The amount of the charge, in Satoshi. 89 | - currenency (str): Must be `Charge.CURRENCY_BTC`. 90 | 91 | Kwargs: 92 | - description (str): Optional invoice description. 93 | - customer_id (str): Optional customer identifier. 94 | - create (bool): Whether to automatically create a 95 | corresponding charge on the Strike 96 | service. 97 | 98 | 99 | """ 100 | 101 | self.api_connection = http.client.HTTPSConnection( 102 | self.api_host, 103 | context=ssl.create_default_context(), 104 | ) 105 | 106 | self.amount = amount 107 | self.currency = currency 108 | self.description = description 109 | self.customer_id = customer_id 110 | 111 | self.id = None 112 | self.amount_satoshi = None 113 | self.payment_request = None 114 | self.payment_hash = None 115 | self.paid = False 116 | self.created = None 117 | self.updated = None 118 | 119 | if create: 120 | self.update() 121 | 122 | def _make_request(self, method, path, body, headers, retry=True): 123 | 124 | try: 125 | self.api_connection.request( 126 | method, 127 | path, 128 | body=body, 129 | headers=headers, 130 | ) 131 | except socket.gaierror: 132 | raise ConnectionException("Unable to communicate with host.") 133 | 134 | try: 135 | response = self.api_connection.getresponse() 136 | except http.client.RemoteDisconnected: 137 | """ 138 | I found that the Strike server will prematurely close 139 | the connection the _first_ time I make a GET request 140 | after the invoice has been paid. 141 | 142 | This `except` clause represents a retry on that close 143 | condition. 144 | """ 145 | 146 | if method == 'GET' and retry: 147 | return self._make_request( 148 | method, path, body, headers, retry=False, 149 | ) 150 | else: 151 | raise ConnectionException( 152 | "Remote host disconnected without sending " + 153 | "a response" 154 | ) 155 | except: 156 | raise ConnectionException("Unable to communicate with host.") 157 | 158 | return json.loads(response.read().decode()) 159 | 160 | def _fill_from_data_dict(self, data): 161 | self.id = data['id'] 162 | self.amount = data['amount'] 163 | self.currency = data['currency'] 164 | self.amount_satoshi = data['amount_satoshi'] 165 | self.payment_hash = data['payment_hash'] 166 | self.payment_request = data['payment_request'] 167 | self.description = data['description'] 168 | self.paid = data['paid'] 169 | self.created = data['created'] 170 | self.updated = data['updated'] 171 | 172 | 173 | def update(self): 174 | """ 175 | Update the charge from the server. 176 | 177 | If this charge has an `id`, then the method will _retrieve_ the 178 | charge from the server. If this charge does not have an `id`, 179 | then this method will _create_ the charge on the server and 180 | then fill the local charge from the attributes created and 181 | returned by the Strike server. 182 | """ 183 | 184 | auth = base64.b64encode(self.api_key.encode() + b':').decode('ascii') 185 | must_create = super().__getattribute__('id') is None 186 | 187 | if must_create: 188 | method = 'POST' 189 | path = self.api_base + 'charges' 190 | body = urllib.parse.urlencode({ 191 | 'amount': self.amount, 192 | 'currency': self.currency, 193 | 'description': self.description, 194 | 'customer_id': self.customer_id, 195 | }) 196 | headers = { 197 | 'Authorization': 'Basic ' + auth, 198 | 'Content-Type': 'application/x-www-form-urlencoded', 199 | 'Accept': '*/*', 200 | 'User-Agent': 'pystrikev0.5.1', 201 | } 202 | 203 | else: 204 | method = 'GET' 205 | path = self.api_base + 'charges/' + self.id 206 | body = None 207 | headers = { 208 | 'Authorization': 'Basic ' + auth, 209 | 'Accept': '*/*', 210 | 'User-Agent': 'pystrikev0.5.1', 211 | } 212 | 213 | data = self._make_request(method, path, body, headers) 214 | 215 | try: 216 | self._fill_from_data_dict(data) 217 | except KeyError: 218 | if 'code' in data: 219 | if data['code'] == 404: 220 | raise ChargeNotFoundException(data['message']) 221 | elif data['code'] >= 400 and data['code'] <= 499: 222 | raise ClientRequestException(data['message']) 223 | elif data['code'] >= 500 and data['code'] <= 599: 224 | raise ServerErrorException(data['message']) 225 | 226 | raise UnexpectedResponseException( 227 | "The strike server returned an unexpected response: " + 228 | json.dumps(data) 229 | ) 230 | 231 | 232 | @classmethod 233 | def from_charge_id(cls, charge_id): 234 | """ 235 | Class method to create and an instance of `Charge` and fill it 236 | from the Strike server. 237 | 238 | Args: 239 | - charge_id (str): The id of a charge on Strike's server. 240 | 241 | Returns: 242 | - An instance of `Charge`, filled from the attributes of 243 | the charge with the given `charge_id`. 244 | 245 | """ 246 | 247 | charge = cls(0, cls.CURRENCY_BTC, create=False) 248 | 249 | charge.id = charge_id 250 | charge.update() 251 | 252 | return charge 253 | 254 | 255 | def make_charge_class(api_key, api_host, api_base): 256 | """ 257 | Generates a Charge class with the given parameters 258 | 259 | Args: 260 | - api_key (str): An API key associated with your Strike account. 261 | - api_host (str): The host name of the Strike server you'd like 262 | to connect to. Probably one of: 263 | - "api.strike.acinq.co" 264 | - "api.dev.strike.acinq.co" 265 | - api_base (str): The base path of the Strike API on the host 266 | server. Probably: "/api/v1/" 267 | 268 | Returns: 269 | A parameterized Charge class object. 270 | """ 271 | 272 | parameters = { 273 | 'api_key': api_key, 274 | 'api_host': api_host, 275 | 'api_base': api_base, 276 | } 277 | 278 | class MyCharge(Charge): 279 | """ 280 | This concrete subclass of `Charge` is defined and returned by 281 | the `make_charge_class` function. 282 | """ 283 | 284 | api_key = parameters['api_key'] 285 | api_host = parameters['api_host'] 286 | api_base = parameters['api_base'] 287 | 288 | return MyCharge 289 | -------------------------------------------------------------------------------- /pystrike/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exceptions for the pystrike module. 3 | """ 4 | 5 | 6 | class ConnectionException(Exception): 7 | """ 8 | Raised when the client is unable to communicate with the indicated 9 | host. 10 | 11 | This exception could indicate that the client is unable to contact 12 | the indicated host, or it could indicate that the client was unable 13 | to send an HTTP request to the indicated host, or that the client 14 | was unable to receive an HTTP response from the indicated host. 15 | """ 16 | 17 | pass 18 | 19 | 20 | class ClientRequestException(Exception): 21 | """ 22 | Raised when the server returns a 4xx response. 23 | 24 | The library code shall include the content of the error message 25 | from Strike, if available. 26 | """ 27 | 28 | pass 29 | 30 | 31 | class ServerErrorException(Exception): 32 | """ 33 | Raised when the server returns a 5xx response. 34 | 35 | The library code shall include the content of the error message 36 | from Strike, if available. 37 | """ 38 | 39 | pass 40 | 41 | 42 | class UnexpectedResponseException(Exception): 43 | """ 44 | Raised when the server returns a response that the library does not 45 | understand. 46 | """ 47 | 48 | pass 49 | 50 | 51 | class ChargeNotFoundException(ClientRequestException): 52 | """ 53 | Raised when the server returns a 404 response. 54 | """ 55 | 56 | pass 57 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | version: 3.6 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.rst", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="pystrike", 8 | version="0.5.3", 9 | author="Joseph Schilz", 10 | author_email="joseph@schilz.org", 11 | description="Python library for interacting with Acinq's Strike lightning network payment web service.", 12 | long_description=long_description, 13 | long_description_content_type="text/x-rst", 14 | url="https://github.com/JASchilz/pystrike", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | "Topic :: Software Development :: Libraries", 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JASchilz/pystrike/fdd166bb8f5e8a6ff19960a6e49596597a80cdf3/test/__init__.py -------------------------------------------------------------------------------- /test/test_charge.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from pystrike.charge import make_charge_class 5 | from pystrike.exceptions import ConnectionException, \ 6 | ClientRequestException, ChargeNotFoundException 7 | 8 | strike_api_key = os.environ.get('STRIKE_TESTNET_API_KEY') 9 | strike_api_host = os.environ.get('STRIKE_TESTNET_HOST', 'api.dev.strike.acinq.co') 10 | strike_api_base = os.environ.get('STRIKE_TESTNET_API_BASE', '/api/v1/') 11 | 12 | retrieve_charge_id = os.environ.get('RETRIEVE_CHARGE_ID') 13 | 14 | class ChargeTestCase(unittest.TestCase): 15 | 16 | def setUp(self): 17 | self.ChargeClass = make_charge_class( 18 | strike_api_key, 19 | strike_api_host, 20 | strike_api_base, 21 | ) 22 | 23 | def test_charge_creation(self): 24 | 25 | # Initialize a charge and create it on the Strike server. 26 | charge = self.ChargeClass( 27 | 100, 28 | self.ChargeClass.CURRENCY_BTC, 29 | 'Charge creation note', 30 | ) 31 | 32 | # The charge should have retrieved a charge.id during the 33 | # process of syncing to the server. This id should be a string 34 | # and it should be fairly long. 35 | self.assertGreater(len(charge.id), 10) 36 | 37 | def test_retrieve(self): 38 | 39 | # Retrieve a charge from Strike with a given charge id. 40 | charge = self.ChargeClass.from_charge_id(retrieve_charge_id) 41 | 42 | # The retrieved charge should have a charge.payment_request. 43 | # This payment request should be a string, and it should be 44 | # fairly long. 45 | self.assertGreater(len(charge.payment_request), 10) 46 | 47 | def test_charge_not_found_exception(self): 48 | 49 | with self.assertRaises(ChargeNotFoundException): 50 | 51 | # Retrieve a charge from Strike with a non-existant charge 52 | # id. 53 | charge = self.ChargeClass.from_charge_id('ch_madeupchargeid') 54 | 55 | def test_client_request_exception(self): 56 | 57 | with self.assertRaises(ClientRequestException): 58 | 59 | # Attempt to create a charge with a non-existant currency. 60 | charge = self.ChargeClass( 61 | 100, 62 | 'non-existant-currency', 63 | 'Charge creation note', 64 | ) 65 | 66 | --------------------------------------------------------------------------------