test: add boltz fundingsource to regtest (#3677)

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
dni ⚡ 2025-12-22 09:23:46 +01:00 committed by GitHub
parent 281c3df826
commit 132192bc94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 137 additions and 57 deletions

View file

@ -18,7 +18,6 @@ ENABLE_LOG_TO_FILE=true
# https://loguru.readthedocs.io/en/stable/api/logger.html#file # https://loguru.readthedocs.io/en/stable/api/logger.html#file
LOG_ROTATION="100 MB" LOG_ROTATION="100 MB"
LOG_RETENTION="3 months" LOG_RETENTION="3 months"
# for database cleanup commands # for database cleanup commands
# CLEANUP_WALLETS_DAYS=90 # CLEANUP_WALLETS_DAYS=90
@ -187,7 +186,6 @@ BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon" BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
# HEXSTRING instead of path also possible # HEXSTRING instead of path also possible
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert" BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
BOLTZ_CLIENT_WALLET="lnbits"
# StrikeWallet # StrikeWallet
STRIKE_API_ENDPOINT=https://api.strike.me/v1 STRIKE_API_ENDPOINT=https://api.strike.me/v1
@ -333,4 +331,3 @@ LNBITS_RESERVE_FEE_PERCENT=1.0
###################################### ######################################
###### Logging and Development ####### ###### Logging and Development #######
###################################### ######################################

View file

@ -75,7 +75,14 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: ["3.10"] python-version: ["3.10"]
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"] backend-wallet-class:
- BoltzWallet
- LndRestWallet
- LndWallet
- CoreLightningWallet
- CoreLightningRestWallet
- LNbitsWallet
- EclairWallet
with: with:
custom-pytest: "uv run pytest tests/regtest" custom-pytest: "uv run pytest tests/regtest"
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}

View file

@ -40,8 +40,8 @@ jobs:
run: | run: |
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker cd docker
chmod +x ./tests chmod +x ./start-regtest
./tests ./start-regtest
sudo chmod -R a+rwx . sudo chmod -R a+rwx .
- name: Run pytest - name: Run pytest
@ -63,6 +63,8 @@ jobs:
LNBITS_ENDPOINT: http://localhost:5001 LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee" LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
ECLAIR_URL: http://127.0.0.1:8082 ECLAIR_URL: http://127.0.0.1:8082
BOLTZ_CLIENT_ENDPOINT: 127.0.0.1:9002
BOLTZ_MNEMONIC: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000 LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000 LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
ECLAIR_PASS: lnbits ECLAIR_PASS: lnbits

View file

@ -337,7 +337,7 @@ async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
return {"paid": False, "status": "failed"} return {"paid": False, "status": "failed"}
try: try:
status = await payment.check_status() payment = await update_pending_payment(payment)
except Exception: except Exception:
if wallet and wallet.id == payment.wallet_id: if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment} return {"paid": False, "details": payment}
@ -346,7 +346,7 @@ async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
if wallet and wallet.id == payment.wallet_id: if wallet and wallet.id == payment.wallet_id:
return { return {
"paid": payment.success, "paid": payment.success,
"status": f"{status!s}", "status": f"{payment.status!s}",
"preimage": payment.preimage, "preimage": payment.preimage,
"details": payment, "details": payment,
} }

View file

@ -43,12 +43,13 @@ class BoltzWallet(Wallet):
settings.boltz_client_endpoint, add_proto=True 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 self.metadata = None
if settings.boltz_client_macaroon:
try:
macaroon = load_macaroon(settings.boltz_client_macaroon)
self.metadata = [("macaroon", macaroon)]
except Exception as e:
logger.error(f"BoltzWallet failed to load macaroon: {e}")
if settings.boltz_client_cert: if settings.boltz_client_cert:
cert = open(settings.boltz_client_cert, "rb").read() cert = open(settings.boltz_client_cert, "rb").read()
@ -60,17 +61,18 @@ class BoltzWallet(Wallet):
self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel) self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel)
self.wallet_id = 0 self.wallet_id = 0
self.wallet_name = "lnbits" self.wallet_name = "lnbits"
self.wallet_ready = False
if settings.boltz_mnemonic: # restore wallet from mnemonic
self._init_wallet_task = asyncio.create_task(
self._restore_boltz_wallet(
settings.boltz_mnemonic, settings.boltz_client_password
)
)
else: # create new wallet
self._init_wallet_task = asyncio.create_task(self._create_boltz_wallet())
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
if self.wallet_ready is False:
self.wallet_ready = True
if settings.boltz_mnemonic: # restore wallet from mnemonic
await self._restore_boltz_wallet(
settings.boltz_mnemonic,
settings.boltz_client_password,
)
else: # create new wallet
await self._create_boltz_wallet()
try: try:
request = boltzrpc_pb2.GetWalletRequest(name=self.wallet_name) request = boltzrpc_pb2.GetWalletRequest(name=self.wallet_name)
response: boltzrpc_pb2.Wallet = await self.rpc.GetWallet( response: boltzrpc_pb2.Wallet = await self.rpc.GetWallet(
@ -111,12 +113,18 @@ class BoltzWallet(Wallet):
try: try:
response = await self.rpc.CreateReverseSwap(request, metadata=self.metadata) response = await self.rpc.CreateReverseSwap(request, metadata=self.metadata)
except AioRpcError as exc: except AioRpcError as exc:
logger.warning(exc)
return InvoiceResponse(ok=False, error_message=exc.details()) return InvoiceResponse(ok=False, error_message=exc.details())
fee_msat = response.routing_fee_milli_sat
return InvoiceResponse( return InvoiceResponse(
ok=True, checking_id=response.id, payment_request=response.invoice ok=True,
checking_id=response.id,
payment_request=response.invoice,
fee_msat=fee_msat,
) )
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
pair = boltzrpc_pb2.Pair(**{"from": boltzrpc_pb2.LBTC}) pair = boltzrpc_pb2.Pair(**{"from": boltzrpc_pb2.LBTC})
try: try:
pair_info: boltzrpc_pb2.PairInfo pair_info: boltzrpc_pb2.PairInfo
@ -128,8 +136,9 @@ class BoltzWallet(Wallet):
if not invoice.amount_msat: if not invoice.amount_msat:
raise ValueError("amountless invoice") raise ValueError("amountless invoice")
service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100 service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100
estimate = service_fee + pair_info.fees.miner_fees * 1000 estimate = int(service_fee + pair_info.fees.miner_fees * 1000)
if estimate > fee_limit_msat: if estimate > fee_limit_msat:
error = f"fee of {estimate} msat exceeds limit of {fee_limit_msat} msat" error = f"fee of {estimate} msat exceeds limit of {fee_limit_msat} msat"
@ -151,11 +160,12 @@ class BoltzWallet(Wallet):
if response.id == "": if response.id == "":
# note that there is no way to provide a checking id here, # note that there is no way to provide a checking id here,
# but there is no need since it immediately is considered as successfull # but there is no need since it immediately is considered as successfull
return PaymentResponse( logger.warning(
ok=True, "Boltz invoice paid directly on liquid network using magic routing"
checking_id=response.id,
) )
return PaymentResponse(ok=True, checking_id=invoice.payment_hash)
except AioRpcError as exc: except AioRpcError as exc:
logger.warning(exc)
return PaymentResponse(ok=False, error_message=exc.details()) return PaymentResponse(ok=False, error_message=exc.details())
try: try:
@ -165,10 +175,15 @@ class BoltzWallet(Wallet):
info_request, metadata=self.metadata info_request, metadata=self.metadata
): ):
if info.swap.state == boltzrpc_pb2.SUCCESSFUL: if info.swap.state == boltzrpc_pb2.SUCCESSFUL:
fee_msat = (info.swap.onchain_fee + info.swap.service_fee) * 1000
logger.debug(
f"Boltz swap successful, status: {info.swap.status}"
f"fee_msat: {fee_msat}"
)
return PaymentResponse( return PaymentResponse(
ok=True, ok=True,
checking_id=response.id, checking_id=invoice.payment_hash,
fee_msat=(info.swap.onchain_fee + info.swap.service_fee) * 1000, fee_msat=fee_msat,
preimage=info.swap.preimage, preimage=info.swap.preimage,
) )
elif info.swap.error != "": elif info.swap.error != "":
@ -177,21 +192,29 @@ class BoltzWallet(Wallet):
ok=False, error_message="stream stopped unexpectedly" ok=False, error_message="stream stopped unexpectedly"
) )
except AioRpcError as exc: except AioRpcError as exc:
logger.warning(exc)
return PaymentResponse(ok=False, error_message=exc.details()) return PaymentResponse(ok=False, error_message=exc.details())
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try: try:
request = boltzrpc_pb2.GetSwapInfoRequest(id=checking_id)
response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo( response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo(
boltzrpc_pb2.GetSwapInfoRequest(id=checking_id), metadata=self.metadata request,
metadata=self.metadata,
) )
swap = response.reverse_swap swap = response.reverse_swap
except AioRpcError: except AioRpcError as exc:
logger.warning(exc)
return PaymentPendingStatus() return PaymentPendingStatus()
if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL: if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL:
fee_msat = (
swap.service_fee + swap.onchain_fee
) * 1000 + swap.routing_fee_msat
logger.debug(
f"Boltz swap successful, status: {swap.status}, fee_msat: {fee_msat}"
)
return PaymentSuccessStatus( return PaymentSuccessStatus(
fee_msat=( fee_msat=fee_msat,
(swap.service_fee + swap.onchain_fee) * 1000 + swap.routing_fee_msat
),
preimage=swap.preimage, preimage=swap.preimage,
) )
elif swap.state == boltzrpc_pb2.SwapState.PENDING: elif swap.state == boltzrpc_pb2.SwapState.PENDING:
@ -201,18 +224,23 @@ class BoltzWallet(Wallet):
async def get_payment_status(self, checking_id: str) -> PaymentStatus: async def get_payment_status(self, checking_id: str) -> PaymentStatus:
try: try:
checking_id_bytes = bytes.fromhex(checking_id)
request = boltzrpc_pb2.GetSwapInfoRequest(payment_hash=checking_id_bytes)
response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo( response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo(
boltzrpc_pb2.GetSwapInfoRequest( request,
payment_hash=bytes.fromhex(checking_id)
),
metadata=self.metadata, metadata=self.metadata,
) )
swap = response.swap swap = response.swap
except AioRpcError: except AioRpcError as exc:
logger.warning(exc)
return PaymentPendingStatus() return PaymentPendingStatus()
if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL: if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL:
fee_msat = (swap.service_fee + swap.onchain_fee) * 1000
logger.debug(
f"Boltz swap successful, status: {swap.status}, fee_msat: {fee_msat}"
)
return PaymentSuccessStatus( return PaymentSuccessStatus(
fee_msat=(swap.service_fee + swap.onchain_fee) * 1000, fee_msat=fee_msat,
preimage=swap.preimage, preimage=swap.preimage,
) )
elif swap.state == boltzrpc_pb2.SwapState.PENDING: elif swap.state == boltzrpc_pb2.SwapState.PENDING:
@ -229,7 +257,16 @@ class BoltzWallet(Wallet):
request, metadata=self.metadata request, metadata=self.metadata
): ):
reverse = info.reverse_swap reverse = info.reverse_swap
if reverse and reverse.state == boltzrpc_pb2.SUCCESSFUL: if (
reverse
and reverse.state == boltzrpc_pb2.SUCCESSFUL
and reverse.status == "invoice.settled"
):
fee_msat = ((reverse.service_fee + reverse.onchain_fee) * 1000,)
logger.debug(
f"Boltz reverse swap settled: {reverse.id}, "
f"fee_msat: {fee_msat}"
)
yield reverse.id yield reverse.id
except Exception as exc: except Exception as exc:
logger.error( logger.error(
@ -244,13 +281,8 @@ class BoltzWallet(Wallet):
try: try:
request = boltzrpc_pb2.GetWalletRequest(name=wallet_name) request = boltzrpc_pb2.GetWalletRequest(name=wallet_name)
response = await self.rpc.GetWallet(request, metadata=self.metadata) response = await self.rpc.GetWallet(request, metadata=self.metadata)
logger.info(f"Wallet '{wallet_name}' already exists with ID {response.id}")
return response return response
except AioRpcError as exc: except AioRpcError:
if exc.code() != grpc.StatusCode.NOT_FOUND:
logger.warning(f"Wallet '{wallet_name}' does not exist.")
return None
logger.error(f"Error checking wallet existence: {exc.details()}")
return None return None
async def _delete_wallet(self, wallet_id: int) -> None: async def _delete_wallet(self, wallet_id: int) -> None:

View file

@ -331,8 +331,8 @@ def _settings_cleanup(settings: Settings):
settings.lnbits_allowed_users = [] settings.lnbits_allowed_users = []
settings.auth_allowed_methods = AuthMethods.all() settings.auth_allowed_methods = AuthMethods.all()
settings.auth_credetials_update_threshold = 120 settings.auth_credetials_update_threshold = 120
settings.lnbits_reserve_fee_percent = 1 settings.lnbits_reserve_fee_percent = 2
settings.lnbits_reserve_fee_min = 2000 settings.lnbits_reserve_fee_min = 20000
settings.lnbits_service_fee = 0 settings.lnbits_service_fee = 0
settings.lnbits_reserve_fee_percent = 0 settings.lnbits_reserve_fee_percent = 0
settings.lnbits_wallet_limit_daily_max_withdraw = 0 settings.lnbits_wallet_limit_daily_max_withdraw = 0

View file

@ -6,6 +6,11 @@ from subprocess import PIPE, Popen, TimeoutExpired
from loguru import logger from loguru import logger
from lnbits.wallets import get_funding_source
funding_source = get_funding_source()
is_boltz_wallet = funding_source.__class__.__name__ == "BoltzWallet"
docker_lightning_cli = [ docker_lightning_cli = [
"docker", "docker",
"exec", "exec",
@ -27,6 +32,16 @@ docker_bitcoin_cli = [
] ]
docker_elements_cli = [
"docker",
"exec",
"lnbits-elementsd-1",
"elements-cli",
"-rpcport=18884",
"-chain=liquidregtest",
]
docker_lightning_unconnected_cli = [ docker_lightning_unconnected_cli = [
"docker", "docker",
"exec", "exec",
@ -50,7 +65,7 @@ docker_lightning_noroute_cli = [
def run_cmd(cmd: list) -> str: def run_cmd(cmd: list) -> str:
timeout = 10 timeout = 30
process = Popen(cmd, stdout=PIPE, stderr=PIPE) process = Popen(cmd, stdout=PIPE, stderr=PIPE)
logger.debug(f"running command: {cmd}") logger.debug(f"running command: {cmd}")
@ -128,6 +143,12 @@ def mine_blocks(blocks: int = 1) -> str:
return run_cmd(cmd) return run_cmd(cmd)
def mine_blocks_liquid(blocks: int = 1) -> str:
cmd = docker_elements_cli.copy()
cmd.extend(["-generate", str(blocks)])
return run_cmd(cmd)
def get_unconnected_node_uri() -> str: def get_unconnected_node_uri() -> str:
cmd = docker_lightning_unconnected_cli.copy() cmd = docker_lightning_unconnected_cli.copy()
cmd.append("getinfo") cmd.append("getinfo")

View file

@ -18,6 +18,7 @@ from ..helpers import FakeError, is_fake, is_regtest
from .helpers import ( from .helpers import (
cancel_invoice, cancel_invoice,
get_real_invoice, get_real_invoice,
mine_blocks_liquid,
pay_real_invoice, pay_real_invoice,
settle_invoice, settle_invoice,
) )
@ -66,7 +67,7 @@ async def test_pay_real_invoice(
await asyncio.sleep(1) await asyncio.sleep(1)
balance = await get_node_balance_sats() balance = await get_node_balance_sats()
assert prev_balance - balance == 100 assert prev_balance - balance == 100 + abs(payment.fee // 1000)
@pytest.mark.anyio @pytest.mark.anyio
@ -153,7 +154,8 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
async def on_paid(payment: Payment): async def on_paid(payment: Payment):
assert payment.checking_id == invoice["payment_hash"] assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"]
response = await client.get( response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
@ -164,7 +166,8 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
await asyncio.sleep(1) await asyncio.sleep(1)
balance = await get_node_balance_sats() balance = await get_node_balance_sats()
assert balance - prev_balance == create_invoice.amount fee = abs(payment_status.get("details", {}).get("fee", 0) // 1000)
assert balance - prev_balance == create_invoice.amount - fee
assert payment_status.get("preimage") is not None assert payment_status.get("preimage") is not None
@ -200,6 +203,8 @@ async def test_pay_real_invoice_set_pending_and_check_state(
assert len(invoice["payment_hash"]) == 64 assert len(invoice["payment_hash"]) == 64
assert len(invoice["checking_id"]) > 0 assert len(invoice["checking_id"]) > 0
mine_blocks_liquid(1)
await asyncio.sleep(1)
# check the payment status # check the payment status
response = await client.get( response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
@ -341,6 +346,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
assert preimage_hash == invoice_obj.payment_hash assert preimage_hash == invoice_obj.payment_hash
cancel_invoice(preimage_hash) cancel_invoice(preimage_hash)
mine_blocks_liquid(1)
# check if paid # check if paid
await asyncio.sleep(1) await asyncio.sleep(1)
@ -382,7 +388,9 @@ async def test_receive_real_invoice_set_pending_and_check_state(
assert not payment_status["paid"] assert not payment_status["paid"]
async def on_paid(payment: Payment): async def on_paid(payment: Payment):
assert payment.checking_id == invoice["payment_hash"]
assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"]
response = await client.get( response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from

View file

@ -7,6 +7,8 @@ from lnbits.core.services import (
from lnbits.wallets import get_funding_source from lnbits.wallets import get_funding_source
from lnbits.wallets.base import PaymentStatus from lnbits.wallets.base import PaymentStatus
from .helpers import is_boltz_wallet
description = "test create invoice" description = "test create invoice"
@ -17,6 +19,9 @@ async def test_create_invoice(from_wallet):
amount=1000, amount=1000,
memo=description, memo=description,
) )
# we cannot know the preimage of the swap yet
if not is_boltz_wallet:
assert payment.preimage assert payment.preimage
invoice = decode(payment.bolt11) invoice = decode(payment.bolt11)
@ -35,6 +40,9 @@ async def test_create_internal_invoice(from_wallet):
payment = await create_invoice( payment = await create_invoice(
wallet_id=from_wallet.id, amount=1000, memo=description, internal=True wallet_id=from_wallet.id, amount=1000, memo=description, internal=True
) )
# we cannot know the preimage of the swap yet
if not is_boltz_wallet:
assert payment.preimage assert payment.preimage
invoice = decode(payment.bolt11) invoice = decode(payment.bolt11)

View file

@ -6,6 +6,8 @@ from lnbits.core.services import (
) )
from lnbits.exceptions import PaymentError from lnbits.exceptions import PaymentError
from .helpers import is_boltz_wallet
description = "test pay invoice" description = "test pay invoice"
@ -17,9 +19,12 @@ async def test_services_pay_invoice(to_wallet, real_invoice):
description=description, description=description,
) )
assert payment assert payment
assert payment.status == PaymentState.SUCCESS
assert payment.memo == description assert payment.memo == description
if not is_boltz_wallet:
assert payment.status == PaymentState.SUCCESS
assert payment.preimage assert payment.preimage
else:
assert payment.status == PaymentState.PENDING
@pytest.mark.anyio @pytest.mark.anyio