diff --git a/squeaknode/core/squeak_core.py b/squeaknode/core/squeak_core.py index 0d5e3883..3d9e478c 100644 --- a/squeaknode/core/squeak_core.py +++ b/squeaknode/core/squeak_core.py @@ -48,7 +48,7 @@ from squeaknode.core.squeaks import get_decrypted_content from squeaknode.core.squeaks import get_hash from squeaknode.core.squeaks import get_payment_point_of_secret_key from squeaknode.core.squeaks import make_squeak_with_block -from squeaknode.lightning.lnd_lightning_client import LNDLightningClient +from squeaknode.lightning.lightning_client import LightningClient logger = logging.getLogger(__name__) @@ -58,7 +58,7 @@ class SqueakCore: def __init__( self, bitcoin_client: BitcoinClient, - lightning_client: LNDLightningClient, + lightning_client: LightningClient, ): self.bitcoin_client = bitcoin_client self.lightning_client = lightning_client @@ -247,7 +247,7 @@ class SqueakCore: pay_req = self.lightning_client.decode_pay_req( offer.payment_request) squeak_payment_point = squeak.paymentPoint - payment_hash = bytes.fromhex(pay_req.payment_hash) + payment_hash = pay_req.payment_hash price_msat = pay_req.num_msat destination = pay_req.destination invoice_timestamp = pay_req.timestamp @@ -286,7 +286,7 @@ class SqueakCore: SentPayment: A record of the sent payment. """ # Pay the invoice - payment = self.lightning_client.pay_invoice_sync( + payment = self.lightning_client.pay_invoice( received_offer.payment_request) preimage = payment.payment_preimage if not preimage: diff --git a/squeaknode/lightning/info.py b/squeaknode/lightning/info.py new file mode 100644 index 00000000..eb7ecfe0 --- /dev/null +++ b/squeaknode/lightning/info.py @@ -0,0 +1,28 @@ +# MIT License +# +# Copyright (c) 2020 Jonathan Zernik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from typing import List +from typing import NamedTuple + + +class Info(NamedTuple): + """Represents info about the lightning node.""" + uris: List[str] diff --git a/squeaknode/lightning/invoice.py b/squeaknode/lightning/invoice.py index a5c71b38..4fe333e1 100644 --- a/squeaknode/lightning/invoice.py +++ b/squeaknode/lightning/invoice.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from typing import NamedTuple +from typing import Optional class Invoice(NamedTuple): @@ -28,6 +29,6 @@ class Invoice(NamedTuple): payment_request: str value_msat: int settled: bool - settle_index: int + settle_index: Optional[int] creation_date: int expiry: int diff --git a/squeaknode/lightning/lightning_client.py b/squeaknode/lightning/lightning_client.py index a3a52150..faefd2ac 100644 --- a/squeaknode/lightning/lightning_client.py +++ b/squeaknode/lightning/lightning_client.py @@ -23,7 +23,10 @@ import logging from abc import ABC from abc import abstractmethod +from squeaknode.lightning.info import Info from squeaknode.lightning.invoice import Invoice +from squeaknode.lightning.pay_req import PayReq +from squeaknode.lightning.payment import Payment logger = logging.getLogger(__name__) @@ -31,7 +34,18 @@ logger = logging.getLogger(__name__) class LightningClient(ABC): @abstractmethod - def create_invoice(self, preimage, amount_msat) -> Invoice: + def get_info(self) -> Info: + """Get info about the lightning node. + + Returns: + Info: an object containing information about the node. + + Raises: + LightningRequestError: If the request fails. + """ + + @abstractmethod + def create_invoice(self, preimage: bytes, amount_msat: int) -> Invoice: """Add a new invoice. Args: @@ -44,3 +58,43 @@ class LightningClient(ABC): Raises: LightningRequestError: If the request fails. """ + + @abstractmethod + def decode_pay_req(self, payment_request: str) -> PayReq: + """Get the decoded payment request. + + Args: + payment_request: The payment request as a string. + + Returns: + PayReq: an object representing the payment request. + + Raises: + LightningRequestError: If the request fails. + """ + + @abstractmethod + def pay_invoice(self, payment_request: str) -> Payment: + """Pay an invoice with a given payment_request. + + Args: + payment_request: The payment request as a string. + + args: + payment_request -- the payment_request as a string + """ + + @abstractmethod + def subscribe_invoices(self, settle_index: int): + """Get a stream of settled invoices for received payments. + # TODO: use map function to convert type of items in stream. + + Args: + settle_index: The settle index from which to start streaming. + + Returns: + TODO: + + Raises: + LightningRequestError: If the request fails. + """ diff --git a/squeaknode/lightning/lnd_lightning_client.py b/squeaknode/lightning/lnd_lightning_client.py index 3ff8dbe0..645b1e9d 100644 --- a/squeaknode/lightning/lnd_lightning_client.py +++ b/squeaknode/lightning/lnd_lightning_client.py @@ -27,7 +27,10 @@ import grpc from proto import lnd_pb2 from proto import lnd_pb2_grpc +from squeaknode.lightning.info import Info from squeaknode.lightning.invoice import Invoice +from squeaknode.lightning.pay_req import PayReq +from squeaknode.lightning.payment import Payment logger = logging.getLogger(__name__) @@ -53,7 +56,7 @@ class LNDLightningClient: self.port = port self.tls_cert_path = tls_cert_path self.macaroon_path = macaroon_path - self.stub = None + # self.stub = None def init(self): self.stub = self._get_stub() @@ -89,180 +92,48 @@ class LNDLightningClient: channel = grpc.secure_channel(url, combined_creds) return lnd_pb2_grpc.LightningStub(channel) - def get_wallet_balance(self): - # Retrieve and display the wallet balance - request = lnd_pb2.WalletBalanceRequest() - return self.stub.WalletBalance(request) - - def add_invoice(self, preimage, amount_msat): - """Create a new invoice with the given hash pre-image. - - args: - preimage -- the preimage bytes used to create the invoice - amount -- the value of the invoice - """ + def add_invoice(self, preimage: bytes, amount_msat: int) -> lnd_pb2.AddInvoiceResponse: invoice = lnd_pb2.Invoice( r_preimage=preimage, value_msat=amount_msat, ) return self.stub.AddInvoice(invoice) - def pay_invoice_sync(self, payment_request): - """Pay an invoice with a given payment_request - - args: - payment_request -- the payment_request as a string - """ + def pay_invoice(self, payment_request: str) -> Payment: send_payment_request = lnd_pb2.SendRequest( payment_request=payment_request, ) - return self.stub.SendPaymentSync(send_payment_request) - - def connect_peer(self, pubkey, host): - """Connect to a lightning node peer. - - args: - pubkey -- The identity pubkey of the Lightning node - host -- The network location of the lightning node - """ - lightning_address = lnd_pb2.LightningAddress( - pubkey=pubkey, - host=host, - ) - connect_peer_request = lnd_pb2.ConnectPeerRequest( - addr=lightning_address, - ) - return self.stub.ConnectPeer(connect_peer_request) - - def disconnect_peer(self, pubkey): - """Disconnect a lightning node peer. - - args: - pubkey -- The identity pubkey of the Lightning node - """ - disconnect_peer_request = lnd_pb2.DisconnectPeerRequest( - pub_key=pubkey, - ) - return self.stub.DisconnectPeer( - disconnect_peer_request, + send_payment_response = self.stub.SendPaymentSync(send_payment_request) + return Payment( + payment_preimage=send_payment_response.payment_preimage, + payment_error=send_payment_response.payment_error, ) - def get_info(self): - """Get info about the lightning network node.""" + def get_info(self) -> Info: get_info_request = lnd_pb2.GetInfoRequest() - return self.stub.GetInfo( + get_info_response = self.stub.GetInfo( get_info_request, ) - - def open_channel_sync(self, pubkey_str, local_amount): - """Open a channel with a remote lightning node. - - args: - pubkey (str) -- The identity pubkey of the Lightning node - local_amount -- The number of satoshis the wallet should commit to the channel - """ - open_channel_request = lnd_pb2.OpenChannelRequest( - node_pubkey_string=pubkey_str, - local_funding_amount=local_amount, - ) - return self.stub.OpenChannelSync( - open_channel_request, + return Info( + uris=get_info_response.uris, ) - def list_channels(self): - """List the channels""" - list_channels_request = lnd_pb2.ListChannelsRequest() - return self.stub.ListChannels( - list_channels_request, - ) - - def pending_channels(self): - """List the pending channels""" - pending_channels_request = lnd_pb2.PendingChannelsRequest() - return self.stub.PendingChannels( - pending_channels_request, - ) - - def list_peers(self): - """List the peers""" - list_peers_request = lnd_pb2.ListPeersRequest() - return self.stub.ListPeers( - list_peers_request, - ) - - def open_channel(self, pubkey, local_amount): - """Open a channel - - args: - pubkey (bytes) -- The identity pubkey of the Lightning node - local_amount -- The number of satoshis the wallet should commit to the channel - """ - open_channel_request = lnd_pb2.OpenChannelRequest( - node_pubkey=pubkey, - local_funding_amount=local_amount, - ) - return self.stub.OpenChannel( - open_channel_request, - ) - - def close_channel(self, channel_point): - """Close a channel - - args: - channel_point (str) -- The outpoint (txid:index) of the funding transaction. - """ - close_channel_request = lnd_pb2.CloseChannelRequest( - channel_point=channel_point, - ) - return self.stub.CloseChannel( - close_channel_request, - ) - - def decode_pay_req(self, payment_request): - """Decode a payment request - - args: - pay_req (str) -- The payment request string - """ + def decode_pay_req(self, payment_request: str) -> PayReq: decode_pay_req_request = lnd_pb2.PayReqString( pay_req=payment_request, ) - return self.stub.DecodePayReq( + decode_pay_req_response = self.stub.DecodePayReq( decode_pay_req_request, ) - - def new_address(self, address_type): - # NewAddress creates a new address under control of the local wallet. - new_address_request = lnd_pb2.NewAddressRequest( - type=address_type, - ) - return self.stub.NewAddress( - new_address_request, + return PayReq( + payment_hash=bytes.fromhex(decode_pay_req_response.payment_hash), + num_msat=decode_pay_req_response.num_msat, + destination=decode_pay_req_response.destination, + timestamp=decode_pay_req_response.timestamp, + expiry=decode_pay_req_response.expiry, ) - def subscribe_channel_events(self): - subscribe_channel_events_request = lnd_pb2.ChannelEventSubscription() - return self.stub.SubscribeChannelEvents( - subscribe_channel_events_request, - ) - - def get_transactions(self): - # Get transactions - get_transactions_request = lnd_pb2.GetTransactionsRequest() - return self.stub.GetTransactions( - get_transactions_request, - ) - - def send_coins(self, addr, amount): - send_coins_request = lnd_pb2.SendCoinsRequest( - addr=addr, - amount=amount, - ) - return self.stub.SendCoins( - send_coins_request, - ) - - def subscribe_invoices(self, settle_index): + def subscribe_invoices(self, settle_index: int): subscribe_invoices_request = lnd_pb2.InvoiceSubscription( settle_index=settle_index, ) @@ -270,18 +141,13 @@ class LNDLightningClient: subscribe_invoices_request, ) - def lookup_invoice(self, r_hash_str): - """Look up an invoice. - - args: - r_hash_str -- The hex-encoded payment hash of the invoice to be looked up. - """ + def lookup_invoice(self, r_hash_str: str) -> lnd_pb2.Invoice: payment_hash = lnd_pb2.PaymentHash( r_hash_str=r_hash_str, ) return self.stub.LookupInvoice(payment_hash) - def create_invoice(self, preimage, amount_msat) -> Invoice: + def create_invoice(self, preimage: bytes, amount_msat: int) -> Invoice: add_invoice_response = self.add_invoice(preimage, amount_msat) payment_hash = add_invoice_response.r_hash lookup_invoice_response = self.lookup_invoice( diff --git a/squeaknode/lightning/pay_req.py b/squeaknode/lightning/pay_req.py new file mode 100644 index 00000000..01e02aa1 --- /dev/null +++ b/squeaknode/lightning/pay_req.py @@ -0,0 +1,31 @@ +# MIT License +# +# Copyright (c) 2020 Jonathan Zernik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from typing import NamedTuple + + +class PayReq(NamedTuple): + """Represents info about the lightning node.""" + payment_hash: bytes + num_msat: int + destination: str + timestamp: int + expiry: int diff --git a/squeaknode/lightning/payment.py b/squeaknode/lightning/payment.py new file mode 100644 index 00000000..77fee063 --- /dev/null +++ b/squeaknode/lightning/payment.py @@ -0,0 +1,29 @@ +# MIT License +# +# Copyright (c) 2020 Jonathan Zernik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from typing import NamedTuple +from typing import Optional + + +class Payment(NamedTuple): + """Represents the result of a paid invoice.""" + payment_preimage: Optional[bytes] + payment_error: Optional[str] diff --git a/tests/lightning/test_lnd_lightning_client.py b/tests/lightning/test_lnd_lightning_client.py index 18c24f58..ec38addb 100644 --- a/tests/lightning/test_lnd_lightning_client.py +++ b/tests/lightning/test_lnd_lightning_client.py @@ -23,7 +23,10 @@ import mock import pytest from proto import lnd_pb2 +from squeaknode.lightning.info import Info +from squeaknode.lightning.invoice import Invoice from squeaknode.lightning.lnd_lightning_client import LNDLightningClient +from squeaknode.lightning.payment import Payment from tests.utils import gen_random_hash @@ -52,19 +55,105 @@ def preimage(): yield gen_random_hash() +@pytest.fixture +def payment_hash(preimage): + # TODO: This should be the hash of the preimage + yield gen_random_hash() + + @pytest.fixture def price_msat(): yield 33333 +@pytest.fixture +def creation_date(): + yield 777777 + + +@pytest.fixture +def expiry(): + yield 5555 + + +@pytest.fixture +def payment_request(): + yield "fake_payment_request" + + @pytest.fixture def rpc_invoice(preimage): yield lnd_pb2.Invoice( - memo='hello', r_preimage=preimage, ) +@pytest.fixture +def invoice(payment_hash, payment_request, price_msat, creation_date, expiry): + yield Invoice( + r_hash=payment_hash, + payment_request=payment_request, + value_msat=price_msat, + settled=False, + settle_index=None, + creation_date=creation_date, + expiry=expiry, + ) + + +@pytest.fixture +def add_invoice_response(payment_hash, payment_request): + yield lnd_pb2.AddInvoiceResponse( + r_hash=payment_hash, + payment_request=payment_request, + ) + + +@pytest.fixture +def uris(): + yield [ + 'foobar.com:12345' + 'fakehost.com:56789' + ] + + +@pytest.fixture +def get_info_response(uris): + yield lnd_pb2.GetInfoResponse( + uris=uris, + ) + + +@pytest.fixture +def info(uris): + yield Info( + uris=uris, + ) + + +@pytest.fixture +def send_request(payment_request): + yield lnd_pb2.SendRequest( + payment_request=payment_request, + ) + + +@pytest.fixture +def send_response(preimage, payment_hash): + yield lnd_pb2.SendResponse( + payment_preimage=preimage, + payment_hash=payment_hash, + ) + + +@pytest.fixture +def payment(preimage): + yield Payment( + payment_preimage=preimage, + payment_error=None, + ) + + # @pytest.fixture # def lnd_lightning_client_and_get_stub(lnd_host, lnd_port, tls_cert_path, macaroon_path): # client = LNDLightningClient( @@ -105,15 +194,55 @@ def make_lightning_client(lnd_host, lnd_port, tls_cert_path, macaroon_path): yield fn -def test_add_invoice(make_lightning_client, preimage, price_msat, rpc_invoice): +def test_add_invoice(make_lightning_client, preimage, price_msat, rpc_invoice, invoice, add_invoice_response): mock_stub = mock.MagicMock() - mock_stub.AddInvoice.return_value = rpc_invoice + mock_stub.AddInvoice.return_value = add_invoice_response client = make_lightning_client(mock_stub) - add_invoice_response = client.add_invoice( + response = client.add_invoice( preimage, price_msat) (call_invoice,) = mock_stub.AddInvoice.call_args.args - print(call_invoice) + assert type(call_invoice) is lnd_pb2.Invoice assert call_invoice.r_preimage == preimage assert call_invoice.value_msat == price_msat - assert add_invoice_response == rpc_invoice + assert type(response) is lnd_pb2.AddInvoiceResponse + assert response == add_invoice_response + + +# def test_pay_invoice(make_lightning_client, payment_request, send_request, send_response, payment): +# mock_stub = mock.MagicMock() +# mock_stub.SendPaymentSync.return_value = send_response +# print('mock_stub:') +# print(mock_stub) +# client = make_lightning_client(mock_stub) +# response = client.pay_invoice(payment_request) +# print('mock_stub:') +# print(mock_stub) +# (call_msg,) = mock_stub.SendPayment.call_args.args + +# assert type(call_msg) is lnd_pb2.SendRequest +# assert call_invoice.payment_request == payment_request +# assert type(response) is Payment +# assert response == payment + + +def test_get_info(make_lightning_client, get_info_response, info): + mock_stub = mock.MagicMock() + mock_stub.GetInfo.return_value = get_info_response + client = make_lightning_client(mock_stub) + response = client.get_info() + (call_get_info,) = mock_stub.GetInfo.call_args.args + + assert type(call_get_info) is lnd_pb2.GetInfoRequest + assert response == info + + +# def test_pay_invoice(make_lightning_client, payment_request, info): +# mock_stub = mock.MagicMock() +# mock_stub.GetInfo.return_value = info +# client = make_lightning_client(mock_stub) +# get_info_response = client.get_info() +# (call_get_info,) = mock_stub.GetInfo.call_args.args + +# assert type(call_get_info) is lnd_pb2.GetInfoRequest +# assert get_info_response.uris == uris