mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-22 03:24:18 +01:00
Mint: Add clnrest.py Lightning backend (#551)
* log cln error * return a string * update corelightningrest to work with latest ver using rune * fix mpp spec and backend support check * refactor validation in ledger * remove weird error * fix mpp melt model * corelightningrest.py: Added Multi-Mint payout support lndrest.py: fix `quote.amount` is not always in sats + better checks * small fix * Fix quote.unit str2unit conversion + add missing imports * settings enable mpp corelightning (default false) * small fix * fix `paid_invoice_stream` * make format * handle runes * load rune * rename to MINT_CORELIGHTNING_REST_RUNE * try without cert * port * try except callback dispatcher * clean up cln-rest streaming parser * conftest: mint_corelightning_enable_mpp * enable mpp in regtest.yaml * fix error handling clnrest, remove lndrest changes * CLNRest + CoreLightningRest * clean up corelightningrest and get last index before starting the stream * clean up --------- Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> Co-authored-by: Richard Bensberg <r@coinbatsu.com>
This commit is contained in:
@@ -57,7 +57,7 @@ MINT_DERIVATION_PATH="m/0'/0'/0'"
|
|||||||
MINT_DATABASE=data/mint
|
MINT_DATABASE=data/mint
|
||||||
|
|
||||||
# Funding source backends
|
# Funding source backends
|
||||||
# Supported: FakeWallet, LndRestWallet, CoreLightningRestWallet, BlinkWallet, LNbitsWallet, StrikeWallet
|
# Supported: FakeWallet, LndRestWallet, CLNRestWallet, BlinkWallet, LNbitsWallet, StrikeWallet, CoreLightningRestWallet (deprecated)
|
||||||
MINT_BACKEND_BOLT11_SAT=FakeWallet
|
MINT_BACKEND_BOLT11_SAT=FakeWallet
|
||||||
# Only works if a usd derivation path is set
|
# Only works if a usd derivation path is set
|
||||||
# MINT_BACKEND_BOLT11_SAT=FakeWallet
|
# MINT_BACKEND_BOLT11_SAT=FakeWallet
|
||||||
@@ -72,7 +72,13 @@ MINT_LND_REST_CERT="/home/lnd/.lnd/tls.cert"
|
|||||||
MINT_LND_REST_MACAROON="/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon"
|
MINT_LND_REST_MACAROON="/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon"
|
||||||
MINT_LND_REST_CERT_VERIFY=True
|
MINT_LND_REST_CERT_VERIFY=True
|
||||||
|
|
||||||
|
# Use with CLNRestWallet
|
||||||
|
MINT_CLNREST_URL=https://localhost:3010
|
||||||
|
MINT_CLNREST_CERT="./clightning-2/regtest/ca.pem"
|
||||||
|
MINT_CLNREST_RUNE="Base64string== or path to file containing the rune"
|
||||||
|
|
||||||
# Use with CoreLightningRestWallet
|
# Use with CoreLightningRestWallet
|
||||||
|
# Note: CoreLightningRestWallet is deprecated, use CLNRestWallet instead
|
||||||
MINT_CORELIGHTNING_REST_URL=https://localhost:3001
|
MINT_CORELIGHTNING_REST_URL=https://localhost:3001
|
||||||
MINT_CORELIGHTNING_REST_MACAROON="./clightning-rest/access.macaroon"
|
MINT_CORELIGHTNING_REST_MACAROON="./clightning-rest/access.macaroon"
|
||||||
MINT_CORELIGHTNING_REST_CERT="./clightning-2-rest/certificate.pem"
|
MINT_CORELIGHTNING_REST_CERT="./clightning-2-rest/certificate.pem"
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
python-version: ["3.10"]
|
python-version: ["3.10"]
|
||||||
poetry-version: ["1.7.1"]
|
poetry-version: ["1.7.1"]
|
||||||
backend-wallet-class:
|
backend-wallet-class:
|
||||||
["LndRestWallet", "CoreLightningRestWallet", "LNbitsWallet"]
|
["LndRestWallet", "CLNRestWallet", "CoreLightningRestWallet", "LNbitsWallet"]
|
||||||
# mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"]
|
# mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"]
|
||||||
mint-database: ["./test_data/test_mint"]
|
mint-database: ["./test_data/test_mint"]
|
||||||
with:
|
with:
|
||||||
|
|||||||
7
.github/workflows/regtest.yml
vendored
7
.github/workflows/regtest.yml
vendored
@@ -70,10 +70,15 @@ jobs:
|
|||||||
# LND_GRPC_PORT: 10009
|
# LND_GRPC_PORT: 10009
|
||||||
# LND_GRPC_CERT: ./regtest/data/lnd-3/tls.cert
|
# LND_GRPC_CERT: ./regtest/data/lnd-3/tls.cert
|
||||||
# LND_GRPC_MACAROON: ./regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
# LND_GRPC_MACAROON: ./regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||||
# CORELIGHTNING_RPC: ./regtest/data/clightning-1/regtest/lightning-rpc
|
# CoreLightningRestWallet
|
||||||
MINT_CORELIGHTNING_REST_URL: https://localhost:3001
|
MINT_CORELIGHTNING_REST_URL: https://localhost:3001
|
||||||
MINT_CORELIGHTNING_REST_MACAROON: ./regtest/data/clightning-2-rest/access.macaroon
|
MINT_CORELIGHTNING_REST_MACAROON: ./regtest/data/clightning-2-rest/access.macaroon
|
||||||
MINT_CORELIGHTNING_REST_CERT: ./regtest/data/clightning-2-rest/certificate.pem
|
MINT_CORELIGHTNING_REST_CERT: ./regtest/data/clightning-2-rest/certificate.pem
|
||||||
|
# CLNRestWallet
|
||||||
|
MINT_CLNREST_URL: https://localhost:3010
|
||||||
|
MINT_CLNREST_RUNE: ./regtest/data/clightning-2/rune
|
||||||
|
MINT_CLNREST_CERT: ./regtest/data/clightning-2/regtest/ca.pem
|
||||||
|
MINT_CLNENABLE_MPP: false
|
||||||
run: |
|
run: |
|
||||||
sudo chmod -R 777 .
|
sudo chmod -R 777 .
|
||||||
make test
|
make test
|
||||||
|
|||||||
@@ -193,6 +193,13 @@ class LndRestFundingSource(MintSettings):
|
|||||||
mint_lnd_enable_mpp: bool = Field(default=False)
|
mint_lnd_enable_mpp: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class CLNRestFundingSource(MintSettings):
|
||||||
|
mint_clnrest_url: Optional[str] = Field(default=None)
|
||||||
|
mint_clnrest_cert: Optional[str] = Field(default=None)
|
||||||
|
mint_clnrest_rune: Optional[str] = Field(default=None)
|
||||||
|
mint_clnrest_enable_mpp: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
class CoreLightningRestFundingSource(MintSettings):
|
class CoreLightningRestFundingSource(MintSettings):
|
||||||
mint_corelightning_rest_url: Optional[str] = Field(default=None)
|
mint_corelightning_rest_url: Optional[str] = Field(default=None)
|
||||||
mint_corelightning_rest_macaroon: Optional[str] = Field(default=None)
|
mint_corelightning_rest_macaroon: Optional[str] = Field(default=None)
|
||||||
@@ -203,6 +210,7 @@ class Settings(
|
|||||||
EnvSettings,
|
EnvSettings,
|
||||||
LndRestFundingSource,
|
LndRestFundingSource,
|
||||||
CoreLightningRestFundingSource,
|
CoreLightningRestFundingSource,
|
||||||
|
CLNRestFundingSource,
|
||||||
FakeWalletSettings,
|
FakeWalletSettings,
|
||||||
MintLimits,
|
MintLimits,
|
||||||
MintBackends,
|
MintBackends,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# type: ignore
|
# type: ignore
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from .blink import BlinkWallet # noqa: F401
|
from .blink import BlinkWallet # noqa: F401
|
||||||
|
from .clnrest import CLNRestWallet # noqa: F401
|
||||||
from .corelightningrest import CoreLightningRestWallet # noqa: F401
|
from .corelightningrest import CoreLightningRestWallet # noqa: F401
|
||||||
from .fake import FakeWallet # noqa: F401
|
from .fake import FakeWallet # noqa: F401
|
||||||
from .lnbits import LNbitsWallet # noqa: F401
|
from .lnbits import LNbitsWallet # noqa: F401
|
||||||
|
|||||||
364
cashu/lightning/clnrest.py
Normal file
364
cashu/lightning/clnrest.py
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from bolt11 import (
|
||||||
|
Bolt11Exception,
|
||||||
|
decode,
|
||||||
|
)
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from ..core.base import Amount, MeltQuote, Unit
|
||||||
|
from ..core.helpers import fee_reserve
|
||||||
|
from ..core.models import PostMeltQuoteRequest
|
||||||
|
from ..core.settings import settings
|
||||||
|
from .base import (
|
||||||
|
InvoiceResponse,
|
||||||
|
LightningBackend,
|
||||||
|
PaymentQuoteResponse,
|
||||||
|
PaymentResponse,
|
||||||
|
PaymentStatus,
|
||||||
|
StatusResponse,
|
||||||
|
Unsupported,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CLNRestWallet(LightningBackend):
|
||||||
|
supported_units = set([Unit.sat, Unit.msat])
|
||||||
|
unit = Unit.sat
|
||||||
|
supports_mpp = False # settings.mint_clnrest_enable_mpp
|
||||||
|
supports_incoming_payment_stream: bool = True
|
||||||
|
|
||||||
|
def __init__(self, unit: Unit = Unit.sat, **kwargs):
|
||||||
|
self.assert_unit_supported(unit)
|
||||||
|
self.unit = unit
|
||||||
|
rune_settings = settings.mint_clnrest_rune
|
||||||
|
if not rune_settings:
|
||||||
|
raise Exception("missing rune for clnrest")
|
||||||
|
# load from file or use as is
|
||||||
|
if os.path.exists(rune_settings):
|
||||||
|
with open(rune_settings, "r") as f:
|
||||||
|
rune = f.read()
|
||||||
|
rune = rune.strip()
|
||||||
|
else:
|
||||||
|
rune = rune_settings
|
||||||
|
self.rune = rune
|
||||||
|
|
||||||
|
url = settings.mint_clnrest_url
|
||||||
|
if not url:
|
||||||
|
raise Exception("missing url for clnrest")
|
||||||
|
if not rune:
|
||||||
|
raise Exception("missing rune for clnrest")
|
||||||
|
|
||||||
|
self.url = url[:-1] if url.endswith("/") else url
|
||||||
|
self.url = (
|
||||||
|
f"https://{self.url}" if not self.url.startswith("http") else self.url
|
||||||
|
)
|
||||||
|
self.auth = {
|
||||||
|
"rune": self.rune,
|
||||||
|
"accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cert = settings.mint_clnrest_cert or False
|
||||||
|
self.client = httpx.AsyncClient(
|
||||||
|
base_url=self.url, verify=self.cert, headers=self.auth
|
||||||
|
)
|
||||||
|
self.last_pay_index = 0
|
||||||
|
self.statuses = {
|
||||||
|
"paid": True,
|
||||||
|
"complete": True,
|
||||||
|
"failed": False,
|
||||||
|
"pending": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
try:
|
||||||
|
await self.client.aclose()
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.warning(f"Error closing wallet connection: {e}")
|
||||||
|
|
||||||
|
async def status(self) -> StatusResponse:
|
||||||
|
r = await self.client.post("/v1/listfunds", timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
if r.is_error or "message" in r.json():
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
error_message = data["message"]
|
||||||
|
except Exception:
|
||||||
|
error_message = r.text
|
||||||
|
return StatusResponse(
|
||||||
|
error_message=(
|
||||||
|
f"Failed to connect to {self.url}, got: '{error_message}...'"
|
||||||
|
),
|
||||||
|
balance=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
if len(data) == 0:
|
||||||
|
return StatusResponse(error_message="no data", balance=0)
|
||||||
|
balance_msat = int(sum([c["our_amount_msat"] for c in data["channels"]]))
|
||||||
|
return StatusResponse(error_message=None, balance=balance_msat)
|
||||||
|
|
||||||
|
async def create_invoice(
|
||||||
|
self,
|
||||||
|
amount: Amount,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> InvoiceResponse:
|
||||||
|
self.assert_unit_supported(amount.unit)
|
||||||
|
label = f"lbl{random.random()}"
|
||||||
|
data: Dict = {
|
||||||
|
"amount_msat": amount.to(Unit.msat, round="up").amount,
|
||||||
|
"description": memo,
|
||||||
|
"label": label,
|
||||||
|
}
|
||||||
|
if description_hash and not unhashed_description:
|
||||||
|
raise Unsupported(
|
||||||
|
"'description_hash' unsupported by CLNRestWallet, "
|
||||||
|
"provide 'unhashed_description'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if unhashed_description:
|
||||||
|
data["description"] = unhashed_description.decode("utf-8")
|
||||||
|
|
||||||
|
if kwargs.get("expiry"):
|
||||||
|
data["expiry"] = kwargs["expiry"]
|
||||||
|
|
||||||
|
if kwargs.get("preimage"):
|
||||||
|
data["preimage"] = kwargs["preimage"]
|
||||||
|
|
||||||
|
r = await self.client.post(
|
||||||
|
"/v1/invoice",
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.is_error or "message" in r.json():
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
error_message = data["message"]
|
||||||
|
except Exception:
|
||||||
|
error_message = r.text
|
||||||
|
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False,
|
||||||
|
checking_id=None,
|
||||||
|
payment_request=None,
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
assert "payment_hash" in data
|
||||||
|
assert "bolt11" in data
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=data["payment_hash"],
|
||||||
|
payment_request=data["bolt11"],
|
||||||
|
error_message=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pay_invoice(
|
||||||
|
self, quote: MeltQuote, fee_limit_msat: int
|
||||||
|
) -> PaymentResponse:
|
||||||
|
try:
|
||||||
|
invoice = decode(quote.request)
|
||||||
|
except Bolt11Exception as exc:
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=False,
|
||||||
|
checking_id=None,
|
||||||
|
fee=None,
|
||||||
|
preimage=None,
|
||||||
|
error_message=str(exc),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||||
|
error_message = "0 amount invoices are not allowed"
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=False,
|
||||||
|
checking_id=None,
|
||||||
|
fee=None,
|
||||||
|
preimage=None,
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
quote_amount_msat = Amount(Unit[quote.unit], quote.amount).to(Unit.msat).amount
|
||||||
|
fee_limit_percent = fee_limit_msat / quote_amount_msat * 100
|
||||||
|
post_data = {
|
||||||
|
"bolt11": quote.request,
|
||||||
|
"maxfeepercent": f"{fee_limit_percent:.11}",
|
||||||
|
"exemptfee": 0, # so fee_limit_percent is applied even on payments
|
||||||
|
# with fee < 5000 millisatoshi (which is default value of exemptfee)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle Multi-Mint payout where we must only pay part of the invoice amount
|
||||||
|
if quote_amount_msat != invoice.amount_msat:
|
||||||
|
if self.supports_mpp:
|
||||||
|
post_data["partial_msat"] = quote_amount_msat
|
||||||
|
else:
|
||||||
|
error_message = "mint does not support MPP"
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=False,
|
||||||
|
checking_id=None,
|
||||||
|
fee=None,
|
||||||
|
preimage=None,
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
r = await self.client.post("/v1/pay", data=post_data, timeout=None)
|
||||||
|
|
||||||
|
if r.is_error or "message" in r.json():
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
error_message = str(data["message"])
|
||||||
|
except Exception:
|
||||||
|
error_message = r.text
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=False,
|
||||||
|
checking_id=None,
|
||||||
|
fee=None,
|
||||||
|
preimage=None,
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if data["status"] != "complete":
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=False,
|
||||||
|
checking_id=None,
|
||||||
|
fee=None,
|
||||||
|
preimage=None,
|
||||||
|
error_message="payment failed",
|
||||||
|
)
|
||||||
|
|
||||||
|
checking_id = data["payment_hash"]
|
||||||
|
preimage = data["payment_preimage"]
|
||||||
|
fee_msat = data["amount_sent_msat"] - data["amount_msat"]
|
||||||
|
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=self.statuses.get(data["status"]),
|
||||||
|
checking_id=checking_id,
|
||||||
|
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||||
|
preimage=preimage,
|
||||||
|
error_message=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
r = await self.client.post(
|
||||||
|
"/v1/listinvoices",
|
||||||
|
data={"payment_hash": checking_id},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if r.is_error or "message" in data or data.get("invoices") is None:
|
||||||
|
raise Exception("error in cln response")
|
||||||
|
return PaymentStatus(paid=self.statuses.get(data["invoices"][0]["status"]))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting invoice status: {e}")
|
||||||
|
return PaymentStatus(paid=None)
|
||||||
|
|
||||||
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
r = await self.client.post(
|
||||||
|
"/v1/listpays",
|
||||||
|
data={"payment_hash": checking_id},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if not data.get("pays"):
|
||||||
|
# payment not found
|
||||||
|
logger.error(f"payment not found: {data.get('pays')}")
|
||||||
|
raise Exception("payment not found")
|
||||||
|
|
||||||
|
if r.is_error or "message" in data:
|
||||||
|
message = data.get("message") or data
|
||||||
|
raise Exception(f"error in clnrest response: {message}")
|
||||||
|
|
||||||
|
pay = data["pays"][0]
|
||||||
|
|
||||||
|
fee_msat, preimage = None, None
|
||||||
|
if self.statuses[pay["status"]]:
|
||||||
|
# cut off "msat" and convert to int
|
||||||
|
fee_msat = -int(pay["amount_sent_msat"]) - int(pay["amount_msat"])
|
||||||
|
preimage = pay["preimage"]
|
||||||
|
|
||||||
|
return PaymentStatus(
|
||||||
|
paid=self.statuses.get(pay["status"]),
|
||||||
|
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting payment status: {e}")
|
||||||
|
return PaymentStatus(paid=None)
|
||||||
|
|
||||||
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
# call listinvoices to determine the last pay_index
|
||||||
|
r = await self.client.post("/v1/listinvoices")
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
if r.is_error or "message" in data:
|
||||||
|
raise Exception("error in cln response")
|
||||||
|
self.last_pay_index = data["invoices"][-1]["pay_index"]
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
url = "/v1/waitanyinvoice"
|
||||||
|
async with self.client.stream(
|
||||||
|
"POST",
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
"lastpay_index": self.last_pay_index,
|
||||||
|
},
|
||||||
|
timeout=None,
|
||||||
|
) as r:
|
||||||
|
async for line in r.aiter_lines():
|
||||||
|
inv = json.loads(line)
|
||||||
|
if "code" in inv and "message" in inv:
|
||||||
|
logger.error("Error in paid_invoices_stream:", inv)
|
||||||
|
raise Exception(inv["message"])
|
||||||
|
try:
|
||||||
|
paid = inv["status"] == "paid"
|
||||||
|
self.last_pay_index = inv["pay_index"]
|
||||||
|
if not paid:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in paid_invoices_stream: {e}")
|
||||||
|
continue
|
||||||
|
logger.trace(f"paid invoice: {inv}")
|
||||||
|
payment_hash = inv.get("payment_hash")
|
||||||
|
if payment_hash:
|
||||||
|
yield payment_hash
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(
|
||||||
|
f"lost connection to clnrest invoices stream: '{exc}', "
|
||||||
|
"reconnecting..."
|
||||||
|
)
|
||||||
|
await asyncio.sleep(0.02)
|
||||||
|
|
||||||
|
async def get_payment_quote(
|
||||||
|
self, melt_quote: PostMeltQuoteRequest
|
||||||
|
) -> PaymentQuoteResponse:
|
||||||
|
invoice_obj = decode(melt_quote.request)
|
||||||
|
assert invoice_obj.amount_msat, "invoice has no amount."
|
||||||
|
assert invoice_obj.amount_msat > 0, "invoice has 0 amount."
|
||||||
|
amount_msat = invoice_obj.amount_msat
|
||||||
|
if melt_quote.is_mpp:
|
||||||
|
amount_msat = (
|
||||||
|
Amount(Unit[melt_quote.unit], melt_quote.mpp_amount)
|
||||||
|
.to(Unit.msat)
|
||||||
|
.amount
|
||||||
|
)
|
||||||
|
fees_msat = fee_reserve(amount_msat)
|
||||||
|
fees = Amount(unit=Unit.msat, amount=fees_msat)
|
||||||
|
amount = Amount(unit=Unit.msat, amount=amount_msat)
|
||||||
|
return PaymentQuoteResponse(
|
||||||
|
checking_id=invoice_obj.payment_hash,
|
||||||
|
fee=fees.to(self.unit, round="up"),
|
||||||
|
amount=amount.to(self.unit, round="up"),
|
||||||
|
)
|
||||||
@@ -56,7 +56,9 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.cert = settings.mint_corelightning_rest_cert or False
|
self.cert = settings.mint_corelightning_rest_cert or False
|
||||||
self.client = httpx.AsyncClient(verify=self.cert, headers=self.auth)
|
self.client = httpx.AsyncClient(
|
||||||
|
base_url=self.url, verify=self.cert, headers=self.auth
|
||||||
|
)
|
||||||
self.last_pay_index = 0
|
self.last_pay_index = 0
|
||||||
self.statuses = {
|
self.statuses = {
|
||||||
"paid": True,
|
"paid": True,
|
||||||
@@ -72,7 +74,7 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
logger.warning(f"Error closing wallet connection: {e}")
|
logger.warning(f"Error closing wallet connection: {e}")
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
r = await self.client.get(f"{self.url}/v1/listFunds", timeout=5)
|
r = await self.client.get("/v1/listFunds", timeout=5)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
if r.is_error or "error" in r.json():
|
if r.is_error or "error" in r.json():
|
||||||
try:
|
try:
|
||||||
@@ -124,7 +126,7 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
data["preimage"] = kwargs["preimage"]
|
data["preimage"] = kwargs["preimage"]
|
||||||
|
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
f"{self.url}/v1/invoice/genInvoice",
|
"/v1/invoice/genInvoice",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -177,7 +179,7 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
)
|
)
|
||||||
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
f"{self.url}/v1/pay",
|
"/v1/pay",
|
||||||
data={
|
data={
|
||||||
"invoice": quote.request,
|
"invoice": quote.request,
|
||||||
"maxfeepercent": f"{fee_limit_percent:.11}",
|
"maxfeepercent": f"{fee_limit_percent:.11}",
|
||||||
@@ -226,7 +228,7 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(
|
r = await self.client.get(
|
||||||
f"{self.url}/v1/invoice/listInvoices",
|
"/v1/invoice/listInvoices",
|
||||||
params={"payment_hash": checking_id},
|
params={"payment_hash": checking_id},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -242,7 +244,7 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(
|
r = await self.client.get(
|
||||||
f"{self.url}/v1/pay/listPays",
|
"/v1/pay/listPays",
|
||||||
params={"payment_hash": checking_id},
|
params={"payment_hash": checking_id},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -276,9 +278,17 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(paid=None)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
# call listinvoices to determine the last pay_index
|
||||||
|
r = await self.client.get("/v1/invoice/listInvoices")
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
if r.is_error or "error" in data:
|
||||||
|
raise Exception("error in cln response")
|
||||||
|
self.last_pay_index = data["invoices"][-1]["pay_index"]
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
url = f"{self.url}/v1/invoice/waitAnyInvoice/{self.last_pay_index}"
|
url = f"/v1/invoice/waitAnyInvoice/{self.last_pay_index}"
|
||||||
async with self.client.stream("GET", url, timeout=None) as r:
|
async with self.client.stream("GET", url, timeout=None) as r:
|
||||||
async for line in r.aiter_lines():
|
async for line in r.aiter_lines():
|
||||||
inv = json.loads(line)
|
inv = json.loads(line)
|
||||||
@@ -299,7 +309,7 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
# yield payment_hash
|
# yield payment_hash
|
||||||
# hack to return payment_hash if the above shouldn't work
|
# hack to return payment_hash if the above shouldn't work
|
||||||
r = await self.client.get(
|
r = await self.client.get(
|
||||||
f"{self.url}/v1/invoice/listInvoices",
|
"/v1/invoice/listInvoices",
|
||||||
params={"label": inv["label"]},
|
params={"label": inv["label"]},
|
||||||
)
|
)
|
||||||
paid_invoce = r.json()
|
paid_invoce = r.json()
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ for key, value in settings.dict().items():
|
|||||||
"mint_lnd_rest_admin_macaroon",
|
"mint_lnd_rest_admin_macaroon",
|
||||||
"mint_lnd_rest_invoice_macaroon",
|
"mint_lnd_rest_invoice_macaroon",
|
||||||
"mint_corelightning_rest_macaroon",
|
"mint_corelightning_rest_macaroon",
|
||||||
|
"mint_clnrest_rune",
|
||||||
]:
|
]:
|
||||||
value = "********" if value is not None else None
|
value = "********" if value is not None else None
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents):
|
|||||||
|
|
||||||
async def invoice_callback_dispatcher(self, checking_id: str) -> None:
|
async def invoice_callback_dispatcher(self, checking_id: str) -> None:
|
||||||
logger.debug(f"Invoice callback dispatcher: {checking_id}")
|
logger.debug(f"Invoice callback dispatcher: {checking_id}")
|
||||||
# TODO: Explicitly check for the quote payment state before setting it as paid
|
# TODO: db read, quote.paid = True, db write should be refactored and moved to ledger.py
|
||||||
# db read, quote.paid = True, db write should be refactored and moved to ledger.py
|
|
||||||
quote = await self.crud.get_mint_quote(checking_id=checking_id, db=self.db)
|
quote = await self.crud.get_mint_quote(checking_id=checking_id, db=self.db)
|
||||||
if not quote:
|
if not quote:
|
||||||
logger.error(f"Quote not found for {checking_id}")
|
logger.error(f"Quote not found for {checking_id}")
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ settings.mint_seed_decryption_key = ""
|
|||||||
settings.mint_max_balance = 0
|
settings.mint_max_balance = 0
|
||||||
settings.mint_transaction_rate_limit_per_minute = 60
|
settings.mint_transaction_rate_limit_per_minute = 60
|
||||||
settings.mint_lnd_enable_mpp = True
|
settings.mint_lnd_enable_mpp = True
|
||||||
|
settings.mint_clnrest_enable_mpp = False
|
||||||
settings.mint_input_fee_ppk = 0
|
settings.mint_input_fee_ppk = 0
|
||||||
|
|
||||||
assert "test" in settings.cashu_dir
|
assert "test" in settings.cashu_dir
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger):
|
|||||||
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
|
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
# wallet pays 32 sat of the invoice
|
# wallet pays 32 sat of the invoice
|
||||||
quote = await wallet.melt_quote(invoice_payment_request, amount=32)
|
quote = await wallet.melt_quote(invoice_payment_request, amount=amount)
|
||||||
assert quote.amount == amount
|
assert quote.amount == amount
|
||||||
await wallet.melt(
|
await wallet.melt(
|
||||||
proofs,
|
proofs,
|
||||||
|
|||||||
Reference in New Issue
Block a user