mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2024-11-19 01:43:42 +01:00
1900cf9aa4
* feat: add boltz client standalone fundingsource WIP. https://docs.boltz.exchange/v/boltz-client this fundingsource utilizing the boltz client standalone function: https://github.com/BoltzExchange/boltz-client/pull/123 this makes him act like a lightning node while submarine swapping everything on liquid network. like aqua does in its wallet. * feat: paid_invoices_stream * feat: proper invoice and payment status check * feat: authenticate over insecure channel aswell * chore: lint * docs: add more setup instructions * chore: add `boltz_client_cert` in frontend * feat: populate fee_msat in get_payment_status and get_invoice_status * fixup! * chore: bundle * added boltz logo * add BoltzWallet to __all__ * chore: bump grpcio and protobuf deps and add grpcio-tools as dev dependency * chore: update protos * feat: pass description when creating swap * fixup! * chore: bundle --------- Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
215 lines
8.0 KiB
Python
215 lines
8.0 KiB
Python
import asyncio
|
|
from typing import AsyncGenerator, Optional
|
|
|
|
from bolt11.decode import decode
|
|
from grpc.aio import AioRpcError
|
|
from loguru import logger
|
|
|
|
from lnbits.settings import settings
|
|
from lnbits.wallets.boltz_grpc_files import boltzrpc_pb2, boltzrpc_pb2_grpc
|
|
from lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc import grpc
|
|
from lnbits.wallets.macaroon.macaroon import load_macaroon
|
|
|
|
from .base import (
|
|
InvoiceResponse,
|
|
PaymentFailedStatus,
|
|
PaymentPendingStatus,
|
|
PaymentResponse,
|
|
PaymentStatus,
|
|
PaymentSuccessStatus,
|
|
StatusResponse,
|
|
Wallet,
|
|
)
|
|
|
|
|
|
class BoltzWallet(Wallet):
|
|
"""
|
|
Utilizing Boltz Client gRPC interface
|
|
|
|
gRPC Bindings can be updated by running lnbits/wallets/boltz_grpc_files/update.sh
|
|
"""
|
|
|
|
async def cleanup(self):
|
|
logger.warning("Cleaning up BoltzWallet...")
|
|
|
|
def __init__(self):
|
|
if not settings.boltz_client_endpoint:
|
|
raise ValueError(
|
|
"cannot initialize BoltzWallet: missing boltz_client_endpoint"
|
|
)
|
|
if not settings.boltz_client_wallet:
|
|
raise ValueError(
|
|
"cannot initialize BoltzWallet: missing boltz_client_wallet"
|
|
)
|
|
|
|
self.endpoint = self.normalize_endpoint(
|
|
settings.boltz_client_endpoint, add_proto=True
|
|
)
|
|
|
|
if settings.boltz_client_macaroon:
|
|
self.metadata = [
|
|
("macaroon", load_macaroon(settings.boltz_client_macaroon))
|
|
]
|
|
else:
|
|
self.metadata = None
|
|
|
|
if settings.boltz_client_cert:
|
|
cert = open(settings.boltz_client_cert, "rb").read()
|
|
creds = grpc.ssl_channel_credentials(cert)
|
|
channel = grpc.aio.secure_channel(settings.boltz_client_endpoint, creds)
|
|
else:
|
|
channel = grpc.aio.insecure_channel(settings.boltz_client_endpoint)
|
|
|
|
self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel)
|
|
self.wallet_id: int = 0
|
|
|
|
async def status(self) -> StatusResponse:
|
|
try:
|
|
request = boltzrpc_pb2.GetWalletRequest(name=settings.boltz_client_wallet)
|
|
response: boltzrpc_pb2.Wallet = await self.rpc.GetWallet(
|
|
request, metadata=self.metadata
|
|
)
|
|
except AioRpcError as exc:
|
|
return StatusResponse(
|
|
exc.details()
|
|
+ " make sure you have macaroon and certificate configured, unless your client runs without", # noqa: E501
|
|
0,
|
|
)
|
|
|
|
self.wallet_id = response.id
|
|
|
|
return StatusResponse(None, response.balance.total * 1000)
|
|
|
|
async def create_invoice(
|
|
self,
|
|
amount: int,
|
|
memo: Optional[str] = None,
|
|
description_hash: Optional[bytes] = None,
|
|
unhashed_description: Optional[bytes] = None,
|
|
**_,
|
|
) -> InvoiceResponse:
|
|
pair = boltzrpc_pb2.Pair(to=boltzrpc_pb2.LBTC)
|
|
request = boltzrpc_pb2.CreateReverseSwapRequest(
|
|
amount=amount,
|
|
pair=pair,
|
|
wallet_id=self.wallet_id,
|
|
accept_zero_conf=True,
|
|
external_pay=True,
|
|
description=memo,
|
|
)
|
|
response: boltzrpc_pb2.CreateReverseSwapResponse
|
|
try:
|
|
response = await self.rpc.CreateReverseSwap(request, metadata=self.metadata)
|
|
except AioRpcError as exc:
|
|
return InvoiceResponse(ok=False, error_message=exc.details())
|
|
return InvoiceResponse(
|
|
ok=True, checking_id=response.id, payment_request=response.invoice
|
|
)
|
|
|
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
|
pair = boltzrpc_pb2.Pair(**{"from": boltzrpc_pb2.LBTC})
|
|
try:
|
|
pair_info: boltzrpc_pb2.PairInfo
|
|
pair_request = boltzrpc_pb2.GetPairInfoRequest(
|
|
type=boltzrpc_pb2.SUBMARINE, pair=pair
|
|
)
|
|
pair_info = await self.rpc.GetPairInfo(pair_request, metadata=self.metadata)
|
|
invoice = decode(bolt11)
|
|
|
|
assert invoice.amount_msat, "amountless invoice"
|
|
service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100
|
|
estimate = service_fee + pair_info.fees.miner_fees * 1000
|
|
if estimate > fee_limit_msat:
|
|
error = f"fee of {estimate} msat exceeds limit of {fee_limit_msat} msat"
|
|
|
|
return PaymentResponse(ok=False, error_message=error)
|
|
|
|
request = boltzrpc_pb2.CreateSwapRequest(
|
|
invoice=bolt11,
|
|
pair=pair,
|
|
wallet_id=self.wallet_id,
|
|
zero_conf=True,
|
|
send_from_internal=True,
|
|
)
|
|
response: boltzrpc_pb2.CreateSwapResponse
|
|
response = await self.rpc.CreateSwap(request, metadata=self.metadata)
|
|
except AioRpcError as exc:
|
|
return PaymentResponse(ok=False, error_message=exc.details())
|
|
|
|
try:
|
|
info_request = boltzrpc_pb2.GetSwapInfoRequest(id=response.id)
|
|
info: boltzrpc_pb2.GetSwapInfoResponse
|
|
async for info in self.rpc.GetSwapInfoStream(
|
|
info_request, metadata=self.metadata
|
|
):
|
|
if info.swap.state == boltzrpc_pb2.SUCCESSFUL:
|
|
return PaymentResponse(
|
|
ok=True,
|
|
checking_id=response.id,
|
|
fee_msat=(info.swap.onchain_fee + info.swap.service_fee) * 1000,
|
|
preimage=info.swap.preimage,
|
|
)
|
|
elif info.swap.error != "":
|
|
return PaymentResponse(ok=False, error_message=info.swap.error)
|
|
return PaymentResponse(
|
|
ok=False, error_message="stream stopped unexpectedly"
|
|
)
|
|
except AioRpcError as exc:
|
|
return PaymentResponse(ok=False, error_message=exc.details())
|
|
|
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
|
try:
|
|
response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo(
|
|
boltzrpc_pb2.GetSwapInfoRequest(id=checking_id), metadata=self.metadata
|
|
)
|
|
swap = response.reverse_swap
|
|
except AioRpcError:
|
|
return PaymentPendingStatus()
|
|
if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL:
|
|
return PaymentSuccessStatus(
|
|
fee_msat=(
|
|
(swap.service_fee + swap.onchain_fee) * 1000 + swap.routing_fee_msat
|
|
),
|
|
preimage=swap.preimage,
|
|
)
|
|
elif swap.state == boltzrpc_pb2.SwapState.PENDING:
|
|
return PaymentPendingStatus()
|
|
|
|
return PaymentFailedStatus()
|
|
|
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
|
try:
|
|
response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo(
|
|
boltzrpc_pb2.GetSwapInfoRequest(id=checking_id), metadata=self.metadata
|
|
)
|
|
swap = response.swap
|
|
except AioRpcError:
|
|
return PaymentPendingStatus()
|
|
if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL:
|
|
return PaymentSuccessStatus(
|
|
fee_msat=(swap.service_fee + swap.onchain_fee) * 1000,
|
|
preimage=swap.preimage,
|
|
)
|
|
elif swap.state == boltzrpc_pb2.SwapState.PENDING:
|
|
return PaymentPendingStatus()
|
|
|
|
return PaymentFailedStatus()
|
|
|
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
|
while True:
|
|
try:
|
|
request = boltzrpc_pb2.GetSwapInfoRequest()
|
|
info: boltzrpc_pb2.GetSwapInfoResponse
|
|
async for info in self.rpc.GetSwapInfoStream(
|
|
request, metadata=self.metadata
|
|
):
|
|
reverse = info.reverse_swap
|
|
if reverse and reverse.state == boltzrpc_pb2.SUCCESSFUL:
|
|
yield reverse.id
|
|
except Exception as exc:
|
|
logger.error(
|
|
f"lost connection to boltz client swap stream: '{exc}', retrying in"
|
|
" 5 seconds"
|
|
)
|
|
await asyncio.sleep(5)
|