├── .gitignore ├── .gitmodules ├── .travis.yml ├── .travis └── travis_before_install.sh ├── LICENSE ├── README.md ├── contributing.md ├── lnd_grpc ├── __init__.py ├── base_client.py ├── config.py ├── invoices.py ├── lightning.py ├── lnd_grpc.py ├── protos │ ├── __init__.py │ ├── download_proto_files.py │ ├── generate_python_protos.md │ ├── invoices.proto │ ├── invoices_pb2.py │ ├── invoices_pb2_grpc.py │ ├── rpc.proto │ ├── rpc_pb2.py │ └── rpc_pb2_grpc.py ├── utilities.py └── wallet_unlocker.py ├── loop_rpc ├── __init__.py ├── loop_rpc.py └── protos │ ├── __init__.py │ ├── generate_python_loop_protos.txt │ ├── loop_client.proto │ ├── loop_client_pb2.py │ └── loop_client_pb2_grpc.py ├── requirements.txt ├── setup.py ├── test-requirements.txt └── tests ├── conftest.py ├── pytest.ini ├── test.md ├── test.py └── test_utils ├── __init__.py ├── btcproxy.py ├── fixtures.py ├── lnd.py ├── loop.py ├── test-tls.cert ├── test-tls.key └── utils.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Pycharm workspace settings 108 | .idea 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lnd_grpc/protos/googleapis"] 2 | path = lnd_grpc/protos/googleapis 3 | url = https://github.com/googleapis/googleapis.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 3.6 4 | before_install: ./.travis/travis_before_install.sh 5 | install: 6 | - pip install -r test-requirements.txt 7 | - pip install -e . 8 | script: 9 | - py.test -v -s tests/test.py -------------------------------------------------------------------------------- /.travis/travis_before_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Exit immediately at non-zero exit code 4 | set -ev 5 | 6 | ####################### 7 | ## Install Bitcoin Core 8 | ####################### 9 | 10 | export CORE_VERSION="0.18.0" 11 | 12 | wget https://bitcoincore.org/bin/bitcoin-core-${CORE_VERSION}/bitcoin-${CORE_VERSION}-x86_64-linux-gnu.tar.gz 13 | tar -xzf bitcoin-${CORE_VERSION}-x86_64-linux-gnu.tar.gz -C ${TRAVIS_BUILD_DIR} 14 | sudo cp ${TRAVIS_BUILD_DIR}/bitcoin-${CORE_VERSION}/bin/bitcoind /usr/local/bin/bitcoind 15 | sudo cp ${TRAVIS_BUILD_DIR}/bitcoin-${CORE_VERSION}/bin/bitcoin-cli /usr/local/bin/bitcoin-cli 16 | 17 | 18 | ############# 19 | # Install LND 20 | ############# 21 | 22 | export LND_VERSION="v0.7.1-beta" 23 | 24 | # Install LND 25 | wget https://github.com/lightningnetwork/lnd/releases/download/${LND_VERSION}/lnd-linux-amd64-${LND_VERSION}.tar.gz 26 | tar -xzf lnd-linux-amd64-${LND_VERSION}.tar.gz -C ${TRAVIS_BUILD_DIR} 27 | sudo cp ${TRAVIS_BUILD_DIR}/lnd-linux-amd64-${LND_VERSION}/lnd /usr/local/bin/lnd 28 | sudo cp ${TRAVIS_BUILD_DIR}/lnd-linux-amd64-${LND_VERSION}/lncli /usr/local/bin/lncli 29 | 30 | 31 | ############## 32 | # Install loop 33 | ############## 34 | 35 | export LOOP_VERSION="v0.2.1-alpha" 36 | 37 | # Install Loop 38 | wget https://github.com/lightninglabs/loop/releases/download/${LOOP_VERSION}/loop-linux-amd64-${LOOP_VERSION}.tar.gz 39 | tar -xzf loop-linux-amd64-${LOOP_VERSION}.tar.gz -C ${TRAVIS_BUILD_DIR} 40 | sudo cp ${TRAVIS_BUILD_DIR}/loop-linux-amd64-${LOOP_VERSION}/loopd /usr/local/bin/loopd 41 | sudo cp ${TRAVIS_BUILD_DIR}/loop-linux-amd64-${LOOP_VERSION}/loop /usr/local/bin/loop -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2019] [Will Clark] 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lnd-grpc 2 | 3 | Version 0.4.0 4 | 5 | Requires python >=3.6 6 | 7 | [![Build Status](https://travis-ci.org/willcl-ark/lnd_grpc.svg?branch=master)](https://travis-ci.org/willcl-ark/lnd_grpc) [![CodeFactor](https://www.codefactor.io/repository/github/willcl-ark/lnd_grpc/badge)](https://www.codefactor.io/repository/github/willcl-ark/lnd_grpc) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | A simple library to provide a Python 3 interface to the lnd lightning client gRPC. 10 | 11 | This version of the library has been compiled with lnd proto files from the v0.7.1-beta tag on github. 12 | This version has been tested using Bitcoin Core v0.18.0 as a backend 13 | 14 | ## Install requires: 15 | * `grpcio` 16 | * `grpcio-tools` 17 | * `googleapis-common-protos` 18 | 19 | Note: Configuration for coins other than bitcoin will require modifying the source code directly. 20 | 21 | ## Installation 22 | #### Via pip: 23 | 24 | `pip install lnd-grpc` 25 | 26 | #### Cloning and installing source as editable package: 27 | 28 | `git clone https://github.com/willcl-ark/lnd_grpc.git` 29 | 30 | `cd lnd_grpc` 31 | 32 | Activate virtual env as required 33 | 34 | `pip install -e .` 35 | 36 | ## Bitcoin setup 37 | 38 | bitcoind or btcd must be running and be ready to accept rpc connections from lnd. 39 | 40 | ## LND setup 41 | lnd daemon must be running on the host machine. This can typically be accomplished in a screen/tmux session. 42 | 43 | If lnd.conf is not already configured to communicate with your bitcoin client, an example lnd daemon startup command for bitcoind connection might look like: 44 | 45 | ``` 46 | lnd --bitcoin.active \ 47 | --bitcoin.mainnet \ 48 | --debuglevel=debug \ 49 | --bitcoin.node=bitcoind \ 50 | --bitcoind.rpcuser=xxxxx \ 51 | --bitcoind.rpcpass=xxxxxxxxxxxxxx \ 52 | --externalip=xx.xx.xx.xx \ 53 | --bitcoind.zmqpubrawblock=tcp://host:port \ 54 | --bitcoind.zmqpubrawtx=tcp://host:port \ 55 | --rpclisten=host:port 56 | ``` 57 | 58 | ## Using 59 | Import the module into your project: 60 | 61 | `import lnd_grpc` 62 | 63 | Create an instance of the client class: 64 | 65 | `lnd_rpc = lnd_grpc.Client()` 66 | 67 | Note: The class is instantiated to work with default bitcoind rpc port and lnd in default installation directory, on mainnet, unless additional arguments are passed. 68 | 69 | The class instantiation takes the the following arguments which you can change as required: 70 | 71 | ``` 72 | ( 73 | lnd_dir: str = None, \ 74 | macaroon_path: str = None, \ 75 | tls_cert_path: str = None \ 76 | network: str = 'mainnet', \ 77 | grpc_host: str = 'localhost', \ 78 | grpc_port: str = '10009' 79 | ) 80 | ``` 81 | 82 | #### Initialization of a new lnd installation 83 | 84 | Note: If you have already created a wallet during lnd setup/installation you can skip this section. 85 | 86 | If this is the first time you have run lnd you will not have a wallet created. 'Macaroons', the authentication technique used to communicate securely with lnd, are tied to a wallet (seed) and therefore an alternative connection must be made with lnd to create the wallet, before recreating the connection stub using the wallet's macaroon. 87 | 88 | Initialization requires the following steps: 89 | 1. Generate a new seed `lnd_rpc.gen_seed()` 90 | 2. Initialize a new wallet `lnd_rpc.init_wallet()` 91 | 92 | 93 | ## Connecting and re-connecting after wallet created 94 | If you did not run the initialization sequence above, you will only need to unlock your wallet before issuing further RPC commands: 95 | 96 | `lnd_rpc.unlock_wallet(password='wallet_password')` 97 | 98 | ## Interface conventions 99 | Further RPC commands can then be issued to the lnd gRPC interface using the following convention, where LND gRPC commands are converted from CamelCase to lowercase_with_underscores and keyword arguments named to exactly match the parameters the gRPC uses: 100 | 101 | `lnd_rpc.grpc_command(keyword_arg=value)` 102 | 103 | Valid gRPC commands and their keyword arguments can be found [here](https://api.lightning.community/?python#lnd-grpc-api-reference) 104 | 105 | Connection stubs will be generated dynamically as required to ensure channel freshness. 106 | 107 | ## Iterables 108 | Response-streaming RPCs now return the python iterators themselves to be operated on, e.g. with `.__next__()` or `for resp in response:` 109 | 110 | ## Threading 111 | The backend LND server (Golang) has asynchronous capability so any limitations are on the client side. 112 | The Python gRPC Client is not natively async-compatible (e.g. using asyncio). There are wrappers which exist that can 'wrap' python gRPC Client methods into async methods, but using threading is the officially support technique at this moment. 113 | 114 | For Python client threading to work correctly you must use the same **channel** for each thread. This is easy with this library if you use a single Client() instance in your application, as the same channel is used for each RPC for that Client object. This makes threading relatively easy, e.g.: 115 | 116 | ``` 117 | # get a queue to add responses to 118 | queue = queue.Queue() 119 | 120 | # create a function to perform the work you want the thread to target: 121 | def inv_sub_worker(_hash): 122 | for _response in lnd_rpc.subscribe_single_invoice(_hash): 123 | queue.put(_response) 124 | 125 | # create the thread 126 | # useful to use daemon mode for subscriptions 127 | inv_sub = threading.Thread(target=inv_sub_worker, args=[_hash, ], daemon=True) 128 | 129 | # start the thread 130 | inv_sub.start() 131 | ``` 132 | 133 | # BTCPay 134 | BTCPay run their LND node's grpc behind an nginx proxy. In order to authenticate with this, the easiest way is to use your OS root certificate store for the tls cert path: 135 | 136 | OSX: `/etc/ssl/cert.pem` 137 | 138 | Debian-based: `/etc/ssl/certs/ca-certificates.crt` 139 | 140 | Other OS: Google it :) 141 | 142 | BTCPay server also presents the user with the admin.macaroon in hex format via the web interface, whereas lnd_grpc expects the raw binary file. The easiest way to obtain this is to SSH into the BTCPay instance and transfer the file from `/var/lib/docker/volumes/generated_lnd_bitcoin_datadir/_data/admin.macaroon` onto your local machine. 143 | 144 | # Loop 145 | LND must be re-built and installed as per the loop instructions found at the [Loop Readme](https://github.com/lightninglabs/loop/blob/master/README.md). 146 | 147 | Loopd should then be installed as per the same instructions and started manually. 148 | 149 | Then you can import and use the RPC client using the following code: 150 | 151 | ``` 152 | import loop_rpc 153 | 154 | loop = loop_rpc.LoopClient() 155 | ``` -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to lnd_grpc 2 | 3 | The lnd_grpc project operates an open contributor model where anyone is 4 | welcome to contribute towards development in the form of peer review, testing 5 | and patches. This document explains the practical process and guidelines for 6 | contributing. 7 | 8 | ## Contributor Workflow 9 | 10 | The codebase is maintained using the "contributor workflow" where everyone 11 | without exception contributes patch proposals using "pull requests". This 12 | facilitates social contribution, easy testing and peer review. 13 | 14 | To contribute a patch, the workflow is as follows: 15 | 16 | 1. Fork repository 17 | 1. Create topic branch 18 | 1. Commit patches 19 | 20 | In general [commits should be atomic](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention) 21 | and diffs should be easy to read. For this reason do not mix any formatting 22 | fixes or code moves with actual code changes. 23 | 24 | Commit messages should be verbose by default consisting of a short subject line 25 | (50 chars max), a blank line and detailed explanatory text as separate 26 | paragraph(s), unless the title alone is self-explanatory (like "Corrected typo 27 | in init.cpp") in which case a single title line is sufficient. Commit messages should be 28 | helpful to people reading your code in the future, so explain the reasoning for 29 | your decisions. Further explanation [here](https://chris.beams.io/posts/git-commit/). 30 | 31 | If a particular commit references another issue, please add the reference. For 32 | example: `refs #12` or `fixes #21`. 33 | 34 | - Push changes to your fork 35 | - Create pull request 36 | 37 | The body of the pull request should contain enough description about what the 38 | patch does together with any justification/reasoning. 39 | 40 | ## Squashing Commits 41 | 42 | If your pull request is accepted for merging, you may need to squash and or 43 | [rebase](https://git-scm.com/docs/git-rebase) your commits 44 | before it will be merged. The basic squashing workflow is shown below. 45 | 46 | git checkout your_branch_name 47 | git rebase -i HEAD~n 48 | # n is normally the number of commits in the pull request. 49 | # Set commits (except the one in the first line) from 'pick' to 'squash', save and quit. 50 | # On the next screen, edit/refine commit messages. 51 | # Save and quit. 52 | git push -f # (force push to GitHub) 53 | 54 | Please update the resulting commit message if needed. 55 | 56 | ## Copyright 57 | 58 | By contributing to this repository, you agree to license your work under the 59 | MIT license unless specified otherwise in `contrib/debian/copyright` or at 60 | the top of the file itself. Any work contributed where you are not the original 61 | author must contain its license header with the original author(s) and source. 62 | 63 | 64 | # Contributor Covenant Code of Conduct 65 | 66 | ## Our Pledge 67 | 68 | In the interest of fostering an open and welcoming environment, we as 69 | contributors and maintainers pledge to making participation in our project and 70 | our community a harassment-free experience for everyone, regardless of age, body 71 | size, disability, ethnicity, sex characteristics, gender identity and expression, 72 | level of experience, education, socio-economic status, nationality, personal 73 | appearance, race, religion, or sexual identity and orientation. 74 | 75 | ## Our Standards 76 | 77 | Examples of behavior that contributes to creating a positive environment 78 | include: 79 | 80 | * Using welcoming and inclusive language 81 | * Being respectful of differing viewpoints and experiences 82 | * Gracefully accepting constructive criticism 83 | * Focusing on what is best for the community 84 | * Showing empathy towards other community members 85 | 86 | Examples of unacceptable behavior by participants include: 87 | 88 | * The use of sexualized language or imagery and unwelcome sexual attention or 89 | advances 90 | * Trolling, insulting/derogatory comments, and personal or political attacks 91 | * Public or private harassment 92 | * Publishing others' private information, such as a physical or electronic 93 | address, without explicit permission 94 | * Other conduct which could reasonably be considered inappropriate in a 95 | professional setting 96 | 97 | ## Our Responsibilities 98 | 99 | Project maintainers are responsible for clarifying the standards of acceptable 100 | behavior and are expected to take appropriate and fair corrective action in 101 | response to any instances of unacceptable behavior. 102 | 103 | Project maintainers have the right and responsibility to remove, edit, or 104 | reject comments, commits, code, wiki edits, issues, and other contributions 105 | that are not aligned to this Code of Conduct, or to ban temporarily or 106 | permanently any contributor for other behaviors that they deem inappropriate, 107 | threatening, offensive, or harmful. 108 | 109 | ## Scope 110 | 111 | This Code of Conduct applies both within project spaces and in public spaces 112 | when an individual is representing the project or its community. Examples of 113 | representing a project or community include using an official project e-mail 114 | address, posting via an official social media account, or acting as an appointed 115 | representative at an online or offline event. Representation of a project may be 116 | further defined and clarified by project maintainers. 117 | 118 | ## Enforcement 119 | 120 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 121 | reported by contacting the project team at [will8clark@gmail.com](mailto:will8clark@gmail.com) All complaints will be reviewed and investigated and will result in a response 122 | that is deemed necessary and appropriate to the circumstances. The project team is 123 | obligated to maintain confidentiality with regard to the reporter of an incident. 124 | Further details of specific enforcement policies may be posted separately. 125 | 126 | Project maintainers who do not follow or enforce the Code of Conduct in good 127 | faith may face temporary or permanent repercussions as determined by other 128 | members of the project's leadership. 129 | 130 | ## Attribution 131 | 132 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 133 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 134 | 135 | [homepage]: https://www.contributor-covenant.org 136 | 137 | For answers to common questions about this code of conduct, see 138 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /lnd_grpc/__init__.py: -------------------------------------------------------------------------------- 1 | from lnd_grpc.lnd_grpc import * 2 | 3 | name = "lnd_grpc" 4 | -------------------------------------------------------------------------------- /lnd_grpc/base_client.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | from pathlib import Path 3 | import sys 4 | from os import environ 5 | 6 | import grpc 7 | 8 | from lnd_grpc.config import * 9 | import lnd_grpc.protos.rpc_pb2 as ln 10 | from lnd_grpc.utilities import get_lnd_dir 11 | 12 | # tell gRPC which cypher suite to use 13 | environ["GRPC_SSL_CIPHER_SUITES"] = ( 14 | "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:" 15 | "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:" 16 | "ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384" 17 | ) 18 | 19 | 20 | class BaseClient: 21 | """ 22 | A Base client which the other client services can build from. Can find tls cert and 23 | keys, and macaroons in 'default' locations based off lnd_dir and network parameters. 24 | 25 | Has some static helper methods for various applications. 26 | """ 27 | 28 | def __init__( 29 | self, 30 | lnd_dir: str = None, 31 | macaroon_path: str = None, 32 | tls_cert_path: str = None, 33 | network: str = defaultNetwork, 34 | grpc_host: str = defaultRPCHost, 35 | grpc_port: str = defaultRPCPort, 36 | ): 37 | 38 | self.lnd_dir = lnd_dir 39 | self.macaroon_path = macaroon_path 40 | self.tls_cert_path = tls_cert_path 41 | self.network = network 42 | self.grpc_host = grpc_host 43 | self.grpc_port = str(grpc_port) 44 | self.channel = None 45 | self.connection_status = None 46 | self.connection_status_change = False 47 | self.grpc_options = GRPC_OPTIONS 48 | 49 | @property 50 | def lnd_dir(self): 51 | """ 52 | try automatically if not set as object init attribute 53 | :return: lnd_dir 54 | """ 55 | if self._lnd_dir: 56 | return self._lnd_dir 57 | else: 58 | self._lnd_dir = get_lnd_dir() 59 | return self._lnd_dir 60 | 61 | @lnd_dir.setter 62 | def lnd_dir(self, path): 63 | self._lnd_dir = path 64 | 65 | @property 66 | def tls_cert_path(self): 67 | """ 68 | :return: tls_cert_path 69 | """ 70 | if self._tls_cert_path is None: 71 | self._tls_cert_path = Path(self.lnd_dir) / defaultTLSCertFilename 72 | return str(self._tls_cert_path) 73 | 74 | @tls_cert_path.setter 75 | def tls_cert_path(self, path): 76 | self._tls_cert_path = path 77 | 78 | @property 79 | def tls_cert(self) -> bytes: 80 | """ 81 | :return: tls.cert as bytestring 82 | """ 83 | try: 84 | with open(self.tls_cert_path, "rb") as r: 85 | _tls_cert = r.read() 86 | except FileNotFoundError: 87 | sys.stderr.write("TLS cert not found at %s" % self.tls_cert_path) 88 | raise 89 | if not _tls_cert.startswith(b"-----BEGIN CERTIFICATE-----"): 90 | sys.stderr.write( 91 | "TLS cert at %s did not start with b'-----BEGIN CERTIFICATE-----')" 92 | % self.tls_cert_path 93 | ) 94 | return _tls_cert 95 | 96 | @property 97 | def macaroon_path(self) -> str: 98 | """ 99 | :return: macaroon path 100 | """ 101 | if not self._macaroon_path: 102 | self._macaroon_path = ( 103 | Path(self.lnd_dir) 104 | / f"{defaultDataDirname}/{defaultChainSubDirname}/bitcoin/" 105 | f"{self.network}/{defaultAdminMacFilename}" 106 | ) 107 | return str(self._macaroon_path) 108 | else: 109 | return self._macaroon_path 110 | 111 | @macaroon_path.setter 112 | def macaroon_path(self, path: str): 113 | self._macaroon_path = path 114 | 115 | @property 116 | def macaroon(self): 117 | """ 118 | try to open the macaroon and return it as a byte string 119 | """ 120 | try: 121 | with open(self.macaroon_path, "rb") as f: 122 | macaroon_bytes = f.read() 123 | macaroon = codecs.encode(macaroon_bytes, "hex") 124 | return macaroon 125 | except FileNotFoundError: 126 | sys.stderr.write( 127 | f"Could not find macaroon in {self.macaroon_path}. This might happen" 128 | f"in versions of lnd < v0.5-beta or those not using default" 129 | f"installation path. Set client object's macaroon_path attribute" 130 | f"manually." 131 | ) 132 | 133 | def metadata_callback(self, context, callback): 134 | """ 135 | automatically incorporate the macaroon into all requests 136 | :return: macaroon callback 137 | """ 138 | callback([("macaroon", self.macaroon)], None) 139 | 140 | def connectivity_event_logger(self, channel_connectivity): 141 | """ 142 | Channel connectivity callback logger 143 | """ 144 | self.connection_status = channel_connectivity._name_ 145 | if ( 146 | self.connection_status == "SHUTDOWN" 147 | or self.connection_status == "TRANSIENT_FAILURE" 148 | ): 149 | self.connection_status_change = True 150 | 151 | @property 152 | def combined_credentials(self) -> grpc.CallCredentials: 153 | """ 154 | Combine ssl and macaroon credentials 155 | :return: grpc.composite_channel_credentials 156 | """ 157 | cert_creds = grpc.ssl_channel_credentials(self.tls_cert) 158 | auth_creds = grpc.metadata_call_credentials(self.metadata_callback) 159 | return grpc.composite_channel_credentials(cert_creds, auth_creds) 160 | 161 | @property 162 | def grpc_address(self) -> str: 163 | return str(self.grpc_host + ":" + self.grpc_port) 164 | 165 | @staticmethod 166 | def channel_point_generator(funding_txid, output_index): 167 | """ 168 | Generate a ln.ChannelPoint object from a funding_txid and output_index 169 | :return: ln.ChannelPoint 170 | """ 171 | return ln.ChannelPoint( 172 | funding_txid_str=funding_txid, output_index=int(output_index) 173 | ) 174 | 175 | @staticmethod 176 | def lightning_address(pubkey, host): 177 | """ 178 | Generate a ln.LightningAddress object from a pubkey + host 179 | :return: ln.LightningAddress 180 | """ 181 | return ln.LightningAddress(pubkey=pubkey, host=host) 182 | 183 | @staticmethod 184 | def hex_to_bytes(hex_string: str): 185 | return bytes.fromhex(hex_string) 186 | 187 | @staticmethod 188 | def bytes_to_hex(bytestring: bytes): 189 | return bytestring.hex() 190 | -------------------------------------------------------------------------------- /lnd_grpc/config.py: -------------------------------------------------------------------------------- 1 | # LND default params 2 | # source: https://github.com/lightningnetwork/lnd/blob/master/config.go 3 | 4 | defaultConfigFilename = "lnd.conf" 5 | defaultDataDirname = "data" 6 | defaultChainSubDirname = "chain" 7 | defaultGraphSubDirname = "graph" 8 | defaultTLSCertFilename = "tls.cert" 9 | defaultTLSKeyFilename = "tls.key" 10 | defaultAdminMacFilename = "admin.macaroon" 11 | defaultReadMacFilename = "readonly.macaroon" 12 | defaultInvoiceMacFilename = "invoice.macaroon" 13 | defaultLogLevel = "info" 14 | defaultLogDirname = "logs" 15 | defaultLogFilename = "lnd.log" 16 | defaultRPCPort = 10009 17 | defaultRESTPort = 8080 18 | defaultPeerPort = 9735 19 | defaultRPCHost = "localhost" 20 | defaultNetwork = "mainnet" 21 | defaultNoSeedBackup = False 22 | defaultTorSOCKSPort = 9050 23 | defaultTorDNSHost = "soa.nodes.lightning.directory" 24 | defaultTorDNSPort = 53 25 | defaultTorControlPort = 9051 26 | defaultTorV2PrivateKeyFilename = "v2_onion_private_key" 27 | defaultTorV3PrivateKeyFilename = "v3_onion_private_key" 28 | 29 | # lnd_grpc default params 30 | GRPC_OPTIONS = [ 31 | ("grpc.max_receive_message_length", 33554432), 32 | ("grpc.max_send_message_length", 33554432), 33 | ] 34 | -------------------------------------------------------------------------------- /lnd_grpc/invoices.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | import grpc 4 | 5 | import lnd_grpc.protos.invoices_pb2 as inv 6 | import lnd_grpc.protos.invoices_pb2_grpc as invrpc 7 | import lnd_grpc.protos.rpc_pb2 as ln 8 | from lnd_grpc.base_client import BaseClient 9 | from lnd_grpc.config import defaultNetwork, defaultRPCHost, defaultRPCPort 10 | 11 | # tell gRPC which cypher suite to use 12 | environ["GRPC_SSL_CIPHER_SUITES"] = \ 13 | 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:' \ 14 | 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:' \ 15 | 'ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384' 16 | 17 | 18 | 19 | class Invoices(BaseClient): 20 | """ 21 | Provides a super-class to interface with the Invoices sub-system. Currently mainly used only 22 | for hold invoice applications. 23 | """ 24 | 25 | def __init__( 26 | self, 27 | lnd_dir: str = None, 28 | macaroon_path: str = None, 29 | tls_cert_path: str = None, 30 | network: str = defaultNetwork, 31 | grpc_host: str = defaultRPCHost, 32 | grpc_port: str = defaultRPCPort, 33 | ): 34 | self._inv_stub: invrpc.InvoicesStub = None 35 | 36 | super().__init__( 37 | lnd_dir=lnd_dir, 38 | macaroon_path=macaroon_path, 39 | tls_cert_path=tls_cert_path, 40 | network=network, 41 | grpc_host=grpc_host, 42 | grpc_port=grpc_port, 43 | ) 44 | 45 | @property 46 | def invoice_stub(self) -> invrpc.InvoicesStub: 47 | if self._inv_stub is None: 48 | ssl_creds = grpc.ssl_channel_credentials(self.tls_cert) 49 | self._inv_channel = grpc.secure_channel( 50 | target=self.grpc_address, 51 | credentials=self.combined_credentials, 52 | options=self.grpc_options, 53 | ) 54 | self._inv_stub = invrpc.InvoicesStub(self._inv_channel) 55 | return self._inv_stub 56 | 57 | def subscribe_single_invoice( 58 | self, r_hash: bytes = b"", r_hash_str: str = "" 59 | ) -> ln.Invoice: 60 | """ 61 | Returns a uni-directional stream (server -> client) for notifying the client of invoice 62 | state changes. 63 | 64 | This is particularly useful in hold invoices where invoices might be paid by the 'payer' 65 | but not settled immediately by the 'receiver'; the 'payer' will want to watch for settlement 66 | or cancellation 67 | 68 | :return: an iterable of Invoice updates with 20 attributes per update 69 | """ 70 | request = ln.PaymentHash(r_hash=r_hash, r_hash_str=r_hash_str) 71 | response = self.invoice_stub.SubscribeSingleInvoice(request) 72 | return response 73 | 74 | def cancel_invoice(self, payment_hash: bytes = b"") -> inv.CancelInvoiceResp: 75 | """ 76 | Cancels a currently open invoice. If the invoice is already canceled, this call will 77 | succeed. If the invoice is already settled, it will fail. 78 | 79 | Once a hold invoice is accepted in lnd system it is held there until either a cancel or 80 | settle rpc is received. 81 | 82 | :return: CancelInvoiceResponse with no attributes 83 | """ 84 | request = inv.CancelInvoiceMsg(payment_hash=payment_hash) 85 | response = self.invoice_stub.CancelInvoice(request) 86 | return response 87 | 88 | def add_hold_invoice( 89 | self, 90 | memo: str = "", 91 | hash: bytes = b"", 92 | value: int = 0, 93 | expiry: int = 3600, 94 | fallback_addr: str = "", 95 | cltv_expiry: int = 36, 96 | route_hints: ln.RouteHint = [], 97 | private: bool = 1, 98 | ) -> inv.AddHoldInvoiceResp: 99 | """ 100 | Attempts to add a new hold invoice to the invoice database. Any duplicated invoices are 101 | rejected, therefore all invoices *must* have a unique payment hash. 102 | 103 | Quick "hold" invoices: 104 | Instead of immediately locking in and settling the htlc when the payment arrives, 105 | the htlc for a hold invoice is only locked in and not yet settled. At that point, 106 | it is not possible anymore for the sender to revoke the payment, but the receiver still 107 | can choose whether to settle or cancel the htlc and invoice. 108 | 109 | :return: AddHoldInvoiceResponse with 1 attribute: 'payment_request' 110 | """ 111 | request = inv.AddHoldInvoiceRequest( 112 | memo=memo, 113 | hash=hash, 114 | value=value, 115 | expiry=expiry, 116 | fallback_addr=fallback_addr, 117 | cltv_expiry=cltv_expiry, 118 | route_hints=route_hints, 119 | private=private, 120 | ) 121 | response = self.invoice_stub.AddHoldInvoice(request) 122 | return response 123 | 124 | def settle_invoice(self, preimage: bytes = b"") -> inv.SettleInvoiceResp: 125 | """ 126 | Settles an accepted invoice. If the invoice is already settled, this call will succeed. 127 | 128 | Once a hold invoice is accepted in lnd system it is held there until either a cancel or 129 | settle rpc is received. 130 | 131 | :return: SettleInvoiceResponse with no attributes 132 | """ 133 | request = inv.SettleInvoiceMsg(preimage=preimage) 134 | response = self.invoice_stub.SettleInvoice(request) 135 | return response 136 | -------------------------------------------------------------------------------- /lnd_grpc/lnd_grpc.py: -------------------------------------------------------------------------------- 1 | from lnd_grpc.base_client import BaseClient 2 | from lnd_grpc.invoices import Invoices 3 | from lnd_grpc.lightning import Lightning 4 | from lnd_grpc.wallet_unlocker import WalletUnlocker 5 | from lnd_grpc.config import defaultNetwork, defaultRPCHost, defaultRPCPort 6 | 7 | 8 | class Client(Lightning, WalletUnlocker, Invoices): 9 | def __init__( 10 | self, 11 | lnd_dir: str = None, 12 | macaroon_path: str = None, 13 | tls_cert_path: str = None, 14 | network: str = defaultNetwork, 15 | grpc_host: str = defaultRPCHost, 16 | grpc_port: str = defaultRPCPort, 17 | ): 18 | super().__init__( 19 | lnd_dir=lnd_dir, 20 | macaroon_path=macaroon_path, 21 | tls_cert_path=tls_cert_path, 22 | network=network, 23 | grpc_host=grpc_host, 24 | grpc_port=grpc_port, 25 | ) 26 | 27 | 28 | __all__ = ["BaseClient", "WalletUnlocker", "Lightning", "Invoices", "Client"] 29 | -------------------------------------------------------------------------------- /lnd_grpc/protos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willcl-ark/lnd_grpc/cf938c51c201f078e8bbe9e19ffc2d038f3abf7f/lnd_grpc/protos/__init__.py -------------------------------------------------------------------------------- /lnd_grpc/protos/download_proto_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | 4 | from lnd_grpc import lnd_grpc 5 | 6 | cwd = os.getcwd() 7 | 8 | """ 9 | Note: The script will attempt to connect to your instance of lnd to determine the correct version 10 | of the proto file to download. 11 | 12 | The script will currently only detect and download lnd and invoice proto files, not loop. 13 | """ 14 | 15 | 16 | # TODO: add invoice and loop support 17 | 18 | 19 | def capture_info(): 20 | lnd_dir = input("LND dir [default: searched by Client()]:") 21 | network = input("network [default: mainnet]:") or "mainnet" 22 | grpc_host = input("GRPC host address [default: '127.0.0.1']:") or "127.0.0.1" 23 | grpc_port = input("gRPC port [default: '10009']:") or "10009" 24 | return lnd_dir, network, grpc_host, grpc_port 25 | 26 | 27 | def create_lnd_client( 28 | lnd_dir: str = None, 29 | macaroon_path: str = None, 30 | network: str = "mainnet", 31 | grpc_host: str = "localhost", 32 | grpc_port: str = "10009", 33 | ): 34 | lncli = lnd_grpc.Client( 35 | lnd_dir=lnd_dir, 36 | network=network, 37 | grpc_host=grpc_host, 38 | grpc_port=grpc_port, 39 | macaroon_path=macaroon_path, 40 | ) 41 | 42 | return lncli 43 | 44 | 45 | def get_version(lncli): 46 | lnd_version = lncli.get_info().version.split("commit=")[1] 47 | return lnd_version 48 | 49 | 50 | def get_rpc_proto(lnd_version): 51 | try: 52 | url = f"https://raw.githubusercontent.com/lightningnetwork/lnd/{lnd_version}/lnrpc/rpc.proto" 53 | print(f"Connecting to: {url}") 54 | proto = requests.get(url) 55 | except requests.HTTPError as e: 56 | print(e) 57 | return 58 | 59 | # Write the proto file to the current working directory 60 | proto_file_name = cwd + "/" + "rpc.proto" 61 | proto_file = open(proto_file_name, "w") 62 | proto_file.write(proto.text) 63 | proto_file.close() 64 | 65 | # Test the written proto file 66 | proto_file = open(proto_file_name, "r") 67 | proto_file_first_line = proto_file.readline().strip() 68 | test_first_line = 'syntax = "proto3";' 69 | if proto_file_first_line == test_first_line: 70 | print("Proto file looks good") 71 | else: 72 | print( 73 | f"Proto file did not have expected first line\n" 74 | f"Expected: {test_first_line}\n" 75 | f"Read: {proto_file_first_line}\n" 76 | f"Exiting..." 77 | ) 78 | return 79 | 80 | 81 | def get_invoices_proto(lnd_version): 82 | try: 83 | url = f"https://raw.githubusercontent.com/lightningnetwork/lnd/{lnd_version}/lnrpc/invoicesrpc/invoices.proto" 84 | print(f"Connecting to: {url}") 85 | proto = requests.get(url) 86 | except requests.HTTPError as e: 87 | print(e) 88 | return 89 | 90 | # Write the proto file to the current working directory 91 | proto_file_name = cwd + "/" + "invoices.proto" 92 | proto_file = open(proto_file_name, "w") 93 | proto_file.write(proto.text) 94 | proto_file.close() 95 | 96 | # Test the written proto file 97 | with open(proto_file_name, "r") as proto_file: 98 | proto_file_first_line = proto_file.readline().strip() 99 | test_first_line = 'syntax = "proto3";' 100 | if proto_file_first_line == test_first_line: 101 | print("Proto file looks good") 102 | else: 103 | print( 104 | f"Proto file did not have expected first line\n" 105 | f"Expected: {test_first_line}\n" 106 | f"Read: {proto_file_first_line}\n" 107 | f"Exiting..." 108 | ) 109 | return 110 | 111 | # Fix Line 4 import for lnd_grpc package 112 | temp = None 113 | with open(proto_file_name, "r") as proto_file: 114 | temp = proto_file.readlines() 115 | temp[3] = 'import "lnd_grpc/protos/rpc.proto";\n' 116 | with open(proto_file_name, "w") as proto_file: 117 | proto_file.writelines(temp) 118 | 119 | 120 | def download_proto_files(): 121 | # Capture info used for LND client 122 | lnd_dir, network, grpc_host, grpc_port = capture_info() 123 | 124 | # Create a gRPC client that can query the version number for us 125 | lncli = create_lnd_client( 126 | lnd_dir=lnd_dir, network=network, grpc_host=grpc_host, grpc_port=grpc_port 127 | ) 128 | 129 | # Get the version number for the lnd instance 130 | lnd_version = get_version(lncli) 131 | print(f"Version: {lnd_version}") 132 | 133 | # Try to connect to the raw Github page for the rpc proto file for LND version 134 | get_rpc_proto(lnd_version) 135 | 136 | # Try to connect to the raw Github page for the invoices proto file for LND version 137 | get_invoices_proto(lnd_version) 138 | 139 | print("All tasks completed successfully") 140 | 141 | 142 | if __name__ == "__main__": 143 | download_proto_files() 144 | -------------------------------------------------------------------------------- /lnd_grpc/protos/generate_python_protos.md: -------------------------------------------------------------------------------- 1 | For when the project is cloned from github the process proceeds as follows: 2 | 3 | * Acquire raw proto file(s). 4 | 5 | * Generate python-specific proto files (python metaclasses) from these raw protos. 6 | 7 | 8 | 1) Either, you can manually download the appropriate rpc.proto file from: 9 | 10 | `https://raw.githubusercontent.com/lightningnetwork/lnd/v{lnd_version}/lnrpc/rpc.proto` 11 | 12 | Alternatively you can use the included 'download_proto_file.py' script. Make sure your current working directory is 13 | inside this directory .../lnd_grpc/protos/ and then run using: 14 | 15 | `python download_proto_files.py` 16 | 17 | Please read the notes at the top of the script before running. 18 | 19 | 1) Make sure googleapis is cloned in this folder: 20 | 21 | `git clone https://github.com/googleapis/googleapis.git` 22 | 23 | 2) Activate your venv if necessary! 24 | 25 | 3) Run command to generate **lnd** gRPC metaclass files: 26 | 27 | `python -m grpc_tools.protoc --proto_path=lnd_grpc/protos/googleapis:. --python_out=. --grpc_python_out=. lnd_grpc/protos/rpc.proto` 28 | 29 | 4) To generate **loop** proto metaclasses: 30 | 31 | `python -m grpc_tools.protoc --proto_path=lnd_grpc/protos/googleapis:. --python_out=. --grpc_python_out=. loop_rpc/protos/loop_client.proto` 32 | 33 | 5) To generate both **lnd rpc and invoice_rpc** python gRPC metaclasses: 34 | 35 | 1. Manually modify L4 of invoices.proto to read exactly: `import "lnd_grpc/protos/rpc.proto";` 36 | 37 | 2. Then run the command: 38 | 39 | `python -m grpc_tools.protoc --proto_path=lnd_grpc/protos/googleapis:. --python_out=. --grpc_python_out=. lnd_grpc/protos/rpc.proto lnd_grpc/protos/invoices.proto` -------------------------------------------------------------------------------- /lnd_grpc/protos/invoices.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/api/annotations.proto"; 4 | import "lnd_grpc/protos/rpc.proto"; 5 | 6 | package invoicesrpc; 7 | 8 | option go_package = "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"; 9 | 10 | // Invoices is a service that can be used to create, accept, settle and cancel 11 | // invoices. 12 | service Invoices { 13 | /** 14 | SubscribeSingleInvoice returns a uni-directional stream (server -> client) 15 | to notify the client of state transitions of the specified invoice. 16 | Initially the current invoice state is always sent out. 17 | */ 18 | rpc SubscribeSingleInvoice (SubscribeSingleInvoiceRequest) returns (stream lnrpc.Invoice); 19 | 20 | /** 21 | CancelInvoice cancels a currently open invoice. If the invoice is already 22 | canceled, this call will succeed. If the invoice is already settled, it will 23 | fail. 24 | */ 25 | rpc CancelInvoice(CancelInvoiceMsg) returns (CancelInvoiceResp); 26 | 27 | /** 28 | AddHoldInvoice creates a hold invoice. It ties the invoice to the hash 29 | supplied in the request. 30 | */ 31 | rpc AddHoldInvoice(AddHoldInvoiceRequest) returns (AddHoldInvoiceResp); 32 | 33 | /** 34 | SettleInvoice settles an accepted invoice. If the invoice is already 35 | settled, this call will succeed. 36 | */ 37 | rpc SettleInvoice(SettleInvoiceMsg) returns (SettleInvoiceResp); 38 | } 39 | 40 | message CancelInvoiceMsg { 41 | /// Hash corresponding to the (hold) invoice to cancel. 42 | bytes payment_hash = 1; 43 | } 44 | message CancelInvoiceResp {} 45 | 46 | message AddHoldInvoiceRequest { 47 | /** 48 | An optional memo to attach along with the invoice. Used for record keeping 49 | purposes for the invoice's creator, and will also be set in the description 50 | field of the encoded payment request if the description_hash field is not 51 | being used. 52 | */ 53 | string memo = 1 [json_name = "memo"]; 54 | 55 | /// The hash of the preimage 56 | bytes hash = 2 [json_name = "hash"]; 57 | 58 | /// The value of this invoice in satoshis 59 | int64 value = 3 [json_name = "value"]; 60 | 61 | /** 62 | Hash (SHA-256) of a description of the payment. Used if the description of 63 | payment (memo) is too long to naturally fit within the description field 64 | of an encoded payment request. 65 | */ 66 | bytes description_hash = 4 [json_name = "description_hash"]; 67 | 68 | /// Payment request expiry time in seconds. Default is 3600 (1 hour). 69 | int64 expiry = 5 [json_name = "expiry"]; 70 | 71 | /// Fallback on-chain address. 72 | string fallback_addr = 6 [json_name = "fallback_addr"]; 73 | 74 | /// Delta to use for the time-lock of the CLTV extended to the final hop. 75 | uint64 cltv_expiry = 7 [json_name = "cltv_expiry"]; 76 | 77 | /** 78 | Route hints that can each be individually used to assist in reaching the 79 | invoice's destination. 80 | */ 81 | repeated lnrpc.RouteHint route_hints = 8 [json_name = "route_hints"]; 82 | 83 | /// Whether this invoice should include routing hints for private channels. 84 | bool private = 9 [json_name = "private"]; 85 | } 86 | 87 | message AddHoldInvoiceResp { 88 | /** 89 | A bare-bones invoice for a payment within the Lightning Network. With the 90 | details of the invoice, the sender has all the data necessary to send a 91 | payment to the recipient. 92 | */ 93 | string payment_request = 1 [json_name = "payment_request"]; 94 | } 95 | 96 | message SettleInvoiceMsg { 97 | /// Externally discovered pre-image that should be used to settle the hold invoice. 98 | bytes preimage = 1; 99 | } 100 | 101 | message SettleInvoiceResp {} 102 | 103 | message SubscribeSingleInvoiceRequest { 104 | reserved 1; 105 | 106 | /// Hash corresponding to the (hold) invoice to subscribe to. 107 | bytes r_hash = 2 [json_name = "r_hash"]; 108 | } -------------------------------------------------------------------------------- /lnd_grpc/protos/invoices_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: lnd_grpc/protos/invoices.proto 4 | 5 | import sys 6 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 7 | from google.protobuf import descriptor as _descriptor 8 | from google.protobuf import message as _message 9 | from google.protobuf import reflection as _reflection 10 | from google.protobuf import symbol_database as _symbol_database 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 17 | from lnd_grpc.protos import rpc_pb2 as lnd__grpc_dot_protos_dot_rpc__pb2 18 | 19 | 20 | DESCRIPTOR = _descriptor.FileDescriptor( 21 | name='lnd_grpc/protos/invoices.proto', 22 | package='invoicesrpc', 23 | syntax='proto3', 24 | serialized_options=_b('Z1github.com/lightningnetwork/lnd/lnrpc/invoicesrpc'), 25 | serialized_pb=_b('\n\x1elnd_grpc/protos/invoices.proto\x12\x0binvoicesrpc\x1a\x1cgoogle/api/annotations.proto\x1a\x19lnd_grpc/protos/rpc.proto\"(\n\x10\x43\x61ncelInvoiceMsg\x12\x14\n\x0cpayment_hash\x18\x01 \x01(\x0c\"\x13\n\x11\x43\x61ncelInvoiceResp\"\xaf\x02\n\x15\x41\x64\x64HoldInvoiceRequest\x12\x12\n\x04memo\x18\x01 \x01(\tR\x04memo\x12\x12\n\x04hash\x18\x02 \x01(\x0cR\x04hash\x12\x14\n\x05value\x18\x03 \x01(\x03R\x05value\x12*\n\x10\x64\x65scription_hash\x18\x04 \x01(\x0cR\x10\x64\x65scription_hash\x12\x16\n\x06\x65xpiry\x18\x05 \x01(\x03R\x06\x65xpiry\x12$\n\rfallback_addr\x18\x06 \x01(\tR\rfallback_addr\x12 \n\x0b\x63ltv_expiry\x18\x07 \x01(\x04R\x0b\x63ltv_expiry\x12\x32\n\x0broute_hints\x18\x08 \x03(\x0b\x32\x10.lnrpc.RouteHintR\x0broute_hints\x12\x18\n\x07private\x18\t \x01(\x08R\x07private\">\n\x12\x41\x64\x64HoldInvoiceResp\x12(\n\x0fpayment_request\x18\x01 \x01(\tR\x0fpayment_request\"$\n\x10SettleInvoiceMsg\x12\x10\n\x08preimage\x18\x01 \x01(\x0c\"\x13\n\x11SettleInvoiceResp\"=\n\x1dSubscribeSingleInvoiceRequest\x12\x16\n\x06r_hash\x18\x02 \x01(\x0cR\x06r_hashJ\x04\x08\x01\x10\x02\x32\xd9\x02\n\x08Invoices\x12V\n\x16SubscribeSingleInvoice\x12*.invoicesrpc.SubscribeSingleInvoiceRequest\x1a\x0e.lnrpc.Invoice0\x01\x12N\n\rCancelInvoice\x12\x1d.invoicesrpc.CancelInvoiceMsg\x1a\x1e.invoicesrpc.CancelInvoiceResp\x12U\n\x0e\x41\x64\x64HoldInvoice\x12\".invoicesrpc.AddHoldInvoiceRequest\x1a\x1f.invoicesrpc.AddHoldInvoiceResp\x12N\n\rSettleInvoice\x12\x1d.invoicesrpc.SettleInvoiceMsg\x1a\x1e.invoicesrpc.SettleInvoiceRespB3Z1github.com/lightningnetwork/lnd/lnrpc/invoicesrpcb\x06proto3') 26 | , 27 | dependencies=[google_dot_api_dot_annotations__pb2.DESCRIPTOR,lnd__grpc_dot_protos_dot_rpc__pb2.DESCRIPTOR,]) 28 | 29 | 30 | 31 | 32 | _CANCELINVOICEMSG = _descriptor.Descriptor( 33 | name='CancelInvoiceMsg', 34 | full_name='invoicesrpc.CancelInvoiceMsg', 35 | filename=None, 36 | file=DESCRIPTOR, 37 | containing_type=None, 38 | fields=[ 39 | _descriptor.FieldDescriptor( 40 | name='payment_hash', full_name='invoicesrpc.CancelInvoiceMsg.payment_hash', index=0, 41 | number=1, type=12, cpp_type=9, label=1, 42 | has_default_value=False, default_value=_b(""), 43 | message_type=None, enum_type=None, containing_type=None, 44 | is_extension=False, extension_scope=None, 45 | serialized_options=None, file=DESCRIPTOR), 46 | ], 47 | extensions=[ 48 | ], 49 | nested_types=[], 50 | enum_types=[ 51 | ], 52 | serialized_options=None, 53 | is_extendable=False, 54 | syntax='proto3', 55 | extension_ranges=[], 56 | oneofs=[ 57 | ], 58 | serialized_start=104, 59 | serialized_end=144, 60 | ) 61 | 62 | 63 | _CANCELINVOICERESP = _descriptor.Descriptor( 64 | name='CancelInvoiceResp', 65 | full_name='invoicesrpc.CancelInvoiceResp', 66 | filename=None, 67 | file=DESCRIPTOR, 68 | containing_type=None, 69 | fields=[ 70 | ], 71 | extensions=[ 72 | ], 73 | nested_types=[], 74 | enum_types=[ 75 | ], 76 | serialized_options=None, 77 | is_extendable=False, 78 | syntax='proto3', 79 | extension_ranges=[], 80 | oneofs=[ 81 | ], 82 | serialized_start=146, 83 | serialized_end=165, 84 | ) 85 | 86 | 87 | _ADDHOLDINVOICEREQUEST = _descriptor.Descriptor( 88 | name='AddHoldInvoiceRequest', 89 | full_name='invoicesrpc.AddHoldInvoiceRequest', 90 | filename=None, 91 | file=DESCRIPTOR, 92 | containing_type=None, 93 | fields=[ 94 | _descriptor.FieldDescriptor( 95 | name='memo', full_name='invoicesrpc.AddHoldInvoiceRequest.memo', index=0, 96 | number=1, type=9, cpp_type=9, label=1, 97 | has_default_value=False, default_value=_b("").decode('utf-8'), 98 | message_type=None, enum_type=None, containing_type=None, 99 | is_extension=False, extension_scope=None, 100 | serialized_options=None, json_name='memo', file=DESCRIPTOR), 101 | _descriptor.FieldDescriptor( 102 | name='hash', full_name='invoicesrpc.AddHoldInvoiceRequest.hash', index=1, 103 | number=2, type=12, cpp_type=9, label=1, 104 | has_default_value=False, default_value=_b(""), 105 | message_type=None, enum_type=None, containing_type=None, 106 | is_extension=False, extension_scope=None, 107 | serialized_options=None, json_name='hash', file=DESCRIPTOR), 108 | _descriptor.FieldDescriptor( 109 | name='value', full_name='invoicesrpc.AddHoldInvoiceRequest.value', index=2, 110 | number=3, type=3, cpp_type=2, label=1, 111 | has_default_value=False, default_value=0, 112 | message_type=None, enum_type=None, containing_type=None, 113 | is_extension=False, extension_scope=None, 114 | serialized_options=None, json_name='value', file=DESCRIPTOR), 115 | _descriptor.FieldDescriptor( 116 | name='description_hash', full_name='invoicesrpc.AddHoldInvoiceRequest.description_hash', index=3, 117 | number=4, type=12, cpp_type=9, label=1, 118 | has_default_value=False, default_value=_b(""), 119 | message_type=None, enum_type=None, containing_type=None, 120 | is_extension=False, extension_scope=None, 121 | serialized_options=None, json_name='description_hash', file=DESCRIPTOR), 122 | _descriptor.FieldDescriptor( 123 | name='expiry', full_name='invoicesrpc.AddHoldInvoiceRequest.expiry', index=4, 124 | number=5, type=3, cpp_type=2, label=1, 125 | has_default_value=False, default_value=0, 126 | message_type=None, enum_type=None, containing_type=None, 127 | is_extension=False, extension_scope=None, 128 | serialized_options=None, json_name='expiry', file=DESCRIPTOR), 129 | _descriptor.FieldDescriptor( 130 | name='fallback_addr', full_name='invoicesrpc.AddHoldInvoiceRequest.fallback_addr', index=5, 131 | number=6, type=9, cpp_type=9, label=1, 132 | has_default_value=False, default_value=_b("").decode('utf-8'), 133 | message_type=None, enum_type=None, containing_type=None, 134 | is_extension=False, extension_scope=None, 135 | serialized_options=None, json_name='fallback_addr', file=DESCRIPTOR), 136 | _descriptor.FieldDescriptor( 137 | name='cltv_expiry', full_name='invoicesrpc.AddHoldInvoiceRequest.cltv_expiry', index=6, 138 | number=7, type=4, cpp_type=4, label=1, 139 | has_default_value=False, default_value=0, 140 | message_type=None, enum_type=None, containing_type=None, 141 | is_extension=False, extension_scope=None, 142 | serialized_options=None, json_name='cltv_expiry', file=DESCRIPTOR), 143 | _descriptor.FieldDescriptor( 144 | name='route_hints', full_name='invoicesrpc.AddHoldInvoiceRequest.route_hints', index=7, 145 | number=8, type=11, cpp_type=10, label=3, 146 | has_default_value=False, default_value=[], 147 | message_type=None, enum_type=None, containing_type=None, 148 | is_extension=False, extension_scope=None, 149 | serialized_options=None, json_name='route_hints', file=DESCRIPTOR), 150 | _descriptor.FieldDescriptor( 151 | name='private', full_name='invoicesrpc.AddHoldInvoiceRequest.private', index=8, 152 | number=9, type=8, cpp_type=7, label=1, 153 | has_default_value=False, default_value=False, 154 | message_type=None, enum_type=None, containing_type=None, 155 | is_extension=False, extension_scope=None, 156 | serialized_options=None, json_name='private', file=DESCRIPTOR), 157 | ], 158 | extensions=[ 159 | ], 160 | nested_types=[], 161 | enum_types=[ 162 | ], 163 | serialized_options=None, 164 | is_extendable=False, 165 | syntax='proto3', 166 | extension_ranges=[], 167 | oneofs=[ 168 | ], 169 | serialized_start=168, 170 | serialized_end=471, 171 | ) 172 | 173 | 174 | _ADDHOLDINVOICERESP = _descriptor.Descriptor( 175 | name='AddHoldInvoiceResp', 176 | full_name='invoicesrpc.AddHoldInvoiceResp', 177 | filename=None, 178 | file=DESCRIPTOR, 179 | containing_type=None, 180 | fields=[ 181 | _descriptor.FieldDescriptor( 182 | name='payment_request', full_name='invoicesrpc.AddHoldInvoiceResp.payment_request', index=0, 183 | number=1, type=9, cpp_type=9, label=1, 184 | has_default_value=False, default_value=_b("").decode('utf-8'), 185 | message_type=None, enum_type=None, containing_type=None, 186 | is_extension=False, extension_scope=None, 187 | serialized_options=None, json_name='payment_request', file=DESCRIPTOR), 188 | ], 189 | extensions=[ 190 | ], 191 | nested_types=[], 192 | enum_types=[ 193 | ], 194 | serialized_options=None, 195 | is_extendable=False, 196 | syntax='proto3', 197 | extension_ranges=[], 198 | oneofs=[ 199 | ], 200 | serialized_start=473, 201 | serialized_end=535, 202 | ) 203 | 204 | 205 | _SETTLEINVOICEMSG = _descriptor.Descriptor( 206 | name='SettleInvoiceMsg', 207 | full_name='invoicesrpc.SettleInvoiceMsg', 208 | filename=None, 209 | file=DESCRIPTOR, 210 | containing_type=None, 211 | fields=[ 212 | _descriptor.FieldDescriptor( 213 | name='preimage', full_name='invoicesrpc.SettleInvoiceMsg.preimage', index=0, 214 | number=1, type=12, cpp_type=9, label=1, 215 | has_default_value=False, default_value=_b(""), 216 | message_type=None, enum_type=None, containing_type=None, 217 | is_extension=False, extension_scope=None, 218 | serialized_options=None, file=DESCRIPTOR), 219 | ], 220 | extensions=[ 221 | ], 222 | nested_types=[], 223 | enum_types=[ 224 | ], 225 | serialized_options=None, 226 | is_extendable=False, 227 | syntax='proto3', 228 | extension_ranges=[], 229 | oneofs=[ 230 | ], 231 | serialized_start=537, 232 | serialized_end=573, 233 | ) 234 | 235 | 236 | _SETTLEINVOICERESP = _descriptor.Descriptor( 237 | name='SettleInvoiceResp', 238 | full_name='invoicesrpc.SettleInvoiceResp', 239 | filename=None, 240 | file=DESCRIPTOR, 241 | containing_type=None, 242 | fields=[ 243 | ], 244 | extensions=[ 245 | ], 246 | nested_types=[], 247 | enum_types=[ 248 | ], 249 | serialized_options=None, 250 | is_extendable=False, 251 | syntax='proto3', 252 | extension_ranges=[], 253 | oneofs=[ 254 | ], 255 | serialized_start=575, 256 | serialized_end=594, 257 | ) 258 | 259 | 260 | _SUBSCRIBESINGLEINVOICEREQUEST = _descriptor.Descriptor( 261 | name='SubscribeSingleInvoiceRequest', 262 | full_name='invoicesrpc.SubscribeSingleInvoiceRequest', 263 | filename=None, 264 | file=DESCRIPTOR, 265 | containing_type=None, 266 | fields=[ 267 | _descriptor.FieldDescriptor( 268 | name='r_hash', full_name='invoicesrpc.SubscribeSingleInvoiceRequest.r_hash', index=0, 269 | number=2, type=12, cpp_type=9, label=1, 270 | has_default_value=False, default_value=_b(""), 271 | message_type=None, enum_type=None, containing_type=None, 272 | is_extension=False, extension_scope=None, 273 | serialized_options=None, json_name='r_hash', file=DESCRIPTOR), 274 | ], 275 | extensions=[ 276 | ], 277 | nested_types=[], 278 | enum_types=[ 279 | ], 280 | serialized_options=None, 281 | is_extendable=False, 282 | syntax='proto3', 283 | extension_ranges=[], 284 | oneofs=[ 285 | ], 286 | serialized_start=596, 287 | serialized_end=657, 288 | ) 289 | 290 | _ADDHOLDINVOICEREQUEST.fields_by_name['route_hints'].message_type = lnd__grpc_dot_protos_dot_rpc__pb2._ROUTEHINT 291 | DESCRIPTOR.message_types_by_name['CancelInvoiceMsg'] = _CANCELINVOICEMSG 292 | DESCRIPTOR.message_types_by_name['CancelInvoiceResp'] = _CANCELINVOICERESP 293 | DESCRIPTOR.message_types_by_name['AddHoldInvoiceRequest'] = _ADDHOLDINVOICEREQUEST 294 | DESCRIPTOR.message_types_by_name['AddHoldInvoiceResp'] = _ADDHOLDINVOICERESP 295 | DESCRIPTOR.message_types_by_name['SettleInvoiceMsg'] = _SETTLEINVOICEMSG 296 | DESCRIPTOR.message_types_by_name['SettleInvoiceResp'] = _SETTLEINVOICERESP 297 | DESCRIPTOR.message_types_by_name['SubscribeSingleInvoiceRequest'] = _SUBSCRIBESINGLEINVOICEREQUEST 298 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 299 | 300 | CancelInvoiceMsg = _reflection.GeneratedProtocolMessageType('CancelInvoiceMsg', (_message.Message,), dict( 301 | DESCRIPTOR = _CANCELINVOICEMSG, 302 | __module__ = 'lnd_grpc.protos.invoices_pb2' 303 | # @@protoc_insertion_point(class_scope:invoicesrpc.CancelInvoiceMsg) 304 | )) 305 | _sym_db.RegisterMessage(CancelInvoiceMsg) 306 | 307 | CancelInvoiceResp = _reflection.GeneratedProtocolMessageType('CancelInvoiceResp', (_message.Message,), dict( 308 | DESCRIPTOR = _CANCELINVOICERESP, 309 | __module__ = 'lnd_grpc.protos.invoices_pb2' 310 | # @@protoc_insertion_point(class_scope:invoicesrpc.CancelInvoiceResp) 311 | )) 312 | _sym_db.RegisterMessage(CancelInvoiceResp) 313 | 314 | AddHoldInvoiceRequest = _reflection.GeneratedProtocolMessageType('AddHoldInvoiceRequest', (_message.Message,), dict( 315 | DESCRIPTOR = _ADDHOLDINVOICEREQUEST, 316 | __module__ = 'lnd_grpc.protos.invoices_pb2' 317 | # @@protoc_insertion_point(class_scope:invoicesrpc.AddHoldInvoiceRequest) 318 | )) 319 | _sym_db.RegisterMessage(AddHoldInvoiceRequest) 320 | 321 | AddHoldInvoiceResp = _reflection.GeneratedProtocolMessageType('AddHoldInvoiceResp', (_message.Message,), dict( 322 | DESCRIPTOR = _ADDHOLDINVOICERESP, 323 | __module__ = 'lnd_grpc.protos.invoices_pb2' 324 | # @@protoc_insertion_point(class_scope:invoicesrpc.AddHoldInvoiceResp) 325 | )) 326 | _sym_db.RegisterMessage(AddHoldInvoiceResp) 327 | 328 | SettleInvoiceMsg = _reflection.GeneratedProtocolMessageType('SettleInvoiceMsg', (_message.Message,), dict( 329 | DESCRIPTOR = _SETTLEINVOICEMSG, 330 | __module__ = 'lnd_grpc.protos.invoices_pb2' 331 | # @@protoc_insertion_point(class_scope:invoicesrpc.SettleInvoiceMsg) 332 | )) 333 | _sym_db.RegisterMessage(SettleInvoiceMsg) 334 | 335 | SettleInvoiceResp = _reflection.GeneratedProtocolMessageType('SettleInvoiceResp', (_message.Message,), dict( 336 | DESCRIPTOR = _SETTLEINVOICERESP, 337 | __module__ = 'lnd_grpc.protos.invoices_pb2' 338 | # @@protoc_insertion_point(class_scope:invoicesrpc.SettleInvoiceResp) 339 | )) 340 | _sym_db.RegisterMessage(SettleInvoiceResp) 341 | 342 | SubscribeSingleInvoiceRequest = _reflection.GeneratedProtocolMessageType('SubscribeSingleInvoiceRequest', (_message.Message,), dict( 343 | DESCRIPTOR = _SUBSCRIBESINGLEINVOICEREQUEST, 344 | __module__ = 'lnd_grpc.protos.invoices_pb2' 345 | # @@protoc_insertion_point(class_scope:invoicesrpc.SubscribeSingleInvoiceRequest) 346 | )) 347 | _sym_db.RegisterMessage(SubscribeSingleInvoiceRequest) 348 | 349 | 350 | DESCRIPTOR._options = None 351 | 352 | _INVOICES = _descriptor.ServiceDescriptor( 353 | name='Invoices', 354 | full_name='invoicesrpc.Invoices', 355 | file=DESCRIPTOR, 356 | index=0, 357 | serialized_options=None, 358 | serialized_start=660, 359 | serialized_end=1005, 360 | methods=[ 361 | _descriptor.MethodDescriptor( 362 | name='SubscribeSingleInvoice', 363 | full_name='invoicesrpc.Invoices.SubscribeSingleInvoice', 364 | index=0, 365 | containing_service=None, 366 | input_type=_SUBSCRIBESINGLEINVOICEREQUEST, 367 | output_type=lnd__grpc_dot_protos_dot_rpc__pb2._INVOICE, 368 | serialized_options=None, 369 | ), 370 | _descriptor.MethodDescriptor( 371 | name='CancelInvoice', 372 | full_name='invoicesrpc.Invoices.CancelInvoice', 373 | index=1, 374 | containing_service=None, 375 | input_type=_CANCELINVOICEMSG, 376 | output_type=_CANCELINVOICERESP, 377 | serialized_options=None, 378 | ), 379 | _descriptor.MethodDescriptor( 380 | name='AddHoldInvoice', 381 | full_name='invoicesrpc.Invoices.AddHoldInvoice', 382 | index=2, 383 | containing_service=None, 384 | input_type=_ADDHOLDINVOICEREQUEST, 385 | output_type=_ADDHOLDINVOICERESP, 386 | serialized_options=None, 387 | ), 388 | _descriptor.MethodDescriptor( 389 | name='SettleInvoice', 390 | full_name='invoicesrpc.Invoices.SettleInvoice', 391 | index=3, 392 | containing_service=None, 393 | input_type=_SETTLEINVOICEMSG, 394 | output_type=_SETTLEINVOICERESP, 395 | serialized_options=None, 396 | ), 397 | ]) 398 | _sym_db.RegisterServiceDescriptor(_INVOICES) 399 | 400 | DESCRIPTOR.services_by_name['Invoices'] = _INVOICES 401 | 402 | # @@protoc_insertion_point(module_scope) 403 | -------------------------------------------------------------------------------- /lnd_grpc/protos/invoices_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | from lnd_grpc.protos import invoices_pb2 as lnd__grpc_dot_protos_dot_invoices__pb2 5 | from lnd_grpc.protos import rpc_pb2 as lnd__grpc_dot_protos_dot_rpc__pb2 6 | 7 | 8 | class InvoicesStub(object): 9 | """Invoices is a service that can be used to create, accept, settle and cancel 10 | invoices. 11 | """ 12 | 13 | def __init__(self, channel): 14 | """Constructor. 15 | 16 | Args: 17 | channel: A grpc.Channel. 18 | """ 19 | self.SubscribeSingleInvoice = channel.unary_stream( 20 | '/invoicesrpc.Invoices/SubscribeSingleInvoice', 21 | request_serializer=lnd__grpc_dot_protos_dot_invoices__pb2.SubscribeSingleInvoiceRequest.SerializeToString, 22 | response_deserializer=lnd__grpc_dot_protos_dot_rpc__pb2.Invoice.FromString, 23 | ) 24 | self.CancelInvoice = channel.unary_unary( 25 | '/invoicesrpc.Invoices/CancelInvoice', 26 | request_serializer=lnd__grpc_dot_protos_dot_invoices__pb2.CancelInvoiceMsg.SerializeToString, 27 | response_deserializer=lnd__grpc_dot_protos_dot_invoices__pb2.CancelInvoiceResp.FromString, 28 | ) 29 | self.AddHoldInvoice = channel.unary_unary( 30 | '/invoicesrpc.Invoices/AddHoldInvoice', 31 | request_serializer=lnd__grpc_dot_protos_dot_invoices__pb2.AddHoldInvoiceRequest.SerializeToString, 32 | response_deserializer=lnd__grpc_dot_protos_dot_invoices__pb2.AddHoldInvoiceResp.FromString, 33 | ) 34 | self.SettleInvoice = channel.unary_unary( 35 | '/invoicesrpc.Invoices/SettleInvoice', 36 | request_serializer=lnd__grpc_dot_protos_dot_invoices__pb2.SettleInvoiceMsg.SerializeToString, 37 | response_deserializer=lnd__grpc_dot_protos_dot_invoices__pb2.SettleInvoiceResp.FromString, 38 | ) 39 | 40 | 41 | class InvoicesServicer(object): 42 | """Invoices is a service that can be used to create, accept, settle and cancel 43 | invoices. 44 | """ 45 | 46 | def SubscribeSingleInvoice(self, request, context): 47 | """* 48 | SubscribeSingleInvoice returns a uni-directional stream (server -> client) 49 | to notify the client of state transitions of the specified invoice. 50 | Initially the current invoice state is always sent out. 51 | """ 52 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 53 | context.set_details('Method not implemented!') 54 | raise NotImplementedError('Method not implemented!') 55 | 56 | def CancelInvoice(self, request, context): 57 | """* 58 | CancelInvoice cancels a currently open invoice. If the invoice is already 59 | canceled, this call will succeed. If the invoice is already settled, it will 60 | fail. 61 | """ 62 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 63 | context.set_details('Method not implemented!') 64 | raise NotImplementedError('Method not implemented!') 65 | 66 | def AddHoldInvoice(self, request, context): 67 | """* 68 | AddHoldInvoice creates a hold invoice. It ties the invoice to the hash 69 | supplied in the request. 70 | """ 71 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 72 | context.set_details('Method not implemented!') 73 | raise NotImplementedError('Method not implemented!') 74 | 75 | def SettleInvoice(self, request, context): 76 | """* 77 | SettleInvoice settles an accepted invoice. If the invoice is already 78 | settled, this call will succeed. 79 | """ 80 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 81 | context.set_details('Method not implemented!') 82 | raise NotImplementedError('Method not implemented!') 83 | 84 | 85 | def add_InvoicesServicer_to_server(servicer, server): 86 | rpc_method_handlers = { 87 | 'SubscribeSingleInvoice': grpc.unary_stream_rpc_method_handler( 88 | servicer.SubscribeSingleInvoice, 89 | request_deserializer=lnd__grpc_dot_protos_dot_invoices__pb2.SubscribeSingleInvoiceRequest.FromString, 90 | response_serializer=lnd__grpc_dot_protos_dot_rpc__pb2.Invoice.SerializeToString, 91 | ), 92 | 'CancelInvoice': grpc.unary_unary_rpc_method_handler( 93 | servicer.CancelInvoice, 94 | request_deserializer=lnd__grpc_dot_protos_dot_invoices__pb2.CancelInvoiceMsg.FromString, 95 | response_serializer=lnd__grpc_dot_protos_dot_invoices__pb2.CancelInvoiceResp.SerializeToString, 96 | ), 97 | 'AddHoldInvoice': grpc.unary_unary_rpc_method_handler( 98 | servicer.AddHoldInvoice, 99 | request_deserializer=lnd__grpc_dot_protos_dot_invoices__pb2.AddHoldInvoiceRequest.FromString, 100 | response_serializer=lnd__grpc_dot_protos_dot_invoices__pb2.AddHoldInvoiceResp.SerializeToString, 101 | ), 102 | 'SettleInvoice': grpc.unary_unary_rpc_method_handler( 103 | servicer.SettleInvoice, 104 | request_deserializer=lnd__grpc_dot_protos_dot_invoices__pb2.SettleInvoiceMsg.FromString, 105 | response_serializer=lnd__grpc_dot_protos_dot_invoices__pb2.SettleInvoiceResp.SerializeToString, 106 | ), 107 | } 108 | generic_handler = grpc.method_handlers_generic_handler( 109 | 'invoicesrpc.Invoices', rpc_method_handlers) 110 | server.add_generic_rpc_handlers((generic_handler,)) 111 | -------------------------------------------------------------------------------- /lnd_grpc/utilities.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | from pathlib import Path 4 | from os import environ, path 5 | 6 | 7 | def get_lnd_dir(): 8 | """ 9 | :return: default LND directory based on detected OS platform 10 | """ 11 | lnd_dir = None 12 | _platform = platform.system() 13 | home_dir = str(Path.home()) 14 | if _platform == "Darwin": 15 | lnd_dir = home_dir + "/Library/Application Support/Lnd/" 16 | elif _platform == "Linux": 17 | lnd_dir = home_dir + "/.lnd/" 18 | elif _platform == "Windows": 19 | lnd_dir = path.abspath(environ.get("LOCALAPPDATA") + "Lnd/") 20 | return lnd_dir 21 | -------------------------------------------------------------------------------- /lnd_grpc/wallet_unlocker.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | import grpc 4 | 5 | import lnd_grpc.protos.rpc_pb2 as ln 6 | import lnd_grpc.protos.rpc_pb2_grpc as lnrpc 7 | from lnd_grpc.base_client import BaseClient 8 | from lnd_grpc.config import defaultNetwork, defaultRPCHost, defaultRPCPort 9 | 10 | # tell gRPC which cypher suite to use 11 | environ["GRPC_SSL_CIPHER_SUITES"] = \ 12 | 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:' \ 13 | 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:' \ 14 | 'ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384' 15 | 16 | 17 | 18 | class WalletUnlocker(BaseClient): 19 | """ 20 | A superclass of BaseClient to interact with the WalletUnlocker sub-service 21 | """ 22 | 23 | def __init__( 24 | self, 25 | lnd_dir: str = None, 26 | macaroon_path: str = None, 27 | tls_cert_path: str = None, 28 | network: str = defaultNetwork, 29 | grpc_host: str = defaultRPCHost, 30 | grpc_port: str = defaultRPCPort, 31 | ): 32 | self._w_stub: lnrpc.WalletUnlockerStub = None 33 | 34 | super().__init__( 35 | lnd_dir=lnd_dir, 36 | macaroon_path=macaroon_path, 37 | tls_cert_path=tls_cert_path, 38 | network=network, 39 | grpc_host=grpc_host, 40 | grpc_port=grpc_port, 41 | ) 42 | 43 | @property 44 | def wallet_unlocker_stub(self) -> lnrpc.WalletUnlockerStub: 45 | if self._w_stub is None: 46 | ssl_creds = grpc.ssl_channel_credentials(self.tls_cert) 47 | self._w_channel = grpc.secure_channel( 48 | target=self.grpc_address, 49 | credentials=ssl_creds, 50 | options=self.grpc_options, 51 | ) 52 | self._w_stub = lnrpc.WalletUnlockerStub(self._w_channel) 53 | 54 | # simulate connection status change after wallet stub used (typically wallet unlock) which 55 | # stimulates lightning stub regeneration when necessary 56 | self.connection_status_change = True 57 | 58 | return self._w_stub 59 | 60 | def gen_seed(self, **kwargs): 61 | """ 62 | the first method that should be used to instantiate a new lnd instance. This method 63 | allows a caller to generate a new aezeed cipher seed given an optional passphrase. If 64 | provided, the passphrase will be necessary to decrypt the cipherseed to expose the 65 | internal wallet seed. Once the cipherseed is obtained and verified by the user, 66 | the InitWallet method should be used to commit the newly generated seed, and create the 67 | wallet. 68 | 69 | :return: GenSeedResponse with 2 attributes: 'cipher_seed_mnemonic' and 'enciphered_seed' 70 | """ 71 | request = ln.GenSeedRequest(**kwargs) 72 | response = self.wallet_unlocker_stub.GenSeed(request) 73 | return response 74 | 75 | def init_wallet(self, wallet_password: str = None, **kwargs): 76 | """ 77 | used when lnd is starting up for the first time to fully initialize the daemon and its 78 | internal wallet. At the very least a wallet password must be provided. This will be used 79 | to encrypt sensitive material on disk. In the case of a recovery scenario, the user can 80 | also specify their aezeed mnemonic and passphrase. If set, then the daemon will use this 81 | prior state to initialize its internal wallet. Alternatively, this can be used along with 82 | the GenSeed RPC to obtain a seed, then present it to the user. Once it has been verified 83 | by the user, the seed can be fed into this RPC in order to commit the new wallet. 84 | 85 | :return: InitWalletResponse with no attributes 86 | """ 87 | request = ln.InitWalletRequest( 88 | wallet_password=wallet_password.encode("utf-8"), **kwargs 89 | ) 90 | response = self.wallet_unlocker_stub.InitWallet(request) 91 | return response 92 | 93 | def unlock_wallet(self, wallet_password: str, recovery_window: int = 0): 94 | """ 95 | used at startup of lnd to provide a password to unlock the wallet database 96 | 97 | :return: UnlockWalletResponse with no attributes 98 | """ 99 | request = ln.UnlockWalletRequest( 100 | wallet_password=wallet_password.encode("utf-8"), 101 | recovery_window=recovery_window, 102 | ) 103 | response = self.wallet_unlocker_stub.UnlockWallet(request) 104 | return response 105 | 106 | def change_password(self, current_password: str, new_password: str): 107 | """ 108 | changes the password of the encrypted wallet. This will automatically unlock the wallet 109 | database if successful. 110 | 111 | :return: ChangePasswordResponse with no attributes 112 | """ 113 | request = ln.ChangePasswordRequest( 114 | current_password=current_password.encode("utf-8"), 115 | new_password=new_password.encode("utf-8"), 116 | ) 117 | response = self.wallet_unlocker_stub.ChangePassword(request) 118 | return response 119 | -------------------------------------------------------------------------------- /loop_rpc/__init__.py: -------------------------------------------------------------------------------- 1 | from loop_rpc.loop_rpc import * 2 | 3 | name = "loop_rpc" 4 | -------------------------------------------------------------------------------- /loop_rpc/loop_rpc.py: -------------------------------------------------------------------------------- 1 | from grpc import insecure_channel 2 | from loop_rpc.protos import loop_client_pb2 as loop, loop_client_pb2_grpc as looprpc 3 | 4 | 5 | class LoopClient: 6 | """ 7 | As per the instructions at https://github.com/lightninglabs/loop/blob/master/README.md both 8 | LND and loopd must be installed and running. 9 | 10 | If loopd is running with default configuration you will not need to change the LoopClient 11 | constructors from default (loop_host='localhost', loop_port='11010'). 12 | """ 13 | 14 | def __init__(self, loop_host: str = "localhost", loop_port: str = "11010"): 15 | self._loop_stub: looprpc.SwapClientStub = None 16 | self.loop_host = loop_host 17 | self.loop_port = loop_port 18 | 19 | @property 20 | def loop_stub(self) -> looprpc.SwapClientStub: 21 | if self._loop_stub is None: 22 | loop_channel = insecure_channel(self.loop_host + ":" + self.loop_port) 23 | self._loop_stub = looprpc.SwapClientStub(loop_channel) 24 | return self._loop_stub 25 | 26 | def loop_out(self, amt: int, **kwargs): 27 | request = loop.LoopOutRequest(amt=amt, **kwargs) 28 | response = self.loop_stub.LoopOut(request) 29 | return response 30 | 31 | def monitor(self): 32 | """ 33 | returns an iterable stream 34 | """ 35 | request = loop.MonitorRequest() 36 | return self.loop_stub.Monitor(request) 37 | 38 | def loop_out_terms(self): 39 | request = loop.TermsRequest() 40 | response = self.loop_stub.LoopOutTerms(request) 41 | return response 42 | 43 | def loop_out_quote(self, amt: int): 44 | request = loop.QuoteRequest(amt=amt) 45 | response = self.loop_stub.LoopOutQuote(request) 46 | return response 47 | 48 | 49 | __all__ = ["LoopClient"] 50 | -------------------------------------------------------------------------------- /loop_rpc/protos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willcl-ark/lnd_grpc/cf938c51c201f078e8bbe9e19ffc2d038f3abf7f/loop_rpc/protos/__init__.py -------------------------------------------------------------------------------- /loop_rpc/protos/generate_python_loop_protos.txt: -------------------------------------------------------------------------------- 1 | For when the project is cloned from source (github), the process is as follows: 2 | 3 | 1) Download rpc.proto file from lnd github 4 | 2) Generate python-specific proto files from this base proto 5 | 6 | 7 | 1) You can manually download the appropriate client.proto file from (or substitute appropriate branch/tag for 'master'): 8 | https://raw.githubusercontent.com/lightninglabs/loop/master/looprpc/client.proto 9 | 10 | 2) Navigate to project protos directory: 11 | cd .../lnd_grpc/loop_rpc/protos/ 12 | 13 | Make sure googleapis is cloned in this protos folder: 14 | > git clone https://github.com/googleapis/googleapis.git 15 | 16 | Activate your venv if necessary! 17 | 18 | Move up *two* directories: 19 | > cd ../.. 20 | 21 | Run command to generate proto gRPC files: 22 | > python -m grpc_tools.protoc --proto_path=loop_rpc/protos/googleapis:. --python_out=. --grpc_python_out=. loop_rpc/protos/loop_client.proto 23 | -------------------------------------------------------------------------------- /loop_rpc/protos/loop_client.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/api/annotations.proto"; 4 | 5 | package looprpc; 6 | 7 | /** 8 | SwapClient is a service that handles the client side process of onchain/offchain 9 | swaps. The service is designed for a single client. 10 | */ 11 | service SwapClient { 12 | /** loop: `out` 13 | LoopOut initiates an loop out swap with the given parameters. The call 14 | returns after the swap has been set up with the swap server. From that 15 | point onwards, progress can be tracked via the SwapStatus stream that is 16 | returned from Monitor(). 17 | */ 18 | rpc LoopOut(LoopOutRequest) returns (SwapResponse) { 19 | option (google.api.http) = { 20 | post: "/v1/loop/out" 21 | body: "*" 22 | }; 23 | } 24 | 25 | /** 26 | LoopIn initiates a loop in swap with the given parameters. The call 27 | returns after the swap has been set up with the swap server. From that 28 | point onwards, progress can be tracked via the SwapStatus stream 29 | that is returned from Monitor(). 30 | */ 31 | rpc LoopIn(LoopInRequest) returns (SwapResponse); 32 | 33 | /** loop: `monitor` 34 | Monitor will return a stream of swap updates for currently active swaps. 35 | TODO: add MonitorSync version for REST clients. 36 | */ 37 | rpc Monitor(MonitorRequest) returns(stream SwapStatus); 38 | 39 | /** loop: `terms` 40 | LoopOutTerms returns the terms that the server enforces for a loop out swap. 41 | */ 42 | rpc LoopOutTerms(TermsRequest) returns(TermsResponse) { 43 | option (google.api.http) = { 44 | get: "/v1/loop/out/terms" 45 | }; 46 | } 47 | 48 | /** loop: `quote` 49 | LoopOutQuote returns a quote for a loop out swap with the provided 50 | parameters. 51 | */ 52 | rpc LoopOutQuote(QuoteRequest) returns(QuoteResponse) { 53 | option (google.api.http) = { 54 | get: "/v1/loop/out/quote/{amt}" 55 | }; 56 | } 57 | 58 | /** 59 | GetTerms returns the terms that the server enforces for swaps. 60 | */ 61 | rpc GetLoopInTerms(TermsRequest) returns(TermsResponse); 62 | 63 | /** 64 | GetQuote returns a quote for a swap with the provided parameters. 65 | */ 66 | rpc GetLoopInQuote(QuoteRequest) returns(QuoteResponse); 67 | } 68 | 69 | message LoopOutRequest { 70 | /** 71 | Requested swap amount in sat. This does not include the swap and miner fee. 72 | */ 73 | int64 amt = 1; 74 | 75 | /** 76 | Base58 encoded destination address for the swap. 77 | */ 78 | string dest = 2; 79 | 80 | /** 81 | Maximum off-chain fee in msat that may be paid for payment to the server. 82 | This limit is applied during path finding. Typically this value is taken 83 | from the response of the GetQuote call. 84 | */ 85 | int64 max_swap_routing_fee = 3; 86 | 87 | /** 88 | Maximum off-chain fee in msat that may be paid for payment to the server. 89 | This limit is applied during path finding. Typically this value is taken 90 | from the response of the GetQuote call. 91 | */ 92 | int64 max_prepay_routing_fee = 4; 93 | 94 | /** 95 | Maximum we are willing to pay the server for the swap. This value is not 96 | disclosed in the swap initiation call, but if the server asks for a 97 | higher fee, we abort the swap. Typically this value is taken from the 98 | response of the GetQuote call. It includes the prepay amount. 99 | */ 100 | int64 max_swap_fee = 5; 101 | 102 | /** 103 | Maximum amount of the swap fee that may be charged as a prepayment. 104 | */ 105 | int64 max_prepay_amt = 6; 106 | 107 | /** 108 | Maximum in on-chain fees that we are willing to spent. If we want to 109 | sweep the on-chain htlc and the fee estimate turns out higher than this 110 | value, we cancel the swap. If the fee estimate is lower, we publish the 111 | sweep tx. 112 | 113 | If the sweep tx is not confirmed, we are forced to ratchet up fees until it 114 | is swept. Possibly even exceeding max_miner_fee if we get close to the htlc 115 | timeout. Because the initial publication revealed the preimage, we have no 116 | other choice. The server may already have pulled the off-chain htlc. Only 117 | when the fee becomes higher than the swap amount, we can only wait for fees 118 | to come down and hope - if we are past the timeout - that the server is not 119 | publishing the revocation. 120 | 121 | max_miner_fee is typically taken from the response of the GetQuote call. 122 | */ 123 | int64 max_miner_fee = 7; 124 | 125 | /** 126 | The channel to loop out, the channel to loop out is selected based on the 127 | lowest routing fee for the swap payment to the server. 128 | */ 129 | uint64 loop_out_channel = 8; 130 | 131 | /** 132 | The number of blocks from the on-chain HTLC's confirmation height that it 133 | should be swept within. 134 | */ 135 | int32 sweep_conf_target = 9; 136 | } 137 | 138 | message LoopInRequest { 139 | /** 140 | Requested swap amount in sat. This does not include the swap and miner 141 | fee. 142 | */ 143 | int64 amt = 1; 144 | 145 | /** 146 | Maximum we are willing to pay the server for the swap. This value is not 147 | disclosed in the swap initiation call, but if the server asks for a 148 | higher fee, we abort the swap. Typically this value is taken from the 149 | response of the GetQuote call. 150 | */ 151 | int64 max_swap_fee = 2; 152 | 153 | /** 154 | Maximum in on-chain fees that we are willing to spent. If we want to 155 | publish the on-chain htlc and the fee estimate turns out higher than this 156 | value, we cancel the swap. 157 | 158 | max_miner_fee is typically taken from the response of the GetQuote call. 159 | */ 160 | int64 max_miner_fee = 3; 161 | 162 | /** 163 | The channel to loop in. If zero, the channel to loop in is selected based 164 | on the lowest routing fee for the swap payment from the server. 165 | 166 | Note: NOT YET IMPLEMENTED 167 | */ 168 | uint64 loop_in_channel = 4; 169 | 170 | /** 171 | If external_htlc is true, we expect the htlc to be published by an external 172 | actor. 173 | */ 174 | bool external_htlc = 5; 175 | } 176 | 177 | message SwapResponse { 178 | /** 179 | Swap identifier to track status in the update stream that is returned from 180 | the Start() call. Currently this is the hash that locks the htlcs. 181 | */ 182 | string id = 1; 183 | 184 | /** 185 | The address of the on-chain htlc. 186 | */ 187 | string htlc_address = 2; 188 | } 189 | 190 | message MonitorRequest{ 191 | } 192 | 193 | message SwapStatus { 194 | /** 195 | Requested swap amount in sat. This does not include the swap and miner 196 | fee. 197 | */ 198 | int64 amt = 1; 199 | 200 | /** 201 | Swap identifier to track status in the update stream that is returned from 202 | the Start() call. Currently this is the hash that locks the htlcs. 203 | */ 204 | string id = 2; 205 | 206 | /** 207 | Swap type 208 | */ 209 | SwapType type = 3; 210 | 211 | /** 212 | State the swap is currently in, see State enum. 213 | */ 214 | SwapState state = 4; 215 | 216 | /** 217 | Initiation time of the swap. 218 | */ 219 | int64 initiation_time = 5; 220 | 221 | /** 222 | Initiation time of the swap. 223 | */ 224 | int64 last_update_time = 6; 225 | 226 | /** 227 | Htlc address. 228 | */ 229 | string htlc_address = 7; 230 | 231 | /// Swap server cost 232 | int64 cost_server = 8; 233 | 234 | // On-chain transaction cost 235 | int64 cost_onchain = 9; 236 | 237 | // Off-chain routing fees 238 | int64 cost_offchain = 10; 239 | } 240 | 241 | enum SwapType { 242 | // LOOP_OUT indicates an loop out swap (off-chain to on-chain) 243 | LOOP_OUT = 0; 244 | 245 | // LOOP_IN indicates a loop in swap (on-chain to off-chain) 246 | LOOP_IN = 1; 247 | } 248 | 249 | enum SwapState { 250 | /** 251 | INITIATED is the initial state of a swap. At that point, the initiation 252 | call to the server has been made and the payment process has been started 253 | for the swap and prepayment invoices. 254 | */ 255 | INITIATED = 0; 256 | 257 | /** 258 | PREIMAGE_REVEALED is reached when the sweep tx publication is first 259 | attempted. From that point on, we should consider the preimage to no 260 | longer be secret and we need to do all we can to get the sweep confirmed. 261 | This state will mostly coalesce with StateHtlcConfirmed, except in the 262 | case where we wait for fees to come down before we sweep. 263 | */ 264 | PREIMAGE_REVEALED = 1; 265 | 266 | /** 267 | HTLC_PUBLISHED is reached when the htlc tx has been published in a loop in 268 | swap. 269 | */ 270 | HTLC_PUBLISHED = 2; 271 | 272 | /** 273 | SUCCESS is the final swap state that is reached when the sweep tx has 274 | the required confirmation depth. 275 | */ 276 | SUCCESS = 3; 277 | 278 | /** 279 | FAILED is the final swap state for a failed swap with or without loss of 280 | the swap amount. 281 | */ 282 | FAILED = 4; 283 | 284 | /** 285 | INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been 286 | paid, but we are still waiting for the htlc spend to confirm. 287 | */ 288 | INVOICE_SETTLED = 5; 289 | } 290 | 291 | message TermsRequest { 292 | } 293 | 294 | message TermsResponse { 295 | /** 296 | The node pubkey where the swap payment needs to be paid 297 | to. This can be used to test connectivity before initiating the swap. 298 | */ 299 | string swap_payment_dest = 1; 300 | 301 | /** 302 | The base fee for a swap (sat) 303 | */ 304 | int64 swap_fee_base = 2; 305 | 306 | /** 307 | The fee rate for a swap (parts per million) 308 | */ 309 | int64 swap_fee_rate = 3; 310 | 311 | /** 312 | Required prepay amount 313 | */ 314 | int64 prepay_amt = 4; 315 | 316 | /** 317 | Minimum swap amount (sat) 318 | */ 319 | int64 min_swap_amount = 5; 320 | 321 | /** 322 | Maximum swap amount (sat) 323 | */ 324 | int64 max_swap_amount = 6; 325 | 326 | /** 327 | On-chain cltv expiry delta 328 | */ 329 | int32 cltv_delta = 7; 330 | } 331 | 332 | message QuoteRequest { 333 | /** 334 | The amount to swap in satoshis. 335 | */ 336 | int64 amt = 1; 337 | 338 | /** 339 | The confirmation target that should be used either for the sweep of the 340 | on-chain HTLC broadcast by the swap server in the case of a Loop Out, or for 341 | the confirmation of the on-chain HTLC broadcast by the swap client in the 342 | case of a Loop In. 343 | */ 344 | int32 conf_target = 2; 345 | } 346 | 347 | message QuoteResponse { 348 | /** 349 | The fee that the swap server is charging for the swap. 350 | */ 351 | int64 swap_fee = 1; 352 | 353 | /** 354 | The part of the swap fee that is requested as a prepayment. 355 | */ 356 | int64 prepay_amt = 2; 357 | 358 | /** 359 | An estimate of the on-chain fee that needs to be paid to sweep the HTLC. 360 | */ 361 | int64 miner_fee = 3; 362 | } 363 | -------------------------------------------------------------------------------- /loop_rpc/protos/loop_client_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: loop_rpc/protos/loop_client.proto 4 | 5 | import sys 6 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 7 | from google.protobuf.internal import enum_type_wrapper 8 | from google.protobuf import descriptor as _descriptor 9 | from google.protobuf import message as _message 10 | from google.protobuf import reflection as _reflection 11 | from google.protobuf import symbol_database as _symbol_database 12 | # @@protoc_insertion_point(imports) 13 | 14 | _sym_db = _symbol_database.Default() 15 | 16 | 17 | from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 18 | 19 | 20 | DESCRIPTOR = _descriptor.FileDescriptor( 21 | name='loop_rpc/protos/loop_client.proto', 22 | package='looprpc', 23 | syntax='proto3', 24 | serialized_options=None, 25 | serialized_pb=_b('\n!loop_rpc/protos/loop_client.proto\x12\x07looprpc\x1a\x1cgoogle/api/annotations.proto\"\xe3\x01\n\x0eLoopOutRequest\x12\x0b\n\x03\x61mt\x18\x01 \x01(\x03\x12\x0c\n\x04\x64\x65st\x18\x02 \x01(\t\x12\x1c\n\x14max_swap_routing_fee\x18\x03 \x01(\x03\x12\x1e\n\x16max_prepay_routing_fee\x18\x04 \x01(\x03\x12\x14\n\x0cmax_swap_fee\x18\x05 \x01(\x03\x12\x16\n\x0emax_prepay_amt\x18\x06 \x01(\x03\x12\x15\n\rmax_miner_fee\x18\x07 \x01(\x03\x12\x18\n\x10loop_out_channel\x18\x08 \x01(\x04\x12\x19\n\x11sweep_conf_target\x18\t \x01(\x05\"y\n\rLoopInRequest\x12\x0b\n\x03\x61mt\x18\x01 \x01(\x03\x12\x14\n\x0cmax_swap_fee\x18\x02 \x01(\x03\x12\x15\n\rmax_miner_fee\x18\x03 \x01(\x03\x12\x17\n\x0floop_in_channel\x18\x04 \x01(\x04\x12\x15\n\rexternal_htlc\x18\x05 \x01(\x08\"0\n\x0cSwapResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0chtlc_address\x18\x02 \x01(\t\"\x10\n\x0eMonitorRequest\"\xf4\x01\n\nSwapStatus\x12\x0b\n\x03\x61mt\x18\x01 \x01(\x03\x12\n\n\x02id\x18\x02 \x01(\t\x12\x1f\n\x04type\x18\x03 \x01(\x0e\x32\x11.looprpc.SwapType\x12!\n\x05state\x18\x04 \x01(\x0e\x32\x12.looprpc.SwapState\x12\x17\n\x0finitiation_time\x18\x05 \x01(\x03\x12\x18\n\x10last_update_time\x18\x06 \x01(\x03\x12\x14\n\x0chtlc_address\x18\x07 \x01(\t\x12\x13\n\x0b\x63ost_server\x18\x08 \x01(\x03\x12\x14\n\x0c\x63ost_onchain\x18\t \x01(\x03\x12\x15\n\rcost_offchain\x18\n \x01(\x03\"\x0e\n\x0cTermsRequest\"\xb2\x01\n\rTermsResponse\x12\x19\n\x11swap_payment_dest\x18\x01 \x01(\t\x12\x15\n\rswap_fee_base\x18\x02 \x01(\x03\x12\x15\n\rswap_fee_rate\x18\x03 \x01(\x03\x12\x12\n\nprepay_amt\x18\x04 \x01(\x03\x12\x17\n\x0fmin_swap_amount\x18\x05 \x01(\x03\x12\x17\n\x0fmax_swap_amount\x18\x06 \x01(\x03\x12\x12\n\ncltv_delta\x18\x07 \x01(\x05\"0\n\x0cQuoteRequest\x12\x0b\n\x03\x61mt\x18\x01 \x01(\x03\x12\x13\n\x0b\x63onf_target\x18\x02 \x01(\x05\"H\n\rQuoteResponse\x12\x10\n\x08swap_fee\x18\x01 \x01(\x03\x12\x12\n\nprepay_amt\x18\x02 \x01(\x03\x12\x11\n\tminer_fee\x18\x03 \x01(\x03*%\n\x08SwapType\x12\x0c\n\x08LOOP_OUT\x10\x00\x12\x0b\n\x07LOOP_IN\x10\x01*s\n\tSwapState\x12\r\n\tINITIATED\x10\x00\x12\x15\n\x11PREIMAGE_REVEALED\x10\x01\x12\x12\n\x0eHTLC_PUBLISHED\x10\x02\x12\x0b\n\x07SUCCESS\x10\x03\x12\n\n\x06\x46\x41ILED\x10\x04\x12\x13\n\x0fINVOICE_SETTLED\x10\x05\x32\x92\x04\n\nSwapClient\x12R\n\x07LoopOut\x12\x17.looprpc.LoopOutRequest\x1a\x15.looprpc.SwapResponse\"\x17\x82\xd3\xe4\x93\x02\x11\"\x0c/v1/loop/out:\x01*\x12\x37\n\x06LoopIn\x12\x16.looprpc.LoopInRequest\x1a\x15.looprpc.SwapResponse\x12\x39\n\x07Monitor\x12\x17.looprpc.MonitorRequest\x1a\x13.looprpc.SwapStatus0\x01\x12Y\n\x0cLoopOutTerms\x12\x15.looprpc.TermsRequest\x1a\x16.looprpc.TermsResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/loop/out/terms\x12_\n\x0cLoopOutQuote\x12\x15.looprpc.QuoteRequest\x1a\x16.looprpc.QuoteResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/v1/loop/out/quote/{amt}\x12?\n\x0eGetLoopInTerms\x12\x15.looprpc.TermsRequest\x1a\x16.looprpc.TermsResponse\x12?\n\x0eGetLoopInQuote\x12\x15.looprpc.QuoteRequest\x1a\x16.looprpc.QuoteResponseb\x06proto3') 26 | , 27 | dependencies=[google_dot_api_dot_annotations__pb2.DESCRIPTOR,]) 28 | 29 | _SWAPTYPE = _descriptor.EnumDescriptor( 30 | name='SwapType', 31 | full_name='looprpc.SwapType', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | values=[ 35 | _descriptor.EnumValueDescriptor( 36 | name='LOOP_OUT', index=0, number=0, 37 | serialized_options=None, 38 | type=None), 39 | _descriptor.EnumValueDescriptor( 40 | name='LOOP_IN', index=1, number=1, 41 | serialized_options=None, 42 | type=None), 43 | ], 44 | containing_type=None, 45 | serialized_options=None, 46 | serialized_start=1065, 47 | serialized_end=1102, 48 | ) 49 | _sym_db.RegisterEnumDescriptor(_SWAPTYPE) 50 | 51 | SwapType = enum_type_wrapper.EnumTypeWrapper(_SWAPTYPE) 52 | _SWAPSTATE = _descriptor.EnumDescriptor( 53 | name='SwapState', 54 | full_name='looprpc.SwapState', 55 | filename=None, 56 | file=DESCRIPTOR, 57 | values=[ 58 | _descriptor.EnumValueDescriptor( 59 | name='INITIATED', index=0, number=0, 60 | serialized_options=None, 61 | type=None), 62 | _descriptor.EnumValueDescriptor( 63 | name='PREIMAGE_REVEALED', index=1, number=1, 64 | serialized_options=None, 65 | type=None), 66 | _descriptor.EnumValueDescriptor( 67 | name='HTLC_PUBLISHED', index=2, number=2, 68 | serialized_options=None, 69 | type=None), 70 | _descriptor.EnumValueDescriptor( 71 | name='SUCCESS', index=3, number=3, 72 | serialized_options=None, 73 | type=None), 74 | _descriptor.EnumValueDescriptor( 75 | name='FAILED', index=4, number=4, 76 | serialized_options=None, 77 | type=None), 78 | _descriptor.EnumValueDescriptor( 79 | name='INVOICE_SETTLED', index=5, number=5, 80 | serialized_options=None, 81 | type=None), 82 | ], 83 | containing_type=None, 84 | serialized_options=None, 85 | serialized_start=1104, 86 | serialized_end=1219, 87 | ) 88 | _sym_db.RegisterEnumDescriptor(_SWAPSTATE) 89 | 90 | SwapState = enum_type_wrapper.EnumTypeWrapper(_SWAPSTATE) 91 | LOOP_OUT = 0 92 | LOOP_IN = 1 93 | INITIATED = 0 94 | PREIMAGE_REVEALED = 1 95 | HTLC_PUBLISHED = 2 96 | SUCCESS = 3 97 | FAILED = 4 98 | INVOICE_SETTLED = 5 99 | 100 | 101 | 102 | _LOOPOUTREQUEST = _descriptor.Descriptor( 103 | name='LoopOutRequest', 104 | full_name='looprpc.LoopOutRequest', 105 | filename=None, 106 | file=DESCRIPTOR, 107 | containing_type=None, 108 | fields=[ 109 | _descriptor.FieldDescriptor( 110 | name='amt', full_name='looprpc.LoopOutRequest.amt', index=0, 111 | number=1, type=3, cpp_type=2, label=1, 112 | has_default_value=False, default_value=0, 113 | message_type=None, enum_type=None, containing_type=None, 114 | is_extension=False, extension_scope=None, 115 | serialized_options=None, file=DESCRIPTOR), 116 | _descriptor.FieldDescriptor( 117 | name='dest', full_name='looprpc.LoopOutRequest.dest', index=1, 118 | number=2, type=9, cpp_type=9, label=1, 119 | has_default_value=False, default_value=_b("").decode('utf-8'), 120 | message_type=None, enum_type=None, containing_type=None, 121 | is_extension=False, extension_scope=None, 122 | serialized_options=None, file=DESCRIPTOR), 123 | _descriptor.FieldDescriptor( 124 | name='max_swap_routing_fee', full_name='looprpc.LoopOutRequest.max_swap_routing_fee', index=2, 125 | number=3, type=3, cpp_type=2, label=1, 126 | has_default_value=False, default_value=0, 127 | message_type=None, enum_type=None, containing_type=None, 128 | is_extension=False, extension_scope=None, 129 | serialized_options=None, file=DESCRIPTOR), 130 | _descriptor.FieldDescriptor( 131 | name='max_prepay_routing_fee', full_name='looprpc.LoopOutRequest.max_prepay_routing_fee', index=3, 132 | number=4, type=3, cpp_type=2, label=1, 133 | has_default_value=False, default_value=0, 134 | message_type=None, enum_type=None, containing_type=None, 135 | is_extension=False, extension_scope=None, 136 | serialized_options=None, file=DESCRIPTOR), 137 | _descriptor.FieldDescriptor( 138 | name='max_swap_fee', full_name='looprpc.LoopOutRequest.max_swap_fee', index=4, 139 | number=5, type=3, cpp_type=2, label=1, 140 | has_default_value=False, default_value=0, 141 | message_type=None, enum_type=None, containing_type=None, 142 | is_extension=False, extension_scope=None, 143 | serialized_options=None, file=DESCRIPTOR), 144 | _descriptor.FieldDescriptor( 145 | name='max_prepay_amt', full_name='looprpc.LoopOutRequest.max_prepay_amt', index=5, 146 | number=6, type=3, cpp_type=2, label=1, 147 | has_default_value=False, default_value=0, 148 | message_type=None, enum_type=None, containing_type=None, 149 | is_extension=False, extension_scope=None, 150 | serialized_options=None, file=DESCRIPTOR), 151 | _descriptor.FieldDescriptor( 152 | name='max_miner_fee', full_name='looprpc.LoopOutRequest.max_miner_fee', index=6, 153 | number=7, type=3, cpp_type=2, label=1, 154 | has_default_value=False, default_value=0, 155 | message_type=None, enum_type=None, containing_type=None, 156 | is_extension=False, extension_scope=None, 157 | serialized_options=None, file=DESCRIPTOR), 158 | _descriptor.FieldDescriptor( 159 | name='loop_out_channel', full_name='looprpc.LoopOutRequest.loop_out_channel', index=7, 160 | number=8, type=4, cpp_type=4, label=1, 161 | has_default_value=False, default_value=0, 162 | message_type=None, enum_type=None, containing_type=None, 163 | is_extension=False, extension_scope=None, 164 | serialized_options=None, file=DESCRIPTOR), 165 | _descriptor.FieldDescriptor( 166 | name='sweep_conf_target', full_name='looprpc.LoopOutRequest.sweep_conf_target', index=8, 167 | number=9, type=5, cpp_type=1, label=1, 168 | has_default_value=False, default_value=0, 169 | message_type=None, enum_type=None, containing_type=None, 170 | is_extension=False, extension_scope=None, 171 | serialized_options=None, file=DESCRIPTOR), 172 | ], 173 | extensions=[ 174 | ], 175 | nested_types=[], 176 | enum_types=[ 177 | ], 178 | serialized_options=None, 179 | is_extendable=False, 180 | syntax='proto3', 181 | extension_ranges=[], 182 | oneofs=[ 183 | ], 184 | serialized_start=77, 185 | serialized_end=304, 186 | ) 187 | 188 | 189 | _LOOPINREQUEST = _descriptor.Descriptor( 190 | name='LoopInRequest', 191 | full_name='looprpc.LoopInRequest', 192 | filename=None, 193 | file=DESCRIPTOR, 194 | containing_type=None, 195 | fields=[ 196 | _descriptor.FieldDescriptor( 197 | name='amt', full_name='looprpc.LoopInRequest.amt', index=0, 198 | number=1, type=3, cpp_type=2, label=1, 199 | has_default_value=False, default_value=0, 200 | message_type=None, enum_type=None, containing_type=None, 201 | is_extension=False, extension_scope=None, 202 | serialized_options=None, file=DESCRIPTOR), 203 | _descriptor.FieldDescriptor( 204 | name='max_swap_fee', full_name='looprpc.LoopInRequest.max_swap_fee', index=1, 205 | number=2, type=3, cpp_type=2, label=1, 206 | has_default_value=False, default_value=0, 207 | message_type=None, enum_type=None, containing_type=None, 208 | is_extension=False, extension_scope=None, 209 | serialized_options=None, file=DESCRIPTOR), 210 | _descriptor.FieldDescriptor( 211 | name='max_miner_fee', full_name='looprpc.LoopInRequest.max_miner_fee', index=2, 212 | number=3, type=3, cpp_type=2, label=1, 213 | has_default_value=False, default_value=0, 214 | message_type=None, enum_type=None, containing_type=None, 215 | is_extension=False, extension_scope=None, 216 | serialized_options=None, file=DESCRIPTOR), 217 | _descriptor.FieldDescriptor( 218 | name='loop_in_channel', full_name='looprpc.LoopInRequest.loop_in_channel', index=3, 219 | number=4, type=4, cpp_type=4, label=1, 220 | has_default_value=False, default_value=0, 221 | message_type=None, enum_type=None, containing_type=None, 222 | is_extension=False, extension_scope=None, 223 | serialized_options=None, file=DESCRIPTOR), 224 | _descriptor.FieldDescriptor( 225 | name='external_htlc', full_name='looprpc.LoopInRequest.external_htlc', index=4, 226 | number=5, type=8, cpp_type=7, label=1, 227 | has_default_value=False, default_value=False, 228 | message_type=None, enum_type=None, containing_type=None, 229 | is_extension=False, extension_scope=None, 230 | serialized_options=None, file=DESCRIPTOR), 231 | ], 232 | extensions=[ 233 | ], 234 | nested_types=[], 235 | enum_types=[ 236 | ], 237 | serialized_options=None, 238 | is_extendable=False, 239 | syntax='proto3', 240 | extension_ranges=[], 241 | oneofs=[ 242 | ], 243 | serialized_start=306, 244 | serialized_end=427, 245 | ) 246 | 247 | 248 | _SWAPRESPONSE = _descriptor.Descriptor( 249 | name='SwapResponse', 250 | full_name='looprpc.SwapResponse', 251 | filename=None, 252 | file=DESCRIPTOR, 253 | containing_type=None, 254 | fields=[ 255 | _descriptor.FieldDescriptor( 256 | name='id', full_name='looprpc.SwapResponse.id', index=0, 257 | number=1, type=9, cpp_type=9, label=1, 258 | has_default_value=False, default_value=_b("").decode('utf-8'), 259 | message_type=None, enum_type=None, containing_type=None, 260 | is_extension=False, extension_scope=None, 261 | serialized_options=None, file=DESCRIPTOR), 262 | _descriptor.FieldDescriptor( 263 | name='htlc_address', full_name='looprpc.SwapResponse.htlc_address', index=1, 264 | number=2, type=9, cpp_type=9, label=1, 265 | has_default_value=False, default_value=_b("").decode('utf-8'), 266 | message_type=None, enum_type=None, containing_type=None, 267 | is_extension=False, extension_scope=None, 268 | serialized_options=None, file=DESCRIPTOR), 269 | ], 270 | extensions=[ 271 | ], 272 | nested_types=[], 273 | enum_types=[ 274 | ], 275 | serialized_options=None, 276 | is_extendable=False, 277 | syntax='proto3', 278 | extension_ranges=[], 279 | oneofs=[ 280 | ], 281 | serialized_start=429, 282 | serialized_end=477, 283 | ) 284 | 285 | 286 | _MONITORREQUEST = _descriptor.Descriptor( 287 | name='MonitorRequest', 288 | full_name='looprpc.MonitorRequest', 289 | filename=None, 290 | file=DESCRIPTOR, 291 | containing_type=None, 292 | fields=[ 293 | ], 294 | extensions=[ 295 | ], 296 | nested_types=[], 297 | enum_types=[ 298 | ], 299 | serialized_options=None, 300 | is_extendable=False, 301 | syntax='proto3', 302 | extension_ranges=[], 303 | oneofs=[ 304 | ], 305 | serialized_start=479, 306 | serialized_end=495, 307 | ) 308 | 309 | 310 | _SWAPSTATUS = _descriptor.Descriptor( 311 | name='SwapStatus', 312 | full_name='looprpc.SwapStatus', 313 | filename=None, 314 | file=DESCRIPTOR, 315 | containing_type=None, 316 | fields=[ 317 | _descriptor.FieldDescriptor( 318 | name='amt', full_name='looprpc.SwapStatus.amt', index=0, 319 | number=1, type=3, cpp_type=2, label=1, 320 | has_default_value=False, default_value=0, 321 | message_type=None, enum_type=None, containing_type=None, 322 | is_extension=False, extension_scope=None, 323 | serialized_options=None, file=DESCRIPTOR), 324 | _descriptor.FieldDescriptor( 325 | name='id', full_name='looprpc.SwapStatus.id', index=1, 326 | number=2, type=9, cpp_type=9, label=1, 327 | has_default_value=False, default_value=_b("").decode('utf-8'), 328 | message_type=None, enum_type=None, containing_type=None, 329 | is_extension=False, extension_scope=None, 330 | serialized_options=None, file=DESCRIPTOR), 331 | _descriptor.FieldDescriptor( 332 | name='type', full_name='looprpc.SwapStatus.type', index=2, 333 | number=3, type=14, cpp_type=8, label=1, 334 | has_default_value=False, default_value=0, 335 | message_type=None, enum_type=None, containing_type=None, 336 | is_extension=False, extension_scope=None, 337 | serialized_options=None, file=DESCRIPTOR), 338 | _descriptor.FieldDescriptor( 339 | name='state', full_name='looprpc.SwapStatus.state', index=3, 340 | number=4, type=14, cpp_type=8, label=1, 341 | has_default_value=False, default_value=0, 342 | message_type=None, enum_type=None, containing_type=None, 343 | is_extension=False, extension_scope=None, 344 | serialized_options=None, file=DESCRIPTOR), 345 | _descriptor.FieldDescriptor( 346 | name='initiation_time', full_name='looprpc.SwapStatus.initiation_time', index=4, 347 | number=5, type=3, cpp_type=2, label=1, 348 | has_default_value=False, default_value=0, 349 | message_type=None, enum_type=None, containing_type=None, 350 | is_extension=False, extension_scope=None, 351 | serialized_options=None, file=DESCRIPTOR), 352 | _descriptor.FieldDescriptor( 353 | name='last_update_time', full_name='looprpc.SwapStatus.last_update_time', index=5, 354 | number=6, type=3, cpp_type=2, label=1, 355 | has_default_value=False, default_value=0, 356 | message_type=None, enum_type=None, containing_type=None, 357 | is_extension=False, extension_scope=None, 358 | serialized_options=None, file=DESCRIPTOR), 359 | _descriptor.FieldDescriptor( 360 | name='htlc_address', full_name='looprpc.SwapStatus.htlc_address', index=6, 361 | number=7, type=9, cpp_type=9, label=1, 362 | has_default_value=False, default_value=_b("").decode('utf-8'), 363 | message_type=None, enum_type=None, containing_type=None, 364 | is_extension=False, extension_scope=None, 365 | serialized_options=None, file=DESCRIPTOR), 366 | _descriptor.FieldDescriptor( 367 | name='cost_server', full_name='looprpc.SwapStatus.cost_server', index=7, 368 | number=8, type=3, cpp_type=2, label=1, 369 | has_default_value=False, default_value=0, 370 | message_type=None, enum_type=None, containing_type=None, 371 | is_extension=False, extension_scope=None, 372 | serialized_options=None, file=DESCRIPTOR), 373 | _descriptor.FieldDescriptor( 374 | name='cost_onchain', full_name='looprpc.SwapStatus.cost_onchain', index=8, 375 | number=9, type=3, cpp_type=2, label=1, 376 | has_default_value=False, default_value=0, 377 | message_type=None, enum_type=None, containing_type=None, 378 | is_extension=False, extension_scope=None, 379 | serialized_options=None, file=DESCRIPTOR), 380 | _descriptor.FieldDescriptor( 381 | name='cost_offchain', full_name='looprpc.SwapStatus.cost_offchain', index=9, 382 | number=10, type=3, cpp_type=2, label=1, 383 | has_default_value=False, default_value=0, 384 | message_type=None, enum_type=None, containing_type=None, 385 | is_extension=False, extension_scope=None, 386 | serialized_options=None, file=DESCRIPTOR), 387 | ], 388 | extensions=[ 389 | ], 390 | nested_types=[], 391 | enum_types=[ 392 | ], 393 | serialized_options=None, 394 | is_extendable=False, 395 | syntax='proto3', 396 | extension_ranges=[], 397 | oneofs=[ 398 | ], 399 | serialized_start=498, 400 | serialized_end=742, 401 | ) 402 | 403 | 404 | _TERMSREQUEST = _descriptor.Descriptor( 405 | name='TermsRequest', 406 | full_name='looprpc.TermsRequest', 407 | filename=None, 408 | file=DESCRIPTOR, 409 | containing_type=None, 410 | fields=[ 411 | ], 412 | extensions=[ 413 | ], 414 | nested_types=[], 415 | enum_types=[ 416 | ], 417 | serialized_options=None, 418 | is_extendable=False, 419 | syntax='proto3', 420 | extension_ranges=[], 421 | oneofs=[ 422 | ], 423 | serialized_start=744, 424 | serialized_end=758, 425 | ) 426 | 427 | 428 | _TERMSRESPONSE = _descriptor.Descriptor( 429 | name='TermsResponse', 430 | full_name='looprpc.TermsResponse', 431 | filename=None, 432 | file=DESCRIPTOR, 433 | containing_type=None, 434 | fields=[ 435 | _descriptor.FieldDescriptor( 436 | name='swap_payment_dest', full_name='looprpc.TermsResponse.swap_payment_dest', index=0, 437 | number=1, type=9, cpp_type=9, label=1, 438 | has_default_value=False, default_value=_b("").decode('utf-8'), 439 | message_type=None, enum_type=None, containing_type=None, 440 | is_extension=False, extension_scope=None, 441 | serialized_options=None, file=DESCRIPTOR), 442 | _descriptor.FieldDescriptor( 443 | name='swap_fee_base', full_name='looprpc.TermsResponse.swap_fee_base', index=1, 444 | number=2, type=3, cpp_type=2, label=1, 445 | has_default_value=False, default_value=0, 446 | message_type=None, enum_type=None, containing_type=None, 447 | is_extension=False, extension_scope=None, 448 | serialized_options=None, file=DESCRIPTOR), 449 | _descriptor.FieldDescriptor( 450 | name='swap_fee_rate', full_name='looprpc.TermsResponse.swap_fee_rate', index=2, 451 | number=3, type=3, cpp_type=2, label=1, 452 | has_default_value=False, default_value=0, 453 | message_type=None, enum_type=None, containing_type=None, 454 | is_extension=False, extension_scope=None, 455 | serialized_options=None, file=DESCRIPTOR), 456 | _descriptor.FieldDescriptor( 457 | name='prepay_amt', full_name='looprpc.TermsResponse.prepay_amt', index=3, 458 | number=4, type=3, cpp_type=2, label=1, 459 | has_default_value=False, default_value=0, 460 | message_type=None, enum_type=None, containing_type=None, 461 | is_extension=False, extension_scope=None, 462 | serialized_options=None, file=DESCRIPTOR), 463 | _descriptor.FieldDescriptor( 464 | name='min_swap_amount', full_name='looprpc.TermsResponse.min_swap_amount', index=4, 465 | number=5, type=3, cpp_type=2, label=1, 466 | has_default_value=False, default_value=0, 467 | message_type=None, enum_type=None, containing_type=None, 468 | is_extension=False, extension_scope=None, 469 | serialized_options=None, file=DESCRIPTOR), 470 | _descriptor.FieldDescriptor( 471 | name='max_swap_amount', full_name='looprpc.TermsResponse.max_swap_amount', index=5, 472 | number=6, type=3, cpp_type=2, label=1, 473 | has_default_value=False, default_value=0, 474 | message_type=None, enum_type=None, containing_type=None, 475 | is_extension=False, extension_scope=None, 476 | serialized_options=None, file=DESCRIPTOR), 477 | _descriptor.FieldDescriptor( 478 | name='cltv_delta', full_name='looprpc.TermsResponse.cltv_delta', index=6, 479 | number=7, type=5, cpp_type=1, label=1, 480 | has_default_value=False, default_value=0, 481 | message_type=None, enum_type=None, containing_type=None, 482 | is_extension=False, extension_scope=None, 483 | serialized_options=None, file=DESCRIPTOR), 484 | ], 485 | extensions=[ 486 | ], 487 | nested_types=[], 488 | enum_types=[ 489 | ], 490 | serialized_options=None, 491 | is_extendable=False, 492 | syntax='proto3', 493 | extension_ranges=[], 494 | oneofs=[ 495 | ], 496 | serialized_start=761, 497 | serialized_end=939, 498 | ) 499 | 500 | 501 | _QUOTEREQUEST = _descriptor.Descriptor( 502 | name='QuoteRequest', 503 | full_name='looprpc.QuoteRequest', 504 | filename=None, 505 | file=DESCRIPTOR, 506 | containing_type=None, 507 | fields=[ 508 | _descriptor.FieldDescriptor( 509 | name='amt', full_name='looprpc.QuoteRequest.amt', index=0, 510 | number=1, type=3, cpp_type=2, label=1, 511 | has_default_value=False, default_value=0, 512 | message_type=None, enum_type=None, containing_type=None, 513 | is_extension=False, extension_scope=None, 514 | serialized_options=None, file=DESCRIPTOR), 515 | _descriptor.FieldDescriptor( 516 | name='conf_target', full_name='looprpc.QuoteRequest.conf_target', index=1, 517 | number=2, type=5, cpp_type=1, label=1, 518 | has_default_value=False, default_value=0, 519 | message_type=None, enum_type=None, containing_type=None, 520 | is_extension=False, extension_scope=None, 521 | serialized_options=None, file=DESCRIPTOR), 522 | ], 523 | extensions=[ 524 | ], 525 | nested_types=[], 526 | enum_types=[ 527 | ], 528 | serialized_options=None, 529 | is_extendable=False, 530 | syntax='proto3', 531 | extension_ranges=[], 532 | oneofs=[ 533 | ], 534 | serialized_start=941, 535 | serialized_end=989, 536 | ) 537 | 538 | 539 | _QUOTERESPONSE = _descriptor.Descriptor( 540 | name='QuoteResponse', 541 | full_name='looprpc.QuoteResponse', 542 | filename=None, 543 | file=DESCRIPTOR, 544 | containing_type=None, 545 | fields=[ 546 | _descriptor.FieldDescriptor( 547 | name='swap_fee', full_name='looprpc.QuoteResponse.swap_fee', index=0, 548 | number=1, type=3, cpp_type=2, label=1, 549 | has_default_value=False, default_value=0, 550 | message_type=None, enum_type=None, containing_type=None, 551 | is_extension=False, extension_scope=None, 552 | serialized_options=None, file=DESCRIPTOR), 553 | _descriptor.FieldDescriptor( 554 | name='prepay_amt', full_name='looprpc.QuoteResponse.prepay_amt', index=1, 555 | number=2, type=3, cpp_type=2, label=1, 556 | has_default_value=False, default_value=0, 557 | message_type=None, enum_type=None, containing_type=None, 558 | is_extension=False, extension_scope=None, 559 | serialized_options=None, file=DESCRIPTOR), 560 | _descriptor.FieldDescriptor( 561 | name='miner_fee', full_name='looprpc.QuoteResponse.miner_fee', index=2, 562 | number=3, type=3, cpp_type=2, label=1, 563 | has_default_value=False, default_value=0, 564 | message_type=None, enum_type=None, containing_type=None, 565 | is_extension=False, extension_scope=None, 566 | serialized_options=None, file=DESCRIPTOR), 567 | ], 568 | extensions=[ 569 | ], 570 | nested_types=[], 571 | enum_types=[ 572 | ], 573 | serialized_options=None, 574 | is_extendable=False, 575 | syntax='proto3', 576 | extension_ranges=[], 577 | oneofs=[ 578 | ], 579 | serialized_start=991, 580 | serialized_end=1063, 581 | ) 582 | 583 | _SWAPSTATUS.fields_by_name['type'].enum_type = _SWAPTYPE 584 | _SWAPSTATUS.fields_by_name['state'].enum_type = _SWAPSTATE 585 | DESCRIPTOR.message_types_by_name['LoopOutRequest'] = _LOOPOUTREQUEST 586 | DESCRIPTOR.message_types_by_name['LoopInRequest'] = _LOOPINREQUEST 587 | DESCRIPTOR.message_types_by_name['SwapResponse'] = _SWAPRESPONSE 588 | DESCRIPTOR.message_types_by_name['MonitorRequest'] = _MONITORREQUEST 589 | DESCRIPTOR.message_types_by_name['SwapStatus'] = _SWAPSTATUS 590 | DESCRIPTOR.message_types_by_name['TermsRequest'] = _TERMSREQUEST 591 | DESCRIPTOR.message_types_by_name['TermsResponse'] = _TERMSRESPONSE 592 | DESCRIPTOR.message_types_by_name['QuoteRequest'] = _QUOTEREQUEST 593 | DESCRIPTOR.message_types_by_name['QuoteResponse'] = _QUOTERESPONSE 594 | DESCRIPTOR.enum_types_by_name['SwapType'] = _SWAPTYPE 595 | DESCRIPTOR.enum_types_by_name['SwapState'] = _SWAPSTATE 596 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 597 | 598 | LoopOutRequest = _reflection.GeneratedProtocolMessageType('LoopOutRequest', (_message.Message,), dict( 599 | DESCRIPTOR = _LOOPOUTREQUEST, 600 | __module__ = 'loop_rpc.protos.loop_client_pb2' 601 | # @@protoc_insertion_point(class_scope:looprpc.LoopOutRequest) 602 | )) 603 | _sym_db.RegisterMessage(LoopOutRequest) 604 | 605 | LoopInRequest = _reflection.GeneratedProtocolMessageType('LoopInRequest', (_message.Message,), dict( 606 | DESCRIPTOR = _LOOPINREQUEST, 607 | __module__ = 'loop_rpc.protos.loop_client_pb2' 608 | # @@protoc_insertion_point(class_scope:looprpc.LoopInRequest) 609 | )) 610 | _sym_db.RegisterMessage(LoopInRequest) 611 | 612 | SwapResponse = _reflection.GeneratedProtocolMessageType('SwapResponse', (_message.Message,), dict( 613 | DESCRIPTOR = _SWAPRESPONSE, 614 | __module__ = 'loop_rpc.protos.loop_client_pb2' 615 | # @@protoc_insertion_point(class_scope:looprpc.SwapResponse) 616 | )) 617 | _sym_db.RegisterMessage(SwapResponse) 618 | 619 | MonitorRequest = _reflection.GeneratedProtocolMessageType('MonitorRequest', (_message.Message,), dict( 620 | DESCRIPTOR = _MONITORREQUEST, 621 | __module__ = 'loop_rpc.protos.loop_client_pb2' 622 | # @@protoc_insertion_point(class_scope:looprpc.MonitorRequest) 623 | )) 624 | _sym_db.RegisterMessage(MonitorRequest) 625 | 626 | SwapStatus = _reflection.GeneratedProtocolMessageType('SwapStatus', (_message.Message,), dict( 627 | DESCRIPTOR = _SWAPSTATUS, 628 | __module__ = 'loop_rpc.protos.loop_client_pb2' 629 | # @@protoc_insertion_point(class_scope:looprpc.SwapStatus) 630 | )) 631 | _sym_db.RegisterMessage(SwapStatus) 632 | 633 | TermsRequest = _reflection.GeneratedProtocolMessageType('TermsRequest', (_message.Message,), dict( 634 | DESCRIPTOR = _TERMSREQUEST, 635 | __module__ = 'loop_rpc.protos.loop_client_pb2' 636 | # @@protoc_insertion_point(class_scope:looprpc.TermsRequest) 637 | )) 638 | _sym_db.RegisterMessage(TermsRequest) 639 | 640 | TermsResponse = _reflection.GeneratedProtocolMessageType('TermsResponse', (_message.Message,), dict( 641 | DESCRIPTOR = _TERMSRESPONSE, 642 | __module__ = 'loop_rpc.protos.loop_client_pb2' 643 | # @@protoc_insertion_point(class_scope:looprpc.TermsResponse) 644 | )) 645 | _sym_db.RegisterMessage(TermsResponse) 646 | 647 | QuoteRequest = _reflection.GeneratedProtocolMessageType('QuoteRequest', (_message.Message,), dict( 648 | DESCRIPTOR = _QUOTEREQUEST, 649 | __module__ = 'loop_rpc.protos.loop_client_pb2' 650 | # @@protoc_insertion_point(class_scope:looprpc.QuoteRequest) 651 | )) 652 | _sym_db.RegisterMessage(QuoteRequest) 653 | 654 | QuoteResponse = _reflection.GeneratedProtocolMessageType('QuoteResponse', (_message.Message,), dict( 655 | DESCRIPTOR = _QUOTERESPONSE, 656 | __module__ = 'loop_rpc.protos.loop_client_pb2' 657 | # @@protoc_insertion_point(class_scope:looprpc.QuoteResponse) 658 | )) 659 | _sym_db.RegisterMessage(QuoteResponse) 660 | 661 | 662 | 663 | _SWAPCLIENT = _descriptor.ServiceDescriptor( 664 | name='SwapClient', 665 | full_name='looprpc.SwapClient', 666 | file=DESCRIPTOR, 667 | index=0, 668 | serialized_options=None, 669 | serialized_start=1222, 670 | serialized_end=1752, 671 | methods=[ 672 | _descriptor.MethodDescriptor( 673 | name='LoopOut', 674 | full_name='looprpc.SwapClient.LoopOut', 675 | index=0, 676 | containing_service=None, 677 | input_type=_LOOPOUTREQUEST, 678 | output_type=_SWAPRESPONSE, 679 | serialized_options=_b('\202\323\344\223\002\021\"\014/v1/loop/out:\001*'), 680 | ), 681 | _descriptor.MethodDescriptor( 682 | name='LoopIn', 683 | full_name='looprpc.SwapClient.LoopIn', 684 | index=1, 685 | containing_service=None, 686 | input_type=_LOOPINREQUEST, 687 | output_type=_SWAPRESPONSE, 688 | serialized_options=None, 689 | ), 690 | _descriptor.MethodDescriptor( 691 | name='Monitor', 692 | full_name='looprpc.SwapClient.Monitor', 693 | index=2, 694 | containing_service=None, 695 | input_type=_MONITORREQUEST, 696 | output_type=_SWAPSTATUS, 697 | serialized_options=None, 698 | ), 699 | _descriptor.MethodDescriptor( 700 | name='LoopOutTerms', 701 | full_name='looprpc.SwapClient.LoopOutTerms', 702 | index=3, 703 | containing_service=None, 704 | input_type=_TERMSREQUEST, 705 | output_type=_TERMSRESPONSE, 706 | serialized_options=_b('\202\323\344\223\002\024\022\022/v1/loop/out/terms'), 707 | ), 708 | _descriptor.MethodDescriptor( 709 | name='LoopOutQuote', 710 | full_name='looprpc.SwapClient.LoopOutQuote', 711 | index=4, 712 | containing_service=None, 713 | input_type=_QUOTEREQUEST, 714 | output_type=_QUOTERESPONSE, 715 | serialized_options=_b('\202\323\344\223\002\032\022\030/v1/loop/out/quote/{amt}'), 716 | ), 717 | _descriptor.MethodDescriptor( 718 | name='GetLoopInTerms', 719 | full_name='looprpc.SwapClient.GetLoopInTerms', 720 | index=5, 721 | containing_service=None, 722 | input_type=_TERMSREQUEST, 723 | output_type=_TERMSRESPONSE, 724 | serialized_options=None, 725 | ), 726 | _descriptor.MethodDescriptor( 727 | name='GetLoopInQuote', 728 | full_name='looprpc.SwapClient.GetLoopInQuote', 729 | index=6, 730 | containing_service=None, 731 | input_type=_QUOTEREQUEST, 732 | output_type=_QUOTERESPONSE, 733 | serialized_options=None, 734 | ), 735 | ]) 736 | _sym_db.RegisterServiceDescriptor(_SWAPCLIENT) 737 | 738 | DESCRIPTOR.services_by_name['SwapClient'] = _SWAPCLIENT 739 | 740 | # @@protoc_insertion_point(module_scope) 741 | -------------------------------------------------------------------------------- /loop_rpc/protos/loop_client_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | from loop_rpc.protos import loop_client_pb2 as loop__rpc_dot_protos_dot_loop__client__pb2 5 | 6 | 7 | class SwapClientStub(object): 8 | """* 9 | SwapClient is a service that handles the client side process of onchain/offchain 10 | swaps. The service is designed for a single client. 11 | """ 12 | 13 | def __init__(self, channel): 14 | """Constructor. 15 | 16 | Args: 17 | channel: A grpc.Channel. 18 | """ 19 | self.LoopOut = channel.unary_unary( 20 | '/looprpc.SwapClient/LoopOut', 21 | request_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.LoopOutRequest.SerializeToString, 22 | response_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.SwapResponse.FromString, 23 | ) 24 | self.LoopIn = channel.unary_unary( 25 | '/looprpc.SwapClient/LoopIn', 26 | request_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.LoopInRequest.SerializeToString, 27 | response_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.SwapResponse.FromString, 28 | ) 29 | self.Monitor = channel.unary_stream( 30 | '/looprpc.SwapClient/Monitor', 31 | request_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.MonitorRequest.SerializeToString, 32 | response_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.SwapStatus.FromString, 33 | ) 34 | self.LoopOutTerms = channel.unary_unary( 35 | '/looprpc.SwapClient/LoopOutTerms', 36 | request_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.TermsRequest.SerializeToString, 37 | response_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.TermsResponse.FromString, 38 | ) 39 | self.LoopOutQuote = channel.unary_unary( 40 | '/looprpc.SwapClient/LoopOutQuote', 41 | request_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.QuoteRequest.SerializeToString, 42 | response_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.QuoteResponse.FromString, 43 | ) 44 | self.GetLoopInTerms = channel.unary_unary( 45 | '/looprpc.SwapClient/GetLoopInTerms', 46 | request_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.TermsRequest.SerializeToString, 47 | response_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.TermsResponse.FromString, 48 | ) 49 | self.GetLoopInQuote = channel.unary_unary( 50 | '/looprpc.SwapClient/GetLoopInQuote', 51 | request_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.QuoteRequest.SerializeToString, 52 | response_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.QuoteResponse.FromString, 53 | ) 54 | 55 | 56 | class SwapClientServicer(object): 57 | """* 58 | SwapClient is a service that handles the client side process of onchain/offchain 59 | swaps. The service is designed for a single client. 60 | """ 61 | 62 | def LoopOut(self, request, context): 63 | """* loop: `out` 64 | LoopOut initiates an loop out swap with the given parameters. The call 65 | returns after the swap has been set up with the swap server. From that 66 | point onwards, progress can be tracked via the SwapStatus stream that is 67 | returned from Monitor(). 68 | """ 69 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 70 | context.set_details('Method not implemented!') 71 | raise NotImplementedError('Method not implemented!') 72 | 73 | def LoopIn(self, request, context): 74 | """* 75 | LoopIn initiates a loop in swap with the given parameters. The call 76 | returns after the swap has been set up with the swap server. From that 77 | point onwards, progress can be tracked via the SwapStatus stream 78 | that is returned from Monitor(). 79 | """ 80 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 81 | context.set_details('Method not implemented!') 82 | raise NotImplementedError('Method not implemented!') 83 | 84 | def Monitor(self, request, context): 85 | """* loop: `monitor` 86 | Monitor will return a stream of swap updates for currently active swaps. 87 | TODO: add MonitorSync version for REST clients. 88 | """ 89 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 90 | context.set_details('Method not implemented!') 91 | raise NotImplementedError('Method not implemented!') 92 | 93 | def LoopOutTerms(self, request, context): 94 | """* loop: `terms` 95 | LoopOutTerms returns the terms that the server enforces for a loop out swap. 96 | """ 97 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 98 | context.set_details('Method not implemented!') 99 | raise NotImplementedError('Method not implemented!') 100 | 101 | def LoopOutQuote(self, request, context): 102 | """* loop: `quote` 103 | LoopOutQuote returns a quote for a loop out swap with the provided 104 | parameters. 105 | """ 106 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 107 | context.set_details('Method not implemented!') 108 | raise NotImplementedError('Method not implemented!') 109 | 110 | def GetLoopInTerms(self, request, context): 111 | """* 112 | GetTerms returns the terms that the server enforces for swaps. 113 | """ 114 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 115 | context.set_details('Method not implemented!') 116 | raise NotImplementedError('Method not implemented!') 117 | 118 | def GetLoopInQuote(self, request, context): 119 | """* 120 | GetQuote returns a quote for a swap with the provided parameters. 121 | """ 122 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 123 | context.set_details('Method not implemented!') 124 | raise NotImplementedError('Method not implemented!') 125 | 126 | 127 | def add_SwapClientServicer_to_server(servicer, server): 128 | rpc_method_handlers = { 129 | 'LoopOut': grpc.unary_unary_rpc_method_handler( 130 | servicer.LoopOut, 131 | request_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.LoopOutRequest.FromString, 132 | response_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.SwapResponse.SerializeToString, 133 | ), 134 | 'LoopIn': grpc.unary_unary_rpc_method_handler( 135 | servicer.LoopIn, 136 | request_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.LoopInRequest.FromString, 137 | response_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.SwapResponse.SerializeToString, 138 | ), 139 | 'Monitor': grpc.unary_stream_rpc_method_handler( 140 | servicer.Monitor, 141 | request_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.MonitorRequest.FromString, 142 | response_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.SwapStatus.SerializeToString, 143 | ), 144 | 'LoopOutTerms': grpc.unary_unary_rpc_method_handler( 145 | servicer.LoopOutTerms, 146 | request_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.TermsRequest.FromString, 147 | response_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.TermsResponse.SerializeToString, 148 | ), 149 | 'LoopOutQuote': grpc.unary_unary_rpc_method_handler( 150 | servicer.LoopOutQuote, 151 | request_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.QuoteRequest.FromString, 152 | response_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.QuoteResponse.SerializeToString, 153 | ), 154 | 'GetLoopInTerms': grpc.unary_unary_rpc_method_handler( 155 | servicer.GetLoopInTerms, 156 | request_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.TermsRequest.FromString, 157 | response_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.TermsResponse.SerializeToString, 158 | ), 159 | 'GetLoopInQuote': grpc.unary_unary_rpc_method_handler( 160 | servicer.GetLoopInQuote, 161 | request_deserializer=loop__rpc_dot_protos_dot_loop__client__pb2.QuoteRequest.FromString, 162 | response_serializer=loop__rpc_dot_protos_dot_loop__client__pb2.QuoteResponse.SerializeToString, 163 | ), 164 | } 165 | generic_handler = grpc.method_handlers_generic_handler( 166 | 'looprpc.SwapClient', rpc_method_handlers) 167 | server.add_generic_rpc_handlers((generic_handler,)) 168 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | googleapis-common-protos>=1.5.8 2 | grpcio>=1.19.0 3 | grpcio-tools>=1.19.0 4 | protobuf>=3.7.0 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="lnd_grpc", 8 | version="0.4.0", 9 | author="Will Clark", 10 | author_email="will8clark@gmail.com", 11 | description="An LND gRPC client for Python 3.6", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/willcl-ark/lnd_grpc", 15 | packages=setuptools.find_packages(exclude=["googleapis", "misc"]), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3.6", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | keywords="lnd grpc", 22 | install_requires=["grpcio", "grpcio-tools", "googleapis-common-protos"], 23 | python_requires=">=3.6", 24 | ) 25 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==5.1.3 2 | psutil==5.6.3 3 | requests==2.22.0 4 | pytest-json==0.4.0 5 | cheroot==7.0.0 6 | googleapis-common-protos>=1.5.8 7 | grpcio>=1.19.0 8 | grpcio-tools>=1.19.0 9 | protobuf>=3.7.0 10 | pytest-rerunfailures==7.0 11 | pytest-timeout==1.3.3 12 | flask==1.1.1 13 | ephemeral-port-reserve==1.1.1 14 | pytest-xdist==1.29.0 15 | git+https://github.com/petertodd/python-bitcoinlib@master#egg=python-bitcoinlib 16 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | # This function is based upon the example of how to 5 | # "[make] test result information available in fixtures" at: 6 | # https://pytest.org/latest/example/simple.html#making-test-result-information-available-in-fixtures 7 | # and: 8 | # https://github.com/pytest-dev/pytest/issues/288 9 | @pytest.hookimpl(tryfirst=True, hookwrapper=True) 10 | def pytest_runtest_makereport(item, call): 11 | # execute all other hooks to obtain the report object 12 | outcome = yield 13 | rep = outcome.get_result() 14 | 15 | # set a report attribute for each phase of a call, which can 16 | # be "setup", "call", "teardown" 17 | 18 | setattr(item, "rep_" + rep.when, rep) 19 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore::UserWarning 4 | ignore::ResourceWarning 5 | -------------------------------------------------------------------------------- /tests/test.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | ### Setup 3 | Tests are heavily influenced in structure from Christian Decker's lightning-integration test framework: https://github.com/cdecker/lightning-integration 4 | 5 | Recommendation is to run the tests from within a virtualenv. From the parent directory be sure to run the following to install dependencies: 6 | 7 | `pip3 install -r test-requirements.txt` 8 | 9 | lnd v-0.6-beta and bitcoind >0.17 must be installed and be available on the user's PATH. To test availability, in a terminal issue 'which lnd' and `which bitcoind` and ensure that it returns the path to your lnd/bitcoind binary. 10 | 11 | The tests rely on py.test to create fixtures, wire them into the tests and run the tests themselves. Execute all tests by running the following command in this directory: 12 | 13 | `py.test -v test.py` 14 | 15 | To make the tests (extremely) verbose in case of failure, you can run with the following command: 16 | 17 | bash: `TEST_DEBUG=1 py.test -v test.py -s` 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import threading 4 | import queue 5 | from hashlib import sha256 6 | from secrets import token_bytes 7 | 8 | import grpc 9 | 10 | from lnd_grpc.protos import invoices_pb2 as invoices_pb2, rpc_pb2 11 | from loop_rpc.protos import loop_client_pb2 12 | from test_utils.fixtures import * 13 | from test_utils.lnd import LndNode 14 | 15 | impls = [LndNode] 16 | 17 | if TEST_DEBUG: 18 | logging.basicConfig( 19 | level=logging.DEBUG, format="%(name)-12s %(message)s", stream=sys.stdout 20 | ) 21 | logging.info("Tests running in '%s'", TEST_DIR) 22 | 23 | FUND_AMT = 10 ** 7 24 | SEND_AMT = 10 ** 3 25 | 26 | 27 | def get_updates(_queue): 28 | """ 29 | Get all available updates from a queue.Queue() instance and return them as a list 30 | """ 31 | _list = [] 32 | while not _queue.empty(): 33 | _list.append(_queue.get()) 34 | return _list 35 | 36 | 37 | def transact_and_mine(btc): 38 | """ 39 | Generate some transactions and blocks. 40 | To make bitcoind's `estimatesmartfee` succeeded. 41 | """ 42 | addr = btc.rpc.getnewaddress("", "bech32") 43 | for i in range(10): 44 | for j in range(10): 45 | txid = btc.rpc.sendtoaddress(addr, 0.5) 46 | btc.rpc.generatetoaddress(1, addr) 47 | 48 | 49 | def wait_for(success, timeout=30, interval=0.25): 50 | start_time = time.time() 51 | while not success() and time.time() < start_time + timeout: 52 | time.sleep(interval) 53 | if time.time() > start_time + timeout: 54 | raise ValueError("Error waiting for {}", success) 55 | 56 | 57 | def wait_for_bool(success, timeout=30, interval=0.25): 58 | start_time = time.time() 59 | while not success and time.time() < start_time + timeout: 60 | time.sleep(interval) 61 | if time.time() > start_time + timeout: 62 | raise ValueError("Error waiting for {}", success) 63 | 64 | 65 | def sync_blockheight(btc, nodes): 66 | """ 67 | Sync blockheight of nodes by checking logs until timeout 68 | """ 69 | info = btc.rpc.getblockchaininfo() 70 | blocks = info["blocks"] 71 | 72 | for n in nodes: 73 | wait_for(lambda: n.get_info().block_height == blocks, interval=1) 74 | time.sleep(0.25) 75 | 76 | 77 | def generate_until(btc, success, blocks=30, interval=1): 78 | """ 79 | Generate new blocks until `success` returns true. 80 | 81 | Mainly used to wait for transactions to confirm since they might 82 | be delayed and we don't want to add a long waiting time to all 83 | tests just because some are slow. 84 | """ 85 | addr = btc.rpc.getnewaddress("", "bech32") 86 | for i in range(blocks): 87 | time.sleep(interval) 88 | if success(): 89 | return 90 | generate(bitcoind, 1) 91 | time.sleep(interval) 92 | if not success(): 93 | raise ValueError("Generated %d blocks, but still no success", blocks) 94 | 95 | 96 | def gen_and_sync_lnd(bitcoind, nodes): 97 | """ 98 | generate a few blocks and wait for lnd nodes to sync 99 | """ 100 | generate(bitcoind, 3) 101 | sync_blockheight(bitcoind, nodes=nodes) 102 | for node in nodes: 103 | wait_for(lambda: node.get_info().synced_to_chain, interval=0.25) 104 | time.sleep(0.25) 105 | 106 | 107 | def generate(bitcoind, blocks): 108 | addr = bitcoind.rpc.getnewaddress("", "bech32") 109 | bitcoind.rpc.generatetoaddress(blocks, addr) 110 | 111 | 112 | def close_all_channels(bitcoind, nodes): 113 | """ 114 | Recursively close each channel for each node in the list of nodes passed in and assert 115 | """ 116 | gen_and_sync_lnd(bitcoind, nodes) 117 | for node in nodes: 118 | for channel in node.list_channels(): 119 | channel_point = channel.channel_point 120 | node.close_channel(channel_point=channel_point).__next__() 121 | gen_and_sync_lnd(bitcoind, nodes) 122 | assert not node.list_channels() 123 | gen_and_sync_lnd(bitcoind, nodes) 124 | 125 | 126 | def disconnect_all_peers(bitcoind, nodes): 127 | """ 128 | Recursively disconnect each peer from each node in the list of nodes passed in and assert 129 | """ 130 | gen_and_sync_lnd(bitcoind, nodes) 131 | for node in nodes: 132 | peers = [p.pub_key for p in node.list_peers()] 133 | for peer in peers: 134 | node.disconnect_peer(pub_key=peer) 135 | wait_for(lambda: peer not in node.list_peers(), timeout=5) 136 | assert peer not in [p.pub_key for p in node.list_peers()] 137 | gen_and_sync_lnd(bitcoind, nodes) 138 | 139 | 140 | def get_addresses(node, response="str"): 141 | p2wkh_address = node.new_address(address_type="p2wkh") 142 | np2wkh_address = node.new_address(address_type="np2wkh") 143 | if response == "str": 144 | return p2wkh_address.address, np2wkh_address.address 145 | return p2wkh_address, np2wkh_address 146 | 147 | 148 | def setup_nodes(bitcoind, nodes, delay=0): 149 | """ 150 | Break down all nodes, open fresh channels between them with half the balance pushed remotely 151 | and assert 152 | :return: the setup nodes 153 | """ 154 | # Needed by lnd in order to have at least one block in the last 2 hours 155 | generate(bitcoind, 1) 156 | 157 | # First break down nodes. This avoids situations where a test fails and breakdown is not called 158 | break_down_nodes(bitcoind, nodes, delay) 159 | 160 | # setup requested nodes and create a single channel from one to the next 161 | # capacity in one direction only (alphabetical) 162 | setup_channels(bitcoind, nodes, delay) 163 | return nodes 164 | 165 | 166 | def setup_channels(bitcoind, nodes, delay): 167 | for i, node in enumerate(nodes): 168 | if i + 1 == len(nodes): 169 | break 170 | nodes[i].connect( 171 | str(nodes[i + 1].id() + "@localhost:" + str(nodes[i + 1].daemon.port)), 172 | perm=1, 173 | ) 174 | wait_for(lambda: nodes[i].list_peers(), interval=0.25) 175 | wait_for(lambda: nodes[i + 1].list_peers(), interval=0.25) 176 | time.sleep(delay) 177 | 178 | nodes[i].add_funds(bitcoind, 1) 179 | gen_and_sync_lnd(bitcoind, [nodes[i], nodes[i + 1]]) 180 | nodes[i].open_channel_sync( 181 | node_pubkey_string=nodes[i + 1].id(), 182 | local_funding_amount=FUND_AMT, 183 | push_sat=int(FUND_AMT / 2), 184 | spend_unconfirmed=True, 185 | ) 186 | time.sleep(delay) 187 | generate(bitcoind, 3) 188 | gen_and_sync_lnd(bitcoind, [nodes[i], nodes[i + 1]]) 189 | 190 | assert confirm_channel(bitcoind, nodes[i], nodes[i + 1]) 191 | 192 | 193 | def break_down_nodes(bitcoind, nodes, delay=0): 194 | close_all_channels(bitcoind, nodes) 195 | time.sleep(delay) 196 | disconnect_all_peers(bitcoind, nodes) 197 | time.sleep(delay) 198 | 199 | 200 | def confirm_channel(bitcoind, n1, n2): 201 | """ 202 | Confirm that a channel is open between two nodes 203 | """ 204 | assert n1.id() in [p.pub_key for p in n2.list_peers()] 205 | assert n2.id() in [p.pub_key for p in n1.list_peers()] 206 | for i in range(10): 207 | time.sleep(0.5) 208 | if n1.check_channel(n2) and n2.check_channel(n1): 209 | return True 210 | addr = bitcoind.rpc.getnewaddress("", "bech32") 211 | bhash = bitcoind.rpc.generatetoaddress(1, addr)[0] 212 | n1.block_sync(bhash) 213 | n2.block_sync(bhash) 214 | 215 | # Last ditch attempt 216 | return n1.check_channel(n2) and n2.check_channel(n1) 217 | 218 | 219 | # def idfn(impls): 220 | # """ 221 | # Not used currently 222 | # """ 223 | # return "_".join([i.displayName for i in impls]) 224 | 225 | 226 | def wipe_channels_from_disk(node, network="regtest"): 227 | """ 228 | used to test channel backups 229 | """ 230 | _channel_backup = node.lnd_dir + f"chain/bitcoin/{network}/channel.backup" 231 | _channel_db = node.lnd_dir + f"graph/{network}/channel.db" 232 | assert os.path.exists(_channel_backup) 233 | assert os.path.exists(_channel_db) 234 | os.remove(_channel_backup) 235 | os.remove(_channel_db) 236 | assert not os.path.exists(_channel_backup) 237 | assert not os.path.exists(_channel_db) 238 | 239 | 240 | def random_32_byte_hash(): 241 | """ 242 | Can generate an invoice preimage and corresponding payment hash 243 | :return: 32 byte sha256 hash digest, 32 byte preimage 244 | """ 245 | preimage = token_bytes(32) 246 | _hash = sha256(preimage) 247 | return _hash.digest(), preimage 248 | 249 | 250 | ######### 251 | # Tests # 252 | ######### 253 | 254 | 255 | class TestNonInteractiveLightning: 256 | """ 257 | Non-interactive tests will share a common lnd instance because test passes/failures will not 258 | impact future tests. 259 | """ 260 | 261 | def test_start(self, bitcoind, alice): 262 | assert alice.get_info() 263 | sync_blockheight(bitcoind, [alice]) 264 | 265 | def test_wallet_balance(self, alice): 266 | gen_and_sync_lnd(alice.bitcoin, [alice]) 267 | assert isinstance(alice.get_info(), rpc_pb2.GetInfoResponse) 268 | pytest.raises(TypeError, alice.wallet_balance, "please") 269 | 270 | def test_channel_balance(self, alice): 271 | gen_and_sync_lnd(alice.bitcoin, [alice]) 272 | assert isinstance(alice.channel_balance(), rpc_pb2.ChannelBalanceResponse) 273 | pytest.raises(TypeError, alice.channel_balance, "please") 274 | 275 | def test_get_transactions(self, alice): 276 | gen_and_sync_lnd(alice.bitcoin, [alice]) 277 | assert isinstance(alice.get_transactions(), rpc_pb2.TransactionDetails) 278 | pytest.raises(TypeError, alice.get_transactions, "please") 279 | 280 | def test_send_coins(self, alice): 281 | gen_and_sync_lnd(alice.bitcoin, [alice]) 282 | alice.add_funds(alice.bitcoin, 1) 283 | p2wkh_address, np2wkh_address = get_addresses(alice) 284 | 285 | # test passes 286 | send1 = alice.send_coins(addr=p2wkh_address, amount=100000) 287 | generate(alice.bitcoin, 1) 288 | time.sleep(0.5) 289 | send2 = alice.send_coins(addr=np2wkh_address, amount=100000) 290 | 291 | assert isinstance(send1, rpc_pb2.SendCoinsResponse) 292 | assert isinstance(send2, rpc_pb2.SendCoinsResponse) 293 | 294 | # test failures 295 | pytest.raises( 296 | grpc.RpcError, 297 | lambda: alice.send_coins( 298 | alice.new_address(address_type="p2wkh").address, amount=100000 * -1 299 | ), 300 | ) 301 | pytest.raises( 302 | grpc.RpcError, 303 | lambda: alice.send_coins( 304 | alice.new_address(address_type="p2wkh").address, amount=1000000000000000 305 | ), 306 | ) 307 | 308 | def test_send_many(self, alice): 309 | gen_and_sync_lnd(alice.bitcoin, [alice]) 310 | alice.add_funds(alice.bitcoin, 1) 311 | p2wkh_address, np2wkh_address = get_addresses(alice) 312 | send_dict = {p2wkh_address: 100000, np2wkh_address: 100000} 313 | 314 | send = alice.send_many(addr_to_amount=send_dict) 315 | alice.bitcoin.rpc.generatetoaddress(1, p2wkh_address) 316 | time.sleep(0.5) 317 | assert isinstance(send, rpc_pb2.SendManyResponse) 318 | 319 | def test_list_unspent(self, alice): 320 | gen_and_sync_lnd(alice.bitcoin, [alice]) 321 | alice.add_funds(alice.bitcoin, 1) 322 | assert isinstance(alice.list_unspent(0, 1000), rpc_pb2.ListUnspentResponse) 323 | 324 | def test_subscribe_transactions(self, alice): 325 | gen_and_sync_lnd(alice.bitcoin, [alice]) 326 | subscription = alice.subscribe_transactions() 327 | alice.add_funds(alice.bitcoin, 1) 328 | assert isinstance(subscription, grpc._channel._Rendezvous) 329 | assert isinstance(subscription.__next__(), rpc_pb2.Transaction) 330 | 331 | # gen_and_sync_lnd(alice.bitcoin, [alice]) 332 | # transaction_updates = queue.LifoQueue() 333 | # 334 | # def sub_transactions(): 335 | # try: 336 | # for response in alice.subscribe_transactions(): 337 | # transaction_updates.put(response) 338 | # except StopIteration: 339 | # pass 340 | # 341 | # alice_sub = threading.Thread(target=sub_transactions(), daemon=True) 342 | # alice_sub.start() 343 | # time.sleep(1) 344 | # while not alice_sub.is_alive(): 345 | # time.sleep(0.1) 346 | # alice.add_funds(alice.bitcoin, 1) 347 | # 348 | # assert any(isinstance(update) == rpc_pb2.Transaction for update in get_updates(transaction_updates)) 349 | 350 | def test_new_address(self, alice): 351 | gen_and_sync_lnd(alice.bitcoin, [alice]) 352 | p2wkh_address, np2wkh_address = get_addresses(alice, "response") 353 | assert isinstance(p2wkh_address, rpc_pb2.NewAddressResponse) 354 | assert isinstance(np2wkh_address, rpc_pb2.NewAddressResponse) 355 | 356 | def test_sign_verify_message(self, alice): 357 | gen_and_sync_lnd(alice.bitcoin, [alice]) 358 | message = "Test message to sign and verify." 359 | signature = alice.sign_message(message) 360 | assert isinstance(signature, rpc_pb2.SignMessageResponse) 361 | verified_message = alice.verify_message(message, signature.signature) 362 | assert isinstance(verified_message, rpc_pb2.VerifyMessageResponse) 363 | 364 | def test_get_info(self, alice): 365 | gen_and_sync_lnd(alice.bitcoin, [alice]) 366 | assert isinstance(alice.get_info(), rpc_pb2.GetInfoResponse) 367 | 368 | def test_pending_channels(self, alice): 369 | gen_and_sync_lnd(alice.bitcoin, [alice]) 370 | assert isinstance(alice.pending_channels(), rpc_pb2.PendingChannelsResponse) 371 | 372 | # Skipping list_channels and closed_channels as we don't return their responses directly 373 | 374 | def test_add_invoice(self, alice): 375 | gen_and_sync_lnd(alice.bitcoin, [alice]) 376 | invoice = alice.add_invoice(value=SEND_AMT) 377 | assert isinstance(invoice, rpc_pb2.AddInvoiceResponse) 378 | 379 | def test_list_invoices(self, alice): 380 | gen_and_sync_lnd(alice.bitcoin, [alice]) 381 | assert isinstance(alice.list_invoices(), rpc_pb2.ListInvoiceResponse) 382 | 383 | def test_lookup_invoice(self, alice): 384 | gen_and_sync_lnd(alice.bitcoin, [alice]) 385 | payment_hash = alice.add_invoice(value=SEND_AMT).r_hash 386 | assert isinstance(alice.lookup_invoice(r_hash=payment_hash), rpc_pb2.Invoice) 387 | 388 | def test_subscribe_invoices(self, alice): 389 | """ 390 | Invoice subscription run as a thread 391 | """ 392 | gen_and_sync_lnd(alice.bitcoin, [alice]) 393 | invoice_updates = queue.LifoQueue() 394 | 395 | def sub_invoices(): 396 | try: 397 | for response in alice.subscribe_invoices(): 398 | invoice_updates.put(response) 399 | except grpc._channel._Rendezvous: 400 | pass 401 | 402 | alice_sub = threading.Thread(target=sub_invoices, daemon=True) 403 | alice_sub.start() 404 | time.sleep(1) 405 | while not alice_sub.is_alive(): 406 | time.sleep(0.1) 407 | alice.add_invoice(value=SEND_AMT) 408 | alice.daemon.wait_for_log("AddIndex") 409 | time.sleep(0.1) 410 | 411 | assert any( 412 | isinstance(update, rpc_pb2.Invoice) 413 | for update in get_updates(invoice_updates) 414 | ) 415 | 416 | def test_decode_payment_request(self, alice): 417 | gen_and_sync_lnd(alice.bitcoin, [alice]) 418 | pay_req = alice.add_invoice(value=SEND_AMT).payment_request 419 | decoded_req = alice.decode_pay_req(pay_req=pay_req) 420 | assert isinstance(decoded_req, rpc_pb2.PayReq) 421 | 422 | def test_list_payments(self, alice): 423 | gen_and_sync_lnd(alice.bitcoin, [alice]) 424 | assert isinstance(alice.list_payments(), rpc_pb2.ListPaymentsResponse) 425 | 426 | def test_delete_all_payments(self, alice): 427 | gen_and_sync_lnd(alice.bitcoin, [alice]) 428 | assert isinstance( 429 | alice.delete_all_payments(), rpc_pb2.DeleteAllPaymentsResponse 430 | ) 431 | 432 | def test_describe_graph(self, alice): 433 | gen_and_sync_lnd(alice.bitcoin, [alice]) 434 | assert isinstance(alice.describe_graph(), rpc_pb2.ChannelGraph) 435 | 436 | # Skipping get_chan_info, subscribe_chan_events, get_alice_info, query_routes 437 | 438 | def test_get_network_info(self, alice): 439 | gen_and_sync_lnd(alice.bitcoin, [alice]) 440 | assert isinstance(alice.get_network_info(), rpc_pb2.NetworkInfo) 441 | 442 | @pytest.mark.skipif( 443 | TRAVIS is True, 444 | reason="Travis doesn't like this one. Possibly a race" 445 | "condition not worth debugging", 446 | ) 447 | def test_stop_daemon(self, node_factory): 448 | node = node_factory.get_node(implementation=LndNode, node_id="test_stop_node") 449 | node.daemon.wait_for_log("Server listening on") 450 | node.stop_daemon() 451 | # use is_in_log instead of wait_for_log as node daemon should be shutdown 452 | node.daemon.is_in_log("Shutdown complete") 453 | time.sleep(1) 454 | with pytest.raises(grpc.RpcError): 455 | node.get_info() 456 | 457 | def test_debug_level(self, alice): 458 | gen_and_sync_lnd(alice.bitcoin, [alice]) 459 | assert isinstance( 460 | alice.debug_level(level_spec="warn"), rpc_pb2.DebugLevelResponse 461 | ) 462 | 463 | def test_fee_report(self, alice): 464 | gen_and_sync_lnd(alice.bitcoin, [alice]) 465 | assert isinstance(alice.fee_report(), rpc_pb2.FeeReportResponse) 466 | 467 | def test_forwarding_history(self, alice): 468 | gen_and_sync_lnd(alice.bitcoin, [alice]) 469 | assert isinstance(alice.forwarding_history(), rpc_pb2.ForwardingHistoryResponse) 470 | 471 | def test_lightning_stub(self, alice): 472 | gen_and_sync_lnd(alice.bitcoin, [alice]) 473 | original_stub = alice.lightning_stub 474 | # not simulation of actual failure, but failure in the form that should be detected by 475 | # connectivity event logger 476 | alice.connection_status_change = True 477 | # make a call to stimulate stub regeneration 478 | alice.get_info() 479 | new_stub = alice.lightning_stub 480 | assert original_stub != new_stub 481 | 482 | 483 | class TestInteractiveLightning: 484 | def test_peer_connection(self, bob, carol, dave, bitcoind): 485 | # Needed by lnd in order to have at least one block in the last 2 hours 486 | generate(bitcoind, 1) 487 | 488 | # connection tests 489 | connection1 = bob.connect( 490 | str(carol.id() + "@localhost:" + str(carol.daemon.port)) 491 | ) 492 | 493 | wait_for(lambda: bob.list_peers(), timeout=5) 494 | wait_for(lambda: carol.list_peers(), timeout=5) 495 | 496 | # check bob connected to carol using connect() and list_peers() 497 | assert isinstance(connection1, rpc_pb2.ConnectPeerResponse) 498 | assert bob.id() in [p.pub_key for p in carol.list_peers()] 499 | assert carol.id() in [p.pub_key for p in bob.list_peers()] 500 | 501 | dave_ln_addr = dave.lightning_address( 502 | pubkey=dave.id(), host="localhost:" + str(dave.daemon.port) 503 | ) 504 | carol.connect_peer(dave_ln_addr) 505 | 506 | wait_for(lambda: carol.list_peers(), timeout=5) 507 | wait_for(lambda: dave.list_peers(), timeout=5) 508 | 509 | # check carol connected to dave using connect() and list_peers() 510 | assert carol.id() in [p.pub_key for p in dave.list_peers()] 511 | assert dave.id() in [p.pub_key for p in carol.list_peers()] 512 | 513 | generate(bob.bitcoin, 1) 514 | gen_and_sync_lnd(bitcoind, [bob, carol]) 515 | 516 | # Disconnection tests 517 | bob.disconnect_peer(pub_key=str(carol.id())) 518 | 519 | time.sleep(0.25) 520 | 521 | # check bob not connected to carol using connect() and list_peers() 522 | assert bob.id() not in [p.pub_key for p in carol.list_peers()] 523 | assert carol.id() not in [p.pub_key for p in bob.list_peers()] 524 | 525 | carol.disconnect_peer(dave.id()) 526 | 527 | wait_for(lambda: not carol.list_peers(), timeout=5) 528 | wait_for(lambda: not dave.list_peers(), timeout=5) 529 | 530 | # check carol not connected to dave using connect_peer() and list_peers() 531 | assert carol.id() not in [p.pub_key for p in dave.list_peers()] 532 | assert dave.id() not in [p.pub_key for p in carol.list_peers()] 533 | 534 | def test_open_channel_sync(self, bob, carol, bitcoind): 535 | # Needed by lnd in order to have at least one block in the last 2 hours 536 | generate(bitcoind, 1) 537 | disconnect_all_peers(bitcoind, [bob, carol]) 538 | 539 | bob.connect(str(carol.id() + "@localhost:" + str(carol.daemon.port)), perm=1) 540 | 541 | wait_for(lambda: bob.list_peers(), interval=1) 542 | wait_for(lambda: carol.list_peers(), interval=1) 543 | 544 | bob.add_funds(bitcoind, 1) 545 | gen_and_sync_lnd(bitcoind, [bob, carol]) 546 | bob.open_channel_sync( 547 | node_pubkey_string=carol.id(), local_funding_amount=FUND_AMT 548 | ) 549 | gen_and_sync_lnd(bitcoind, [bob, carol]) 550 | 551 | assert confirm_channel(bitcoind, bob, carol) 552 | 553 | assert bob.check_channel(carol) 554 | assert carol.check_channel(bob) 555 | 556 | def test_open_channel(self, bob, carol, bitcoind): 557 | # Needed by lnd in order to have at least one block in the last 2 hours 558 | generate(bitcoind, 1) 559 | break_down_nodes(bitcoind, nodes=[bob, carol]) 560 | 561 | bob.connect(str(carol.id() + "@localhost:" + str(carol.daemon.port)), perm=1) 562 | 563 | wait_for(lambda: bob.list_peers(), interval=0.5) 564 | wait_for(lambda: carol.list_peers(), interval=0.5) 565 | 566 | bob.add_funds(bitcoind, 1) 567 | gen_and_sync_lnd(bitcoind, [bob, carol]) 568 | bob.open_channel( 569 | node_pubkey_string=carol.id(), local_funding_amount=FUND_AMT 570 | ).__next__() 571 | generate(bitcoind, 3) 572 | gen_and_sync_lnd(bitcoind, [bob, carol]) 573 | 574 | assert confirm_channel(bitcoind, bob, carol) 575 | 576 | assert bob.check_channel(carol) 577 | assert carol.check_channel(bob) 578 | 579 | def test_close_channel(self, bob, carol, bitcoind): 580 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 581 | 582 | channel_point = bob.list_channels()[0].channel_point 583 | bob.close_channel(channel_point=channel_point).__next__() 584 | generate(bitcoind, 6) 585 | gen_and_sync_lnd(bitcoind, [bob, carol]) 586 | 587 | assert bob.check_channel(carol) is False 588 | assert carol.check_channel(bob) is False 589 | 590 | def test_send_payment_sync(self, bitcoind, bob, carol): 591 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 592 | 593 | # test payment request method 594 | invoice = carol.add_invoice(value=SEND_AMT) 595 | bob.send_payment_sync(payment_request=invoice.payment_request) 596 | generate(bitcoind, 3) 597 | gen_and_sync_lnd(bitcoind, [bob, carol]) 598 | 599 | payment_hash = carol.decode_pay_req(invoice.payment_request).payment_hash 600 | assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] 601 | assert carol.lookup_invoice(r_hash_str=payment_hash).settled is True 602 | 603 | # test manually specified request 604 | invoice2 = carol.add_invoice(value=SEND_AMT) 605 | bob.send_payment_sync( 606 | dest_string=carol.id(), 607 | amt=SEND_AMT, 608 | payment_hash=invoice2.r_hash, 609 | final_cltv_delta=144, 610 | ) 611 | generate(bitcoind, 3) 612 | gen_and_sync_lnd(bitcoind, [bob, carol]) 613 | 614 | payment_hash2 = carol.decode_pay_req(invoice2.payment_request).payment_hash 615 | assert payment_hash2 in [p.payment_hash for p in bob.list_payments().payments] 616 | assert carol.lookup_invoice(r_hash_str=payment_hash).settled is True 617 | 618 | # test sending any amount to an invoice which requested 0 619 | invoice3 = carol.add_invoice(value=0) 620 | bob.send_payment_sync(payment_request=invoice3.payment_request, amt=SEND_AMT) 621 | generate(bitcoind, 3) 622 | gen_and_sync_lnd(bitcoind, [bob, carol]) 623 | 624 | payment_hash = carol.decode_pay_req(invoice3.payment_request).payment_hash 625 | assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] 626 | inv_paid = carol.lookup_invoice(r_hash_str=payment_hash) 627 | assert inv_paid.settled is True 628 | assert inv_paid.amt_paid_sat == SEND_AMT 629 | 630 | def test_send_payment(self, bitcoind, bob, carol): 631 | # TODO: remove try/except hack for curve generation 632 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 633 | 634 | # test payment request method 635 | invoice = carol.add_invoice(value=SEND_AMT) 636 | try: 637 | bob.send_payment(payment_request=invoice.payment_request).__next__() 638 | except StopIteration: 639 | pass 640 | bob.daemon.wait_for_log("Closed completed SETTLE circuit", timeout=60) 641 | generate(bitcoind, 3) 642 | gen_and_sync_lnd(bitcoind, [bob, carol]) 643 | 644 | payment_hash = carol.decode_pay_req(invoice.payment_request).payment_hash 645 | assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] 646 | assert carol.lookup_invoice(r_hash_str=payment_hash).settled is True 647 | 648 | # test manually specified request 649 | invoice2 = carol.add_invoice(value=SEND_AMT) 650 | try: 651 | bob.send_payment( 652 | dest_string=carol.id(), 653 | amt=SEND_AMT, 654 | payment_hash=invoice2.r_hash, 655 | final_cltv_delta=144, 656 | ).__next__() 657 | except StopIteration: 658 | pass 659 | bob.daemon.wait_for_log("Closed completed SETTLE circuit", timeout=60) 660 | generate(bitcoind, 3) 661 | gen_and_sync_lnd(bitcoind, [bob, carol]) 662 | 663 | payment_hash2 = carol.decode_pay_req(invoice2.payment_request).payment_hash 664 | assert payment_hash2 in [p.payment_hash for p in bob.list_payments().payments] 665 | assert carol.lookup_invoice(r_hash_str=payment_hash).settled is True 666 | 667 | # test sending different amount to invoice where 0 is requested 668 | invoice = carol.add_invoice(value=0) 669 | try: 670 | bob.send_payment( 671 | payment_request=invoice.payment_request, amt=SEND_AMT 672 | ).__next__() 673 | except StopIteration: 674 | pass 675 | bob.daemon.wait_for_log("Closed completed SETTLE circuit", timeout=60) 676 | generate(bitcoind, 3) 677 | gen_and_sync_lnd(bitcoind, [bob, carol]) 678 | 679 | payment_hash = carol.decode_pay_req(invoice.payment_request).payment_hash 680 | assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] 681 | inv_paid = carol.lookup_invoice(r_hash_str=payment_hash) 682 | assert inv_paid.settled is True 683 | assert inv_paid.amt_paid_sat == SEND_AMT 684 | 685 | def test_send_to_route_sync(self, bitcoind, bob, carol, dave): 686 | bob, carol, dave = setup_nodes(bitcoind, [bob, carol, dave]) 687 | gen_and_sync_lnd(bitcoind, [bob, carol, dave]) 688 | invoice = dave.add_invoice(value=SEND_AMT) 689 | route = bob.query_routes(pub_key=dave.id(), amt=SEND_AMT, final_cltv_delta=144) 690 | bob.send_to_route_sync(payment_hash=invoice.r_hash, route=route[0]) 691 | generate(bitcoind, 3) 692 | gen_and_sync_lnd(bitcoind, [bob, carol, dave]) 693 | payment_hash = dave.decode_pay_req(invoice.payment_request).payment_hash 694 | 695 | assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] 696 | assert dave.lookup_invoice(r_hash_str=payment_hash).settled is True 697 | 698 | def test_send_to_route(self, bitcoind, bob, carol, dave): 699 | bob, carol, dave = setup_nodes(bitcoind, [bob, carol, dave]) 700 | gen_and_sync_lnd(bitcoind, [bob, carol, dave]) 701 | invoice = dave.add_invoice(value=SEND_AMT) 702 | route = bob.query_routes(pub_key=dave.id(), amt=SEND_AMT, final_cltv_delta=144) 703 | try: 704 | bob.send_to_route(invoice=invoice, route=route[0]).__next__() 705 | except StopIteration: 706 | pass 707 | bob.daemon.wait_for_log("Closed completed SETTLE circuit", timeout=60) 708 | generate(bitcoind, 3) 709 | gen_and_sync_lnd(bitcoind, [bob, carol, dave]) 710 | payment_hash = dave.decode_pay_req(invoice.payment_request).payment_hash 711 | 712 | assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] 713 | assert dave.lookup_invoice(r_hash_str=payment_hash).settled is True 714 | 715 | def test_subscribe_channel_events(self, bitcoind, bob, carol): 716 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 717 | gen_and_sync_lnd(bitcoind, [bob, carol]) 718 | chan_updates = queue.LifoQueue() 719 | 720 | def sub_channel_events(): 721 | try: 722 | for response in bob.subscribe_channel_events(): 723 | chan_updates.put(response) 724 | except grpc._channel._Rendezvous: 725 | pass 726 | 727 | bob_sub = threading.Thread(target=sub_channel_events, daemon=True) 728 | bob_sub.start() 729 | time.sleep(1) 730 | while not bob_sub.is_alive(): 731 | time.sleep(0.1) 732 | channel_point = bob.list_channels()[0].channel_point 733 | 734 | bob.close_channel(channel_point=channel_point).__next__() 735 | generate(bitcoind, 3) 736 | gen_and_sync_lnd(bitcoind, [bob, carol]) 737 | assert any( 738 | update.closed_channel is not None for update in get_updates(chan_updates) 739 | ) 740 | 741 | def test_subscribe_channel_graph(self, bitcoind, bob, carol, dave): 742 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 743 | new_fee = 5555 744 | subscription = bob.subscribe_channel_graph() 745 | carol.update_channel_policy( 746 | chan_point=None, 747 | base_fee_msat=new_fee, 748 | fee_rate=0.5555, 749 | time_lock_delta=9, 750 | is_global=True, 751 | ) 752 | 753 | assert isinstance(subscription.__next__(), rpc_pb2.GraphTopologyUpdate) 754 | 755 | def test_update_channel_policy(self, bitcoind, bob, carol): 756 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 757 | update = bob.update_channel_policy( 758 | chan_point=None, 759 | base_fee_msat=5555, 760 | fee_rate=0.5555, 761 | time_lock_delta=9, 762 | is_global=True, 763 | ) 764 | assert isinstance(update, rpc_pb2.PolicyUpdateResponse) 765 | 766 | 767 | class TestChannelBackup: 768 | def test_export_verify_restore_multi(self, bitcoind, bob, carol): 769 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 770 | funding_txid, output_index = bob.list_channels()[0].channel_point.split(":") 771 | channel_point = bob.channel_point_generator( 772 | funding_txid=funding_txid, output_index=output_index 773 | ) 774 | 775 | all_backup = bob.export_all_channel_backups() 776 | assert isinstance(all_backup, rpc_pb2.ChanBackupSnapshot) 777 | # assert the multi_chan backup 778 | assert bob.verify_chan_backup(multi_chan_backup=all_backup.multi_chan_backup) 779 | 780 | bob.stop() 781 | wipe_channels_from_disk(bob) 782 | bob.start() 783 | 784 | assert not bob.list_channels() 785 | assert bob.restore_chan_backup( 786 | multi_chan_backup=all_backup.multi_chan_backup.multi_chan_backup 787 | ) 788 | 789 | bob.daemon.wait_for_log("Inserting 1 SCB channel shells into DB") 790 | carol.daemon.wait_for_log("Broadcasting force close transaction") 791 | generate(bitcoind, 6) 792 | bob.daemon.wait_for_log("Publishing sweep tx", timeout=120) 793 | generate(bitcoind, 6) 794 | assert bob.daemon.wait_for_log( 795 | "a contract has been fully resolved!", timeout=120 796 | ) 797 | 798 | def test_export_verify_restore_single(self, bitcoind, bob, carol): 799 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 800 | funding_txid, output_index = bob.list_channels()[0].channel_point.split(":") 801 | channel_point = bob.channel_point_generator( 802 | funding_txid=funding_txid, output_index=output_index 803 | ) 804 | 805 | single_backup = bob.export_chan_backup(chan_point=channel_point) 806 | assert isinstance(single_backup, rpc_pb2.ChannelBackup) 807 | packed_backup = bob.pack_into_channelbackups(single_backup=single_backup) 808 | # assert the single_chan_backup 809 | assert bob.verify_chan_backup(single_chan_backups=packed_backup) 810 | 811 | bob.stop() 812 | wipe_channels_from_disk(bob) 813 | bob.start() 814 | 815 | assert not bob.list_channels() 816 | assert bob.restore_chan_backup(chan_backups=packed_backup) 817 | 818 | bob.daemon.wait_for_log("Inserting 1 SCB channel shells into DB") 819 | carol.daemon.wait_for_log("Broadcasting force close transaction") 820 | generate(bitcoind, 6) 821 | bob.daemon.wait_for_log("Publishing sweep tx", timeout=120) 822 | generate(bitcoind, 6) 823 | assert bob.daemon.wait_for_log( 824 | "a contract has been fully resolved!", timeout=120 825 | ) 826 | 827 | 828 | class TestInvoices: 829 | def test_all_invoice(self, bitcoind, bob, carol): 830 | bob, carol = setup_nodes(bitcoind, [bob, carol]) 831 | _hash, preimage = random_32_byte_hash() 832 | invoice_queue = queue.LifoQueue() 833 | invoice = carol.add_hold_invoice( 834 | memo="pytest hold invoice", hash=_hash, value=SEND_AMT 835 | ) 836 | decoded_invoice = carol.decode_pay_req(pay_req=invoice.payment_request) 837 | assert isinstance(invoice, invoices_pb2.AddHoldInvoiceResp) 838 | 839 | # thread functions 840 | def inv_sub_worker(_hash): 841 | try: 842 | for _response in carol.subscribe_single_invoice(_hash): 843 | invoice_queue.put(_response) 844 | except grpc._channel._Rendezvous: 845 | pass 846 | 847 | def pay_hold_inv_worker(payment_request): 848 | try: 849 | bob.pay_invoice(payment_request=payment_request) 850 | except grpc._channel._Rendezvous: 851 | pass 852 | 853 | def settle_inv_worker(_preimage): 854 | try: 855 | carol.settle_invoice(preimage=_preimage) 856 | except grpc._channel._Rendezvous: 857 | pass 858 | 859 | # setup the threads 860 | inv_sub = threading.Thread( 861 | target=inv_sub_worker, name="inv_sub", args=[_hash], daemon=True 862 | ) 863 | pay_inv = threading.Thread( 864 | target=pay_hold_inv_worker, args=[invoice.payment_request] 865 | ) 866 | settle_inv = threading.Thread(target=settle_inv_worker, args=[preimage]) 867 | 868 | # start the threads 869 | inv_sub.start() 870 | # wait for subscription to start 871 | while not inv_sub.is_alive(): 872 | time.sleep(0.1) 873 | pay_inv.start() 874 | time.sleep(2) 875 | # carol.daemon.wait_for_log(regex=f'Invoice({decoded_invoice.payment_hash}): accepted,') 876 | settle_inv.start() 877 | while settle_inv.is_alive(): 878 | time.sleep(0.1) 879 | inv_sub.join(timeout=1) 880 | 881 | assert any(invoice.settled is True for invoice in get_updates(invoice_queue)) 882 | 883 | 884 | class TestLoop: 885 | @pytest.mark.skip(reason="waiting to configure loop swapserver") 886 | def test_loop_out_quote(self, bitcoind, alice, bob, loopd): 887 | """ 888 | 250000 satoshis is currently middle of range of allowed loop amounts 889 | """ 890 | loop_amount = 250000 891 | alice, bob = setup_nodes(bitcoind, [alice, bob]) 892 | if alice.daemon.invoice_rpc_active: 893 | quote = loopd.loop_out_quote(amt=loop_amount) 894 | assert quote is not None 895 | assert isinstance(quote, loop_client_pb2.QuoteResponse) 896 | else: 897 | logging.info("test_loop_out() skipped as invoice RPC not detected") 898 | 899 | @pytest.mark.skip(reason="waiting to configure loop swapserver") 900 | def test_loop_out_terms(self, bitcoind, alice, bob, loopd): 901 | alice, bob = setup_nodes(bitcoind, [alice, bob]) 902 | if alice.daemon.invoice_rpc_active: 903 | terms = loopd.loop_out_terms() 904 | assert terms is not None 905 | assert isinstance(terms, loop_client_pb2.TermsResponse) 906 | else: 907 | logging.info("test_loop_out() skipped as invoice RPC not detected") 908 | -------------------------------------------------------------------------------- /tests/test_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willcl-ark/lnd_grpc/cf938c51c201f078e8bbe9e19ffc2d038f3abf7f/tests/test_utils/__init__.py -------------------------------------------------------------------------------- /tests/test_utils/btcproxy.py: -------------------------------------------------------------------------------- 1 | """ A bitcoind proxy that allows instrumentation and canned responses 2 | """ 3 | import decimal 4 | import flask 5 | import json 6 | import logging 7 | import os 8 | import threading 9 | 10 | from flask import Flask, request 11 | from bitcoin.rpc import JSONRPCError 12 | from bitcoin.rpc import RawProxy as BitcoinProxy 13 | from test_utils.utils import BitcoinD 14 | from cheroot.wsgi import Server 15 | from cheroot.wsgi import PathInfoDispatcher 16 | 17 | 18 | class DecimalEncoder(json.JSONEncoder): 19 | """By default json.dumps does not handle Decimals correctly, so we override it's handling 20 | """ 21 | 22 | def default(self, o): 23 | if isinstance(o, decimal.Decimal): 24 | return float(o) 25 | return super(DecimalEncoder, self).default(o) 26 | 27 | 28 | class ProxiedBitcoinD(BitcoinD): 29 | def __init__(self, bitcoin_dir, proxyport=0): 30 | BitcoinD.__init__(self, bitcoin_dir, rpcport=None) 31 | self.app = Flask("BitcoindProxy") 32 | self.app.add_url_rule("/", "API entrypoint", self.proxy, methods=["POST"]) 33 | self.proxyport = proxyport 34 | self.mocks = {} 35 | 36 | def _handle_request(self, r): 37 | conf_file = os.path.join(self.bitcoin_dir, "bitcoin.conf") 38 | brpc = BitcoinProxy(btc_conf_file=conf_file) 39 | method = r["method"] 40 | 41 | # If we have set a mock for this method reply with that instead of 42 | # forwarding the request. 43 | if method in self.mocks and type(method) == dict: 44 | return self.mocks[method] 45 | elif method in self.mocks and callable(self.mocks[method]): 46 | return self.mocks[method](r) 47 | 48 | try: 49 | reply = { 50 | "result": brpc._call(r["method"], *r["params"]), 51 | "error": None, 52 | "id": r["id"], 53 | } 54 | except JSONRPCError as e: 55 | reply = {"error": e.error, "id": r["id"]} 56 | return reply 57 | 58 | def proxy(self): 59 | r = json.loads(request.data.decode("ASCII")) 60 | 61 | if isinstance(r, list): 62 | reply = [self._handle_request(subreq) for subreq in r] 63 | else: 64 | reply = self._handle_request(r) 65 | 66 | reply = json.dumps(reply, cls=DecimalEncoder) 67 | logging.debug("Replying to %r with %r", r, reply) 68 | 69 | response = flask.Response(reply) 70 | response.headers["Content-Type"] = "application/json" 71 | return response 72 | 73 | def start(self): 74 | d = PathInfoDispatcher({"/": self.app}) 75 | self.server = Server(("0.0.0.0", self.proxyport), d) 76 | self.proxy_thread = threading.Thread(target=self.server.start) 77 | self.proxy_thread.daemon = True 78 | self.proxy_thread.start() 79 | BitcoinD.start(self) 80 | 81 | # Now that bitcoind is running on the real rpcport, let's tell all 82 | # future callers to talk to the proxyport. We use the bind_addr as a 83 | # signal that the port is bound and accepting connections. 84 | while self.server.bind_addr[1] == 0: 85 | pass 86 | self.proxiedport = self.rpcport 87 | self.rpcport = self.server.bind_addr[1] 88 | logging.debug( 89 | "bitcoind reverse proxy listening on {}, forwarding to {}".format( 90 | self.rpcport, self.proxiedport 91 | ) 92 | ) 93 | 94 | def stop(self): 95 | BitcoinD.stop(self) 96 | self.server.stop() 97 | self.proxy_thread.join() 98 | 99 | def mock_rpc(self, method, response=None): 100 | """Mock the response to a future RPC call of @method 101 | 102 | The response can either be a dict with the full JSON-RPC response, or a 103 | function that returns such a response. If the response is None the mock 104 | is removed and future calls will be passed through to bitcoind again. 105 | 106 | """ 107 | if response is not None: 108 | self.mocks[method] = response 109 | elif method in self.mocks: 110 | del self.mocks[method] 111 | 112 | 113 | # The main entrypoint is mainly used to test the proxy. It is not used during 114 | # lightningd testing. 115 | if __name__ == "__main__": 116 | p = ProxiedBitcoinD(bitcoin_dir="/tmp/bitcoind-test/", proxyport=5000) 117 | p.start() 118 | p.proxy_thread.join() 119 | -------------------------------------------------------------------------------- /tests/test_utils/fixtures.py: -------------------------------------------------------------------------------- 1 | """ 2 | ### 3 | Code Modified from Christian Decker's original work, subject to the following license: 4 | ### 5 | 6 | Copyright Christian Decker (Blockstream) 2017-2019. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | """ 26 | 27 | import logging 28 | import os 29 | import shutil 30 | import tempfile 31 | from concurrent import futures 32 | 33 | import pytest 34 | from ephemeral_port_reserve import reserve 35 | 36 | from test_utils.btcproxy import ProxiedBitcoinD 37 | from test_utils.lnd import LndNode 38 | from test_utils.loop import LoopNode 39 | 40 | TEST_DIR = tempfile.mkdtemp(prefix="lightning-") 41 | TEST_DEBUG = os.getenv("TEST_DEBUG", "0") == "1" 42 | TRAVIS = os.getenv("TRAVIS", "false") == "true" 43 | 44 | 45 | # A dict in which we count how often a particular test has run so far. Used to 46 | # give each attempt its own numbered directory, and avoid clashes. 47 | __attempts = {} 48 | 49 | 50 | class NodeFactory(object): 51 | """ 52 | A factory to setup and start `lightning` daemons. 53 | """ 54 | 55 | def __init__(self, testname, executor, bitcoind): 56 | self.testname = testname 57 | # self.next_id = 1 58 | self.nodes = [] 59 | self.executor = executor 60 | self.bitcoind = bitcoind 61 | 62 | def get_node(self, implementation, node_id): 63 | # node_id = self.next_id 64 | # self.next_id += 1 65 | 66 | lightning_dir = os.path.join( 67 | TEST_DIR, self.testname, "node-{}/".format(node_id) 68 | ) 69 | port = reserve() 70 | 71 | node = implementation( 72 | lightning_dir, port, self.bitcoind, executor=self.executor, node_id=node_id 73 | ) 74 | self.nodes.append(node) 75 | 76 | node.daemon.start() 77 | return node 78 | 79 | def killall(self): 80 | for n in self.nodes: 81 | n.daemon.stop() 82 | 83 | 84 | @pytest.fixture(scope="session") 85 | def directory(request, test_base_dir): 86 | """Return a per-test-session specific directory. 87 | 88 | This makes a unique test-directory even if a test is rerun multiple times. 89 | 90 | """ 91 | # global __attempts 92 | # # Auto set value if it isn't in the dict yet 93 | # __attempts[test_name] = __attempts.get(test_name, 0) + 1 94 | # directory = os.path.join(test_base_dir, "{}_{}".format(test_name, __attempts[test_name])) 95 | directory = test_base_dir 96 | request.node.has_errors = False 97 | 98 | yield directory 99 | 100 | # This uses the status set in conftest.pytest_runtest_makereport to 101 | # determine whether we succeeded or failed. 102 | if not request.node.has_errors: # and request.node.rep_call.outcome == 'passed': 103 | shutil.rmtree(directory) 104 | else: 105 | logging.debug( 106 | "Test execution failed, leaving the test directory {} intact.".format( 107 | directory 108 | ) 109 | ) 110 | 111 | 112 | @pytest.fixture(scope="session") 113 | def test_base_dir(): 114 | directory = tempfile.mkdtemp(prefix="ltests-") 115 | print("Running tests in {}".format(directory)) 116 | 117 | yield directory 118 | 119 | # if not os.listdir(directory) == []: 120 | # shutil.rmtree(directory) 121 | 122 | 123 | @pytest.fixture 124 | def test_name(request): 125 | yield request.function.__name__ 126 | 127 | 128 | @pytest.fixture(scope="session") 129 | def bitcoind(directory): 130 | proxyport = reserve() 131 | btc = ProxiedBitcoinD( 132 | bitcoin_dir=os.path.join(directory, "bitcoind"), proxyport=proxyport 133 | ) 134 | btc.start() 135 | bch_info = btc.rpc.getblockchaininfo() 136 | addr = btc.rpc.getnewaddress("", "bech32") 137 | w_info = btc.rpc.getwalletinfo() 138 | # Make sure we have segwit and some funds 139 | if bch_info["blocks"] < 120: 140 | logging.debug("SegWit not active, generating some more blocks") 141 | btc.rpc.generatetoaddress(120 - bch_info["blocks"], addr) 142 | elif w_info["balance"] < 1: 143 | logging.debug("Insufficient balance, generating 1 block") 144 | btc.rpc.generatetoaddress(1, addr) 145 | 146 | yield btc 147 | 148 | try: 149 | btc.rpc.stop() 150 | except Exception: 151 | btc.proc.kill() 152 | btc.proc.wait() 153 | 154 | 155 | @pytest.fixture(scope="class") 156 | def loopd(alice): 157 | loop = LoopNode(host="localhost", rpc_port="11010", lnd=alice) 158 | loop.start() 159 | 160 | yield loop 161 | 162 | try: 163 | loop.stop() 164 | except Exception: 165 | loop.daemon.stop() 166 | 167 | 168 | @pytest.fixture(scope="class") 169 | def node_factory(request, bitcoind): 170 | executor = futures.ThreadPoolExecutor(max_workers=20) 171 | node_factory = NodeFactory(request._pyfuncitem.name, executor, bitcoind) 172 | yield node_factory 173 | node_factory.killall() 174 | executor.shutdown(wait=False) 175 | 176 | 177 | @pytest.fixture(scope="class") 178 | def alice(node_factory): 179 | alice = node_factory.get_node(implementation=LndNode, node_id="alice") 180 | yield alice 181 | 182 | try: 183 | alice.stop() 184 | except Exception: 185 | print("Issue terminating alice") 186 | 187 | 188 | @pytest.fixture(scope="class") 189 | def bob(node_factory): 190 | bob = node_factory.get_node(implementation=LndNode, node_id="bob") 191 | yield bob 192 | 193 | try: 194 | bob.stop() 195 | except Exception: 196 | print("Issue terminating bob") 197 | 198 | 199 | @pytest.fixture(scope="class") 200 | def carol(node_factory): 201 | carol = node_factory.get_node(implementation=LndNode, node_id="carol") 202 | yield carol 203 | 204 | try: 205 | carol.stop() 206 | except Exception: 207 | print("Issue terminating carol") 208 | 209 | 210 | @pytest.fixture(scope="class") 211 | def dave(node_factory): 212 | dave = node_factory.get_node(implementation=LndNode, node_id="dave") 213 | yield dave 214 | 215 | try: 216 | dave.stop() 217 | except Exception: 218 | print("Issue terminating dave") 219 | -------------------------------------------------------------------------------- /tests/test_utils/lnd.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import time 4 | 5 | from ephemeral_port_reserve import reserve 6 | 7 | from lnd_grpc.lnd_grpc import Client as lndClient 8 | from test_utils.utils import TailableProc, BITCOIND_CONFIG 9 | 10 | 11 | # Needed for grpc to negotiate a valid cipher suite 12 | os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA" 13 | 14 | 15 | class LndD(TailableProc): 16 | 17 | CONF_NAME = "lnd.conf" 18 | 19 | def __init__(self, lightning_dir, bitcoind, port, node_id): 20 | super().__init__(lightning_dir, "lnd({})".format(node_id)) 21 | self.lightning_dir = lightning_dir 22 | self.bitcoind = bitcoind 23 | self.port = port 24 | self.rpc_port = str(reserve()) 25 | self.rest_port = str(reserve()) 26 | self.prefix = f"lnd-{node_id}" 27 | self.invoice_rpc_active = False 28 | try: 29 | if os.environ["TRAVIS_BUILD_DIR"]: 30 | self.tlscertpath = ( 31 | os.environ["TRAVIS_BUILD_DIR"] + "/tests/test_utils/test-tls.cert" 32 | ) 33 | except KeyError: 34 | self.tlscertpath = "test_utils/test-tls.cert" 35 | try: 36 | if os.environ["TRAVIS_BUILD_DIR"]: 37 | self.tlskeypath = ( 38 | os.environ["TRAVIS_BUILD_DIR"] + "/tests/test_utils/test-tls.key" 39 | ) 40 | except KeyError: 41 | self.tlskeypath = "test_utils/test-tls.key" 42 | 43 | self.cmd_line = [ 44 | "lnd", 45 | "--bitcoin.active", 46 | "--bitcoin.regtest", 47 | "--datadir={}".format(lightning_dir), 48 | "--debuglevel=trace", 49 | "--rpclisten=127.0.0.1:{}".format(self.rpc_port), 50 | "--restlisten=127.0.0.1:{}".format(self.rest_port), 51 | "--listen=127.0.0.1:{}".format(self.port), 52 | "--tlscertpath={}".format(self.tlscertpath), 53 | "--tlskeypath={}".format(self.tlskeypath), 54 | "--bitcoin.node=bitcoind", 55 | "--bitcoind.rpchost=127.0.0.1:{}".format( 56 | BITCOIND_CONFIG.get("rpcport", 18332) 57 | ), 58 | "--bitcoind.rpcuser=rpcuser", 59 | "--bitcoind.rpcpass=rpcpass", 60 | "--bitcoind.zmqpubrawblock=tcp://127.0.0.1:{}".format( 61 | self.bitcoind.zmqpubrawblock_port 62 | ), 63 | "--bitcoind.zmqpubrawtx=tcp://127.0.0.1:{}".format( 64 | self.bitcoind.zmqpubrawtx_port 65 | ), 66 | "--configfile={}".format(os.path.join(lightning_dir, self.CONF_NAME)), 67 | "--nobootstrap", 68 | "--noseedbackup", 69 | "--trickledelay=500", 70 | ] 71 | 72 | if not os.path.exists(lightning_dir): 73 | os.makedirs(lightning_dir) 74 | with open(os.path.join(lightning_dir, self.CONF_NAME), "w") as f: 75 | f.write("""[Application Options]\n""") 76 | 77 | def start(self): 78 | super().start() 79 | self.wait_for_log("RPC server listening on") 80 | self.wait_for_log("Done catching up block hashes") 81 | try: 82 | self.wait_for_log("Starting sub RPC server: InvoicesRPC", timeout=10) 83 | self.invoice_rpc_active = True 84 | except ValueError: 85 | pass 86 | time.sleep(3) 87 | 88 | logging.info("LND started (pid: {})".format(self.proc.pid)) 89 | 90 | def stop(self): 91 | self.proc.terminate() 92 | time.sleep(3) 93 | if self.proc.poll() is None: 94 | self.proc.kill() 95 | self.proc.wait() 96 | super().save_log() 97 | 98 | 99 | class LndNode(lndClient): 100 | 101 | displayname = "lnd" 102 | 103 | def __init__( 104 | self, lightning_dir, lightning_port, bitcoind, executor=None, node_id=0 105 | ): 106 | self.bitcoin = bitcoind 107 | self.executor = executor 108 | self.daemon = LndD( 109 | lightning_dir, bitcoind, port=lightning_port, node_id=node_id 110 | ) 111 | self.node_id = node_id 112 | self.logger = logging.getLogger(name="lnd-node({})".format(self.node_id)) 113 | self.myid = None 114 | super().__init__( 115 | lnd_dir=lightning_dir, 116 | grpc_host="localhost", 117 | grpc_port=str(self.daemon.rpc_port), 118 | network="regtest", 119 | tls_cert_path=self.daemon.tlscertpath, 120 | macaroon_path=lightning_dir + "chain/bitcoin/regtest/admin.macaroon", 121 | ) 122 | 123 | def id(self): 124 | if not self.myid: 125 | self.myid = self.get_info().identity_pubkey 126 | return self.myid 127 | 128 | def restart(self): 129 | self.daemon.stop() 130 | time.sleep(5) 131 | self.daemon.start() 132 | 133 | def stop(self): 134 | self.daemon.stop() 135 | 136 | def start(self): 137 | self.daemon.start() 138 | 139 | def add_funds(self, bitcoind, amount): 140 | start_amt = self.wallet_balance().total_balance 141 | addr = self.new_address("p2wkh").address 142 | bitcoind.rpc.sendtoaddress(addr, amount) 143 | self.daemon.wait_for_log("Inserting unconfirmed transaction") 144 | bitcoind.rpc.generatetoaddress(3, addr) 145 | self.daemon.wait_for_log("Marking unconfirmed transaction") 146 | 147 | # The above still doesn't mean the wallet balance is updated, 148 | # so let it settle a bit 149 | i = 0 150 | while ( 151 | self.wallet_balance().total_balance != (start_amt + (amount * 10 ** 8)) 152 | and i < 30 153 | ): 154 | time.sleep(0.25) 155 | i += 1 156 | assert self.wallet_balance().total_balance == start_amt + (amount * 10 ** 8) 157 | 158 | def check_channel(self, remote): 159 | """ Make sure that we have an active channel with remote 160 | """ 161 | self_id = self.id() 162 | remote_id = remote.id() 163 | channels = self.list_channels() 164 | channel_by_remote = {c.remote_pubkey: c for c in channels} 165 | if remote_id not in channel_by_remote: 166 | self.logger.warning("Channel {} -> {} not found".format(self_id, remote_id)) 167 | return False 168 | 169 | channel = channel_by_remote[remote_id] 170 | self.logger.debug( 171 | "Channel {} -> {} state: {}".format(self_id, remote_id, channel) 172 | ) 173 | return channel.active 174 | 175 | def block_sync(self, blockhash): 176 | print("Waiting for node to learn about", blockhash) 177 | self.daemon.wait_for_log( 178 | "NTFN: New block: height=([0-9]+), sha={}".format(blockhash) 179 | ) 180 | -------------------------------------------------------------------------------- /tests/test_utils/loop.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from ephemeral_port_reserve import reserve 5 | 6 | from loop_rpc.loop_rpc import LoopClient as LoopClient 7 | from test_utils.utils import TailableProc 8 | 9 | 10 | class LoopD(TailableProc): 11 | def __init__(self, lnd, network="regtest", host="localhost", rpc_port=None): 12 | self.prefix = "loopd" 13 | super().__init__(prefix=self.prefix) 14 | if rpc_port is None: 15 | rpc_port = reserve() 16 | self.rpc_port = rpc_port 17 | self.host = host 18 | # self.prefix = 'loopd' 19 | self.lnd = lnd 20 | self.cmd_line = [ 21 | f"loopd", 22 | # f'--insecure', 23 | f"--network={network}", 24 | f"--rpclisten={self.host}:{self.rpc_port}", 25 | f"--lnd.host={self.lnd.grpc_host}:{self.lnd.grpc_port}", 26 | f"--lnd.macaroondir={self.lnd.daemon.lightning_dir}/chain/bitcoin/regtest/", 27 | f"--lnd.tlspath={self.lnd.tls_cert_path}", 28 | ] 29 | 30 | def start(self): 31 | super().start() 32 | self.wait_for_log("Connected to lnd") 33 | logging.info("Loop connected to LND node") 34 | self.wait_for_log("Starting event loop at height") 35 | time.sleep(3) 36 | logging.info("Event Loop started") 37 | 38 | def stop(self): 39 | self.proc.terminate() 40 | time.sleep(3) 41 | if self.proc.poll() is None: 42 | self.proc.kill() 43 | self.proc.wait() 44 | super().save_log() 45 | 46 | 47 | class LoopNode(LoopClient): 48 | displayname = "loop" 49 | 50 | def __init__(self, host, rpc_port, lnd, executor=None, node_id=0): 51 | self.executor = executor 52 | self.daemon = LoopD(lnd, host=host, rpc_port=rpc_port) 53 | self.node_id = node_id 54 | self.logger = logging.getLogger(name="loop") 55 | self.myid = None 56 | super().__init__(loop_host=host, loop_port=rpc_port) 57 | 58 | def restart(self): 59 | self.daemon.stop() 60 | time.sleep(5) 61 | self.daemon.start() 62 | 63 | def stop(self): 64 | self.daemon.stop() 65 | 66 | def start(self): 67 | self.daemon.start() 68 | -------------------------------------------------------------------------------- /tests/test_utils/test-tls.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBijCCATGgAwIBAgIJAKD6zs0dG8vpMAoGCCqGSM49BAMCMCIxEjAQBgNVBAMM 3 | CWxvY2FsaG9zdDEMMAoGA1UECgwDbG5kMB4XDTE3MDgwODEwMTc1MFoXDTI3MDgw 4 | NjEwMTc1MFowIjESMBAGA1UEAwwJbG9jYWxob3N0MQwwCgYDVQQKDANsbmQwWTAT 5 | BgcqhkjOPQIBBggqhkjOPQMBBwNCAASp6oi2+jfoyqfhHX8D16gMrBwj0lTDBE7f 6 | qPD7mke/XA8tGW5+x/ytRuRP4e0i3PIyNn3NiNgB01gAIBsxeVJzo1AwTjAdBgNV 7 | HQ4EFgQUXBJTCOjNOpAcpf5pU3FNcnrNpNIwHwYDVR0jBBgwFoAUXBJTCOjNOpAc 8 | pf5pU3FNcnrNpNIwDAYDVR0TBAUwAwEB/zAKBggqhkjOPQQDAgNHADBEAiAb99q1 9 | dk/8l3QjXAkms9hfXvBKl2L5sZqQCrSSWmCSrQIgQGieaNxTVZULhV+6su8oXiHu 10 | C2bi5CebxLPph+Pb2aY= 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /tests/test_utils/test-tls.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BggqhkjOPQMBBw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHcCAQEEIExc3wMePmf9ECeu//VFst3MXV+4m05bsBDaWnDquflwoAoGCCqGSM49 6 | AwEHoUQDQgAEqeqItvo36Mqn4R1/A9eoDKwcI9JUwwRO36jw+5pHv1wPLRlufsf8 7 | rUbkT+HtItzyMjZ9zYjYAdNYACAbMXlScw== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /tests/test_utils/utils.py: -------------------------------------------------------------------------------- 1 | from ephemeral_port_reserve import reserve 2 | 3 | import logging 4 | import re 5 | import subprocess 6 | import threading 7 | import time 8 | import os 9 | import collections 10 | import json 11 | import base64 12 | import requests 13 | 14 | 15 | BITCOIND_CONFIG = collections.OrderedDict( 16 | [ 17 | ("server", 1), 18 | ("deprecatedrpc", "addwitnessaddress"), 19 | ("addresstype", "p2sh-segwit"), 20 | ("deprecatedrpc", "signrawtransaction"), 21 | ("rpcuser", "rpcuser"), 22 | ("rpcpassword", "rpcpass"), 23 | ("listen", 0), 24 | ("deprecatedrpc", "generate"), 25 | ] 26 | ) 27 | 28 | 29 | def write_config(filename, opts): 30 | with open(filename, "w") as f: 31 | write_dict(f, opts) 32 | 33 | 34 | def write_dict(f, opts): 35 | for k, v in opts.items(): 36 | if isinstance(v, dict): 37 | f.write("[{}]\n".format(k)) 38 | write_dict(f, v) 39 | else: 40 | f.write("{}={}\n".format(k, v)) 41 | 42 | 43 | class TailableProc(object): 44 | """A monitorable process that we can start, stop and tail. 45 | 46 | This is the base class for the daemons. It allows us to directly 47 | tail the processes and react to their output. 48 | """ 49 | 50 | def __init__(self, outputDir=None, prefix="proc"): 51 | self.logs = [] 52 | self.logs_cond = threading.Condition(threading.RLock()) 53 | self.cmd_line = None 54 | self.running = False 55 | self.proc = None 56 | self.outputDir = outputDir 57 | self.logger = logging.getLogger(prefix) 58 | 59 | def start(self): 60 | """Start the underlying process and start monitoring it. 61 | """ 62 | self.thread = threading.Thread(target=self.tail) 63 | self.thread.daemon = True 64 | logging.debug("Starting '%s'", " ".join(self.cmd_line)) 65 | self.proc = subprocess.Popen(self.cmd_line, stdout=subprocess.PIPE) 66 | self.thread.start() 67 | self.running = True 68 | 69 | def save_log(self): 70 | if self.outputDir: 71 | logpath = os.path.join(self.outputDir, "log." + str(int(time.time()))) 72 | with open(logpath, "w") as f: 73 | for l in self.logs: 74 | f.write(l + "\n") 75 | 76 | def stop(self): 77 | self.proc.terminate() 78 | self.proc.kill() 79 | self.save_log() 80 | 81 | def tail(self): 82 | """Tail the stdout of the process and remember it. 83 | 84 | Stores the lines of output produced by the process in 85 | self.logs and signals that a new line was read so that it can 86 | be picked up by consumers. 87 | """ 88 | for line in iter(self.proc.stdout.readline, ""): 89 | if len(line) == 0: 90 | break 91 | with self.logs_cond: 92 | self.logs.append(str(line.rstrip())) 93 | self.logger.debug(line.decode().rstrip()) 94 | self.logs_cond.notifyAll() 95 | self.running = False 96 | 97 | def is_in_log(self, regex): 98 | """Look for `regex` in the logs.""" 99 | 100 | ex = re.compile(regex) 101 | for l in self.logs: 102 | if ex.search(l): 103 | logging.debug("Found '%s' in logs", regex) 104 | return True 105 | 106 | logging.debug("Did not find '%s' in logs", regex) 107 | return False 108 | 109 | def wait_for_log(self, regex, offset=1000, timeout=60): 110 | """Look for `regex` in the logs. 111 | 112 | We tail the stdout of the process and look for `regex`, 113 | starting from `offset` lines in the past. We fail if the 114 | timeout is exceeded or if the underlying process exits before 115 | the `regex` was found. The reason we start `offset` lines in 116 | the past is so that we can issue a command and not miss its 117 | effects. 118 | 119 | """ 120 | logging.debug("Waiting for '%s' in the logs", regex) 121 | ex = re.compile(regex) 122 | start_time = time.time() 123 | pos = max(len(self.logs) - offset, 0) 124 | initial_pos = len(self.logs) 125 | while True: 126 | if time.time() > start_time + timeout: 127 | print("Can't find {} in logs".format(regex)) 128 | with self.logs_cond: 129 | for i in range(initial_pos, len(self.logs)): 130 | print(" " + self.logs[i]) 131 | if self.is_in_log(regex): 132 | print("(Was previously in logs!") 133 | raise TimeoutError('Unable to find "{}" in logs.'.format(regex)) 134 | elif not self.running: 135 | print("Logs: {}".format(self.logs)) 136 | raise ValueError("Process died while waiting for logs") 137 | 138 | with self.logs_cond: 139 | if pos >= len(self.logs): 140 | self.logs_cond.wait(1) 141 | continue 142 | 143 | if ex.search(self.logs[pos]): 144 | logging.debug("Found '%s' in logs", regex) 145 | return self.logs[pos] 146 | pos += 1 147 | 148 | 149 | class BitcoinRpc(object): 150 | def __init__(self, url=None, rpcport=8332, rpcuser=None, rpcpassword=None): 151 | self.url = url if url else "http://localhost:{}".format(rpcport) 152 | authpair = "%s:%s" % (rpcuser, rpcpassword) 153 | authpair = authpair.encode("utf8") 154 | self.auth_header = b"Basic " + base64.b64encode(authpair) 155 | self.__id_count = 0 156 | 157 | def _call(self, service_name, *args): 158 | self.__id_count += 1 159 | 160 | r = requests.post( 161 | self.url, 162 | data=json.dumps( 163 | { 164 | "version": "1.1", 165 | "method": service_name, 166 | "params": args, 167 | "id": self.__id_count, 168 | } 169 | ), 170 | headers={ 171 | # 'Host': self.__url.hostname, 172 | "Authorization": self.auth_header, 173 | "Content-type": "application/json", 174 | }, 175 | ) 176 | 177 | response = r.json() 178 | if response["error"] is not None: 179 | raise ValueError(response["error"]) 180 | elif "result" not in response: 181 | raise ValueError({"code": -343, "message": "missing JSON-RPC result"}) 182 | else: 183 | return response["result"] 184 | 185 | def __getattr__(self, name): 186 | if name in self.__dict__: 187 | return self.__dict__[name] 188 | 189 | # Create a callable to do the actual call 190 | f = lambda *args: self._call(name, *args) 191 | 192 | # Make debuggers show rather than > 194 | f.__name__ = name 195 | return f 196 | 197 | 198 | class BitcoinD(TailableProc): 199 | 200 | CONF_NAME = "bitcoin.conf" 201 | 202 | def __init__(self, bitcoin_dir="/tmp/bitcoind-test", rpcport=None): 203 | super().__init__(bitcoin_dir, "bitcoind") 204 | 205 | if rpcport is None: 206 | rpcport = reserve() 207 | 208 | self.bitcoin_dir = bitcoin_dir 209 | 210 | self.prefix = "bitcoind" 211 | BITCOIND_CONFIG["rpcport"] = rpcport 212 | self.rpcport = rpcport 213 | self.zmqpubrawblock_port = reserve() 214 | self.zmqpubrawtx_port = reserve() 215 | 216 | regtestdir = os.path.join(bitcoin_dir, "regtest") 217 | if not os.path.exists(regtestdir): 218 | os.makedirs(regtestdir) 219 | 220 | conf_file = os.path.join(bitcoin_dir, self.CONF_NAME) 221 | 222 | self.cmd_line = [ 223 | "bitcoind", 224 | "-datadir={}".format(bitcoin_dir), 225 | "-conf={}".format(conf_file), 226 | "-regtest", 227 | "-logtimestamps", 228 | "-rpcport={}".format(rpcport), 229 | "-printtoconsole=1" "-debug", 230 | "-rpcuser=rpcuser", 231 | "-rpcpassword=rpcpass", 232 | "-zmqpubrawblock=tcp://127.0.0.1:{}".format(self.zmqpubrawblock_port), 233 | "-zmqpubrawtx=tcp://127.0.0.1:{}".format(self.zmqpubrawtx_port), 234 | # "-zmqpubrawblockhwm=0", 235 | # "-zmqpubrawtxhwm=0", 236 | ] 237 | BITCOIND_CONFIG["rpcport"] = rpcport 238 | write_config(os.path.join(bitcoin_dir, self.CONF_NAME), BITCOIND_CONFIG) 239 | write_config(os.path.join(regtestdir, self.CONF_NAME), BITCOIND_CONFIG) 240 | self.rpc = BitcoinRpc(rpcport=rpcport, rpcuser="rpcuser", rpcpassword="rpcpass") 241 | 242 | def start(self): 243 | super().start() 244 | self.wait_for_log("Done loading", timeout=10) 245 | 246 | logging.info("BitcoinD started") 247 | --------------------------------------------------------------------------------