mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
[Refactor] Mint: remove output.id optional (#504)
* remove output.id optional * asserts * wip * wip * working
This commit is contained in:
@@ -172,7 +172,7 @@ This command runs the mint on your local computer. Skip this step if you want to
|
|||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -d -p 3338:3338 --name nutshell -e MINT_BACKEND_BOLT11_SAT=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.15.2 poetry run mint
|
docker run -d -p 3338:3338 --name nutshell -e MINT_BACKEND_BOLT11_SAT=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.15.3 poetry run mint
|
||||||
```
|
```
|
||||||
|
|
||||||
## From this repository
|
## From this repository
|
||||||
|
|||||||
@@ -101,12 +101,12 @@ class Proof(BaseModel):
|
|||||||
time_created: Union[None, str] = ""
|
time_created: Union[None, str] = ""
|
||||||
time_reserved: Union[None, str] = ""
|
time_reserved: Union[None, str] = ""
|
||||||
derivation_path: Union[None, str] = "" # derivation path of the proof
|
derivation_path: Union[None, str] = "" # derivation path of the proof
|
||||||
mint_id: Union[None, str] = (
|
mint_id: Union[
|
||||||
None # holds the id of the mint operation that created this proof
|
None, str
|
||||||
)
|
] = None # holds the id of the mint operation that created this proof
|
||||||
melt_id: Union[None, str] = (
|
melt_id: Union[
|
||||||
None # holds the id of the melt operation that destroyed this proof
|
None, str
|
||||||
)
|
] = None # holds the id of the melt operation that destroyed this proof
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
@@ -161,20 +161,13 @@ 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: Optional[
|
id: str
|
||||||
str
|
|
||||||
] # DEPRECATION: Only Optional for backwards compatibility with old clients < 0.15 for deprecated API route.
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -204,11 +197,6 @@ class BlindedSignature(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BlindedMessages(BaseModel):
|
|
||||||
# NOTE: not used in Pydantic validation
|
|
||||||
__root__: List[BlindedMessage] = []
|
|
||||||
|
|
||||||
|
|
||||||
# ------- LIGHTNING INVOICE -------
|
# ------- LIGHTNING INVOICE -------
|
||||||
|
|
||||||
|
|
||||||
@@ -341,6 +329,19 @@ 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 -------
|
||||||
|
|
||||||
|
|
||||||
@@ -407,7 +408,7 @@ class GetMintResponse_deprecated(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PostMintRequest_deprecated(BaseModel):
|
class PostMintRequest_deprecated(BaseModel):
|
||||||
outputs: List[BlindedMessage] = Field(
|
outputs: List[BlindedMessage_Deprecated] = Field(
|
||||||
..., max_items=settings.mint_max_request_length
|
..., max_items=settings.mint_max_request_length
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -454,7 +455,7 @@ class PostMeltResponse(BaseModel):
|
|||||||
class PostMeltRequest_deprecated(BaseModel):
|
class PostMeltRequest_deprecated(BaseModel):
|
||||||
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
|
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
|
||||||
pr: str = Field(..., max_length=settings.mint_max_request_length)
|
pr: str = Field(..., max_length=settings.mint_max_request_length)
|
||||||
outputs: Union[List[BlindedMessage], None] = Field(
|
outputs: Union[List[BlindedMessage_Deprecated], None] = Field(
|
||||||
None, max_items=settings.mint_max_request_length
|
None, max_items=settings.mint_max_request_length
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -483,7 +484,7 @@ class PostSplitResponse(BaseModel):
|
|||||||
class PostSplitRequest_Deprecated(BaseModel):
|
class PostSplitRequest_Deprecated(BaseModel):
|
||||||
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
|
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
|
||||||
amount: Optional[int] = None
|
amount: Optional[int] = None
|
||||||
outputs: List[BlindedMessage] = Field(
|
outputs: List[BlindedMessage_Deprecated] = Field(
|
||||||
..., max_items=settings.mint_max_request_length
|
..., max_items=settings.mint_max_request_length
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field
|
|||||||
|
|
||||||
env = Env()
|
env = Env()
|
||||||
|
|
||||||
VERSION = "0.15.2"
|
VERSION = "0.15.3"
|
||||||
|
|
||||||
|
|
||||||
def find_env_file():
|
def find_env_file():
|
||||||
@@ -58,7 +58,7 @@ class MintSettings(CashuSettings):
|
|||||||
|
|
||||||
mint_database: str = Field(default="data/mint")
|
mint_database: str = Field(default="data/mint")
|
||||||
mint_test_database: str = Field(default="test_data/test_mint")
|
mint_test_database: str = Field(default="test_data/test_mint")
|
||||||
mint_duplicate_keysets: bool = Field(
|
mint_duplicate_old_keysets: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
title="Duplicate keysets",
|
title="Duplicate keysets",
|
||||||
description=(
|
description=(
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from ..core.crypto.keys import (
|
|||||||
from ..core.crypto.secp import PrivateKey, PublicKey
|
from ..core.crypto.secp import PrivateKey, PublicKey
|
||||||
from ..core.db import Connection, Database, get_db_connection
|
from ..core.db import Connection, Database, get_db_connection
|
||||||
from ..core.errors import (
|
from ..core.errors import (
|
||||||
|
CashuError,
|
||||||
KeysetError,
|
KeysetError,
|
||||||
KeysetNotFoundError,
|
KeysetNotFoundError,
|
||||||
LightningError,
|
LightningError,
|
||||||
@@ -72,7 +73,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
derivation_path="",
|
derivation_path="",
|
||||||
crud=LedgerCrudSqlite(),
|
crud=LedgerCrudSqlite(),
|
||||||
):
|
):
|
||||||
assert seed, "seed not set"
|
if not seed:
|
||||||
|
raise Exception("seed not set")
|
||||||
|
|
||||||
# decrypt seed if seed_decryption_key is set
|
# decrypt seed if seed_decryption_key is set
|
||||||
try:
|
try:
|
||||||
@@ -189,7 +191,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
Returns:
|
Returns:
|
||||||
MintKeyset: Keyset
|
MintKeyset: Keyset
|
||||||
"""
|
"""
|
||||||
assert derivation_path, "derivation path not set"
|
if not derivation_path:
|
||||||
|
raise Exception("derivation path not set")
|
||||||
seed = seed or self.seed
|
seed = seed or self.seed
|
||||||
tmp_keyset_local = MintKeyset(
|
tmp_keyset_local = MintKeyset(
|
||||||
seed=seed,
|
seed=seed,
|
||||||
@@ -230,7 +233,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
|
|
||||||
# BEGIN BACKWARDS COMPATIBILITY < 0.15.0
|
# BEGIN BACKWARDS COMPATIBILITY < 0.15.0
|
||||||
# set the deprecated id
|
# set the deprecated id
|
||||||
assert keyset.public_keys
|
if not keyset.public_keys:
|
||||||
|
raise KeysetError("no public keys for this keyset")
|
||||||
keyset.duplicate_keyset_id = derive_keyset_id_deprecated(keyset.public_keys)
|
keyset.duplicate_keyset_id = derive_keyset_id_deprecated(keyset.public_keys)
|
||||||
# END BACKWARDS COMPATIBILITY < 0.15.0
|
# END BACKWARDS COMPATIBILITY < 0.15.0
|
||||||
|
|
||||||
@@ -268,17 +272,22 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
logger.info(f"Current keyset: {self.keyset.id}")
|
logger.info(f"Current keyset: {self.keyset.id}")
|
||||||
|
|
||||||
# check that we have a least one active keyset
|
# check that we have a least one active keyset
|
||||||
assert any([k.active for k in self.keysets.values()]), "No active keyset found."
|
if not any([k.active for k in self.keysets.values()]):
|
||||||
|
raise KeysetError("No active keyset found.")
|
||||||
|
|
||||||
# BEGIN BACKWARDS COMPATIBILITY < 0.15.0
|
# BEGIN BACKWARDS COMPATIBILITY < 0.15.0
|
||||||
# we duplicate new keysets and compute their old keyset id, and
|
# we duplicate new keysets and compute their old keyset id, and
|
||||||
# we duplicate old keysets and compute their new keyset id
|
# we duplicate old keysets and compute their new keyset id
|
||||||
if (
|
if duplicate_keysets is not False and (
|
||||||
duplicate_keysets is None and settings.mint_duplicate_keysets
|
settings.mint_duplicate_old_keysets or duplicate_keysets
|
||||||
) or duplicate_keysets:
|
):
|
||||||
for _, keyset in copy.copy(self.keysets).items():
|
for _, keyset in copy.copy(self.keysets).items():
|
||||||
|
# if keyset.version_tuple >= (0, 15, 3) and not duplicate_keysets:
|
||||||
|
# # we do not duplicate keysets from version 0.15.3 and above if not forced by duplicate_keysets
|
||||||
|
# continue
|
||||||
keyset_copy = copy.copy(keyset)
|
keyset_copy = copy.copy(keyset)
|
||||||
assert keyset_copy.public_keys
|
if not keyset_copy.public_keys:
|
||||||
|
raise KeysetError("no public keys for this keyset")
|
||||||
if keyset.version_tuple >= (0, 15):
|
if keyset.version_tuple >= (0, 15):
|
||||||
keyset_copy.id = derive_keyset_id_deprecated(
|
keyset_copy.id = derive_keyset_id_deprecated(
|
||||||
keyset_copy.public_keys
|
keyset_copy.public_keys
|
||||||
@@ -296,7 +305,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
if keyset_id and keyset_id not in self.keysets:
|
if keyset_id and keyset_id not in self.keysets:
|
||||||
raise KeysetNotFoundError()
|
raise KeysetNotFoundError()
|
||||||
keyset = self.keysets[keyset_id] if keyset_id else self.keyset
|
keyset = self.keysets[keyset_id] if keyset_id else self.keyset
|
||||||
assert keyset.public_keys, KeysetError("no public keys for this keyset")
|
if not keyset.public_keys:
|
||||||
|
raise KeysetError("no public keys for this keyset")
|
||||||
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}
|
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}
|
||||||
|
|
||||||
async def get_balance(self) -> int:
|
async def get_balance(self) -> int:
|
||||||
@@ -400,7 +410,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
MintQuote: Mint quote object.
|
MintQuote: Mint quote object.
|
||||||
"""
|
"""
|
||||||
logger.trace("called request_mint")
|
logger.trace("called request_mint")
|
||||||
assert quote_request.amount > 0, "amount must be positive"
|
if not quote_request.amount > 0:
|
||||||
|
raise TransactionError("amount must be positive")
|
||||||
if settings.mint_max_peg_in and quote_request.amount > settings.mint_max_peg_in:
|
if settings.mint_max_peg_in and quote_request.amount > settings.mint_max_peg_in:
|
||||||
raise NotAllowedError(
|
raise NotAllowedError(
|
||||||
f"Maximum mint amount is {settings.mint_max_peg_in} sat."
|
f"Maximum mint amount is {settings.mint_max_peg_in} sat."
|
||||||
@@ -426,9 +437,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
f" {invoice_response.checking_id}"
|
f" {invoice_response.checking_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert (
|
if not (invoice_response.payment_request and invoice_response.checking_id):
|
||||||
invoice_response.payment_request and invoice_response.checking_id
|
raise LightningError("could not fetch bolt11 payment request from backend")
|
||||||
), LightningError("could not fetch bolt11 payment request from backend")
|
|
||||||
|
|
||||||
# get invoice expiry time
|
# get invoice expiry time
|
||||||
invoice_obj = bolt11.decode(invoice_response.payment_request)
|
invoice_obj = bolt11.decode(invoice_response.payment_request)
|
||||||
@@ -478,7 +488,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
unit, method = self._verify_and_get_unit_method(quote.unit, quote.method)
|
unit, method = self._verify_and_get_unit_method(quote.unit, quote.method)
|
||||||
|
|
||||||
if not quote.paid:
|
if not quote.paid:
|
||||||
assert quote.checking_id, "quote has no checking id"
|
if not quote.checking_id:
|
||||||
|
raise CashuError("quote has no checking id")
|
||||||
logger.trace(f"Lightning: checking invoice {quote.checking_id}")
|
logger.trace(f"Lightning: checking invoice {quote.checking_id}")
|
||||||
status: PaymentStatus = await self.backends[method][
|
status: PaymentStatus = await self.backends[method][
|
||||||
unit
|
unit
|
||||||
@@ -518,18 +529,27 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
await self._verify_outputs(outputs)
|
await self._verify_outputs(outputs)
|
||||||
sum_amount_outputs = sum([b.amount for b in outputs])
|
sum_amount_outputs = sum([b.amount for b in outputs])
|
||||||
|
|
||||||
|
output_units = set([k.unit for k in [self.keysets[o.id] for o in outputs]])
|
||||||
|
if not len(output_units) == 1:
|
||||||
|
raise TransactionError("outputs have different units")
|
||||||
|
output_unit = list(output_units)[0]
|
||||||
|
|
||||||
self.locks[quote_id] = (
|
self.locks[quote_id] = (
|
||||||
self.locks.get(quote_id) or asyncio.Lock()
|
self.locks.get(quote_id) or asyncio.Lock()
|
||||||
) # create a new lock if it doesn't exist
|
) # create a new lock if it doesn't exist
|
||||||
async with self.locks[quote_id]:
|
async with self.locks[quote_id]:
|
||||||
quote = await self.get_mint_quote(quote_id=quote_id)
|
quote = await self.get_mint_quote(quote_id=quote_id)
|
||||||
assert quote.paid, QuoteNotPaidError()
|
|
||||||
assert not quote.issued, "quote already issued"
|
if not quote.paid:
|
||||||
assert (
|
raise QuoteNotPaidError()
|
||||||
quote.amount == sum_amount_outputs
|
if quote.issued:
|
||||||
), "amount to mint does not match quote amount"
|
raise TransactionError("quote already issued")
|
||||||
if quote.expiry:
|
if not quote.unit == output_unit.name:
|
||||||
assert quote.expiry > int(time.time()), "quote expired"
|
raise TransactionError("quote unit does not match output unit")
|
||||||
|
if not quote.amount == sum_amount_outputs:
|
||||||
|
raise TransactionError("amount to mint does not match quote amount")
|
||||||
|
if quote.expiry and quote.expiry > int(time.time()):
|
||||||
|
raise TransactionError("quote expired")
|
||||||
|
|
||||||
promises = await self._generate_promises(outputs)
|
promises = await self._generate_promises(outputs)
|
||||||
logger.trace("generated promises")
|
logger.trace("generated promises")
|
||||||
@@ -571,12 +591,19 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
request=request, db=self.db
|
request=request, db=self.db
|
||||||
)
|
)
|
||||||
if mint_quote:
|
if mint_quote:
|
||||||
assert request == mint_quote.request, "bolt11 requests do not match"
|
if not request == mint_quote.request:
|
||||||
assert mint_quote.unit == melt_quote.unit, "units do not match"
|
raise TransactionError("bolt11 requests do not match")
|
||||||
assert mint_quote.method == method.name, "methods do not match"
|
if not mint_quote.unit == melt_quote.unit:
|
||||||
assert not mint_quote.paid, "mint quote already paid"
|
raise TransactionError("units do not match")
|
||||||
assert not mint_quote.issued, "mint quote already issued"
|
if not mint_quote.method == method.name:
|
||||||
assert mint_quote.checking_id, "mint quote has no checking id"
|
raise TransactionError("methods do not match")
|
||||||
|
if mint_quote.paid:
|
||||||
|
raise TransactionError("mint quote already paid")
|
||||||
|
if mint_quote.issued:
|
||||||
|
raise TransactionError("mint quote already issued")
|
||||||
|
if not mint_quote.checking_id:
|
||||||
|
raise TransactionError("mint quote has no checking id")
|
||||||
|
|
||||||
payment_quote = PaymentQuoteResponse(
|
payment_quote = PaymentQuoteResponse(
|
||||||
checking_id=mint_quote.checking_id,
|
checking_id=mint_quote.checking_id,
|
||||||
amount=Amount(unit, mint_quote.amount),
|
amount=Amount(unit, mint_quote.amount),
|
||||||
@@ -589,20 +616,20 @@ 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(request)
|
||||||
assert payment_quote.checking_id, "quote has no checking id"
|
if not payment_quote.checking_id:
|
||||||
|
raise TransactionError("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
|
||||||
assert (
|
if not payment_quote.amount.unit == unit:
|
||||||
payment_quote.amount.unit == unit
|
raise TransactionError("payment quote amount units do not match")
|
||||||
), "payment quote amount units do not match"
|
|
||||||
# fee from the backend must be in the same unit as the amount
|
# fee from the backend must be in the same unit as the amount
|
||||||
assert (
|
if not payment_quote.fee.unit == unit:
|
||||||
payment_quote.fee.unit == unit
|
raise TransactionError("payment quote fee units do not match")
|
||||||
), "payment quote fee units do not match"
|
|
||||||
|
|
||||||
# We assume that the request is a bolt11 invoice, this works since we
|
# We assume that the request is a bolt11 invoice, this works since we
|
||||||
# support only the bol11 method for now.
|
# support only the bol11 method for now.
|
||||||
invoice_obj = bolt11.decode(melt_quote.request)
|
invoice_obj = bolt11.decode(melt_quote.request)
|
||||||
assert invoice_obj.amount_msat, "invoice has no amount."
|
if not invoice_obj.amount_msat:
|
||||||
|
raise TransactionError("invoice has no amount.")
|
||||||
# we set the expiry of this quote to the expiry of the bolt11 invoice
|
# we set the expiry of this quote to the expiry of the bolt11 invoice
|
||||||
expiry = None
|
expiry = None
|
||||||
if invoice_obj.expiry is not None:
|
if invoice_obj.expiry is not None:
|
||||||
@@ -703,23 +730,28 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
if not mint_quote:
|
if not mint_quote:
|
||||||
return melt_quote
|
return melt_quote
|
||||||
# we settle the transaction internally
|
# we settle the transaction internally
|
||||||
assert not melt_quote.paid, "melt quote already paid"
|
if melt_quote.paid:
|
||||||
|
raise TransactionError("melt quote already paid")
|
||||||
|
|
||||||
# verify amounts from bolt11 invoice
|
# verify amounts from bolt11 invoice
|
||||||
bolt11_request = melt_quote.request
|
bolt11_request = melt_quote.request
|
||||||
invoice_obj = bolt11.decode(bolt11_request)
|
invoice_obj = bolt11.decode(bolt11_request)
|
||||||
assert invoice_obj.amount_msat, "invoice has no amount."
|
|
||||||
# invoice_amount_sat = math.ceil(invoice_obj.amount_msat / 1000)
|
if not invoice_obj.amount_msat:
|
||||||
# assert (
|
raise TransactionError("invoice has no amount.")
|
||||||
# Amount(Unit[melt_quote.unit], mint_quote.amount).to(Unit.sat).amount
|
if not mint_quote.amount == melt_quote.amount:
|
||||||
# == invoice_amount_sat
|
raise TransactionError("amounts do not match")
|
||||||
# ), "amounts do not match"
|
if not bolt11_request == mint_quote.request:
|
||||||
assert mint_quote.amount == melt_quote.amount, "amounts do not match"
|
raise TransactionError("bolt11 requests do not match")
|
||||||
assert bolt11_request == mint_quote.request, "bolt11 requests do not match"
|
if not mint_quote.unit == melt_quote.unit:
|
||||||
assert mint_quote.unit == melt_quote.unit, "units do not match"
|
raise TransactionError("units do not match")
|
||||||
assert mint_quote.method == melt_quote.method, "methods do not match"
|
if not mint_quote.method == melt_quote.method:
|
||||||
assert not mint_quote.paid, "mint quote already paid"
|
raise TransactionError("methods do not match")
|
||||||
assert not mint_quote.issued, "mint quote already issued"
|
if mint_quote.paid:
|
||||||
|
raise TransactionError("mint quote already paid")
|
||||||
|
if mint_quote.issued:
|
||||||
|
raise TransactionError("mint quote already issued")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Settling bolt11 payment internally: {melt_quote.quote} ->"
|
f"Settling bolt11 payment internally: {melt_quote.quote} ->"
|
||||||
f" {mint_quote.quote} ({melt_quote.amount} {melt_quote.unit})"
|
f" {mint_quote.quote} ({melt_quote.amount} {melt_quote.unit})"
|
||||||
@@ -764,25 +796,25 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
melt_quote.unit, melt_quote.method
|
melt_quote.unit, melt_quote.method
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not melt_quote.paid, "melt quote already paid"
|
if melt_quote.paid:
|
||||||
|
raise TransactionError("melt quote already paid")
|
||||||
|
|
||||||
# make sure that the outputs (for fee return) are in the same unit as the quote
|
# make sure that the outputs (for fee return) are in the same unit as the quote
|
||||||
if outputs:
|
if outputs:
|
||||||
await self._verify_outputs(outputs, skip_amount_check=True)
|
await self._verify_outputs(outputs, skip_amount_check=True)
|
||||||
assert outputs[0].id, "output id not set"
|
|
||||||
outputs_unit = self.keysets[outputs[0].id].unit
|
outputs_unit = self.keysets[outputs[0].id].unit
|
||||||
assert melt_quote.unit == outputs_unit.name, (
|
if not melt_quote.unit == outputs_unit.name:
|
||||||
f"output unit {outputs_unit.name} does not match quote unit"
|
raise TransactionError(
|
||||||
f" {melt_quote.unit}"
|
f"output unit {outputs_unit.name} does not match quote unit {melt_quote.unit}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# verify that the amount of the input proofs is equal to the amount of the quote
|
# verify that the amount of the input proofs is equal to the amount of the quote
|
||||||
total_provided = sum_proofs(proofs)
|
total_provided = sum_proofs(proofs)
|
||||||
total_needed = melt_quote.amount + (melt_quote.fee_reserve or 0)
|
total_needed = melt_quote.amount + (melt_quote.fee_reserve or 0)
|
||||||
assert total_provided >= total_needed, (
|
if not total_provided >= total_needed:
|
||||||
f"not enough inputs provided for melt. Provided: {total_provided}, needed:"
|
raise TransactionError(
|
||||||
f" {total_needed}"
|
f"not enough inputs provided for melt. Provided: {total_provided}, needed: {total_needed}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# verify that the amount of the proofs is not larger than the maximum allowed
|
# verify that the amount of the proofs is not larger than the maximum allowed
|
||||||
if settings.mint_max_peg_out and total_provided > settings.mint_max_peg_out:
|
if settings.mint_max_peg_out and total_provided > settings.mint_max_peg_out:
|
||||||
@@ -831,7 +863,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
# prepare change to compensate wallet for overpaid fees
|
# prepare change to compensate wallet for overpaid fees
|
||||||
return_promises: List[BlindedSignature] = []
|
return_promises: List[BlindedSignature] = []
|
||||||
if outputs:
|
if outputs:
|
||||||
assert outputs[0].id, "output id not set"
|
|
||||||
return_promises = await self._generate_change_promises(
|
return_promises = await self._generate_change_promises(
|
||||||
input_amount=total_provided,
|
input_amount=total_provided,
|
||||||
output_amount=melt_quote.amount,
|
output_amount=melt_quote.amount,
|
||||||
@@ -871,22 +902,21 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
Tuple[List[BlindSignature],List[BlindSignature]]: Promises on both sides of the split.
|
Tuple[List[BlindSignature],List[BlindSignature]]: Promises on both sides of the split.
|
||||||
"""
|
"""
|
||||||
logger.trace("split called")
|
logger.trace("split called")
|
||||||
|
# explicitly check that amount of inputs is equal to amount of outputs
|
||||||
|
# note: we check this again in verify_inputs_and_outputs but only if any
|
||||||
|
# outputs are provided at all. To make sure of that before calling
|
||||||
|
# verify_inputs_and_outputs, we check it here.
|
||||||
|
self._verify_equation_balanced(proofs, outputs)
|
||||||
|
# verify spending inputs, outputs, and spending conditions
|
||||||
|
await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs)
|
||||||
|
|
||||||
await self._set_proofs_pending(proofs)
|
await self._set_proofs_pending(proofs)
|
||||||
try:
|
try:
|
||||||
# explicitly check that amount of inputs is equal to amount of outputs
|
|
||||||
# note: we check this again in verify_inputs_and_outputs but only if any
|
|
||||||
# outputs are provided at all. To make sure of that before calling
|
|
||||||
# verify_inputs_and_outputs, we check it here.
|
|
||||||
self._verify_equation_balanced(proofs, outputs)
|
|
||||||
# verify spending inputs, outputs, and spending conditions
|
|
||||||
await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs)
|
|
||||||
|
|
||||||
# Mark proofs as used and prepare new promises
|
# Mark proofs as used and prepare new promises
|
||||||
async with get_db_connection(self.db) as conn:
|
async with get_db_connection(self.db) as conn:
|
||||||
# we do this in a single db transaction
|
# we do this in a single db transaction
|
||||||
promises = await self._generate_promises(outputs, keyset, conn)
|
|
||||||
await self._invalidate_proofs(proofs=proofs, conn=conn)
|
await self._invalidate_proofs(proofs=proofs, conn=conn)
|
||||||
|
promises = await self._generate_promises(outputs, keyset, conn)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.trace(f"split failed: {e}")
|
logger.trace(f"split failed: {e}")
|
||||||
@@ -949,15 +979,16 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
] = []
|
] = []
|
||||||
for output in outputs:
|
for output in outputs:
|
||||||
B_ = PublicKey(bytes.fromhex(output.B_), raw=True)
|
B_ = PublicKey(bytes.fromhex(output.B_), raw=True)
|
||||||
assert output.id, "output id not set"
|
|
||||||
keyset = keyset or self.keysets[output.id]
|
keyset = keyset or self.keysets[output.id]
|
||||||
|
if output.id not in self.keysets:
|
||||||
assert output.id in self.keysets, f"keyset {output.id} not found"
|
raise TransactionError(f"keyset {output.id} not found")
|
||||||
assert output.id in [
|
if output.id not in [
|
||||||
keyset.id,
|
keyset.id,
|
||||||
keyset.duplicate_keyset_id,
|
keyset.duplicate_keyset_id,
|
||||||
], "keyset id does not match output id"
|
]:
|
||||||
assert keyset.active, "keyset is not active"
|
raise TransactionError("keyset id does not match output id")
|
||||||
|
if not keyset.active:
|
||||||
|
raise TransactionError("keyset is not active")
|
||||||
keyset_id = output.id
|
keyset_id = output.id
|
||||||
logger.trace(f"Generating promise with keyset {keyset_id}.")
|
logger.trace(f"Generating promise with keyset {keyset_id}.")
|
||||||
private_key_amount = keyset.private_keys[output.amount]
|
private_key_amount = keyset.private_keys[output.amount]
|
||||||
@@ -995,7 +1026,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
|
|
||||||
async def load_used_proofs(self) -> None:
|
async def load_used_proofs(self) -> None:
|
||||||
"""Load all used proofs from database."""
|
"""Load all used proofs from database."""
|
||||||
assert settings.mint_cache_secrets, "MINT_CACHE_SECRETS must be set to TRUE"
|
if not settings.mint_cache_secrets:
|
||||||
|
raise Exception("MINT_CACHE_SECRETS must be set to TRUE")
|
||||||
logger.debug("Loading used proofs into memory")
|
logger.debug("Loading used proofs into memory")
|
||||||
spent_proofs_list = await self.crud.get_spent_proofs(db=self.db) or []
|
spent_proofs_list = await self.crud.get_spent_proofs(db=self.db) or []
|
||||||
logger.debug(f"Loaded {len(spent_proofs_list)} used proofs")
|
logger.debug(f"Loaded {len(spent_proofs_list)} used proofs")
|
||||||
@@ -1082,11 +1114,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: At least one of the proofs is in the pending table.
|
Exception: At least one of the proofs is in the pending table.
|
||||||
"""
|
"""
|
||||||
assert (
|
if not (
|
||||||
len(
|
len(
|
||||||
await self.crud.get_proofs_pending(
|
await self.crud.get_proofs_pending(
|
||||||
Ys=[p.Y for p in proofs], db=self.db, conn=conn
|
Ys=[p.Y for p in proofs], db=self.db, conn=conn
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
== 0
|
== 0
|
||||||
), TransactionError("proofs are pending.")
|
):
|
||||||
|
raise TransactionError("proofs are pending.")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Request
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from ..core.base import (
|
from ..core.base import (
|
||||||
|
BlindedMessage,
|
||||||
BlindedSignature,
|
BlindedSignature,
|
||||||
CheckFeesRequest_deprecated,
|
CheckFeesRequest_deprecated,
|
||||||
CheckFeesResponse_deprecated,
|
CheckFeesResponse_deprecated,
|
||||||
@@ -177,10 +178,14 @@ 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.
|
||||||
for output in payload.outputs:
|
# use the deprecated version of the current keyset
|
||||||
if not output.id:
|
assert ledger.keyset.duplicate_keyset_id
|
||||||
# use the deprecated version of the current keyset
|
outputs: list[BlindedMessage] = [
|
||||||
output.id = ledger.keyset.duplicate_keyset_id
|
BlindedMessage(
|
||||||
|
id=o.id or ledger.keyset.duplicate_keyset_id, **o.dict(exclude={"id"})
|
||||||
|
)
|
||||||
|
for o in payload.outputs
|
||||||
|
]
|
||||||
# END BACKWARDS COMPATIBILITY < 0.15
|
# END BACKWARDS COMPATIBILITY < 0.15
|
||||||
|
|
||||||
# BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash
|
# BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash
|
||||||
@@ -189,7 +194,7 @@ async def mint_deprecated(
|
|||||||
assert hash, "hash must be set."
|
assert hash, "hash must be set."
|
||||||
# END: backwards compatibility < 0.12
|
# END: backwards compatibility < 0.12
|
||||||
|
|
||||||
promises = await ledger.mint(outputs=payload.outputs, quote_id=hash)
|
promises = await ledger.mint(outputs=outputs, quote_id=hash)
|
||||||
blinded_signatures = PostMintResponse_deprecated(promises=promises)
|
blinded_signatures = PostMintResponse_deprecated(promises=promises)
|
||||||
|
|
||||||
logger.trace(f"< POST /mint: {blinded_signatures}")
|
logger.trace(f"< POST /mint: {blinded_signatures}")
|
||||||
@@ -221,15 +226,18 @@ async def melt_deprecated(
|
|||||||
logger.trace(f"> POST /melt: {payload}")
|
logger.trace(f"> POST /melt: {payload}")
|
||||||
# BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs
|
# BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs
|
||||||
if payload.outputs:
|
if payload.outputs:
|
||||||
for output in payload.outputs:
|
outputs: list[BlindedMessage] = [
|
||||||
if not output.id:
|
BlindedMessage(id=o.id or ledger.keyset.id, **o.dict(exclude={"id"}))
|
||||||
output.id = ledger.keyset.id
|
for o in payload.outputs
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
outputs = []
|
||||||
# END BACKWARDS COMPATIBILITY < 0.14
|
# END BACKWARDS COMPATIBILITY < 0.14
|
||||||
quote = await ledger.melt_quote(
|
quote = await ledger.melt_quote(
|
||||||
PostMeltQuoteRequest(request=payload.pr, unit="sat")
|
PostMeltQuoteRequest(request=payload.pr, unit="sat")
|
||||||
)
|
)
|
||||||
preimage, change_promises = await ledger.melt(
|
preimage, change_promises = await ledger.melt(
|
||||||
proofs=payload.proofs, quote=quote.quote, outputs=payload.outputs
|
proofs=payload.proofs, quote=quote.quote, outputs=outputs
|
||||||
)
|
)
|
||||||
resp = PostMeltResponse_deprecated(
|
resp = PostMeltResponse_deprecated(
|
||||||
paid=True, preimage=preimage, change=change_promises
|
paid=True, preimage=preimage, change=change_promises
|
||||||
@@ -290,12 +298,12 @@ async def split_deprecated(
|
|||||||
logger.trace(f"> POST /split: {payload}")
|
logger.trace(f"> POST /split: {payload}")
|
||||||
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
|
||||||
if payload.outputs:
|
outputs: list[BlindedMessage] = [
|
||||||
for output in payload.outputs:
|
BlindedMessage(id=o.id or ledger.keyset.id, **o.dict(exclude={"id"}))
|
||||||
if not output.id:
|
for o in payload.outputs
|
||||||
output.id = ledger.keyset.id
|
]
|
||||||
# END BACKWARDS COMPATIBILITY < 0.14
|
# END BACKWARDS COMPATIBILITY < 0.14
|
||||||
promises = await ledger.split(proofs=payload.proofs, outputs=payload.outputs)
|
promises = await ledger.split(proofs=payload.proofs, outputs=outputs)
|
||||||
|
|
||||||
if payload.amount:
|
if payload.amount:
|
||||||
# BEGIN backwards compatibility < 0.13
|
# BEGIN backwards compatibility < 0.13
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from loguru import logger
|
|||||||
|
|
||||||
from ..core.base import (
|
from ..core.base import (
|
||||||
BlindedMessage,
|
BlindedMessage,
|
||||||
|
BlindedMessage_Deprecated,
|
||||||
BlindedSignature,
|
BlindedSignature,
|
||||||
CheckFeesRequest_deprecated,
|
CheckFeesRequest_deprecated,
|
||||||
CheckFeesResponse_deprecated,
|
CheckFeesResponse_deprecated,
|
||||||
@@ -271,7 +272,8 @@ class LedgerAPIDeprecated(SupportsHttpxClient, SupportsMintURL):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If the minting fails
|
Exception: If the minting fails
|
||||||
"""
|
"""
|
||||||
outputs_payload = PostMintRequest_deprecated(outputs=outputs)
|
outputs_deprecated = [BlindedMessage_Deprecated(**o.dict()) for o in outputs]
|
||||||
|
outputs_payload = PostMintRequest_deprecated(outputs=outputs_deprecated)
|
||||||
|
|
||||||
def _mintrequest_include_fields(outputs: List[BlindedMessage]):
|
def _mintrequest_include_fields(outputs: List[BlindedMessage]):
|
||||||
"""strips away fields from the model that aren't necessary for the /mint"""
|
"""strips away fields from the model that aren't necessary for the /mint"""
|
||||||
@@ -307,7 +309,14 @@ class LedgerAPIDeprecated(SupportsHttpxClient, SupportsMintURL):
|
|||||||
Accepts proofs and a lightning invoice to pay in exchange.
|
Accepts proofs and a lightning invoice to pay in exchange.
|
||||||
"""
|
"""
|
||||||
logger.warning("Using deprecated API call: POST /melt")
|
logger.warning("Using deprecated API call: POST /melt")
|
||||||
payload = PostMeltRequest_deprecated(proofs=proofs, pr=invoice, outputs=outputs)
|
outputs_deprecated = (
|
||||||
|
[BlindedMessage_Deprecated(**o.dict()) for o in outputs]
|
||||||
|
if outputs
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
payload = PostMeltRequest_deprecated(
|
||||||
|
proofs=proofs, pr=invoice, outputs=outputs_deprecated
|
||||||
|
)
|
||||||
|
|
||||||
def _meltrequest_include_fields(proofs: List[Proof]):
|
def _meltrequest_include_fields(proofs: List[Proof]):
|
||||||
"""strips away fields from the model that aren't necessary for the /melt"""
|
"""strips away fields from the model that aren't necessary for the /melt"""
|
||||||
@@ -336,7 +345,10 @@ class LedgerAPIDeprecated(SupportsHttpxClient, SupportsMintURL):
|
|||||||
) -> List[BlindedSignature]:
|
) -> List[BlindedSignature]:
|
||||||
"""Consume proofs and create new promises based on amount split."""
|
"""Consume proofs and create new promises based on amount split."""
|
||||||
logger.warning("Using deprecated API call: Calling split. POST /split")
|
logger.warning("Using deprecated API call: Calling split. POST /split")
|
||||||
split_payload = PostSplitRequest_Deprecated(proofs=proofs, outputs=outputs)
|
outputs_deprecated = [BlindedMessage_Deprecated(**o.dict()) for o in outputs]
|
||||||
|
split_payload = PostSplitRequest_Deprecated(
|
||||||
|
proofs=proofs, outputs=outputs_deprecated
|
||||||
|
)
|
||||||
|
|
||||||
# construct payload
|
# construct payload
|
||||||
def _splitrequest_include_fields(proofs: List[Proof]):
|
def _splitrequest_include_fields(proofs: List[Proof]):
|
||||||
@@ -403,7 +415,8 @@ class LedgerAPIDeprecated(SupportsHttpxClient, SupportsMintURL):
|
|||||||
Asks the mint to restore promises corresponding to outputs.
|
Asks the mint to restore promises corresponding to outputs.
|
||||||
"""
|
"""
|
||||||
logger.warning("Using deprecated API call: POST /restore")
|
logger.warning("Using deprecated API call: POST /restore")
|
||||||
payload = PostMintRequest_deprecated(outputs=outputs)
|
outputs_deprecated = [BlindedMessage_Deprecated(**o.dict()) for o in outputs]
|
||||||
|
payload = PostMintRequest_deprecated(outputs=outputs_deprecated)
|
||||||
resp = await self.httpx.post(join(self.url, "/restore"), json=payload.dict())
|
resp = await self.httpx.post(join(self.url, "/restore"), json=payload.dict())
|
||||||
self.raise_on_error(resp)
|
self.raise_on_error(resp)
|
||||||
response_dict = resp.json()
|
response_dict = resp.json()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "cashu"
|
name = "cashu"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
description = "Ecash wallet and mint"
|
description = "Ecash wallet and mint"
|
||||||
authors = ["calle <callebtc@protonmail.com>"]
|
authors = ["calle <callebtc@protonmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
|
|||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="cashu",
|
name="cashu",
|
||||||
version="0.15.2",
|
version="0.15.3",
|
||||||
description="Ecash wallet and mint",
|
description="Ecash wallet and mint",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ async def test_init_keysets_with_duplicates(ledger: Ledger):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_init_keysets_with_duplicates_via_settings(ledger: Ledger):
|
async def test_init_keysets_with_duplicates_via_settings(ledger: Ledger):
|
||||||
ledger.keysets = {}
|
ledger.keysets = {}
|
||||||
settings.mint_duplicate_keysets = True
|
settings.mint_duplicate_old_keysets = True
|
||||||
await ledger.init_keysets()
|
await ledger.init_keysets()
|
||||||
assert len(ledger.keysets) == 2
|
assert len(ledger.keysets) == 2
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ async def test_init_keysets_without_duplicates(ledger: Ledger):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_init_keysets_without_duplicates_via_settings(ledger: Ledger):
|
async def test_init_keysets_without_duplicates_via_settings(ledger: Ledger):
|
||||||
ledger.keysets = {}
|
ledger.keysets = {}
|
||||||
settings.mint_duplicate_keysets = False
|
settings.mint_duplicate_old_keysets = False
|
||||||
await ledger.init_keysets()
|
await ledger.init_keysets()
|
||||||
assert len(ledger.keysets) == 1
|
assert len(ledger.keysets) == 1
|
||||||
|
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet):
|
|||||||
doublespend = await wallet1.mint(64, id=invoice.id)
|
doublespend = await wallet1.mint(64, id=invoice.id)
|
||||||
await assert_err(
|
await assert_err(
|
||||||
wallet1.split(wallet1.proofs + doublespend, 20),
|
wallet1.split(wallet1.proofs + doublespend, 20),
|
||||||
"Mint Error: Failed to set proofs pending.",
|
"Mint Error: duplicate proofs.",
|
||||||
)
|
)
|
||||||
assert wallet1.balance == 64
|
assert wallet1.balance == 64
|
||||||
assert wallet1.available_balance == 64
|
assert wallet1.available_balance == 64
|
||||||
|
|||||||
Reference in New Issue
Block a user