Mega-merge 4: Reenable LndWallet gRPC and use TrackPaymentV2 (#745)

* readd lndgrpc

* debug logging

* Use TrackPaymentV2

* /v2/router/track

* lnd_router_grpc

* flag for blocking check

* error handling

* fix name

* regtest lndgrpc

* new test pipeline

* fix env

* check for description hash

* remove unnecessary asserts for clarity

* assume that description_hash is a hash already

* no lock

* description hashing in backend

* restore bolt11.py

* /api/v1/payments with hex of description

* comment

* refactor wallets

* forgot eclair

* fix lnpay

* bytes directly

* make format

* mypy check

* make format

* remove old code

* WIP status check

* LND GRPC docs

* restore cln to main

* fix regtest

* import

* remove unused import

* format

* do not expect ok

* check ok

* delete comments
This commit is contained in:
calle 2022-08-09 11:49:39 +02:00 committed by GitHub
parent 1f139884fe
commit 4fc0a25d41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2668 additions and 22025 deletions

View File

@ -40,7 +40,47 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
file: ./coverage.xml
LndWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.1.3
- name: Setup Regtest
run: |
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./tests
./tests
sudo chmod -R a+rwx .
- name: Install dependencies
run: |
poetry install
poetry add grpcio protobuf
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: LndWallet
LND_GRPC_ENDPOINT: localhost
LND_GRPC_PORT: 10009
LND_GRPC_CERT: docker/data/lnd-1/tls.cert
LND_GRPC_MACAROON: docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
CoreLightningWallet:
runs-on: ubuntu-latest
strategy:

View File

@ -37,6 +37,22 @@ or
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
### LND (gRPC)
Using this wallet requires the installation of the `grpcio` and `protobuf` Python packages.
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
- `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port
- `LND_GRPC_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
You can also use an AES-encrypted macaroon (more info) instead by using
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
### LNbits
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**

View File

@ -4,6 +4,8 @@ from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
from uuid import uuid4
from loguru import logger
from lnbits import bolt11
from lnbits.db import COCKROACH, POSTGRES, Connection
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
@ -334,7 +336,7 @@ async def delete_expired_invoices(
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
if expiration_date > datetime.datetime.utcnow():
continue
logger.debug(f"Deleting expired invoice: {invoice.payment_hash}")
await (conn or db).execute(
"""
DELETE FROM apipayments

View File

@ -141,19 +141,25 @@ class Payment(BaseModel):
if self.is_uncheckable:
return
logger.debug(
f"Checking {'outgoing' if self.is_out else 'incoming'} pending payment {self.checking_id}"
)
if self.is_out:
status = await WALLET.get_payment_status(self.checking_id)
else:
status = await WALLET.get_invoice_status(self.checking_id)
logger.debug(f"Status: {status}")
if self.is_out and status.failed:
logger.info(
f" - deleting outgoing failed payment {self.checking_id}: {status}"
f"Deleting outgoing failed payment {self.checking_id}: {status}"
)
await self.delete()
elif not status.pending:
logger.info(
f" - marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
f"Marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
)
await self.set_pending(status.pending)

View File

@ -182,7 +182,7 @@ async def pay_invoice(
payment_request, fee_reserve_msat
)
logger.debug(f"backend: pay_invoice finished {temp_id}")
if payment.checking_id:
if payment.ok and payment.checking_id:
logger.debug(f"creating final payment {payment.checking_id}")
async with db.connect() as conn:
await create_payment(
@ -196,7 +196,7 @@ async def pay_invoice(
logger.debug(f"deleting temporary payment {temp_id}")
await delete_payment(temp_id, conn=conn)
else:
logger.debug(f"backend payment failed, no checking_id {temp_id}")
logger.debug(f"backend payment failed")
async with db.connect() as conn:
logger.debug(f"deleting temporary payment {temp_id}")
await delete_payment(temp_id, conn=conn)
@ -337,13 +337,16 @@ async def perform_lnurlauth(
)
async def check_invoice_status(
async def check_transaction_status(
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
) -> PaymentStatus:
payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
if not payment:
return PaymentStatus(None)
status = await WALLET.get_invoice_status(payment.checking_id)
if payment.is_out:
status = await WALLET.get_payment_status(payment.checking_id)
else:
status = await WALLET.get_invoice_status(payment.checking_id)
if not payment.pending:
return status
if payment.is_out and status.failed:

View File

@ -48,7 +48,7 @@ from ..crud import (
from ..services import (
InvoiceFailure,
PaymentFailure,
check_invoice_status,
check_transaction_status,
create_invoice,
pay_invoice,
perform_lnurlauth,
@ -123,7 +123,7 @@ async def api_payments(
offset=offset,
)
for payment in pendingPayments:
await check_invoice_status(
await check_transaction_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
return await get_payments(
@ -407,7 +407,7 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
await check_invoice_status(payment.wallet_id, payment_hash)
await check_transaction_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
)

View File

@ -148,7 +148,9 @@ async def wallet(
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
logger.debug(
f"Access {'user '+ user.id + ' ' if user else ''} {'wallet ' + wallet_name if wallet_name else ''}"
)
userwallet = user.get_wallet(wallet_id) # type: ignore
if not userwallet:
return template_renderer().TemplateResponse(

View File

@ -6,7 +6,7 @@ from fastapi.params import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import check_invoice_status, create_invoice
from lnbits.core.services import check_transaction_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.lnaddress.models import CreateAddress, CreateDomain
@ -229,7 +229,7 @@ async def api_address_send_address(payment_hash):
address = await get_address(payment_hash)
domain = await get_domain(address.domain)
try:
status = await check_invoice_status(domain.wallet, payment_hash)
status = await check_transaction_status(domain.wallet, payment_hash)
is_paid = not status.pending
except Exception as e:
return {"paid": False, "error": str(e)}

View File

@ -4,7 +4,7 @@ from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import check_invoice_status, create_invoice
from lnbits.core.services import check_transaction_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type
from . import paywall_ext
@ -87,7 +87,7 @@ async def api_paywal_check_invoice(
status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
)
try:
status = await check_invoice_status(paywall.wallet, payment_hash)
status = await check_transaction_status(paywall.wallet, payment_hash)
is_paid = not status.pending
except Exception:
return {"paid": False}

View File

@ -5,7 +5,7 @@ from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import check_invoice_status, create_invoice
from lnbits.core.services import check_transaction_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.subdomains.models import CreateDomain, CreateSubdomain
@ -161,7 +161,7 @@ async def api_subdomain_make_subdomain(domain_id, data: CreateSubdomain):
async def api_subdomain_send_subdomain(payment_hash):
subdomain = await get_subdomain(payment_hash)
try:
status = await check_invoice_status(subdomain.wallet, payment_hash)
status = await check_transaction_status(subdomain.wallet, payment_hash)
is_paid = not status.pending
except Exception:
return {"paid": False}

View File

@ -6,6 +6,7 @@ from .cln import CoreLightningWallet as CLightningWallet
from .eclair import EclairWallet
from .fake import FakeWallet
from .lnbits import LNbitsWallet
from .lndgrpc import LndWallet
from .lndrest import LndRestWallet
from .lnpay import LNPayWallet
from .lntxbot import LntxbotWallet

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,871 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as lightning__pb2
import lnbits.wallets.lnd_grpc_files.router_pb2 as router__pb2
class RouterStub(object):
"""Router is a service that offers advanced interaction with the router
subsystem of the daemon.
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.SendPaymentV2 = channel.unary_stream(
"/routerrpc.Router/SendPaymentV2",
request_serializer=router__pb2.SendPaymentRequest.SerializeToString,
response_deserializer=lightning__pb2.Payment.FromString,
)
self.TrackPaymentV2 = channel.unary_stream(
"/routerrpc.Router/TrackPaymentV2",
request_serializer=router__pb2.TrackPaymentRequest.SerializeToString,
response_deserializer=lightning__pb2.Payment.FromString,
)
self.EstimateRouteFee = channel.unary_unary(
"/routerrpc.Router/EstimateRouteFee",
request_serializer=router__pb2.RouteFeeRequest.SerializeToString,
response_deserializer=router__pb2.RouteFeeResponse.FromString,
)
self.SendToRoute = channel.unary_unary(
"/routerrpc.Router/SendToRoute",
request_serializer=router__pb2.SendToRouteRequest.SerializeToString,
response_deserializer=router__pb2.SendToRouteResponse.FromString,
)
self.SendToRouteV2 = channel.unary_unary(
"/routerrpc.Router/SendToRouteV2",
request_serializer=router__pb2.SendToRouteRequest.SerializeToString,
response_deserializer=lightning__pb2.HTLCAttempt.FromString,
)
self.ResetMissionControl = channel.unary_unary(
"/routerrpc.Router/ResetMissionControl",
request_serializer=router__pb2.ResetMissionControlRequest.SerializeToString,
response_deserializer=router__pb2.ResetMissionControlResponse.FromString,
)
self.QueryMissionControl = channel.unary_unary(
"/routerrpc.Router/QueryMissionControl",
request_serializer=router__pb2.QueryMissionControlRequest.SerializeToString,
response_deserializer=router__pb2.QueryMissionControlResponse.FromString,
)
self.XImportMissionControl = channel.unary_unary(
"/routerrpc.Router/XImportMissionControl",
request_serializer=router__pb2.XImportMissionControlRequest.SerializeToString,
response_deserializer=router__pb2.XImportMissionControlResponse.FromString,
)
self.GetMissionControlConfig = channel.unary_unary(
"/routerrpc.Router/GetMissionControlConfig",
request_serializer=router__pb2.GetMissionControlConfigRequest.SerializeToString,
response_deserializer=router__pb2.GetMissionControlConfigResponse.FromString,
)
self.SetMissionControlConfig = channel.unary_unary(
"/routerrpc.Router/SetMissionControlConfig",
request_serializer=router__pb2.SetMissionControlConfigRequest.SerializeToString,
response_deserializer=router__pb2.SetMissionControlConfigResponse.FromString,
)
self.QueryProbability = channel.unary_unary(
"/routerrpc.Router/QueryProbability",
request_serializer=router__pb2.QueryProbabilityRequest.SerializeToString,
response_deserializer=router__pb2.QueryProbabilityResponse.FromString,
)
self.BuildRoute = channel.unary_unary(
"/routerrpc.Router/BuildRoute",
request_serializer=router__pb2.BuildRouteRequest.SerializeToString,
response_deserializer=router__pb2.BuildRouteResponse.FromString,
)
self.SubscribeHtlcEvents = channel.unary_stream(
"/routerrpc.Router/SubscribeHtlcEvents",
request_serializer=router__pb2.SubscribeHtlcEventsRequest.SerializeToString,
response_deserializer=router__pb2.HtlcEvent.FromString,
)
self.SendPayment = channel.unary_stream(
"/routerrpc.Router/SendPayment",
request_serializer=router__pb2.SendPaymentRequest.SerializeToString,
response_deserializer=router__pb2.PaymentStatus.FromString,
)
self.TrackPayment = channel.unary_stream(
"/routerrpc.Router/TrackPayment",
request_serializer=router__pb2.TrackPaymentRequest.SerializeToString,
response_deserializer=router__pb2.PaymentStatus.FromString,
)
self.HtlcInterceptor = channel.stream_stream(
"/routerrpc.Router/HtlcInterceptor",
request_serializer=router__pb2.ForwardHtlcInterceptResponse.SerializeToString,
response_deserializer=router__pb2.ForwardHtlcInterceptRequest.FromString,
)
self.UpdateChanStatus = channel.unary_unary(
"/routerrpc.Router/UpdateChanStatus",
request_serializer=router__pb2.UpdateChanStatusRequest.SerializeToString,
response_deserializer=router__pb2.UpdateChanStatusResponse.FromString,
)
class RouterServicer(object):
"""Router is a service that offers advanced interaction with the router
subsystem of the daemon.
"""
def SendPaymentV2(self, request, context):
"""
SendPaymentV2 attempts to route a payment described by the passed
PaymentRequest to the final destination. The call returns a stream of
payment updates.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def TrackPaymentV2(self, request, context):
"""
TrackPaymentV2 returns an update stream for the payment identified by the
payment hash.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def EstimateRouteFee(self, request, context):
"""
EstimateRouteFee allows callers to obtain a lower bound w.r.t how much it
may cost to send an HTLC to the target end destination.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def SendToRoute(self, request, context):
"""
Deprecated, use SendToRouteV2. SendToRoute attempts to make a payment via
the specified route. This method differs from SendPayment in that it
allows users to specify a full route manually. This can be used for
things like rebalancing, and atomic swaps. It differs from the newer
SendToRouteV2 in that it doesn't return the full HTLC information.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def SendToRouteV2(self, request, context):
"""
SendToRouteV2 attempts to make a payment via the specified route. This
method differs from SendPayment in that it allows users to specify a full
route manually. This can be used for things like rebalancing, and atomic
swaps.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def ResetMissionControl(self, request, context):
"""
ResetMissionControl clears all mission control state and starts with a clean
slate.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def QueryMissionControl(self, request, context):
"""
QueryMissionControl exposes the internal mission control state to callers.
It is a development feature.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def XImportMissionControl(self, request, context):
"""
XImportMissionControl is an experimental API that imports the state provided
to the internal mission control's state, using all results which are more
recent than our existing values. These values will only be imported
in-memory, and will not be persisted across restarts.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def GetMissionControlConfig(self, request, context):
"""
GetMissionControlConfig returns mission control's current config.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def SetMissionControlConfig(self, request, context):
"""
SetMissionControlConfig will set mission control's config, if the config
provided is valid.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def QueryProbability(self, request, context):
"""
QueryProbability returns the current success probability estimate for a
given node pair and amount.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def BuildRoute(self, request, context):
"""
BuildRoute builds a fully specified route based on a list of hop public
keys. It retrieves the relevant channel policies from the graph in order to
calculate the correct fees and time locks.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def SubscribeHtlcEvents(self, request, context):
"""
SubscribeHtlcEvents creates a uni-directional stream from the server to
the client which delivers a stream of htlc events.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def SendPayment(self, request, context):
"""
Deprecated, use SendPaymentV2. SendPayment attempts to route a payment
described by the passed PaymentRequest to the final destination. The call
returns a stream of payment status updates.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def TrackPayment(self, request, context):
"""
Deprecated, use TrackPaymentV2. TrackPayment returns an update stream for
the payment identified by the payment hash.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def HtlcInterceptor(self, request_iterator, context):
"""*
HtlcInterceptor dispatches a bi-directional streaming RPC in which
Forwarded HTLC requests are sent to the client and the client responds with
a boolean that tells LND if this htlc should be intercepted.
In case of interception, the htlc can be either settled, cancelled or
resumed later by using the ResolveHoldForward endpoint.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def UpdateChanStatus(self, request, context):
"""
UpdateChanStatus attempts to manually set the state of a channel
(enabled, disabled, or auto). A manual "disable" request will cause the
channel to stay disabled until a subsequent manual request of either
"enable" or "auto".
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def add_RouterServicer_to_server(servicer, server):
rpc_method_handlers = {
"SendPaymentV2": grpc.unary_stream_rpc_method_handler(
servicer.SendPaymentV2,
request_deserializer=router__pb2.SendPaymentRequest.FromString,
response_serializer=lightning__pb2.Payment.SerializeToString,
),
"TrackPaymentV2": grpc.unary_stream_rpc_method_handler(
servicer.TrackPaymentV2,
request_deserializer=router__pb2.TrackPaymentRequest.FromString,
response_serializer=lightning__pb2.Payment.SerializeToString,
),
"EstimateRouteFee": grpc.unary_unary_rpc_method_handler(
servicer.EstimateRouteFee,
request_deserializer=router__pb2.RouteFeeRequest.FromString,
response_serializer=router__pb2.RouteFeeResponse.SerializeToString,
),
"SendToRoute": grpc.unary_unary_rpc_method_handler(
servicer.SendToRoute,
request_deserializer=router__pb2.SendToRouteRequest.FromString,
response_serializer=router__pb2.SendToRouteResponse.SerializeToString,
),
"SendToRouteV2": grpc.unary_unary_rpc_method_handler(
servicer.SendToRouteV2,
request_deserializer=router__pb2.SendToRouteRequest.FromString,
response_serializer=lightning__pb2.HTLCAttempt.SerializeToString,
),
"ResetMissionControl": grpc.unary_unary_rpc_method_handler(
servicer.ResetMissionControl,
request_deserializer=router__pb2.ResetMissionControlRequest.FromString,
response_serializer=router__pb2.ResetMissionControlResponse.SerializeToString,
),
"QueryMissionControl": grpc.unary_unary_rpc_method_handler(
servicer.QueryMissionControl,
request_deserializer=router__pb2.QueryMissionControlRequest.FromString,
response_serializer=router__pb2.QueryMissionControlResponse.SerializeToString,
),
"XImportMissionControl": grpc.unary_unary_rpc_method_handler(
servicer.XImportMissionControl,
request_deserializer=router__pb2.XImportMissionControlRequest.FromString,
response_serializer=router__pb2.XImportMissionControlResponse.SerializeToString,
),
"GetMissionControlConfig": grpc.unary_unary_rpc_method_handler(
servicer.GetMissionControlConfig,
request_deserializer=router__pb2.GetMissionControlConfigRequest.FromString,
response_serializer=router__pb2.GetMissionControlConfigResponse.SerializeToString,
),
"SetMissionControlConfig": grpc.unary_unary_rpc_method_handler(
servicer.SetMissionControlConfig,
request_deserializer=router__pb2.SetMissionControlConfigRequest.FromString,
response_serializer=router__pb2.SetMissionControlConfigResponse.SerializeToString,
),
"QueryProbability": grpc.unary_unary_rpc_method_handler(
servicer.QueryProbability,
request_deserializer=router__pb2.QueryProbabilityRequest.FromString,
response_serializer=router__pb2.QueryProbabilityResponse.SerializeToString,
),
"BuildRoute": grpc.unary_unary_rpc_method_handler(
servicer.BuildRoute,
request_deserializer=router__pb2.BuildRouteRequest.FromString,
response_serializer=router__pb2.BuildRouteResponse.SerializeToString,
),
"SubscribeHtlcEvents": grpc.unary_stream_rpc_method_handler(
servicer.SubscribeHtlcEvents,
request_deserializer=router__pb2.SubscribeHtlcEventsRequest.FromString,
response_serializer=router__pb2.HtlcEvent.SerializeToString,
),
"SendPayment": grpc.unary_stream_rpc_method_handler(
servicer.SendPayment,
request_deserializer=router__pb2.SendPaymentRequest.FromString,
response_serializer=router__pb2.PaymentStatus.SerializeToString,
),
"TrackPayment": grpc.unary_stream_rpc_method_handler(
servicer.TrackPayment,
request_deserializer=router__pb2.TrackPaymentRequest.FromString,
response_serializer=router__pb2.PaymentStatus.SerializeToString,
),
"HtlcInterceptor": grpc.stream_stream_rpc_method_handler(
servicer.HtlcInterceptor,
request_deserializer=router__pb2.ForwardHtlcInterceptResponse.FromString,
response_serializer=router__pb2.ForwardHtlcInterceptRequest.SerializeToString,
),
"UpdateChanStatus": grpc.unary_unary_rpc_method_handler(
servicer.UpdateChanStatus,
request_deserializer=router__pb2.UpdateChanStatusRequest.FromString,
response_serializer=router__pb2.UpdateChanStatusResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
"routerrpc.Router", rpc_method_handlers
)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class Router(object):
"""Router is a service that offers advanced interaction with the router
subsystem of the daemon.
"""
@staticmethod
def SendPaymentV2(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_stream(
request,
target,
"/routerrpc.Router/SendPaymentV2",
router__pb2.SendPaymentRequest.SerializeToString,
lightning__pb2.Payment.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def TrackPaymentV2(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_stream(
request,
target,
"/routerrpc.Router/TrackPaymentV2",
router__pb2.TrackPaymentRequest.SerializeToString,
lightning__pb2.Payment.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def EstimateRouteFee(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/EstimateRouteFee",
router__pb2.RouteFeeRequest.SerializeToString,
router__pb2.RouteFeeResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def SendToRoute(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/SendToRoute",
router__pb2.SendToRouteRequest.SerializeToString,
router__pb2.SendToRouteResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def SendToRouteV2(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/SendToRouteV2",
router__pb2.SendToRouteRequest.SerializeToString,
lightning__pb2.HTLCAttempt.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def ResetMissionControl(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/ResetMissionControl",
router__pb2.ResetMissionControlRequest.SerializeToString,
router__pb2.ResetMissionControlResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def QueryMissionControl(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/QueryMissionControl",
router__pb2.QueryMissionControlRequest.SerializeToString,
router__pb2.QueryMissionControlResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def XImportMissionControl(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/XImportMissionControl",
router__pb2.XImportMissionControlRequest.SerializeToString,
router__pb2.XImportMissionControlResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def GetMissionControlConfig(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/GetMissionControlConfig",
router__pb2.GetMissionControlConfigRequest.SerializeToString,
router__pb2.GetMissionControlConfigResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def SetMissionControlConfig(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/SetMissionControlConfig",
router__pb2.SetMissionControlConfigRequest.SerializeToString,
router__pb2.SetMissionControlConfigResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def QueryProbability(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/QueryProbability",
router__pb2.QueryProbabilityRequest.SerializeToString,
router__pb2.QueryProbabilityResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def BuildRoute(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/BuildRoute",
router__pb2.BuildRouteRequest.SerializeToString,
router__pb2.BuildRouteResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def SubscribeHtlcEvents(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_stream(
request,
target,
"/routerrpc.Router/SubscribeHtlcEvents",
router__pb2.SubscribeHtlcEventsRequest.SerializeToString,
router__pb2.HtlcEvent.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def SendPayment(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_stream(
request,
target,
"/routerrpc.Router/SendPayment",
router__pb2.SendPaymentRequest.SerializeToString,
router__pb2.PaymentStatus.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def TrackPayment(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_stream(
request,
target,
"/routerrpc.Router/TrackPayment",
router__pb2.TrackPaymentRequest.SerializeToString,
router__pb2.PaymentStatus.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def HtlcInterceptor(
request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.stream_stream(
request_iterator,
target,
"/routerrpc.Router/HtlcInterceptor",
router__pb2.ForwardHtlcInterceptResponse.SerializeToString,
router__pb2.ForwardHtlcInterceptRequest.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def UpdateChanStatus(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/routerrpc.Router/UpdateChanStatus",
router__pb2.UpdateChanStatusRequest.SerializeToString,
router__pb2.UpdateChanStatusResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)

View File

@ -2,10 +2,11 @@ imports_ok = True
try:
import grpc
from google import protobuf
from grpc import RpcError
except ImportError: # pragma: nocover
imports_ok = False
import asyncio
import base64
import binascii
import hashlib
@ -19,6 +20,8 @@ from .macaroon import AESCipher, load_macaroon
if imports_ok:
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
from .base import (
InvoiceResponse,
@ -111,6 +114,7 @@ class LndWallet(Wallet):
f"{self.endpoint}:{self.port}", composite_creds
)
self.rpc = lnrpc.LightningStub(channel)
self.routerpc = routerrpc.RouterStub(channel)
def metadata_callback(self, _, callback):
callback([("macaroon", self.macaroon)], None)
@ -118,6 +122,8 @@ class LndWallet(Wallet):
async def status(self) -> StatusResponse:
try:
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
except RpcError as exc:
return StatusResponse(str(exc._details), 0)
except Exception as exc:
return StatusResponse(str(exc), 0)
@ -132,9 +138,10 @@ class LndWallet(Wallet):
params: Dict = {"value": amount, "expiry": 600, "private": True}
if description_hash:
params["description_hash"] = base64.b64encode(
hashlib.sha256(description_hash).digest()
) # as bytes directly
params["description_hash"] = hashlib.sha256(
description_hash
).digest() # as bytes directly
else:
params["memo"] = memo or ""
@ -150,18 +157,39 @@ class LndWallet(Wallet):
return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
req = ln.SendRequest(payment_request=bolt11, fee_limit=fee_limit_fixed)
resp = await self.rpc.SendPaymentSync(req)
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
req = router.SendPaymentRequest(
payment_request=bolt11,
fee_limit_msat=fee_limit_msat,
timeout_seconds=30,
no_inflight_updates=True,
)
try:
resp = await self.routerpc.SendPaymentV2(req).read()
except RpcError as exc:
return PaymentResponse(False, "", 0, None, exc._details)
except Exception as exc:
return PaymentResponse(False, "", 0, None, str(exc))
if resp.payment_error:
return PaymentResponse(False, "", 0, None, resp.payment_error)
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
statuses = {
0: None, # NON_EXISTENT
1: None, # IN_FLIGHT
2: True, # SUCCEEDED
3: False, # FAILED
}
r_hash = hashlib.sha256(resp.payment_preimage).digest()
checking_id = stringify_checking_id(r_hash)
fee_msat = resp.payment_route.total_fees_msat
preimage = resp.payment_preimage.hex()
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
if resp.status in [0, 1, 3]:
fee_msat = 0
preimage = ""
checking_id = ""
elif resp.status == 2: # SUCCEEDED
fee_msat = resp.htlcs[-1].route.total_fees_msat
preimage = resp.payment_preimage
checking_id = resp.payment_hash
return PaymentResponse(
statuses[resp.status], checking_id, fee_msat, preimage, None
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
@ -180,20 +208,55 @@ class LndWallet(Wallet):
return PaymentStatus(None)
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(True)
"""
This routine checks the payment status using routerpc.TrackPaymentV2.
"""
try:
r_hash = parse_checking_id(checking_id)
if len(r_hash) != 32:
raise binascii.Error
except binascii.Error:
# this may happen if we switch between backend wallets
# that use different checking_id formats
return PaymentStatus(None)
# for some reason our checking_ids are in base64 but the payment hashes
# returned here are in hex, lnd is weird
checking_id = checking_id.replace("_", "/")
checking_id = base64.b64decode(checking_id).hex()
resp = self.routerpc.TrackPaymentV2(
router.TrackPaymentRequest(payment_hash=r_hash)
)
# HTLCAttempt.HTLCStatus:
# https://github.com/lightningnetwork/lnd/blob/master/lnrpc/lightning.proto#L3641
statuses = {
0: None, # IN_FLIGHT
1: True, # "SUCCEEDED"
2: False, # "FAILED"
}
try:
async for payment in resp:
return PaymentStatus(statuses[payment.htlcs[-1].status])
except: # most likely the payment wasn't found
return PaymentStatus(None)
return PaymentStatus(None)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
request = ln.InvoiceSubscription()
try:
async for i in self.rpc.SubscribeInvoices(request):
if not i.settled:
continue
while True:
request = ln.InvoiceSubscription()
try:
async for i in self.rpc.SubscribeInvoices(request):
if not i.settled:
continue
checking_id = stringify_checking_id(i.r_hash)
yield checking_id
except error:
logger.error(error)
logger.error(
"lost connection to lnd InvoiceSubscription, please restart lnbits."
)
checking_id = stringify_checking_id(i.r_hash)
yield checking_id
except Exception as exc:
logger.error(
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds"
)
await asyncio.sleep(5)

View File

@ -142,15 +142,10 @@ class LndRestWallet(Wallet):
return PaymentStatus(True)
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
async with httpx.AsyncClient(verify=self.cert) as client:
r = await client.get(
url=f"{self.endpoint}/v1/payments",
headers=self.auth,
params={"max_payments": "20", "reversed": True},
)
if r.is_error:
return PaymentStatus(None)
"""
This routine checks the payment status using routerpc.TrackPaymentV2.
"""
url = f"{self.endpoint}/v2/router/track/{checking_id}"
# check payment.status:
# https://api.lightning.community/rest/index.html?python#peersynctype
@ -161,14 +156,27 @@ class LndRestWallet(Wallet):
"FAILED": False,
}
# for some reason our checking_ids are in base64 but the payment hashes
# returned here are in hex, lnd is weird
checking_id = checking_id.replace("_", "/")
checking_id = base64.b64decode(checking_id).hex()
for p in r.json()["payments"]:
if p["payment_hash"] == checking_id:
return PaymentStatus(statuses[p["status"]])
async with httpx.AsyncClient(
timeout=None, headers=self.auth, verify=self.cert
) as client:
async with client.stream("GET", url) as r:
async for l in r.aiter_lines():
try:
line = json.loads(l)
if line.get("error"):
logger.error(
line["error"]["message"]
if "message" in line["error"]
else line["error"]
)
return PaymentStatus(None)
payment = line.get("result")
if payment is not None and payment.get("status"):
return PaymentStatus(statuses[payment["status"]])
else:
return PaymentStatus(None)
except:
continue
return PaymentStatus(None)
@ -191,10 +199,8 @@ class LndRestWallet(Wallet):
payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash
except (OSError, httpx.ConnectError, httpx.ReadError):
pass
logger.error(
"lost connection to lnd invoices stream, retrying in 5 seconds"
)
await asyncio.sleep(5)
except Exception as exc:
logger.error(
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds"
)
await asyncio.sleep(5)