mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
Multinut LND (#492)
* amount in melt request * apply fee limit * more error handling * wip: signal flag in /info * clean up multinut * decode mypy error lndrest * fix test * fix tests * signal feature and blindmessages_deprecated * setting * fix blindedsignature method * fix tests * mint info file * test mpp with lnd regtest * nuts optionsl mint info * try to enable mpp with lnd * test mpp with third payment
This commit is contained in:
1
.github/workflows/regtest.yml
vendored
1
.github/workflows/regtest.yml
vendored
@@ -65,6 +65,7 @@ jobs:
|
|||||||
MINT_LND_REST_ENDPOINT: https://localhost:8081/
|
MINT_LND_REST_ENDPOINT: https://localhost:8081/
|
||||||
MINT_LND_REST_CERT: ./regtest/data/lnd-3/tls.cert
|
MINT_LND_REST_CERT: ./regtest/data/lnd-3/tls.cert
|
||||||
MINT_LND_REST_MACAROON: ./regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
MINT_LND_REST_MACAROON: ./regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||||
|
MINT_LND_ENABLE_MPP: true
|
||||||
# LND_GRPC_ENDPOINT: localhost
|
# LND_GRPC_ENDPOINT: localhost
|
||||||
# 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
|
||||||
|
|||||||
@@ -161,13 +161,18 @@ class Proof(BaseModel):
|
|||||||
return HTLCWitness.from_witness(self.witness).preimage
|
return HTLCWitness.from_witness(self.witness).preimage
|
||||||
|
|
||||||
|
|
||||||
|
class Proofs(BaseModel):
|
||||||
|
# NOTE: not used in Pydantic validation
|
||||||
|
__root__: List[Proof]
|
||||||
|
|
||||||
|
|
||||||
class BlindedMessage(BaseModel):
|
class BlindedMessage(BaseModel):
|
||||||
"""
|
"""
|
||||||
Blinded message or blinded secret or "output" which is to be signed by the mint
|
Blinded message or blinded secret or "output" which is to be signed by the mint
|
||||||
"""
|
"""
|
||||||
|
|
||||||
amount: int
|
amount: int
|
||||||
id: str
|
id: str # Keyset id
|
||||||
B_: str # Hex-encoded blinded message
|
B_: str # Hex-encoded blinded message
|
||||||
witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL)
|
witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL)
|
||||||
|
|
||||||
@@ -177,6 +182,28 @@ class BlindedMessage(BaseModel):
|
|||||||
return P2PKWitness.from_witness(self.witness).signatures
|
return P2PKWitness.from_witness(self.witness).signatures
|
||||||
|
|
||||||
|
|
||||||
|
class BlindedMessage_Deprecated(BaseModel):
|
||||||
|
"""
|
||||||
|
Deprecated: BlindedMessage for v0 protocol (deprecated api routes) have no id field.
|
||||||
|
|
||||||
|
Blinded message or blinded secret or "output" which is to be signed by the mint
|
||||||
|
"""
|
||||||
|
|
||||||
|
amount: int
|
||||||
|
B_: str # Hex-encoded blinded message
|
||||||
|
witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def p2pksigs(self) -> List[str]:
|
||||||
|
assert self.witness, "Witness missing in output"
|
||||||
|
return P2PKWitness.from_witness(self.witness).signatures
|
||||||
|
|
||||||
|
|
||||||
|
class BlindedMessages(BaseModel):
|
||||||
|
# NOTE: not used in Pydantic validation
|
||||||
|
__root__: List[BlindedMessage] = []
|
||||||
|
|
||||||
|
|
||||||
class BlindedSignature(BaseModel):
|
class BlindedSignature(BaseModel):
|
||||||
"""
|
"""
|
||||||
Blinded signature or "promise" which is the signature on a `BlindedMessage`
|
Blinded signature or "promise" which is the signature on a `BlindedMessage`
|
||||||
@@ -314,7 +341,13 @@ class GetInfoResponse(BaseModel):
|
|||||||
description_long: Optional[str] = None
|
description_long: Optional[str] = None
|
||||||
contact: Optional[List[List[str]]] = None
|
contact: Optional[List[List[str]]] = None
|
||||||
motd: Optional[str] = None
|
motd: Optional[str] = None
|
||||||
nuts: Optional[Dict[int, Dict[str, Any]]] = None
|
nuts: Optional[Dict[int, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Nut15MppSupport(BaseModel):
|
||||||
|
method: str
|
||||||
|
unit: str
|
||||||
|
mpp: bool
|
||||||
|
|
||||||
|
|
||||||
class GetInfoResponse_deprecated(BaseModel):
|
class GetInfoResponse_deprecated(BaseModel):
|
||||||
@@ -329,19 +362,6 @@ class GetInfoResponse_deprecated(BaseModel):
|
|||||||
parameter: Optional[dict] = None
|
parameter: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class BlindedMessage_Deprecated(BaseModel):
|
|
||||||
# Same as BlindedMessage, but without the id field
|
|
||||||
amount: int
|
|
||||||
B_: str # Hex-encoded blinded message
|
|
||||||
id: Optional[str] = None
|
|
||||||
witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def p2pksigs(self) -> List[str]:
|
|
||||||
assert self.witness, "Witness missing in output"
|
|
||||||
return P2PKWitness.from_witness(self.witness).signatures
|
|
||||||
|
|
||||||
|
|
||||||
# ------- API: KEYS -------
|
# ------- API: KEYS -------
|
||||||
|
|
||||||
|
|
||||||
@@ -425,6 +445,7 @@ class PostMeltQuoteRequest(BaseModel):
|
|||||||
request: str = Field(
|
request: str = Field(
|
||||||
..., max_length=settings.mint_max_request_length
|
..., max_length=settings.mint_max_request_length
|
||||||
) # output payment request
|
) # output payment request
|
||||||
|
amount: Optional[int] = Field(default=None, gt=0) # input amount
|
||||||
|
|
||||||
|
|
||||||
class PostMeltQuoteResponse(BaseModel):
|
class PostMeltQuoteResponse(BaseModel):
|
||||||
@@ -551,6 +572,12 @@ class PostRestoreRequest(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PostRestoreRequest_Deprecated(BaseModel):
|
||||||
|
outputs: List[BlindedMessage_Deprecated] = Field(
|
||||||
|
..., max_items=settings.mint_max_request_length
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PostRestoreResponse(BaseModel):
|
class PostRestoreResponse(BaseModel):
|
||||||
outputs: List[BlindedMessage] = []
|
outputs: List[BlindedMessage] = []
|
||||||
signatures: List[BlindedSignature] = []
|
signatures: List[BlindedSignature] = []
|
||||||
@@ -656,6 +683,7 @@ class WalletKeyset:
|
|||||||
valid_to=None,
|
valid_to=None,
|
||||||
first_seen=None,
|
first_seen=None,
|
||||||
active=True,
|
active=True,
|
||||||
|
use_deprecated_id=False, # BACKWARDS COMPATIBILITY < 0.15.0
|
||||||
):
|
):
|
||||||
self.valid_from = valid_from
|
self.valid_from = valid_from
|
||||||
self.valid_to = valid_to
|
self.valid_to = valid_to
|
||||||
@@ -670,10 +698,19 @@ class WalletKeyset:
|
|||||||
else:
|
else:
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
|
# BEGIN BACKWARDS COMPATIBILITY < 0.15.0
|
||||||
|
if use_deprecated_id:
|
||||||
|
logger.warning(
|
||||||
|
"Using deprecated keyset id derivation for backwards compatibility <"
|
||||||
|
" 0.15.0"
|
||||||
|
)
|
||||||
|
self.id = derive_keyset_id_deprecated(self.public_keys)
|
||||||
|
# END BACKWARDS COMPATIBILITY < 0.15.0
|
||||||
|
|
||||||
self.unit = Unit[unit]
|
self.unit = Unit[unit]
|
||||||
|
|
||||||
logger.trace(f"Derived keyset id {self.id} from public keys.")
|
logger.trace(f"Derived keyset id {self.id} from public keys.")
|
||||||
if id and id != self.id:
|
if id and id != self.id and use_deprecated_id:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"WARNING: Keyset id {self.id} does not match the given id {id}."
|
f"WARNING: Keyset id {self.id} does not match the given id {id}."
|
||||||
" Overwriting."
|
" Overwriting."
|
||||||
@@ -728,6 +765,8 @@ class MintKeyset:
|
|||||||
first_seen: Optional[str] = None
|
first_seen: Optional[str] = None
|
||||||
version: Optional[str] = None
|
version: Optional[str] = None
|
||||||
|
|
||||||
|
duplicate_keyset_id: Optional[str] = None # BACKWARDS COMPATIBILITY < 0.15.0
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -808,12 +847,6 @@ class MintKeyset:
|
|||||||
assert self.seed, "seed not set"
|
assert self.seed, "seed not set"
|
||||||
assert self.derivation_path, "derivation path not set"
|
assert self.derivation_path, "derivation path not set"
|
||||||
|
|
||||||
# we compute the keyset id from the public keys only if it is not
|
|
||||||
# loaded from the database. This is to allow for backwards compatibility
|
|
||||||
# with old keysets with new id's and vice versa. This code can be removed
|
|
||||||
# if there are only new keysets in the mint (> 0.15.0)
|
|
||||||
id_in_db = self.id
|
|
||||||
|
|
||||||
if self.version_tuple < (0, 12):
|
if self.version_tuple < (0, 12):
|
||||||
# WARNING: Broken key derivation for backwards compatibility with < 0.12
|
# WARNING: Broken key derivation for backwards compatibility with < 0.12
|
||||||
self.private_keys = derive_keys_backwards_compatible_insecure_pre_0_12(
|
self.private_keys = derive_keys_backwards_compatible_insecure_pre_0_12(
|
||||||
@@ -824,8 +857,7 @@ class MintKeyset:
|
|||||||
f"WARNING: Using weak key derivation for keyset {self.id} (backwards"
|
f"WARNING: Using weak key derivation for keyset {self.id} (backwards"
|
||||||
" compatibility < 0.12)"
|
" compatibility < 0.12)"
|
||||||
)
|
)
|
||||||
# load from db or derive
|
self.id = derive_keyset_id_deprecated(self.public_keys) # type: ignore
|
||||||
self.id = id_in_db or derive_keyset_id_deprecated(self.public_keys) # type: ignore
|
|
||||||
elif self.version_tuple < (0, 15):
|
elif self.version_tuple < (0, 15):
|
||||||
self.private_keys = derive_keys_sha256(self.seed, self.derivation_path)
|
self.private_keys = derive_keys_sha256(self.seed, self.derivation_path)
|
||||||
logger.trace(
|
logger.trace(
|
||||||
@@ -833,13 +865,11 @@ class MintKeyset:
|
|||||||
" compatibility < 0.15)"
|
" compatibility < 0.15)"
|
||||||
)
|
)
|
||||||
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
||||||
# load from db or derive
|
self.id = derive_keyset_id_deprecated(self.public_keys) # type: ignore
|
||||||
self.id = id_in_db or derive_keyset_id_deprecated(self.public_keys) # type: ignore
|
|
||||||
else:
|
else:
|
||||||
self.private_keys = derive_keys(self.seed, self.derivation_path)
|
self.private_keys = derive_keys(self.seed, self.derivation_path)
|
||||||
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
||||||
# load from db or derive
|
self.id = derive_keyset_id(self.public_keys) # type: ignore
|
||||||
self.id = id_in_db or derive_keyset_id(self.public_keys) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# ------- TOKEN -------
|
# ------- TOKEN -------
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class MintLimits(MintSettings):
|
|||||||
)
|
)
|
||||||
mint_max_request_length: int = Field(
|
mint_max_request_length: int = Field(
|
||||||
default=1000,
|
default=1000,
|
||||||
|
gt=0,
|
||||||
title="Maximum request length",
|
title="Maximum request length",
|
||||||
description="Maximum length of REST API request arrays.",
|
description="Maximum length of REST API request arrays.",
|
||||||
)
|
)
|
||||||
@@ -100,16 +101,21 @@ class MintLimits(MintSettings):
|
|||||||
)
|
)
|
||||||
mint_max_peg_in: int = Field(
|
mint_max_peg_in: int = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
gt=0,
|
||||||
title="Maximum peg-in",
|
title="Maximum peg-in",
|
||||||
description="Maximum amount for a mint operation.",
|
description="Maximum amount for a mint operation.",
|
||||||
)
|
)
|
||||||
mint_max_peg_out: int = Field(
|
mint_max_peg_out: int = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
gt=0,
|
||||||
title="Maximum peg-out",
|
title="Maximum peg-out",
|
||||||
description="Maximum amount for a melt operation.",
|
description="Maximum amount for a melt operation.",
|
||||||
)
|
)
|
||||||
mint_max_balance: int = Field(
|
mint_max_balance: int = Field(
|
||||||
default=None, title="Maximum mint balance", description="Maximum mint balance."
|
default=None,
|
||||||
|
gt=0,
|
||||||
|
title="Maximum mint balance",
|
||||||
|
description="Maximum mint balance.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,6 +177,7 @@ class LndRestFundingSource(MintSettings):
|
|||||||
mint_lnd_rest_macaroon: Optional[str] = Field(default=None)
|
mint_lnd_rest_macaroon: Optional[str] = Field(default=None)
|
||||||
mint_lnd_rest_admin_macaroon: Optional[str] = Field(default=None)
|
mint_lnd_rest_admin_macaroon: Optional[str] = Field(default=None)
|
||||||
mint_lnd_rest_invoice_macaroon: Optional[str] = Field(default=None)
|
mint_lnd_rest_invoice_macaroon: Optional[str] = Field(default=None)
|
||||||
|
mint_lnd_enable_mpp: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
class CoreLightningRestFundingSource(MintSettings):
|
class CoreLightningRestFundingSource(MintSettings):
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ from typing import Coroutine, Optional, Union
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from ..core.base import Amount, MeltQuote, Unit
|
from ..core.base import (
|
||||||
|
Amount,
|
||||||
|
MeltQuote,
|
||||||
|
PostMeltQuoteRequest,
|
||||||
|
Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StatusResponse(BaseModel):
|
class StatusResponse(BaseModel):
|
||||||
@@ -62,6 +67,7 @@ class PaymentStatus(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class LightningBackend(ABC):
|
class LightningBackend(ABC):
|
||||||
|
supports_mpp: bool = False
|
||||||
supported_units: set[Unit]
|
supported_units: set[Unit]
|
||||||
unit: Unit
|
unit: Unit
|
||||||
|
|
||||||
@@ -107,7 +113,7 @@ class LightningBackend(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_payment_quote(
|
async def get_payment_quote(
|
||||||
self,
|
self,
|
||||||
bolt11: str,
|
melt_quote: PostMeltQuoteRequest,
|
||||||
) -> PaymentQuoteResponse:
|
) -> PaymentQuoteResponse:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from bolt11 import (
|
|||||||
)
|
)
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from ..core.base import Amount, MeltQuote, Unit
|
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
@@ -375,7 +375,10 @@ class BlinkWallet(LightningBackend):
|
|||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
async def get_payment_quote(
|
||||||
|
self, melt_quote: PostMeltQuoteRequest
|
||||||
|
) -> PaymentQuoteResponse:
|
||||||
|
bolt11 = melt_quote.request
|
||||||
variables = {
|
variables = {
|
||||||
"input": {
|
"input": {
|
||||||
"paymentRequest": bolt11,
|
"paymentRequest": bolt11,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from bolt11 import (
|
|||||||
)
|
)
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from ..core.base import Amount, MeltQuote, Unit
|
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
|
||||||
from ..core.helpers import fee_reserve
|
from ..core.helpers import fee_reserve
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -316,8 +316,10 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
)
|
)
|
||||||
await asyncio.sleep(0.02)
|
await asyncio.sleep(0.02)
|
||||||
|
|
||||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
async def get_payment_quote(
|
||||||
invoice_obj = decode(bolt11)
|
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, "invoice has no amount."
|
||||||
amount_msat = int(invoice_obj.amount_msat)
|
amount_msat = int(invoice_obj.amount_msat)
|
||||||
fees_msat = fee_reserve(amount_msat)
|
fees_msat = fee_reserve(amount_msat)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from bolt11 import (
|
|||||||
encode,
|
encode,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..core.base import Amount, MeltQuote, Unit
|
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
|
||||||
from ..core.helpers import fee_reserve
|
from ..core.helpers import fee_reserve
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -152,8 +152,10 @@ class FakeWallet(LightningBackend):
|
|||||||
# amount = invoice_obj.amount_msat
|
# amount = invoice_obj.amount_msat
|
||||||
# return InvoiceQuoteResponse(checking_id="", amount=amount)
|
# return InvoiceQuoteResponse(checking_id="", amount=amount)
|
||||||
|
|
||||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
async def get_payment_quote(
|
||||||
invoice_obj = decode(bolt11)
|
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, "invoice has no amount."
|
||||||
|
|
||||||
if self.unit == Unit.sat:
|
if self.unit == Unit.sat:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from bolt11 import (
|
|||||||
decode,
|
decode,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..core.base import Amount, MeltQuote, Unit
|
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
|
||||||
from ..core.helpers import fee_reserve
|
from ..core.helpers import fee_reserve
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -167,8 +167,10 @@ class LNbitsWallet(LightningBackend):
|
|||||||
preimage=data["preimage"],
|
preimage=data["preimage"],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
async def get_payment_quote(
|
||||||
invoice_obj = decode(bolt11)
|
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, "invoice has no amount."
|
||||||
amount_msat = int(invoice_obj.amount_msat)
|
amount_msat = int(invoice_obj.amount_msat)
|
||||||
fees_msat = fee_reserve(amount_msat)
|
fees_msat = fee_reserve(amount_msat)
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
|
|
||||||
|
import bolt11
|
||||||
import httpx
|
import httpx
|
||||||
from bolt11 import (
|
from bolt11 import (
|
||||||
|
TagChar,
|
||||||
decode,
|
decode,
|
||||||
)
|
)
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from ..core.base import Amount, MeltQuote, Unit
|
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
|
||||||
from ..core.helpers import fee_reserve
|
from ..core.helpers import fee_reserve
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -27,6 +29,7 @@ from .macaroon import load_macaroon
|
|||||||
class LndRestWallet(LightningBackend):
|
class LndRestWallet(LightningBackend):
|
||||||
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
||||||
|
|
||||||
|
supports_mpp = settings.mint_lnd_enable_mpp
|
||||||
supported_units = set([Unit.sat, Unit.msat])
|
supported_units = set([Unit.sat, Unit.msat])
|
||||||
unit = Unit.sat
|
unit = Unit.sat
|
||||||
|
|
||||||
@@ -70,6 +73,8 @@ class LndRestWallet(LightningBackend):
|
|||||||
self.client = httpx.AsyncClient(
|
self.client = httpx.AsyncClient(
|
||||||
base_url=self.endpoint, headers=self.auth, verify=self.cert
|
base_url=self.endpoint, headers=self.auth, verify=self.cert
|
||||||
)
|
)
|
||||||
|
if self.supports_mpp:
|
||||||
|
logger.info("LNDRestWallet enabling MPP feature")
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
try:
|
try:
|
||||||
@@ -148,6 +153,16 @@ class LndRestWallet(LightningBackend):
|
|||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
self, quote: MeltQuote, fee_limit_msat: int
|
self, quote: MeltQuote, fee_limit_msat: int
|
||||||
) -> PaymentResponse:
|
) -> PaymentResponse:
|
||||||
|
# if the amount of the melt quote is different from the request
|
||||||
|
# call pay_partial_invoice instead
|
||||||
|
invoice = bolt11.decode(quote.request)
|
||||||
|
if invoice.amount_msat:
|
||||||
|
amount_msat = int(invoice.amount_msat)
|
||||||
|
if amount_msat != quote.amount * 1000 and self.supports_mpp:
|
||||||
|
return await self.pay_partial_invoice(
|
||||||
|
quote, Amount(Unit.sat, quote.amount), fee_limit_msat
|
||||||
|
)
|
||||||
|
|
||||||
# set the fee limit for the payment
|
# set the fee limit for the payment
|
||||||
lnrpcFeeLimit = dict()
|
lnrpcFeeLimit = dict()
|
||||||
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
||||||
@@ -180,6 +195,91 @@ class LndRestWallet(LightningBackend):
|
|||||||
error_message=None,
|
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
|
||||||
|
lnrpcFeeLimit = dict()
|
||||||
|
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
||||||
|
invoice = bolt11.decode(quote.request)
|
||||||
|
|
||||||
|
invoice_amount = invoice.amount_msat
|
||||||
|
assert invoice_amount, "invoice has no amount."
|
||||||
|
total_amount_msat = int(invoice_amount)
|
||||||
|
|
||||||
|
payee = invoice.tags.get(TagChar.payee)
|
||||||
|
assert payee
|
||||||
|
pubkey = str(payee.data)
|
||||||
|
|
||||||
|
payer_addr_tag = invoice.tags.get(bolt11.TagChar("s"))
|
||||||
|
assert payer_addr_tag
|
||||||
|
payer_addr = str(payer_addr_tag.data)
|
||||||
|
|
||||||
|
# get the route
|
||||||
|
r = await self.client.post(
|
||||||
|
url=f"/v1/graph/routes/{pubkey}/{amount.to(Unit.sat).amount}",
|
||||||
|
json={"fee_limit": lnrpcFeeLimit},
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
if r.is_error or data.get("message"):
|
||||||
|
error_message = data.get("message") or r.text
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=False,
|
||||||
|
checking_id=None,
|
||||||
|
fee=None,
|
||||||
|
preimage=None,
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We need to set the mpp_record for a partial payment
|
||||||
|
mpp_record = {
|
||||||
|
"mpp_record": {
|
||||||
|
"payment_addr": base64.b64encode(bytes.fromhex(payer_addr)).decode(),
|
||||||
|
"total_amt_msat": total_amount_msat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# add the mpp_record to the last hop
|
||||||
|
rout_nr = 0
|
||||||
|
data["routes"][rout_nr]["hops"][-1].update(mpp_record)
|
||||||
|
|
||||||
|
# send to route
|
||||||
|
r = await self.client.post(
|
||||||
|
url="/v2/router/route/send",
|
||||||
|
json={
|
||||||
|
"payment_hash": base64.b64encode(
|
||||||
|
bytes.fromhex(invoice.payment_hash)
|
||||||
|
).decode(),
|
||||||
|
"route": data["routes"][rout_nr],
|
||||||
|
},
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
if r.is_error or data.get("message"):
|
||||||
|
error_message = data.get("message") or r.text
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=False,
|
||||||
|
checking_id=None,
|
||||||
|
fee=None,
|
||||||
|
preimage=None,
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
ok = data.get("status") == "SUCCEEDED"
|
||||||
|
checking_id = invoice.payment_hash
|
||||||
|
fee_msat = int(data["route"]["total_fees_msat"])
|
||||||
|
preimage = base64.b64decode(data["preimage"]).hex()
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=ok,
|
||||||
|
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:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
||||||
|
|
||||||
@@ -270,13 +370,29 @@ class LndRestWallet(LightningBackend):
|
|||||||
)
|
)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
async def get_payment_quote(
|
||||||
invoice_obj = decode(bolt11)
|
self, melt_quote: PostMeltQuoteRequest
|
||||||
|
) -> PaymentQuoteResponse:
|
||||||
|
# get amount from melt_quote or from bolt11
|
||||||
|
amount = (
|
||||||
|
Amount(Unit[melt_quote.unit], melt_quote.amount)
|
||||||
|
if melt_quote.amount
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
invoice_obj = decode(melt_quote.request)
|
||||||
assert invoice_obj.amount_msat, "invoice has no amount."
|
assert invoice_obj.amount_msat, "invoice has no amount."
|
||||||
amount_msat = int(invoice_obj.amount_msat)
|
|
||||||
|
if amount:
|
||||||
|
amount_msat = amount.to(Unit.msat).amount
|
||||||
|
else:
|
||||||
|
amount_msat = int(invoice_obj.amount_msat)
|
||||||
|
|
||||||
fees_msat = fee_reserve(amount_msat)
|
fees_msat = fee_reserve(amount_msat)
|
||||||
fees = Amount(unit=Unit.msat, amount=fees_msat)
|
fees = Amount(unit=Unit.msat, amount=fees_msat)
|
||||||
|
|
||||||
amount = Amount(unit=Unit.msat, amount=amount_msat)
|
amount = Amount(unit=Unit.msat, amount=amount_msat)
|
||||||
|
|
||||||
return PaymentQuoteResponse(
|
return PaymentQuoteResponse(
|
||||||
checking_id=invoice_obj.payment_hash,
|
checking_id=invoice_obj.payment_hash,
|
||||||
fee=fees.to(self.unit, round="up"),
|
fee=fees.to(self.unit, round="up"),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Dict, Optional
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from ..core.base import Amount, MeltQuote, Unit
|
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
@@ -118,7 +118,10 @@ class StrikeUSDWallet(LightningBackend):
|
|||||||
error_message=None,
|
error_message=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
async def get_payment_quote(
|
||||||
|
self, melt_quote: PostMeltQuoteRequest
|
||||||
|
) -> PaymentQuoteResponse:
|
||||||
|
bolt11 = melt_quote.request
|
||||||
try:
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
url=f"{self.endpoint}/v1/payment-quotes/lightning",
|
url=f"{self.endpoint}/v1/payment-quotes/lightning",
|
||||||
|
|||||||
@@ -575,9 +575,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# not internal, get payment quote by backend
|
# not internal, get payment quote by backend
|
||||||
payment_quote = await self.backends[method][unit].get_payment_quote(request)
|
payment_quote = await self.backends[method][unit].get_payment_quote(
|
||||||
if not payment_quote.checking_id:
|
melt_quote=melt_quote
|
||||||
raise TransactionError("quote has no checking id")
|
)
|
||||||
|
assert payment_quote.checking_id, "quote has no checking id"
|
||||||
# make sure the backend returned the amount with a correct unit
|
# make sure the backend returned the amount with a correct unit
|
||||||
if not payment_quote.amount.unit == unit:
|
if not payment_quote.amount.unit == unit:
|
||||||
raise TransactionError("payment quote amount units do not match")
|
raise TransactionError("payment quote amount units do not match")
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ async def info() -> GetInfoResponse:
|
|||||||
|
|
||||||
supported_dict = dict(supported=True)
|
supported_dict = dict(supported=True)
|
||||||
|
|
||||||
mint_features: Dict[int, Dict[str, Any]] = {
|
supported_dict = dict(supported=True)
|
||||||
|
mint_features: Dict[int, Any] = {
|
||||||
4: dict(
|
4: dict(
|
||||||
methods=method_settings[4],
|
methods=method_settings[4],
|
||||||
disabled=settings.mint_peg_out_only,
|
disabled=settings.mint_peg_out_only,
|
||||||
@@ -79,6 +80,21 @@ async def info() -> GetInfoResponse:
|
|||||||
12: supported_dict,
|
12: supported_dict,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# signal which method-unit pairs support MPP
|
||||||
|
for method, unit_dict in ledger.backends.items():
|
||||||
|
for unit in unit_dict.keys():
|
||||||
|
logger.trace(
|
||||||
|
f"method={method.name} unit={unit} supports_mpp={unit_dict[unit].supports_mpp}"
|
||||||
|
)
|
||||||
|
if unit_dict[unit].supports_mpp:
|
||||||
|
mint_features.setdefault(15, []).append(
|
||||||
|
{
|
||||||
|
"method": method.name,
|
||||||
|
"unit": unit.name,
|
||||||
|
"mpp": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return GetInfoResponse(
|
return GetInfoResponse(
|
||||||
name=settings.mint_info_name,
|
name=settings.mint_info_name,
|
||||||
pubkey=ledger.pubkey.serialize().hex() if ledger.pubkey else None,
|
pubkey=ledger.pubkey.serialize().hex() if ledger.pubkey else None,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from ..core.base import (
|
|||||||
PostMintQuoteRequest,
|
PostMintQuoteRequest,
|
||||||
PostMintRequest_deprecated,
|
PostMintRequest_deprecated,
|
||||||
PostMintResponse_deprecated,
|
PostMintResponse_deprecated,
|
||||||
PostRestoreRequest,
|
PostRestoreRequest_Deprecated,
|
||||||
PostRestoreResponse,
|
PostRestoreResponse,
|
||||||
PostSplitRequest_Deprecated,
|
PostSplitRequest_Deprecated,
|
||||||
PostSplitResponse_Deprecated,
|
PostSplitResponse_Deprecated,
|
||||||
@@ -179,7 +179,7 @@ async def mint_deprecated(
|
|||||||
# BEGIN BACKWARDS COMPATIBILITY < 0.15
|
# BEGIN BACKWARDS COMPATIBILITY < 0.15
|
||||||
# Mint expects "id" in outputs to know which keyset to use to sign them.
|
# Mint expects "id" in outputs to know which keyset to use to sign them.
|
||||||
outputs: list[BlindedMessage] = [
|
outputs: list[BlindedMessage] = [
|
||||||
BlindedMessage(id=o.id or ledger.keyset.id, **o.dict(exclude={"id"}))
|
BlindedMessage(id=ledger.keyset.id, **o.dict(exclude={"id"}))
|
||||||
for o in payload.outputs
|
for o in payload.outputs
|
||||||
]
|
]
|
||||||
# END BACKWARDS COMPATIBILITY < 0.15
|
# END BACKWARDS COMPATIBILITY < 0.15
|
||||||
@@ -223,7 +223,7 @@ async def melt_deprecated(
|
|||||||
# BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs
|
# BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs
|
||||||
if payload.outputs:
|
if payload.outputs:
|
||||||
outputs: list[BlindedMessage] = [
|
outputs: list[BlindedMessage] = [
|
||||||
BlindedMessage(id=o.id or ledger.keyset.id, **o.dict(exclude={"id"}))
|
BlindedMessage(id=ledger.keyset.id, **o.dict(exclude={"id"}))
|
||||||
for o in payload.outputs
|
for o in payload.outputs
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
@@ -295,7 +295,7 @@ async def split_deprecated(
|
|||||||
assert payload.outputs, Exception("no outputs provided.")
|
assert payload.outputs, Exception("no outputs provided.")
|
||||||
# BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs
|
# BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs
|
||||||
outputs: list[BlindedMessage] = [
|
outputs: list[BlindedMessage] = [
|
||||||
BlindedMessage(id=o.id or ledger.keyset.id, **o.dict(exclude={"id"}))
|
BlindedMessage(id=ledger.keyset.id, **o.dict(exclude={"id"}))
|
||||||
for o in payload.outputs
|
for o in payload.outputs
|
||||||
]
|
]
|
||||||
# END BACKWARDS COMPATIBILITY < 0.14
|
# END BACKWARDS COMPATIBILITY < 0.14
|
||||||
@@ -372,7 +372,15 @@ async def check_spendable_deprecated(
|
|||||||
),
|
),
|
||||||
deprecated=True,
|
deprecated=True,
|
||||||
)
|
)
|
||||||
async def restore(payload: PostRestoreRequest) -> PostRestoreResponse:
|
async def restore(payload: PostRestoreRequest_Deprecated) -> PostRestoreResponse:
|
||||||
assert payload.outputs, Exception("no outputs provided.")
|
assert payload.outputs, Exception("no outputs provided.")
|
||||||
outputs, promises = await ledger.restore(payload.outputs)
|
if payload.outputs:
|
||||||
|
outputs: list[BlindedMessage] = [
|
||||||
|
BlindedMessage(id=ledger.keyset.id, **o.dict(exclude={"id"}))
|
||||||
|
for o in payload.outputs
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
outputs = []
|
||||||
|
|
||||||
|
outputs, promises = await ledger.restore(outputs)
|
||||||
return PostRestoreResponse(outputs=outputs, signatures=promises)
|
return PostRestoreResponse(outputs=outputs, signatures=promises)
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ async def swap(
|
|||||||
|
|
||||||
# pay invoice from outgoing mint
|
# pay invoice from outgoing mint
|
||||||
await outgoing_wallet.load_proofs(reload=True)
|
await outgoing_wallet.load_proofs(reload=True)
|
||||||
quote = await outgoing_wallet.get_pay_amount_with_fees(invoice.bolt11)
|
quote = await outgoing_wallet.request_melt(invoice.bolt11)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
if outgoing_wallet.available_balance < total_amount:
|
if outgoing_wallet.available_balance < total_amount:
|
||||||
raise Exception("balance too low")
|
raise Exception("balance too low")
|
||||||
@@ -197,7 +197,7 @@ async def swap(
|
|||||||
_, send_proofs = await outgoing_wallet.split_to_send(
|
_, send_proofs = await outgoing_wallet.split_to_send(
|
||||||
outgoing_wallet.proofs, total_amount, set_reserved=True
|
outgoing_wallet.proofs, total_amount, set_reserved=True
|
||||||
)
|
)
|
||||||
await outgoing_wallet.pay_lightning(
|
await outgoing_wallet.melt(
|
||||||
send_proofs, invoice.bolt11, quote.fee_reserve, quote.quote
|
send_proofs, invoice.bolt11, quote.fee_reserve, quote.quote
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -177,16 +177,23 @@ async def cli(ctx: Context, host: str, walletname: str, unit: str, tests: bool):
|
|||||||
|
|
||||||
@cli.command("pay", help="Pay Lightning invoice.")
|
@cli.command("pay", help="Pay Lightning invoice.")
|
||||||
@click.argument("invoice", type=str)
|
@click.argument("invoice", type=str)
|
||||||
|
@click.argument(
|
||||||
|
"amount",
|
||||||
|
type=int,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--yes", "-y", default=False, is_flag=True, help="Skip confirmation.", type=bool
|
"--yes", "-y", default=False, is_flag=True, help="Skip confirmation.", type=bool
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@coro
|
@coro
|
||||||
async def pay(ctx: Context, invoice: str, yes: bool):
|
async def pay(
|
||||||
|
ctx: Context, invoice: str, amount: Optional[int] = None, yes: bool = False
|
||||||
|
):
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
await wallet.load_mint()
|
await wallet.load_mint()
|
||||||
await print_balance(ctx)
|
await print_balance(ctx)
|
||||||
quote = await wallet.get_pay_amount_with_fees(invoice)
|
quote = await wallet.request_melt(invoice, amount)
|
||||||
logger.debug(f"Quote: {quote}")
|
logger.debug(f"Quote: {quote}")
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
if not yes:
|
if not yes:
|
||||||
@@ -209,7 +216,7 @@ async def pay(ctx: Context, invoice: str, yes: bool):
|
|||||||
return
|
return
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
try:
|
try:
|
||||||
melt_response = await wallet.pay_lightning(
|
melt_response = await wallet.melt(
|
||||||
send_proofs, invoice, quote.fee_reserve, quote.quote
|
send_proofs, invoice, quote.fee_reserve, quote.quote
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -334,14 +341,14 @@ async def swap(ctx: Context):
|
|||||||
invoice = await incoming_wallet.request_mint(amount)
|
invoice = await incoming_wallet.request_mint(amount)
|
||||||
|
|
||||||
# pay invoice from outgoing mint
|
# pay invoice from outgoing mint
|
||||||
quote = await outgoing_wallet.get_pay_amount_with_fees(invoice.bolt11)
|
quote = await outgoing_wallet.request_melt(invoice.bolt11)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
if outgoing_wallet.available_balance < total_amount:
|
if outgoing_wallet.available_balance < total_amount:
|
||||||
raise Exception("balance too low")
|
raise Exception("balance too low")
|
||||||
_, send_proofs = await outgoing_wallet.split_to_send(
|
_, send_proofs = await outgoing_wallet.split_to_send(
|
||||||
outgoing_wallet.proofs, total_amount, set_reserved=True
|
outgoing_wallet.proofs, total_amount, set_reserved=True
|
||||||
)
|
)
|
||||||
await outgoing_wallet.pay_lightning(
|
await outgoing_wallet.melt(
|
||||||
send_proofs, invoice.bolt11, quote.fee_reserve, quote.quote
|
send_proofs, invoice.bolt11, quote.fee_reserve, quote.quote
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class LightningWallet(Wallet):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if successful
|
bool: True if successful
|
||||||
"""
|
"""
|
||||||
quote = await self.get_pay_amount_with_fees(pr)
|
quote = await self.request_melt(pr)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
assert total_amount > 0, "amount is not positive"
|
assert total_amount > 0, "amount is not positive"
|
||||||
if self.available_balance < total_amount:
|
if self.available_balance < total_amount:
|
||||||
@@ -63,9 +63,7 @@ class LightningWallet(Wallet):
|
|||||||
return PaymentResponse(ok=False)
|
return PaymentResponse(ok=False)
|
||||||
_, send_proofs = await self.split_to_send(self.proofs, total_amount)
|
_, send_proofs = await self.split_to_send(self.proofs, total_amount)
|
||||||
try:
|
try:
|
||||||
resp = await self.pay_lightning(
|
resp = await self.melt(send_proofs, pr, quote.fee_reserve, quote.quote)
|
||||||
send_proofs, pr, quote.fee_reserve, quote.quote
|
|
||||||
)
|
|
||||||
if resp.change:
|
if resp.change:
|
||||||
fees_paid_sat = quote.fee_reserve - sum_promises(resp.change)
|
fees_paid_sat = quote.fee_reserve - sum_promises(resp.change)
|
||||||
else:
|
else:
|
||||||
|
|||||||
38
cashu/wallet/mint_info.py
Normal file
38
cashu/wallet/mint_info.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ..core.base import Nut15MppSupport, Unit
|
||||||
|
|
||||||
|
|
||||||
|
class MintInfo(BaseModel):
|
||||||
|
name: Optional[str]
|
||||||
|
pubkey: Optional[str]
|
||||||
|
version: Optional[str]
|
||||||
|
description: Optional[str]
|
||||||
|
description_long: Optional[str]
|
||||||
|
contact: Optional[List[List[str]]]
|
||||||
|
motd: Optional[str]
|
||||||
|
nuts: Optional[Dict[int, Any]]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.description})"
|
||||||
|
|
||||||
|
def supports_nut(self, nut: int) -> bool:
|
||||||
|
if self.nuts is None:
|
||||||
|
return False
|
||||||
|
return nut in self.nuts
|
||||||
|
|
||||||
|
def supports_mpp(self, method: str, unit: Unit) -> bool:
|
||||||
|
if not self.nuts:
|
||||||
|
return False
|
||||||
|
nut_15 = self.nuts.get(15)
|
||||||
|
if not nut_15 or not self.supports_nut(15):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for entry in nut_15:
|
||||||
|
entry_obj = Nut15MppSupport.parse_obj(entry)
|
||||||
|
if entry_obj.method == method and entry_obj.unit == unit.name:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -71,6 +71,7 @@ from ..wallet.crud import (
|
|||||||
)
|
)
|
||||||
from . import migrations
|
from . import migrations
|
||||||
from .htlc import WalletHTLC
|
from .htlc import WalletHTLC
|
||||||
|
from .mint_info import MintInfo
|
||||||
from .p2pk import WalletP2PK
|
from .p2pk import WalletP2PK
|
||||||
from .secrets import WalletSecrets
|
from .secrets import WalletSecrets
|
||||||
from .wallet_deprecated import LedgerAPIDeprecated
|
from .wallet_deprecated import LedgerAPIDeprecated
|
||||||
@@ -130,7 +131,7 @@ class LedgerAPI(LedgerAPIDeprecated, object):
|
|||||||
keysets: Dict[str, WalletKeyset] # holds keysets
|
keysets: Dict[str, WalletKeyset] # holds keysets
|
||||||
mint_keyset_ids: List[str] # holds active keyset ids of the mint
|
mint_keyset_ids: List[str] # holds active keyset ids of the mint
|
||||||
unit: Unit
|
unit: Unit
|
||||||
mint_info: GetInfoResponse # holds info about mint
|
mint_info: MintInfo # holds info about mint
|
||||||
tor: TorProxy
|
tor: TorProxy
|
||||||
db: Database
|
db: Database
|
||||||
httpx: httpx.AsyncClient
|
httpx: httpx.AsyncClient
|
||||||
@@ -269,9 +270,10 @@ class LedgerAPI(LedgerAPIDeprecated, object):
|
|||||||
logger.debug(f"Mint keysets: {self.mint_keyset_ids}")
|
logger.debug(f"Mint keysets: {self.mint_keyset_ids}")
|
||||||
return self.mint_keyset_ids
|
return self.mint_keyset_ids
|
||||||
|
|
||||||
async def _load_mint_info(self) -> GetInfoResponse:
|
async def _load_mint_info(self) -> MintInfo:
|
||||||
"""Loads the mint info from the mint."""
|
"""Loads the mint info from the mint."""
|
||||||
self.mint_info = await self._get_info()
|
mint_info_resp = await self._get_info()
|
||||||
|
self.mint_info = MintInfo(**mint_info_resp.dict())
|
||||||
logger.debug(f"Mint info: {self.mint_info}")
|
logger.debug(f"Mint info: {self.mint_info}")
|
||||||
return self.mint_info
|
return self.mint_info
|
||||||
|
|
||||||
@@ -518,11 +520,15 @@ class LedgerAPI(LedgerAPIDeprecated, object):
|
|||||||
|
|
||||||
@async_set_httpx_client
|
@async_set_httpx_client
|
||||||
@async_ensure_mint_loaded
|
@async_ensure_mint_loaded
|
||||||
async def melt_quote(self, payment_request: str) -> PostMeltQuoteResponse:
|
async def melt_quote(
|
||||||
|
self, payment_request: str, amount: Optional[int] = None
|
||||||
|
) -> PostMeltQuoteResponse:
|
||||||
"""Checks whether the Lightning payment is internal."""
|
"""Checks whether the Lightning payment is internal."""
|
||||||
invoice_obj = bolt11.decode(payment_request)
|
invoice_obj = bolt11.decode(payment_request)
|
||||||
assert invoice_obj.amount_msat, "invoice must have amount"
|
assert invoice_obj.amount_msat, "invoice must have amount"
|
||||||
payload = PostMeltQuoteRequest(unit=self.unit.name, request=payment_request)
|
payload = PostMeltQuoteRequest(
|
||||||
|
unit=self.unit.name, request=payment_request, amount=amount
|
||||||
|
)
|
||||||
resp = await self.httpx.post(
|
resp = await self.httpx.post(
|
||||||
join(self.url, "/v1/melt/quote/bolt11"),
|
join(self.url, "/v1/melt/quote/bolt11"),
|
||||||
json=payload.dict(),
|
json=payload.dict(),
|
||||||
@@ -536,7 +542,7 @@ class LedgerAPI(LedgerAPIDeprecated, object):
|
|||||||
quote_id = "deprecated_" + str(uuid.uuid4())
|
quote_id = "deprecated_" + str(uuid.uuid4())
|
||||||
return PostMeltQuoteResponse(
|
return PostMeltQuoteResponse(
|
||||||
quote=quote_id,
|
quote=quote_id,
|
||||||
amount=invoice_obj.amount_msat // 1000,
|
amount=amount or invoice_obj.amount_msat // 1000,
|
||||||
fee_reserve=ret.fee or 0,
|
fee_reserve=ret.fee or 0,
|
||||||
paid=False,
|
paid=False,
|
||||||
expiry=invoice_obj.expiry,
|
expiry=invoice_obj.expiry,
|
||||||
@@ -582,7 +588,7 @@ class LedgerAPI(LedgerAPIDeprecated, object):
|
|||||||
if resp.status_code == 404:
|
if resp.status_code == 404:
|
||||||
invoice = await get_lightning_invoice(id=quote, db=self.db)
|
invoice = await get_lightning_invoice(id=quote, db=self.db)
|
||||||
assert invoice, f"no invoice found for id {quote}"
|
assert invoice, f"no invoice found for id {quote}"
|
||||||
ret: PostMeltResponse_deprecated = await self.pay_lightning_deprecated(
|
ret: PostMeltResponse_deprecated = await self.melt_deprecated(
|
||||||
proofs=proofs, outputs=outputs, invoice=invoice.bolt11
|
proofs=proofs, outputs=outputs, invoice=invoice.bolt11
|
||||||
)
|
)
|
||||||
return PostMeltResponse(
|
return PostMeltResponse(
|
||||||
@@ -987,7 +993,21 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
|||||||
send_proofs = new_proofs[len(frst_outputs) :]
|
send_proofs = new_proofs[len(frst_outputs) :]
|
||||||
return keep_proofs, send_proofs
|
return keep_proofs, send_proofs
|
||||||
|
|
||||||
async def pay_lightning(
|
async def request_melt(
|
||||||
|
self, invoice: str, amount: Optional[int] = None
|
||||||
|
) -> PostMeltQuoteResponse:
|
||||||
|
"""
|
||||||
|
Fetches a melt quote from the mint and either uses the amount in the invoice or the amount provided.
|
||||||
|
"""
|
||||||
|
if amount and not self.mint_info.supports_mpp("bolt11", self.unit):
|
||||||
|
raise Exception("Mint does not support MPP, cannot specify amount.")
|
||||||
|
melt_quote = await self.melt_quote(invoice, amount)
|
||||||
|
logger.debug(
|
||||||
|
f"Mint wants {self.unit.str(melt_quote.fee_reserve)} as fee reserve."
|
||||||
|
)
|
||||||
|
return melt_quote
|
||||||
|
|
||||||
|
async def melt(
|
||||||
self, proofs: List[Proof], invoice: str, fee_reserve_sat: int, quote_id: str
|
self, proofs: List[Proof], invoice: str, fee_reserve_sat: int, quote_id: str
|
||||||
) -> PostMeltResponse:
|
) -> PostMeltResponse:
|
||||||
"""Pays a lightning invoice and returns the status of the payment.
|
"""Pays a lightning invoice and returns the status of the payment.
|
||||||
@@ -1520,17 +1540,6 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
|||||||
|
|
||||||
# ---------- TRANSACTION HELPERS ----------
|
# ---------- TRANSACTION HELPERS ----------
|
||||||
|
|
||||||
async def get_pay_amount_with_fees(self, invoice: str):
|
|
||||||
"""
|
|
||||||
Decodes the amount from a Lightning invoice and returns the
|
|
||||||
total amount (amount+fees) to be paid.
|
|
||||||
"""
|
|
||||||
melt_quote = await self.melt_quote(invoice)
|
|
||||||
logger.debug(
|
|
||||||
f"Mint wants {self.unit.str(melt_quote.fee_reserve)} as fee reserve."
|
|
||||||
)
|
|
||||||
return melt_quote
|
|
||||||
|
|
||||||
async def split_to_send(
|
async def split_to_send(
|
||||||
self,
|
self,
|
||||||
proofs: List[Proof],
|
proofs: List[Proof],
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ class LedgerAPIDeprecated(SupportsHttpxClient, SupportsMintURL):
|
|||||||
|
|
||||||
@async_set_httpx_client
|
@async_set_httpx_client
|
||||||
@async_ensure_mint_loaded_deprecated
|
@async_ensure_mint_loaded_deprecated
|
||||||
async def pay_lightning_deprecated(
|
async def melt_deprecated(
|
||||||
self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]]
|
self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]]
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ settings.mint_derivation_path_list = []
|
|||||||
settings.mint_private_key = "TEST_PRIVATE_KEY"
|
settings.mint_private_key = "TEST_PRIVATE_KEY"
|
||||||
settings.mint_seed_decryption_key = ""
|
settings.mint_seed_decryption_key = ""
|
||||||
settings.mint_max_balance = 0
|
settings.mint_max_balance = 0
|
||||||
|
settings.mint_lnd_enable_mpp = True
|
||||||
|
|
||||||
assert "test" in settings.cashu_dir
|
assert "test" in settings.cashu_dir
|
||||||
shutil.rmtree(settings.cashu_dir, ignore_errors=True)
|
shutil.rmtree(settings.cashu_dir, ignore_errors=True)
|
||||||
|
|||||||
@@ -249,11 +249,11 @@ async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Led
|
|||||||
invoice_payment_request = str(invoice_dict["payment_request"])
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
# wallet pays the invoice
|
# wallet pays the invoice
|
||||||
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
quote = await wallet.melt_quote(invoice_payment_request)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
wallet.pay_lightning(
|
wallet.melt(
|
||||||
proofs=send_proofs,
|
proofs=send_proofs,
|
||||||
invoice=invoice_payment_request,
|
invoice=invoice_payment_request,
|
||||||
fee_reserve_sat=quote.fee_reserve,
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
@@ -294,11 +294,11 @@ async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Led
|
|||||||
invoice_payment_request = str(invoice_dict["payment_request"])
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
# wallet pays the invoice
|
# wallet pays the invoice
|
||||||
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
quote = await wallet.melt_quote(invoice_payment_request)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
wallet.pay_lightning(
|
wallet.melt(
|
||||||
proofs=send_proofs,
|
proofs=send_proofs,
|
||||||
invoice=invoice_payment_request,
|
invoice=invoice_payment_request,
|
||||||
fee_reserve_sat=quote.fee_reserve,
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
@@ -344,11 +344,11 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led
|
|||||||
preimage_hash = invoice_obj.payment_hash
|
preimage_hash = invoice_obj.payment_hash
|
||||||
|
|
||||||
# wallet pays the invoice
|
# wallet pays the invoice
|
||||||
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
quote = await wallet.melt_quote(invoice_payment_request)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
wallet.pay_lightning(
|
wallet.melt(
|
||||||
proofs=send_proofs,
|
proofs=send_proofs,
|
||||||
invoice=invoice_payment_request,
|
invoice=invoice_payment_request,
|
||||||
fee_reserve_sat=quote.fee_reserve,
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import pytest
|
|||||||
import respx
|
import respx
|
||||||
from httpx import Response
|
from httpx import Response
|
||||||
|
|
||||||
from cashu.core.base import Amount, MeltQuote, Unit
|
from cashu.core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
|
||||||
from cashu.core.settings import settings
|
from cashu.core.settings import settings
|
||||||
from cashu.lightning.blink import MINIMUM_FEE_MSAT, BlinkWallet # type: ignore
|
from cashu.lightning.blink import MINIMUM_FEE_MSAT, BlinkWallet # type: ignore
|
||||||
|
|
||||||
@@ -192,7 +192,10 @@ async def test_blink_get_payment_quote():
|
|||||||
# response says 1 sat fees but invoice (1000 sat) * 0.5% is 5 sat so we expect 5 sat
|
# response says 1 sat fees but invoice (1000 sat) * 0.5% is 5 sat so we expect 5 sat
|
||||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 1}}}
|
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 1}}}
|
||||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||||
quote = await blink.get_payment_quote(payment_request)
|
melt_quote_request = PostMeltQuoteRequest(
|
||||||
|
unit=Unit.sat.name, request=payment_request
|
||||||
|
)
|
||||||
|
quote = await blink.get_payment_quote(melt_quote_request)
|
||||||
assert quote.checking_id == payment_request
|
assert quote.checking_id == payment_request
|
||||||
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
||||||
assert quote.fee == Amount(Unit.sat, 5) # sat
|
assert quote.fee == Amount(Unit.sat, 5) # sat
|
||||||
@@ -200,7 +203,10 @@ async def test_blink_get_payment_quote():
|
|||||||
# response says 10 sat fees but invoice (1000 sat) * 0.5% is 5 sat so we expect 10 sat
|
# response says 10 sat fees but invoice (1000 sat) * 0.5% is 5 sat so we expect 10 sat
|
||||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}}
|
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}}
|
||||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||||
quote = await blink.get_payment_quote(payment_request)
|
melt_quote_request = PostMeltQuoteRequest(
|
||||||
|
unit=Unit.sat.name, request=payment_request
|
||||||
|
)
|
||||||
|
quote = await blink.get_payment_quote(melt_quote_request)
|
||||||
assert quote.checking_id == payment_request
|
assert quote.checking_id == payment_request
|
||||||
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
||||||
assert quote.fee == Amount(Unit.sat, 10) # sat
|
assert quote.fee == Amount(Unit.sat, 10) # sat
|
||||||
@@ -208,7 +214,10 @@ async def test_blink_get_payment_quote():
|
|||||||
# response says 10 sat fees but invoice (4973 sat) * 0.5% is 24.865 sat so we expect 25 sat
|
# response says 10 sat fees but invoice (4973 sat) * 0.5% is 24.865 sat so we expect 25 sat
|
||||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}}
|
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}}
|
||||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||||
quote = await blink.get_payment_quote(payment_request_4973)
|
melt_quote_request_4973 = PostMeltQuoteRequest(
|
||||||
|
unit=Unit.sat.name, request=payment_request_4973
|
||||||
|
)
|
||||||
|
quote = await blink.get_payment_quote(melt_quote_request_4973)
|
||||||
assert quote.checking_id == payment_request_4973
|
assert quote.checking_id == payment_request_4973
|
||||||
assert quote.amount == Amount(Unit.sat, 4973) # sat
|
assert quote.amount == Amount(Unit.sat, 4973) # sat
|
||||||
assert quote.fee == Amount(Unit.sat, 25) # sat
|
assert quote.fee == Amount(Unit.sat, 25) # sat
|
||||||
@@ -216,7 +225,10 @@ async def test_blink_get_payment_quote():
|
|||||||
# response says 0 sat fees but invoice (1 sat) * 0.5% is 0.005 sat so we expect MINIMUM_FEE_MSAT/1000 sat
|
# response says 0 sat fees but invoice (1 sat) * 0.5% is 0.005 sat so we expect MINIMUM_FEE_MSAT/1000 sat
|
||||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 0}}}
|
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 0}}}
|
||||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||||
quote = await blink.get_payment_quote(payment_request_1)
|
melt_quote_request_1 = PostMeltQuoteRequest(
|
||||||
|
unit=Unit.sat.name, request=payment_request_1
|
||||||
|
)
|
||||||
|
quote = await blink.get_payment_quote(melt_quote_request_1)
|
||||||
assert quote.checking_id == payment_request_1
|
assert quote.checking_id == payment_request_1
|
||||||
assert quote.amount == Amount(Unit.sat, 1) # sat
|
assert quote.amount == Amount(Unit.sat, 1) # sat
|
||||||
assert quote.fee == Amount(Unit.sat, MINIMUM_FEE_MSAT // 1000) # msat
|
assert quote.fee == Amount(Unit.sat, MINIMUM_FEE_MSAT // 1000) # msat
|
||||||
@@ -228,7 +240,10 @@ async def test_blink_get_payment_quote_backend_error():
|
|||||||
# response says error but invoice (1000 sat) * 0.5% is 5 sat so we expect 10 sat
|
# response says error but invoice (1000 sat) * 0.5% is 5 sat so we expect 10 sat
|
||||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"errors": [{"message": "error"}]}}}
|
mock_response = {"data": {"lnInvoiceFeeProbe": {"errors": [{"message": "error"}]}}}
|
||||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||||
quote = await blink.get_payment_quote(payment_request)
|
melt_quote_request = PostMeltQuoteRequest(
|
||||||
|
unit=Unit.sat.name, request=payment_request
|
||||||
|
)
|
||||||
|
quote = await blink.get_payment_quote(melt_quote_request)
|
||||||
assert quote.checking_id == payment_request
|
assert quote.checking_id == payment_request
|
||||||
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
||||||
assert quote.fee == Amount(Unit.sat, 5) # sat
|
assert quote.fee == Amount(Unit.sat, 5) # sat
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ async def test_melt_external(wallet1: Wallet, ledger: Ledger):
|
|||||||
invoice_dict = get_real_invoice(64)
|
invoice_dict = get_real_invoice(64)
|
||||||
invoice_payment_request = invoice_dict["payment_request"]
|
invoice_payment_request = invoice_dict["payment_request"]
|
||||||
|
|
||||||
mint_quote = await wallet1.get_pay_amount_with_fees(invoice_payment_request)
|
mint_quote = await wallet1.melt_quote(invoice_payment_request)
|
||||||
total_amount = mint_quote.amount + mint_quote.fee_reserve
|
total_amount = mint_quote.amount + mint_quote.fee_reserve
|
||||||
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
||||||
melt_quote = await ledger.melt_quote(
|
melt_quote = await ledger.melt_quote(
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
|||||||
invoice_payment_request = str(invoice_dict["payment_request"])
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
# wallet pays the invoice
|
# wallet pays the invoice
|
||||||
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
quote = await wallet.melt_quote(invoice_payment_request)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
asyncio.create_task(ledger.melt(proofs=send_proofs, quote=quote.quote))
|
asyncio.create_task(ledger.melt(proofs=send_proofs, quote=quote.quote))
|
||||||
# asyncio.create_task(
|
# asyncio.create_task(
|
||||||
# wallet.pay_lightning(
|
# wallet.melt(
|
||||||
# proofs=send_proofs,
|
# proofs=send_proofs,
|
||||||
# invoice=invoice_payment_request,
|
# invoice=invoice_payment_request,
|
||||||
# fee_reserve_sat=quote.fee_reserve,
|
# fee_reserve_sat=quote.fee_reserve,
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ async def test_melt(wallet1: Wallet):
|
|||||||
invoice_payment_hash = str(invoice.payment_hash)
|
invoice_payment_hash = str(invoice.payment_hash)
|
||||||
invoice_payment_request = invoice.bolt11
|
invoice_payment_request = invoice.bolt11
|
||||||
|
|
||||||
quote = await wallet1.get_pay_amount_with_fees(invoice_payment_request)
|
quote = await wallet1.request_melt(invoice_payment_request)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
|
|
||||||
if is_regtest:
|
if is_regtest:
|
||||||
@@ -285,7 +285,7 @@ async def test_melt(wallet1: Wallet):
|
|||||||
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
||||||
|
|
||||||
melt_response = await wallet1.pay_lightning(
|
melt_response = await wallet1.melt(
|
||||||
proofs=send_proofs,
|
proofs=send_proofs,
|
||||||
invoice=invoice_payment_request,
|
invoice=invoice_payment_request,
|
||||||
fee_reserve_sat=quote.fee_reserve,
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
|||||||
invoice_payment_request = str(invoice_dict["payment_request"])
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
# wallet pays the invoice
|
# wallet pays the invoice
|
||||||
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
quote = await wallet.melt_quote(invoice_payment_request)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
wallet.pay_lightning(
|
wallet.melt(
|
||||||
proofs=send_proofs,
|
proofs=send_proofs,
|
||||||
invoice=invoice_payment_request,
|
invoice=invoice_payment_request,
|
||||||
fee_reserve_sat=quote.fee_reserve,
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
@@ -83,11 +83,11 @@ async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger):
|
|||||||
preimage_hash = invoice_obj.payment_hash
|
preimage_hash = invoice_obj.payment_hash
|
||||||
|
|
||||||
# wallet pays the invoice
|
# wallet pays the invoice
|
||||||
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
quote = await wallet.melt_quote(invoice_payment_request)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
wallet.pay_lightning(
|
wallet.melt(
|
||||||
proofs=send_proofs,
|
proofs=send_proofs,
|
||||||
invoice=invoice_payment_request,
|
invoice=invoice_payment_request,
|
||||||
fee_reserve_sat=quote.fee_reserve,
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
|||||||
127
tests/test_wallet_regtest_mpp.py
Normal file
127
tests/test_wallet_regtest_mpp.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
from cashu.core.base import Method, Proof
|
||||||
|
from cashu.mint.ledger import Ledger
|
||||||
|
from cashu.wallet.wallet import Wallet
|
||||||
|
from tests.conftest import SERVER_ENDPOINT
|
||||||
|
from tests.helpers import (
|
||||||
|
get_real_invoice,
|
||||||
|
is_fake,
|
||||||
|
pay_if_regtest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def wallet():
|
||||||
|
wallet = await Wallet.with_db(
|
||||||
|
url=SERVER_ENDPOINT,
|
||||||
|
db="test_data/wallet",
|
||||||
|
name="wallet",
|
||||||
|
)
|
||||||
|
await wallet.load_mint()
|
||||||
|
yield wallet
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
|
async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger):
|
||||||
|
# make sure that mpp is supported by the bolt11-sat backend
|
||||||
|
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
|
||||||
|
pytest.skip("backend does not support mpp")
|
||||||
|
|
||||||
|
# make sure wallet knows the backend supports mpp
|
||||||
|
assert wallet.mint_info.supports_mpp("bolt11", wallet.unit)
|
||||||
|
|
||||||
|
# top up wallet twice so we have enough for two payments
|
||||||
|
topup_invoice = await wallet.request_mint(128)
|
||||||
|
pay_if_regtest(topup_invoice.bolt11)
|
||||||
|
proofs1 = await wallet.mint(128, id=topup_invoice.id)
|
||||||
|
assert wallet.balance == 128
|
||||||
|
|
||||||
|
topup_invoice = await wallet.request_mint(128)
|
||||||
|
pay_if_regtest(topup_invoice.bolt11)
|
||||||
|
proofs2 = await wallet.mint(128, id=topup_invoice.id)
|
||||||
|
assert wallet.balance == 256
|
||||||
|
|
||||||
|
# this is the invoice we want to pay in two parts
|
||||||
|
invoice_dict = get_real_invoice(64)
|
||||||
|
invoice_payment_request = invoice_dict["payment_request"]
|
||||||
|
|
||||||
|
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
# wallet pays 32 sat of the invoice
|
||||||
|
quote = await wallet.melt_quote(invoice_payment_request, amount=32)
|
||||||
|
assert quote.amount == amount
|
||||||
|
await wallet.melt(
|
||||||
|
proofs,
|
||||||
|
invoice_payment_request,
|
||||||
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
quote_id=quote.quote,
|
||||||
|
)
|
||||||
|
|
||||||
|
# call pay_mpp twice in parallel to pay the full invoice
|
||||||
|
# we delay the second payment so that the wallet doesn't derive the same blindedmessages twice due to a race condition
|
||||||
|
await asyncio.gather(pay_mpp(32, proofs1), pay_mpp(32, proofs2, delay=0.5))
|
||||||
|
|
||||||
|
assert wallet.balance <= 256 - 64
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
|
async def test_regtest_pay_mpp_incomplete_payment(wallet: Wallet, ledger: Ledger):
|
||||||
|
# make sure that mpp is supported by the bolt11-sat backend
|
||||||
|
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
|
||||||
|
pytest.skip("backend does not support mpp")
|
||||||
|
|
||||||
|
# make sure wallet knows the backend supports mpp
|
||||||
|
assert wallet.mint_info.supports_mpp("bolt11", wallet.unit)
|
||||||
|
|
||||||
|
# top up wallet twice so we have enough for three payments
|
||||||
|
topup_invoice = await wallet.request_mint(128)
|
||||||
|
pay_if_regtest(topup_invoice.bolt11)
|
||||||
|
proofs1 = await wallet.mint(128, id=topup_invoice.id)
|
||||||
|
assert wallet.balance == 128
|
||||||
|
|
||||||
|
topup_invoice = await wallet.request_mint(128)
|
||||||
|
pay_if_regtest(topup_invoice.bolt11)
|
||||||
|
proofs2 = await wallet.mint(128, id=topup_invoice.id)
|
||||||
|
assert wallet.balance == 256
|
||||||
|
|
||||||
|
topup_invoice = await wallet.request_mint(128)
|
||||||
|
pay_if_regtest(topup_invoice.bolt11)
|
||||||
|
proofs3 = await wallet.mint(128, id=topup_invoice.id)
|
||||||
|
assert wallet.balance == 384
|
||||||
|
|
||||||
|
# this is the invoice we want to pay in two parts
|
||||||
|
invoice_dict = get_real_invoice(64)
|
||||||
|
invoice_payment_request = invoice_dict["payment_request"]
|
||||||
|
|
||||||
|
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
# wallet pays 32 sat of the invoice
|
||||||
|
quote = await wallet.melt_quote(invoice_payment_request, amount=amount)
|
||||||
|
assert quote.amount == amount
|
||||||
|
await wallet.melt(
|
||||||
|
proofs,
|
||||||
|
invoice_payment_request,
|
||||||
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
quote_id=quote.quote,
|
||||||
|
)
|
||||||
|
|
||||||
|
# instead: call pay_mpp twice in the background, sleep for a bit, then check if the payment was successful (it should not be)
|
||||||
|
asyncio.create_task(pay_mpp(32, proofs1))
|
||||||
|
asyncio.create_task(pay_mpp(16, proofs2, delay=0.5))
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# payment is still pending because the full amount has not been paid
|
||||||
|
assert wallet.balance == 384
|
||||||
|
|
||||||
|
# send the remaining 16 sat to complete the payment
|
||||||
|
asyncio.create_task(pay_mpp(16, proofs3, delay=0.5))
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
assert wallet.balance <= 384 - 64
|
||||||
Reference in New Issue
Block a user