mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-07 10:54:20 +01:00
NUT-04: add description (#613)
* NUT-04: add description * skip test for deprecated api * fix for lndgrpc * add test for cli * add two random tests * add max length to request model validator * skip cli test with description for deprecated api * add cli test for invoice command * default value to None
This commit is contained in:
@@ -117,6 +117,9 @@ class KeysetsResponse_deprecated(BaseModel):
|
||||
class PostMintQuoteRequest(BaseModel):
|
||||
unit: str = Field(..., max_length=settings.mint_max_request_length) # output unit
|
||||
amount: int = Field(..., gt=0) # output amount
|
||||
description: Optional[str] = Field(
|
||||
default=None, max_length=settings.mint_max_request_length
|
||||
) # invoice description
|
||||
|
||||
|
||||
class PostMintQuoteResponse(BaseModel):
|
||||
@@ -206,7 +209,7 @@ class PostMeltQuoteResponse(BaseModel):
|
||||
state: Optional[str] # state of the quote
|
||||
expiry: Optional[int] # expiry of the quote
|
||||
payment_preimage: Optional[str] = None # payment preimage
|
||||
change: Union[List[BlindedSignature], None] = None
|
||||
change: Union[List[BlindedSignature], None] = None # NUT-08 change
|
||||
|
||||
@classmethod
|
||||
def from_melt_quote(self, melt_quote: MeltQuote) -> "PostMeltQuoteResponse":
|
||||
|
||||
@@ -70,6 +70,7 @@ class LightningBackend(ABC):
|
||||
supports_mpp: bool = False
|
||||
supports_incoming_payment_stream: bool = False
|
||||
supported_units: set[Unit]
|
||||
supports_description: bool = False
|
||||
unit: Unit
|
||||
|
||||
def assert_unit_supported(self, unit: Unit):
|
||||
|
||||
@@ -47,6 +47,7 @@ class BlinkWallet(LightningBackend):
|
||||
payment_statuses = {"SUCCESS": True, "PENDING": None, "FAILURE": False}
|
||||
|
||||
supported_units = set([Unit.sat, Unit.msat])
|
||||
supports_description: bool = True
|
||||
unit = Unit.sat
|
||||
|
||||
def __init__(self, unit: Unit = Unit.sat, **kwargs):
|
||||
|
||||
@@ -31,6 +31,7 @@ class CLNRestWallet(LightningBackend):
|
||||
unit = Unit.sat
|
||||
supports_mpp = settings.mint_clnrest_enable_mpp
|
||||
supports_incoming_payment_stream: bool = True
|
||||
supports_description: bool = True
|
||||
|
||||
def __init__(self, unit: Unit = Unit.sat, **kwargs):
|
||||
self.assert_unit_supported(unit)
|
||||
|
||||
@@ -30,6 +30,7 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
supported_units = set([Unit.sat, Unit.msat])
|
||||
unit = Unit.sat
|
||||
supports_incoming_payment_stream: bool = True
|
||||
supports_description: bool = True
|
||||
|
||||
def __init__(self, unit: Unit = Unit.sat, **kwargs):
|
||||
self.assert_unit_supported(unit)
|
||||
|
||||
@@ -48,6 +48,7 @@ class FakeWallet(LightningBackend):
|
||||
unit = Unit.sat
|
||||
|
||||
supports_incoming_payment_stream: bool = True
|
||||
supports_description: bool = True
|
||||
|
||||
def __init__(self, unit: Unit = Unit.sat, **kwargs):
|
||||
self.assert_unit_supported(unit)
|
||||
|
||||
@@ -28,6 +28,7 @@ class LNbitsWallet(LightningBackend):
|
||||
supported_units = set([Unit.sat])
|
||||
unit = Unit.sat
|
||||
supports_incoming_payment_stream: bool = True
|
||||
supports_description: bool = True
|
||||
|
||||
def __init__(self, unit: Unit = Unit.sat, **kwargs):
|
||||
self.assert_unit_supported(unit)
|
||||
|
||||
@@ -45,39 +45,43 @@ INVOICE_STATUSES = {
|
||||
lnrpc.Invoice.InvoiceState.ACCEPTED: None,
|
||||
}
|
||||
|
||||
class LndRPCWallet(LightningBackend):
|
||||
|
||||
class LndRPCWallet(LightningBackend):
|
||||
supports_mpp = settings.mint_lnd_enable_mpp
|
||||
supports_incoming_payment_stream = True
|
||||
supported_units = set([Unit.sat, Unit.msat])
|
||||
supports_description: bool = True
|
||||
|
||||
unit = Unit.sat
|
||||
|
||||
def __init__(self, unit: Unit = Unit.sat, **kwargs):
|
||||
self.assert_unit_supported(unit)
|
||||
self.unit = unit
|
||||
self.endpoint = settings.mint_lnd_rpc_endpoint
|
||||
cert_path = settings.mint_lnd_rpc_cert
|
||||
|
||||
macaroon_path = settings.mint_lnd_rpc_macaroon
|
||||
|
||||
if not self.endpoint:
|
||||
if not settings.mint_lnd_rpc_endpoint:
|
||||
raise Exception("cannot initialize LndRPCWallet: no endpoint")
|
||||
|
||||
self.endpoint = settings.mint_lnd_rpc_endpoint
|
||||
|
||||
if not macaroon_path:
|
||||
raise Exception("cannot initialize LndRPCWallet: no macaroon")
|
||||
|
||||
if not cert_path:
|
||||
raise Exception("no certificate for LndRPCWallet provided")
|
||||
|
||||
self.macaroon = codecs.encode(open(macaroon_path, 'rb').read(), 'hex')
|
||||
self.macaroon = codecs.encode(open(macaroon_path, "rb").read(), "hex")
|
||||
|
||||
def metadata_callback(context, callback):
|
||||
callback([('macaroon', self.macaroon)], None)
|
||||
callback([("macaroon", self.macaroon)], None)
|
||||
|
||||
auth_creds = grpc.metadata_call_credentials(metadata_callback)
|
||||
|
||||
# create SSL credentials
|
||||
os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
|
||||
cert = open(cert_path, 'rb').read()
|
||||
os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
|
||||
cert = open(cert_path, "rb").read()
|
||||
ssl_creds = grpc.ssl_channel_credentials(cert)
|
||||
|
||||
# combine macaroon and SSL credentials
|
||||
@@ -86,11 +90,12 @@ class LndRPCWallet(LightningBackend):
|
||||
if self.supports_mpp:
|
||||
logger.info("LndRPCWallet enabling MPP feature")
|
||||
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
r = None
|
||||
try:
|
||||
async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel:
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
lnstub = lightningstub.LightningStub(channel)
|
||||
r = await lnstub.ChannelBalance(lnrpc.ChannelBalanceRequest())
|
||||
except AioRpcError as e:
|
||||
@@ -98,9 +103,8 @@ class LndRPCWallet(LightningBackend):
|
||||
error_message=f"Error calling Lnd gRPC: {e}", balance=0
|
||||
)
|
||||
# NOTE: `balance` field is deprecated. Change this.
|
||||
return StatusResponse(error_message=None, balance=r.balance*1000)
|
||||
return StatusResponse(error_message=None, balance=r.balance * 1000)
|
||||
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: Amount,
|
||||
@@ -124,7 +128,9 @@ class LndRPCWallet(LightningBackend):
|
||||
|
||||
r = None
|
||||
try:
|
||||
async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel:
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
lnstub = lightningstub.LightningStub(channel)
|
||||
r = await lnstub.AddInvoice(data)
|
||||
except AioRpcError as e:
|
||||
@@ -144,7 +150,7 @@ class LndRPCWallet(LightningBackend):
|
||||
payment_request=payment_request,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
|
||||
async def pay_invoice(
|
||||
self, quote: MeltQuote, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
@@ -159,12 +165,12 @@ class LndRPCWallet(LightningBackend):
|
||||
)
|
||||
|
||||
# set the fee limit for the payment
|
||||
feelimit = lnrpc.FeeLimit(
|
||||
fixed_msat=fee_limit_msat
|
||||
)
|
||||
feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat)
|
||||
r = None
|
||||
try:
|
||||
async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel:
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
lnstub = lightningstub.LightningStub(channel)
|
||||
r = await lnstub.SendPaymentSync(
|
||||
lnrpc.SendRequest(
|
||||
@@ -195,14 +201,12 @@ class LndRPCWallet(LightningBackend):
|
||||
preimage=preimage,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
|
||||
async def pay_partial_invoice(
|
||||
self, quote: MeltQuote, amount: Amount, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
# set the fee limit for the payment
|
||||
feelimit = lnrpc.FeeLimit(
|
||||
fixed_msat=fee_limit_msat
|
||||
)
|
||||
feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat)
|
||||
invoice = bolt11.decode(quote.request)
|
||||
|
||||
invoice_amount = invoice.amount_msat
|
||||
@@ -220,7 +224,9 @@ class LndRPCWallet(LightningBackend):
|
||||
# get the route
|
||||
r = None
|
||||
try:
|
||||
async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel:
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
lnstub = lightningstub.LightningStub(channel)
|
||||
router_stub = routerstub.RouterStub(channel)
|
||||
r = await lnstub.QueryRoutes(
|
||||
@@ -230,23 +236,27 @@ class LndRPCWallet(LightningBackend):
|
||||
fee_limit=feelimit,
|
||||
)
|
||||
)
|
||||
'''
|
||||
"""
|
||||
# We need to set the mpp_record for a partial payment
|
||||
mpp_record = lnrpc.MPPRecord(
|
||||
payment_addr=bytes.fromhex(payer_addr),
|
||||
total_amt_msat=total_amount_msat,
|
||||
)
|
||||
'''
|
||||
"""
|
||||
# modify the mpp_record in the last hop
|
||||
route_nr = 0
|
||||
r.routes[route_nr].hops[-1].mpp_record.payment_addr = bytes.fromhex(payer_addr)
|
||||
r.routes[route_nr].hops[-1].mpp_record.total_amt_msat = total_amount_msat
|
||||
r.routes[route_nr].hops[-1].mpp_record.payment_addr = bytes.fromhex( # type: ignore
|
||||
payer_addr
|
||||
)
|
||||
r.routes[route_nr].hops[ # type: ignore
|
||||
-1
|
||||
].mpp_record.total_amt_msat = total_amount_msat
|
||||
|
||||
# Send to route request
|
||||
r = await router_stub.SendToRouteV2(
|
||||
routerrpc.SendToRouteRequest(
|
||||
payment_hash=bytes.fromhex(invoice.payment_hash),
|
||||
route=r.routes[route_nr],
|
||||
route=r.routes[route_nr], # type: ignore
|
||||
)
|
||||
)
|
||||
except AioRpcError as e:
|
||||
@@ -275,16 +285,16 @@ class LndRPCWallet(LightningBackend):
|
||||
preimage=preimage,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = None
|
||||
try:
|
||||
async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel:
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
lnstub = lightningstub.LightningStub(channel)
|
||||
r = await lnstub.LookupInvoice(
|
||||
lnrpc.PaymentHash(
|
||||
r_hash=bytes.fromhex(checking_id)
|
||||
)
|
||||
lnrpc.PaymentHash(r_hash=bytes.fromhex(checking_id))
|
||||
)
|
||||
except AioRpcError as e:
|
||||
error_message = f"LookupInvoice failed: {e}"
|
||||
@@ -292,7 +302,7 @@ class LndRPCWallet(LightningBackend):
|
||||
return PaymentStatus(paid=None)
|
||||
|
||||
return PaymentStatus(paid=INVOICE_STATUSES[r.state])
|
||||
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
"""
|
||||
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||
@@ -307,7 +317,9 @@ class LndRPCWallet(LightningBackend):
|
||||
request = routerrpc.TrackPaymentRequest(payment_hash=checking_id_bytes)
|
||||
|
||||
try:
|
||||
async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel:
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
router_stub = routerstub.RouterStub(channel)
|
||||
async for payment in router_stub.TrackPaymentV2(request):
|
||||
if payment is not None and payment.status:
|
||||
@@ -325,21 +337,23 @@ class LndRPCWallet(LightningBackend):
|
||||
logger.error(error_message)
|
||||
|
||||
return PaymentStatus(paid=None)
|
||||
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
try:
|
||||
async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel:
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
lnstub = lightningstub.LightningStub(channel)
|
||||
async for invoice in lnstub.SubscribeInvoices(lnrpc.InvoiceSubscription()):
|
||||
async for invoice in lnstub.SubscribeInvoices(
|
||||
lnrpc.InvoiceSubscription()
|
||||
):
|
||||
if invoice.state != lnrpc.Invoice.InvoiceState.SETTLED:
|
||||
continue
|
||||
payment_hash = invoice.r_hash.hex()
|
||||
yield payment_hash
|
||||
except AioRpcError as exc:
|
||||
logger.error(
|
||||
f"SubscribeInvoices failed: {exc}. Retrying in 1 sec..."
|
||||
)
|
||||
logger.error(f"SubscribeInvoices failed: {exc}. Retrying in 1 sec...")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def get_payment_quote(
|
||||
|
||||
@@ -33,6 +33,7 @@ class LndRestWallet(LightningBackend):
|
||||
supports_mpp = settings.mint_lnd_enable_mpp
|
||||
supports_incoming_payment_stream = True
|
||||
supported_units = set([Unit.sat, Unit.msat])
|
||||
supports_description: bool = True
|
||||
unit = Unit.sat
|
||||
|
||||
def __init__(self, unit: Unit = Unit.sat, **kwargs):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# type: ignore
|
||||
import secrets
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -23,6 +23,7 @@ class StrikeWallet(LightningBackend):
|
||||
"""https://docs.strike.me/api/"""
|
||||
|
||||
supported_units = [Unit.sat, Unit.usd, Unit.eur]
|
||||
supports_description: bool = False
|
||||
currency_map = {Unit.sat: "BTC", Unit.usd: "USD", Unit.eur: "EUR"}
|
||||
|
||||
def __init__(self, unit: Unit, **kwargs):
|
||||
@@ -95,13 +96,6 @@ class StrikeWallet(LightningBackend):
|
||||
) -> InvoiceResponse:
|
||||
self.assert_unit_supported(amount.unit)
|
||||
|
||||
data: Dict = {"out": False, "amount": amount}
|
||||
if description_hash:
|
||||
data["description_hash"] = description_hash.hex()
|
||||
if unhashed_description:
|
||||
data["unhashed_description"] = unhashed_description.hex()
|
||||
|
||||
data["memo"] = memo or ""
|
||||
payload = {
|
||||
"correlationId": secrets.token_hex(16),
|
||||
"description": "Invoice for order 123",
|
||||
|
||||
@@ -403,6 +403,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
quote_request.unit, Method.bolt11.name
|
||||
)
|
||||
|
||||
if (
|
||||
quote_request.description
|
||||
and not self.backends[method][unit].supports_description
|
||||
):
|
||||
raise NotAllowedError("Backend does not support descriptions.")
|
||||
|
||||
if settings.mint_max_balance:
|
||||
balance = await self.get_balance()
|
||||
if balance + quote_request.amount > settings.mint_max_balance:
|
||||
@@ -411,7 +417,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
logger.trace(f"requesting invoice for {unit.str(quote_request.amount)}")
|
||||
invoice_response: InvoiceResponse = await self.backends[method][
|
||||
unit
|
||||
].create_invoice(Amount(unit=unit, amount=quote_request.amount))
|
||||
].create_invoice(
|
||||
amount=Amount(unit=unit, amount=quote_request.amount),
|
||||
memo=quote_request.description,
|
||||
)
|
||||
logger.trace(
|
||||
f"got invoice {invoice_response.payment_request} with checking id"
|
||||
f" {invoice_response.checking_id}"
|
||||
|
||||
@@ -241,6 +241,7 @@ async def pay(
|
||||
|
||||
@cli.command("invoice", help="Create Lighting invoice.")
|
||||
@click.argument("amount", type=float)
|
||||
@click.option("memo", "-m", default="", help="Memo for the invoice.", type=str)
|
||||
@click.option("--id", default="", help="Id of the paid invoice.", type=str)
|
||||
@click.option(
|
||||
"--split",
|
||||
@@ -259,7 +260,14 @@ async def pay(
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bool):
|
||||
async def invoice(
|
||||
ctx: Context,
|
||||
amount: float,
|
||||
memo: str,
|
||||
id: str,
|
||||
split: int,
|
||||
no_check: bool,
|
||||
):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
await print_balance(ctx)
|
||||
@@ -324,11 +332,11 @@ async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bo
|
||||
)
|
||||
if mint_supports_websockets and not no_check:
|
||||
invoice, subscription = await wallet.request_mint_with_callback(
|
||||
amount, callback=mint_invoice_callback
|
||||
amount, callback=mint_invoice_callback, memo=memo
|
||||
)
|
||||
invoice_nonlocal, subscription_nonlocal = invoice, subscription
|
||||
else:
|
||||
invoice = await wallet.request_mint(amount)
|
||||
invoice = await wallet.request_mint(amount, memo=memo)
|
||||
if invoice.bolt11:
|
||||
print("")
|
||||
print(f"Pay invoice to mint {wallet.unit.str(amount)}:")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
import bolt11
|
||||
|
||||
from ...core.base import Amount, ProofSpentState, Unit
|
||||
@@ -33,15 +35,18 @@ class LightningWallet(Wallet):
|
||||
pass
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def create_invoice(self, amount: int) -> InvoiceResponse:
|
||||
async def create_invoice(
|
||||
self, amount: int, memo: Optional[str] = None
|
||||
) -> InvoiceResponse:
|
||||
"""Create lightning invoice
|
||||
|
||||
Args:
|
||||
amount (int): amount in satoshis
|
||||
memo (str, optional): invoice memo. Defaults to None.
|
||||
Returns:
|
||||
str: invoice
|
||||
"""
|
||||
invoice = await self.request_mint(amount)
|
||||
invoice = await self.request_mint(amount, memo)
|
||||
return InvoiceResponse(
|
||||
ok=True, payment_request=invoice.bolt11, checking_id=invoice.payment_hash
|
||||
)
|
||||
|
||||
@@ -283,11 +283,15 @@ class LedgerAPI(LedgerAPIDeprecated, object):
|
||||
|
||||
@async_set_httpx_client
|
||||
@async_ensure_mint_loaded
|
||||
async def mint_quote(self, amount: int, unit: Unit) -> PostMintQuoteResponse:
|
||||
async def mint_quote(
|
||||
self, amount: int, unit: Unit, memo: Optional[str] = None
|
||||
) -> PostMintQuoteResponse:
|
||||
"""Requests a mint quote from the server and returns a payment request.
|
||||
|
||||
Args:
|
||||
amount (int): Amount of tokens to mint
|
||||
unit (Unit): Unit of the amount
|
||||
memo (Optional[str], optional): Memo to attach to Lightning invoice. Defaults to None.
|
||||
|
||||
Returns:
|
||||
PostMintQuoteResponse: Mint Quote Response
|
||||
@@ -296,7 +300,7 @@ class LedgerAPI(LedgerAPIDeprecated, object):
|
||||
Exception: If the mint request fails
|
||||
"""
|
||||
logger.trace("Requesting mint: POST /v1/mint/bolt11")
|
||||
payload = PostMintQuoteRequest(unit=unit.name, amount=amount)
|
||||
payload = PostMintQuoteRequest(unit=unit.name, amount=amount, description=memo)
|
||||
resp = await self.httpx.post(
|
||||
join(self.url, "/v1/mint/quote/bolt11"), json=payload.dict()
|
||||
)
|
||||
|
||||
@@ -379,18 +379,19 @@ class Wallet(
|
||||
logger.trace("Secret check complete.")
|
||||
|
||||
async def request_mint_with_callback(
|
||||
self, amount: int, callback: Callable
|
||||
self, amount: int, callback: Callable, memo: Optional[str] = None
|
||||
) -> Tuple[Invoice, SubscriptionManager]:
|
||||
"""Request a Lightning invoice for minting tokens.
|
||||
|
||||
Args:
|
||||
amount (int): Amount for Lightning invoice in satoshis
|
||||
callback (Callable): Callback function to be called when the invoice is paid.
|
||||
memo (Optional[str], optional): Memo for the Lightning invoice. Defaults
|
||||
|
||||
Returns:
|
||||
Invoice: Lightning invoice
|
||||
"""
|
||||
mint_qoute = await super().mint_quote(amount, self.unit)
|
||||
mint_qoute = await super().mint_quote(amount, self.unit, memo)
|
||||
subscriptions = SubscriptionManager(self.url)
|
||||
threading.Thread(
|
||||
target=subscriptions.connect, name="SubscriptionManager", daemon=True
|
||||
@@ -413,17 +414,18 @@ class Wallet(
|
||||
await store_lightning_invoice(db=self.db, invoice=invoice)
|
||||
return invoice, subscriptions
|
||||
|
||||
async def request_mint(self, amount: int) -> Invoice:
|
||||
async def request_mint(self, amount: int, memo: Optional[str] = None) -> Invoice:
|
||||
"""Request a Lightning invoice for minting tokens.
|
||||
|
||||
Args:
|
||||
amount (int): Amount for Lightning invoice in satoshis
|
||||
callback (Optional[Callable], optional): Callback function to be called when the invoice is paid. Defaults to None.
|
||||
memo (Optional[str], optional): Memo for the Lightning invoice. Defaults to None.
|
||||
|
||||
Returns:
|
||||
PostMintQuoteResponse: Mint Quote Response
|
||||
"""
|
||||
mint_quote_response = await super().mint_quote(amount, self.unit)
|
||||
mint_quote_response = await super().mint_quote(amount, self.unit, memo)
|
||||
decoded_invoice = bolt11.decode(mint_quote_response.request)
|
||||
invoice = Invoice(
|
||||
amount=amount,
|
||||
@@ -480,11 +482,12 @@ class Wallet(
|
||||
|
||||
return amounts
|
||||
|
||||
async def mint_quote(self, amount: int) -> Invoice:
|
||||
async def mint_quote(self, amount: int, memo: Optional[str] = None) -> Invoice:
|
||||
"""Request a Lightning invoice for minting tokens.
|
||||
|
||||
Args:
|
||||
amount (int): Amount for Lightning invoice in satoshis
|
||||
memo (Optional[str], optional): Memo for the Lightning invoice. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Invoice: Lightning invoice for minting tokens
|
||||
|
||||
3
mypy.ini
3
mypy.ini
@@ -6,3 +6,6 @@ ignore_missing_imports = True
|
||||
|
||||
[mypy-cashu.nostr.*]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-cashu.lightning.lnd_grpc.protos.*]
|
||||
ignore_errors = True
|
||||
|
||||
@@ -86,6 +86,22 @@ async def test_mint(ledger: Ledger):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_quote(ledger: Ledger):
|
||||
await assert_err(
|
||||
ledger.get_mint_quote(quote_id="invalid_quote_id"),
|
||||
"quote not found",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_invalid_quote(ledger: Ledger):
|
||||
await assert_err(
|
||||
ledger.get_melt_quote(quote_id="invalid_quote_id"),
|
||||
"quote not found",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
from typing import Tuple
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
@@ -8,7 +9,7 @@ from cashu.core.base import TokenV4
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.cli.cli import cli
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.helpers import is_fake, pay_if_regtest
|
||||
from tests.helpers import is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
@@ -107,6 +108,19 @@ def test_balance(cli_prefix):
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
def test_invoice(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "1000"],
|
||||
)
|
||||
|
||||
wallet = asyncio.run(init_wallet())
|
||||
assert wallet.available_balance >= 1000
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_invoice_return_immediately(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
@@ -130,6 +144,30 @@ def test_invoice_return_immediately(mint, cli_prefix):
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API")
|
||||
def test_invoice_with_memo(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "-n", "1000", "-m", "test memo"],
|
||||
)
|
||||
assert result.exception is None
|
||||
|
||||
# find word starting with ln in the output
|
||||
lines = result.output.split("\n")
|
||||
invoice_str = ""
|
||||
for line in lines:
|
||||
for word in line.split(" "):
|
||||
if word.startswith("ln"):
|
||||
invoice_str = word
|
||||
break
|
||||
if not invoice_str:
|
||||
raise Exception("No invoice found in the output")
|
||||
invoice_obj = bolt11.decode(invoice_str)
|
||||
assert invoice_obj.amount_msat == 1000_000
|
||||
assert invoice_obj.description == "test memo"
|
||||
|
||||
|
||||
def test_invoice_with_split(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import List, Union
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
@@ -7,7 +8,13 @@ from cashu.core.base import Proof
|
||||
from cashu.core.errors import CashuError
|
||||
from cashu.wallet.lightning import LightningWallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
from tests.helpers import (
|
||||
get_real_invoice,
|
||||
is_deprecated_api_only,
|
||||
is_fake,
|
||||
is_regtest,
|
||||
pay_if_regtest,
|
||||
)
|
||||
|
||||
|
||||
async def assert_err(f, msg: Union[str, CashuError]):
|
||||
@@ -58,6 +65,16 @@ async def test_create_invoice(wallet: LightningWallet):
|
||||
assert invoice.payment_request.startswith("ln")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API")
|
||||
async def test_create_invoice_with_description(wallet: LightningWallet):
|
||||
invoice = await wallet.create_invoice(64, "test description")
|
||||
assert invoice.payment_request
|
||||
assert invoice.payment_request.startswith("ln")
|
||||
invoiceObj = bolt11.decode(invoice.payment_request)
|
||||
assert invoiceObj.description == "test description"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_check_invoice_internal(wallet: LightningWallet):
|
||||
|
||||
Reference in New Issue
Block a user