Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NUT-04: add description #613

Merged
merged 9 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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":
Expand Down
1 change: 1 addition & 0 deletions cashu/lightning/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions cashu/lightning/blink.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions cashu/lightning/clnrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cashu/lightning/corelightningrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cashu/lightning/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cashu/lightning/lnbits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
94 changes: 54 additions & 40 deletions cashu/lightning/lnd_grpc/lnd_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,39 +45,43 @@
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
Expand All @@ -86,21 +90,21 @@ def metadata_callback(context, callback):
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:
return StatusResponse(
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,
Expand All @@ -124,7 +128,9 @@ async def create_invoice(

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:
Expand All @@ -144,7 +150,7 @@ async def create_invoice(
payment_request=payment_request,
error_message=None,
)

async def pay_invoice(
self, quote: MeltQuote, fee_limit_msat: int
) -> PaymentResponse:
Expand All @@ -159,12 +165,12 @@ async def pay_invoice(
)

# 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(
Expand Down Expand Up @@ -195,14 +201,12 @@ async def pay_invoice(
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
Expand All @@ -220,7 +224,9 @@ async def pay_partial_invoice(
# 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(
Expand All @@ -230,23 +236,27 @@ async def pay_partial_invoice(
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:
Expand Down Expand Up @@ -275,24 +285,24 @@ async def pay_partial_invoice(
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}"
logger.error(error_message)
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.
Expand All @@ -307,7 +317,9 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus:
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:
Expand All @@ -325,21 +337,23 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus:
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(
Expand Down
1 change: 1 addition & 0 deletions cashu/lightning/lndrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 2 additions & 8 deletions cashu/lightning/strike.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# type: ignore
import secrets
from typing import AsyncGenerator, Dict, Optional
from typing import AsyncGenerator, Optional

import httpx

Expand All @@ -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):
Expand Down Expand Up @@ -95,13 +96,6 @@ async def create_invoice(
) -> 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",
Expand Down
11 changes: 10 additions & 1 deletion cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,12 @@ async def mint_quote(self, quote_request: PostMintQuoteRequest) -> MintQuote:
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:
Expand All @@ -411,7 +417,10 @@ async def mint_quote(self, quote_request: PostMintQuoteRequest) -> MintQuote:
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}"
Expand Down
Loading
Loading