From 89ae6cd4a6bb5368e81a8126e020d872cf393eed Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 7 Nov 2023 16:12:53 +0000 Subject: [PATCH] Add draft LND gRCP mocks --- api/lightning/lnd.py | 39 +++++++++++++-- docker/lnd/Dockerfile | 2 +- tests/mocks/lnd.py | 114 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 144 insertions(+), 11 deletions(-) diff --git a/api/lightning/lnd.py b/api/lightning/lnd.py index 6c495e53..fdda1093 100644 --- a/api/lightning/lnd.py +++ b/api/lightning/lnd.py @@ -45,6 +45,17 @@ DISABLE_ONCHAIN = config("DISABLE_ONCHAIN", cast=bool, default=True) MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500_000) +# Logger function used to build tests/mocks/lnd.py +def log(name, request, response): + if not config("LOG_LND", cast=bool, default=True): + return + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_message = f"######################################\nEvent: {name}\nTime: {current_time}\nRequest:\n{request}\nResponse:\n{response}\nType: {type(response)}\n" + + with open("lnd_log.txt", "a") as file: + file.write(log_message) + + class LNDNode: os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA" @@ -76,6 +87,7 @@ class LNDNode: try: request = verrpc.VersionRequest() response = cls.verstub.GetVersion(request) + log("verstub.GetVersion", request, response) return "v" + response.version except Exception as e: print(e) @@ -86,17 +98,21 @@ class LNDNode: """Decodes a lightning payment request (invoice)""" request = lnrpc.PayReqString(pay_req=invoice) response = cls.lightningstub.DecodePayReq(request) + log("lightningstub.DecodePayReq", request, response) return response @classmethod def estimate_fee(cls, amount_sats, target_conf=2, min_confs=1): """Returns estimated fee for onchain payouts""" - is_testnet = lightningstub.GetInfo(lnrpc.GetInfoRequest()).testnet - if is_testnet: + + request = lnrpc.GetInfoRequest() + response = lightningstub.GetInfo(request) + log("lightningstub.GetInfo", request, response) + + if response.testnet: dummy_address = "tb1qehyqhruxwl2p5pt52k6nxj4v8wwc3f3pg7377x" else: dummy_address = "bc1qgxwaqe4m9mypd7ltww53yv3lyxhcfnhzzvy5j3" - # We assume segwit. Use hardcoded address as shortcut so there is no need of user inputs yet. request = lnrpc.EstimateFeeRequest( AddrToAmount={dummy_address: amount_sats}, @@ -106,6 +122,7 @@ class LNDNode: ) response = cls.lightningstub.EstimateFee(request) + log("lightningstub.EstimateFee", request, response) return { "mining_fee_sats": response.fee_sat, @@ -120,6 +137,7 @@ class LNDNode: """Returns onchain balance""" request = lnrpc.WalletBalanceRequest() response = cls.lightningstub.WalletBalance(request) + log("lightningstub.WalletBalance", request, response) return { "total_balance": response.total_balance, @@ -135,6 +153,7 @@ class LNDNode: """Returns channels balance""" request = lnrpc.ChannelBalanceRequest() response = cls.lightningstub.ChannelBalance(request) + log("lightningstub.ChannelBalance", request, response) return { "local_balance": response.local_balance.sat, @@ -169,6 +188,7 @@ class LNDNode: onchainpayment.status = on_mempool_code onchainpayment.save(update_fields=["status"]) response = cls.lightningstub.SendCoins(request) + log("lightningstub.SendCoins", request, response) if response.txid: onchainpayment.txid = response.txid @@ -192,6 +212,7 @@ class LNDNode: """Cancels or returns a hold invoice""" request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash)) response = cls.invoicesstub.CancelInvoice(request) + log("invoicesstub.CancelInvoice", request, response) # Fix this: tricky because canceling sucessfully an invoice has no response. TODO return str(response) == "" # True if no response, false otherwise. @@ -200,6 +221,7 @@ class LNDNode: """settles a hold invoice""" request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage)) response = cls.invoicesstub.SettleInvoice(request) + log("invoicesstub.SettleInvoice", request, response) # Fix this: tricky because settling sucessfully an invoice has None response. TODO return str(response) == "" # True if no response, false otherwise. @@ -215,7 +237,6 @@ class LNDNode: time, ): """Generates hold invoice""" - hold_payment = {} # The preimage is a random hash of 256 bits entropy preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest() @@ -233,6 +254,7 @@ class LNDNode: cltv_expiry=cltv_expiry_blocks, ) response = cls.invoicesstub.AddHoldInvoice(request) + log("invoicesstub.AddHoldInvoice", request, response) hold_payment["invoice"] = response.payment_request payreq_decoded = cls.decode_payreq(hold_payment["invoice"]) @@ -257,6 +279,7 @@ class LNDNode: payment_hash=bytes.fromhex(lnpayment.payment_hash) ) response = cls.invoicesstub.LookupInvoiceV2(request) + log("invoicesstub.LookupInvoiceV2", request, response) # Will fail if 'unable to locate invoice'. Happens if invoice expiry # time has passed (but these are 15% padded at the moment). Should catch it @@ -297,6 +320,7 @@ class LNDNode: payment_hash=bytes.fromhex(lnpayment.payment_hash) ) response = cls.invoicesstub.LookupInvoiceV2(request) + log("invoicesstub.LookupInvoiceV2", request, response) status = lnd_response_state_to_lnpayment_status[response.state] @@ -442,6 +466,7 @@ class LNDNode: ) for response in cls.routerstub.SendPaymentV2(request): + log("routerstub.SendPaymentV2", request, response) if ( response.status == lnrpc.Payment.PaymentStatus.UNKNOWN ): # Status 0 'UNKNOWN' @@ -597,6 +622,7 @@ class LNDNode: try: for response in cls.routerstub.SendPaymentV2(request): + log("routerstub.SendPaymentV2", request, response) handle_response(response) except Exception as e: @@ -609,6 +635,7 @@ class LNDNode: ) for response in cls.routerstub.TrackPaymentV2(request): + log("routerstub.TrackPaymentV2", request, response) handle_response(response, was_in_transit=True) except Exception as e: @@ -648,6 +675,7 @@ class LNDNode: ) for response in cls.routerstub.TrackPaymentV2(request): + log("routerstub.TrackPaymentV2", request, response) handle_response(response, was_in_transit=True) elif "invoice is already paid" in str(e): @@ -658,6 +686,7 @@ class LNDNode: ) for response in cls.routerstub.TrackPaymentV2(request): + log("routerstub.TrackPaymentV2", request, response) handle_response(response) else: @@ -721,6 +750,7 @@ class LNDNode: allow_self_payment=ALLOW_SELF_KEYSEND, ) for response in cls.routerstub.SendPaymentV2(request): + log("routerstub.SendPaymentV2", request, response) if response.status == lnrpc.Payment.PaymentStatus.IN_FLIGHT: keysend_payment["status"] = LNPayment.Status.FLIGHT if response.status == lnrpc.Payment.PaymentStatus.SUCCEEDED: @@ -744,6 +774,7 @@ class LNDNode: """Just as it sounds. Better safe than sorry!""" request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash)) response = cls.invoicesstub.LookupInvoiceV2(request) + log("invoicesstub.LookupInvoiceV2", request, response) return ( response.state == lnrpc.Invoice.InvoiceState.SETTLED diff --git a/docker/lnd/Dockerfile b/docker/lnd/Dockerfile index e6c0bd27..3093169b 100644 --- a/docker/lnd/Dockerfile +++ b/docker/lnd/Dockerfile @@ -1,4 +1,4 @@ -FROM lightninglabs/lnd:v0.16.3-beta +FROM lightninglabs/lnd:v0.17.0-beta ARG LOCAL_USER_ID=9999 ARG LOCAL_GROUP_ID=9999 diff --git a/tests/mocks/lnd.py b/tests/mocks/lnd.py index a732aded..9286da9f 100644 --- a/tests/mocks/lnd.py +++ b/tests/mocks/lnd.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock - # Mock up of LND gRPC responses + + class MockLightningStub: def GetInfo(self, request): response = MagicMock() - # Set the testnet attribute to True for testing purposes response.testnet = True return response @@ -15,18 +15,120 @@ class MockLightningStub: response.sat_per_vbyte = 13 return response + def DecodePayReq(self, request): + response = MagicMock() + if request.pay_req == "lntb17314....x": + response.destination = "00000000" + response.payment_hash = "00000000" + response.num_satoshis = 1731 + response.timestamp = 1699359597 + response.expiry = 450 + response.description = "Payment reference: xxxxxxxxxxxxxxxxxxxxxxx. This payment WILL FREEZE IN YOUR WALLET, check on RoboSats if the lock was successful. It will be unlocked (fail) unless you cheat or cancel unilaterally." + response.cltv_expiry = 650 + response.payment_addr = "\275\205\224\002\036h\322" + response.num_msat = 1731000 + + def CancelInvoice(self, request): + response = MagicMock() + if request == b"xU\305\212\306": + response = {} + return response + + def WalletBalance(self, request): + response = MagicMock() + response.total_balance = 10_000_000 + response.confirmed_balance = 9_000_000 + response.unconfirmed_balance = 1_000_000 + response.reserved_balance_anchor_chan = 30_000 + response.account_balance = {} + return response + + def ChannelBalance(self, request): + response = MagicMock() + response.balance: 10_000_000 + response.local_balance.sat = 10_000_000 + response.local_balance.msat = 10_000_000_000 + response.remote_balance.sat = 30_000_000 + response.remote_balance.msat = 30_000_000_000 + response.unsettled_local_balance.sat = 500_000 + response.unsettled_local_balance.msat = 500_000_000 + response.unsettled_remote_balance.sat = 100_000 + response.unsettled_remote_balance.msat = 100_000_000 + response.pending_open_local_balance = 2_000_000 + response.pending_open_local_balance = 2_000_000_000 + response.pending_open_remote_balance = 5_000_000 + response.pending_open_remote_balance = 5_000_000_000 + return response + + def SendCoins(self, request): + response = MagicMock() + return response + class MockInvoicesStub: - pass + def AddHoldInvoice(self, request): + response = MagicMock() + if request.value == 1731: + response.payment_request = "lntb17314....x" + response.add_index = 1 + response.payment_addr = b"\275\205\322" + + def CancelInvoice(self, request): + response = MagicMock() + return response + + def SettleInvoice(self, request): + response = MagicMock() + return response + + def LookupInvoiceV2(self, request): + response = MagicMock() + return response class MockRouterStub: - pass + def ResetMissionControl(self, request): + response = MagicMock() + return response + + def SendPaymentV2(self, request): + response = MagicMock() + return response + + def TrackPaymentV2(self, request): + response = MagicMock() + return response class MockSignerStub: - pass + def SignMessage(self, request): + response = MagicMock() + return response class MockVersionerStub: - pass + def GetVersion(self, request): + response = MagicMock() + response.commit = "v0.17.0-beta" + response.commit_hash = "2fb150c8fe827df9df0520ef9916b3afb7b03a8d" + response.version = "0.17.0-beta" + response.app_minor = 17 + response.app_patch = 0 + response.app_pre_release = "beta" + response.build_tags = [ + "autopilotrpc", + "signrpc", + "walletrpc", + "chainrpc", + "invoicesrpc", + "watchtowerrpc", + "neutrinorpc", + "monitoring", + "peersrpc", + "kvdb_postgres", + "kvdb_etcd", + "kvdb_sqlite", + "go1.20.3", + ] + response.go_version = "go1.21.0" + return response