diff --git a/README.md b/README.md
index f2942fb..31a3c8e 100644
--- a/README.md
+++ b/README.md
@@ -115,7 +115,7 @@ cashu info
Returns:
```bash
-Version: 0.9.4
+Version: 0.10.0
Debug: False
Cashu dir: /home/user/.cashu
Wallet: wallet
diff --git a/cashu/core/base.py b/cashu/core/base.py
index a386941..a6237a9 100644
--- a/cashu/core/base.py
+++ b/cashu/core/base.py
@@ -139,11 +139,13 @@ class GetMintResponse(BaseModel):
class PostMeltRequest(BaseModel):
proofs: List[Proof]
pr: str
+ outputs: Union[List[BlindedMessage], None]
class GetMeltResponse(BaseModel):
paid: Union[bool, None]
preimage: Union[str, None]
+ change: Union[List[BlindedSignature], None] = None
# ------- API: SPLIT -------
diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py
index aa53bd5..27c1190 100644
--- a/cashu/mint/crud.py
+++ b/cashu/mint/crud.py
@@ -197,7 +197,7 @@ async def get_lightning_invoice(
""",
(hash,),
)
- return Invoice(**row)
+ return Invoice(**row) if row else None
async def update_lightning_invoice(
diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py
index a170543..190f8f5 100644
--- a/cashu/mint/ledger.py
+++ b/cashu/mint/ledger.py
@@ -1,5 +1,5 @@
import math
-from typing import Dict, List, Optional, Set
+from typing import Dict, List, Literal, Optional, Set, Union
from loguru import logger
@@ -23,8 +23,6 @@ from cashu.core.split import amount_split
from cashu.lightning.base import Wallet
from cashu.mint.crud import LedgerCrud
-# from starlette_context import context
-
class Ledger:
def __init__(
@@ -49,7 +47,15 @@ class Ledger:
self.proofs_used = set(proofs_used)
async def load_keyset(self, derivation_path, autosave=True):
- """Load current keyset keyset or generate new one."""
+ """Load current keyset keyset or generate new one.
+
+ Args:
+ derivation_path (_type_): Derivation path from which the keyset is generated.
+ autosave (bool, optional): Store newly-generated keyset if not already in database. Defaults to True.
+
+ Returns:
+ MintKeyset: Keyset
+ """
keyset = MintKeyset(
seed=self.master_key,
derivation_path=derivation_path,
@@ -70,7 +76,12 @@ class Ledger:
return keyset
async def init_keysets(self, autosave=True):
- """Loads all keysets from db."""
+ """Loads all keysets from db.
+
+ Args:
+ autosave (bool, optional): Whether the keyset should be saved if it is
+ not in the database yet. Will be passed to `self.load_keyset`. Defaults to True.
+ """
# load all past keysets from db
tmp_keysets: List[MintKeyset] = await self.crud.get_keyset(db=self.db)
self.keysets = MintKeysets(tmp_keysets)
@@ -84,8 +95,16 @@ class Ledger:
async def _generate_promises(
self, B_s: List[BlindedMessage], keyset: Optional[MintKeyset] = None
- ):
- """Generates promises that sum to the given amount."""
+ ) -> list[BlindedSignature]:
+ """Generates promises that sum to the given amount.
+
+ Args:
+ B_s (List[BlindedMessage]): _description_
+ keyset (Optional[MintKeyset], optional): _description_. Defaults to None.
+
+ Returns:
+ list[BlindedSignature]: _description_
+ """
return [
await self._generate_promise(
b.amount, PublicKey(bytes.fromhex(b.B_), raw=True), keyset
@@ -95,8 +114,17 @@ class Ledger:
async def _generate_promise(
self, amount: int, B_: PublicKey, keyset: Optional[MintKeyset] = None
- ):
- """Generates a promise for given amount and returns a pair (amount, C')."""
+ ) -> BlindedSignature:
+ """Generates a promise (Blind signature) for given amount and returns a pair (amount, C').
+
+ Args:
+ amount (int): Amount of the promise.
+ B_ (PublicKey): Blinded secret (point on curve)
+ keyset (Optional[MintKeyset], optional): Which keyset to use. Private keys will be taken from this keyset. Defaults to None.
+
+ Returns:
+ BlindedSignature: Generated promise.
+ """
keyset = keyset if keyset else self.keyset
private_key_amount = keyset.private_keys[amount]
C_ = b_dhke.step2_bob(B_, private_key_amount)
@@ -109,7 +137,7 @@ class Ledger:
"""Checks whether the proof was already spent."""
return not proof.secret in self.proofs_used
- def _verify_secret_criteria(self, proof: Proof):
+ def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:
"""Verifies that a secret is present and is not too long (DOS prevention)."""
if proof.secret is None or proof.secret == "":
raise Exception("no secret in proof.")
@@ -133,7 +161,7 @@ class Ledger:
C = PublicKey(bytes.fromhex(proof.C), raw=True)
return b_dhke.verify(private_key_amount, C, proof.secret)
- def _verify_script(self, idx: int, proof: Proof):
+ def _verify_script(self, idx: int, proof: Proof) -> bool:
"""
Verify bitcoin script in proof.script commited to by
in proof.secret.
proof.secret format: P2SH::
@@ -163,7 +191,9 @@ class Ledger:
), f"secret does not contain correct P2SH address: {proof.secret.split(':')[1]} is not {txin_p2sh_address}."
return valid
- def _verify_outputs(self, total: int, amount: int, outputs: List[BlindedMessage]):
+ def _verify_outputs(
+ self, total: int, amount: int, outputs: List[BlindedMessage]
+ ) -> bool:
"""Verifies the expected split was correctly computed"""
frst_amt, scnd_amt = total - amount, amount # we have two amounts to split to
frst_outputs = amount_split(frst_amt)
@@ -172,19 +202,19 @@ class Ledger:
given = [o.amount for o in outputs]
return given == expected
- def _verify_no_duplicate_proofs(self, proofs: List[Proof]):
+ def _verify_no_duplicate_proofs(self, proofs: List[Proof]) -> bool:
secrets = [p.secret for p in proofs]
if len(secrets) != len(list(set(secrets))):
return False
return True
- def _verify_no_duplicate_outputs(self, outputs: List[BlindedMessage]):
+ def _verify_no_duplicate_outputs(self, outputs: List[BlindedMessage]) -> bool:
B_s = [od.B_ for od in outputs]
if len(B_s) != len(list(set(B_s))):
return False
return True
- def _verify_split_amount(self, amount: int):
+ def _verify_split_amount(self, amount: int) -> None:
"""Split amount like output amount can't be negative or too big."""
try:
self._verify_amount(amount)
@@ -192,7 +222,7 @@ class Ledger:
# For better error message
raise Exception("invalid split amount: " + str(amount))
- def _verify_amount(self, amount: int):
+ def _verify_amount(self, amount: int) -> int:
"""Any amount used should be a positive integer not larger than 2^MAX_ORDER."""
valid = (
isinstance(amount, int) and amount > 0 and amount < 2**settings.max_order
@@ -203,14 +233,24 @@ class Ledger:
def _verify_equation_balanced(
self, proofs: List[Proof], outs: List[BlindedSignature]
- ):
+ ) -> None:
"""Verify that Σoutputs - Σinputs = 0."""
sum_inputs = sum(self._verify_amount(p.amount) for p in proofs)
sum_outputs = sum(self._verify_amount(p.amount) for p in outs)
assert sum_outputs - sum_inputs == 0
async def _request_lightning_invoice(self, amount: int):
- """Returns an invoice from the Lightning backend."""
+ """Generate a Lightning invoice using the funding source backend.
+
+ Args:
+ amount (int): Amount of invoice (in Satoshis)
+
+ Raises:
+ Exception: Error with funding source.
+
+ Returns:
+ Tuple[str, str]: Bolt11 invoice and payment hash (for lookup)
+ """
error, balance = await self.lightning.status()
if error:
raise Exception(f"Lightning wallet not responding: {error}")
@@ -222,12 +262,26 @@ class Ledger:
) = await self.lightning.create_invoice(amount, "cashu deposit")
return payment_request, checking_id
- async def _check_lightning_invoice(self, amount: int, payment_hash: str):
+ async def _check_lightning_invoice(
+ self, amount: int, payment_hash: str
+ ) -> Literal[True]:
+ """Checks with the Lightning backend whether an invoice with this payment_hash was paid.
+
+ Args:
+ amount (int): Amount of the outputs the wallet wants in return (in Satoshis).
+ payment_hash (str): Payment hash of Lightning invoice (for lookup).
+
+ Raises:
+ Exception: Invoice not found.
+ Exception: Tokens for invoice already issued.
+ Exception: Amount larger than invoice amount.
+ Exception: Invoice not paid yet
+ e: Update database and pass through error.
+
+ Returns:
+ bool: True if invoice has been paid, else False
"""
- Checks with the Lightning backend whether an invoice with this payment_hash was paid.
- Raises exception if invoice is unpaid.
- """
- invoice: Invoice = await self.crud.get_lightning_invoice(
+ invoice: Union[Invoice, None] = await self.crud.get_lightning_invoice(
hash=payment_hash, db=self.db
)
if invoice is None:
@@ -259,7 +313,18 @@ class Ledger:
raise e
async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int):
- """Returns an invoice from the Lightning backend."""
+ """Pays a Lightning invoice via the funding source backend.
+
+ Args:
+ invoice (str): Bolt11 Lightning invoice
+ fee_limit_msat (int): Maximum fee reserve for payment (in Millisatoshi)
+
+ Raises:
+ Exception: Funding source error.
+
+ Returns:
+ Tuple[bool, string, int]: Returns payment status, preimage of invoice, paid fees (in Millisatoshi)
+ """
error, _ = await self.lightning.status()
if error:
raise Exception(f"Lightning wallet not responding: {error}")
@@ -270,12 +335,16 @@ class Ledger:
preimage,
error_message,
) = await self.lightning.pay_invoice(invoice, fee_limit_msat=fee_limit_msat)
- return ok, preimage
+ # make sure that fee is positive
+ fee_msat = abs(fee_msat) if fee_msat else fee_msat
+ return ok, preimage, fee_msat
async def _invalidate_proofs(self, proofs: List[Proof]):
- """
- Adds secrets of proofs to the list of known secrets and stores them in the db.
- Removes proofs from pending table.
+ """Adds secrets of proofs to the list of known secrets and stores them in the db.
+ Removes proofs from pending table. This is executed if the ecash has been redeemed.
+
+ Args:
+ proofs (List[Proof]): Proofs to add to known secret table.
"""
# Mark proofs as used and prepare new promises
proof_msgs = set([p.secret for p in proofs])
@@ -285,9 +354,14 @@ class Ledger:
await self.crud.invalidate_proof(proof=p, db=self.db)
async def _set_proofs_pending(self, proofs: List[Proof]):
- """
- If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
+ """If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
the list of pending proofs or removes them. Used as a mutex for proofs.
+
+ Args:
+ proofs (List[Proof]): Proofs to add to pending table.
+
+ Raises:
+ Exception: At least one proof already in pending table.
"""
# first we check whether these proofs are pending aready
await self._validate_proofs_pending(proofs)
@@ -298,7 +372,11 @@ class Ledger:
raise Exception("proofs already pending.")
async def _unset_proofs_pending(self, proofs: List[Proof]):
- """Deletes proofs from pending table."""
+ """Deletes proofs from pending table.
+
+ Args:
+ proofs (List[Proof]): Proofs to delete.
+ """
# we try: except: this block in order to avoid that any errors here
# could block the _invalidate_proofs() call that happens afterwards.
try:
@@ -309,7 +387,14 @@ class Ledger:
pass
async def _validate_proofs_pending(self, proofs: List[Proof]):
- """Checks if any of the provided proofs is in the pending proofs table. Raises exception for at least one match."""
+ """Checks if any of the provided proofs is in the pending proofs table.
+
+ Args:
+ proofs (List[Proof]): Proofs to check.
+
+ Raises:
+ Exception: At least one of the proofs is in the pending table.
+ """
proofs_pending = await self.crud.get_proofs_pending(db=self.db)
for p in proofs:
for pp in proofs_pending:
@@ -317,7 +402,17 @@ class Ledger:
raise Exception("proofs are pending.")
async def _verify_proofs(self, proofs: List[Proof]):
- """Checks a series of criteria for the verification of proofs."""
+ """Checks a series of criteria for the verification of proofs.
+
+ Args:
+ proofs (List[Proof]): List of proofs to check.
+
+ Raises:
+ Exception: Scripts did not validate.
+ Exception: Criteria for provided secrets not met.
+ Exception: Duplicate proofs provided.
+ Exception: BDHKE verification failed.
+ """
# Verify scripts
if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]):
raise Exception("script validation failed.")
@@ -331,16 +426,86 @@ class Ledger:
if not all([self._verify_proof_bdhke(p) for p in proofs]):
raise Exception("could not verify proofs.")
+ async def _generate_change_promises(
+ self,
+ total_provided: int,
+ invoice_amount: int,
+ ln_fee_msat: int,
+ outputs: List[BlindedMessage],
+ ):
+ """Generates a set of new promises (blinded signatures) from a set of blank outputs
+ (outputs with no or ignored amount) by looking at the difference between the Lightning
+ fee reserve provided by the wallet and the actual Lightning fee paid by the mint.
+
+ If there is a positive difference, produces maximum `n_return_outputs` new outputs
+ with values close or equal to the fee difference. We can't be sure that we hit the
+ fee perfectly because we can only work with a limited set of blanket outputs and
+ their values are limited to 2^n.
+
+ Args:
+ total_provided (int): Amount of the proofs provided by the wallet.
+ invoice_amount (int): Amount of the invoice to be paid.
+ ln_fee_msat (int): Actually paid Lightning network fees.
+ outputs (List[BlindedMessage]): Outputs to sign for returning the overpaid fees.
+
+ Raises:
+ Exception: Output validation failed.
+
+ Returns:
+ List[BlindedSignature]: Signatures on the outputs.
+ """
+ # we make sure that the fee is positive
+ ln_fee_msat = abs(ln_fee_msat)
+ # maximum number of change outputs (must be in consensus with wallet)
+ n_return_outputs = 4
+ ln_fee_sat = math.ceil(ln_fee_msat / 1000)
+ user_paid_fee_sat = total_provided - invoice_amount
+ logger.debug(
+ f"Lightning fee was: {ln_fee_sat}. User paid: {user_paid_fee_sat}. Returning difference."
+ )
+ if user_paid_fee_sat - ln_fee_sat > 0 and outputs is not None:
+ # we will only accept at maximum n_return_outputs outputs
+ assert len(outputs) <= n_return_outputs, Exception(
+ "too many change outputs provided"
+ )
+
+ return_amounts = amount_split(user_paid_fee_sat - ln_fee_sat)
+ # we only need as many outputs as we have change to return
+ outputs = outputs[: len(return_amounts)]
+ # we sort the return_amounts in descending order so we only
+ # take the largest values in the next step
+ return_amounts_sorted = sorted(return_amounts, reverse=True)
+ # we need to imprint these amounts into the blanket outputs
+ for i in range(len(outputs)):
+ outputs[i].amount = return_amounts_sorted[i]
+ if not self._verify_no_duplicate_outputs(outputs):
+ raise Exception("duplicate promises.")
+ return_promises = await self._generate_promises(outputs)
+ return return_promises
+ else:
+ return []
+
# Public methods
def get_keyset(self, keyset_id: Optional[str] = None):
+ """Returns a dictionary of hex public keys of a specific keyset for each supported amount"""
if keyset_id and keyset_id not in self.keysets.keysets:
raise Exception("keyset does not exist")
keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset
assert keyset.public_keys, Exception("no public keys for this keyset")
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}
- async def request_mint(self, amount):
- """Returns Lightning invoice and stores it in the db."""
+ async def request_mint(self, amount: int):
+ """Returns Lightning invoice and stores it in the db.
+
+ Args:
+ amount (int): Amount of the mint request in Satoshis.
+
+ Raises:
+ Exception: Invoice creation failed.
+
+ Returns:
+ Tuple[str, str]: Bolt11 invoice and payment hash (for looking it up later)
+ """
payment_request, checking_id = await self._request_lightning_invoice(amount)
assert payment_request, Exception(
"could not fetch invoice from Lightning backend"
@@ -356,10 +521,24 @@ class Ledger:
async def mint(
self,
B_s: List[BlindedMessage],
- payment_hash=None,
+ payment_hash: Optional[str] = None,
keyset: Optional[MintKeyset] = None,
):
- """Mints a promise for coins for B_."""
+ """Mints a promise for coins for B_.
+
+ Args:
+ B_s (List[BlindedMessage]): Outputs (blinded messages) to sign.
+ payment_hash (Optional[str], optional): Payment hash of (paid) Lightning invoice. Defaults to None.
+ keyset (Optional[MintKeyset], optional): Keyset to use. If not provided, uses active keyset. Defaults to None.
+
+ Raises:
+ Exception: Lightning is turned on but no payment hash is provided.
+ e: Something went wrong with the invoice check.
+ Exception: Amount too large.
+
+ Returns:
+ List[BlindedSignature]: Signatures on the outputs.
+ """
amounts = [b.amount for b in B_s]
amount = sum(amounts)
# check if lightning invoice was paid
@@ -380,8 +559,22 @@ class Ledger:
promises = await self._generate_promises(B_s, keyset)
return promises
- async def melt(self, proofs: List[Proof], invoice: str):
- """Invalidates proofs and pays a Lightning invoice."""
+ async def melt(
+ self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]]
+ ):
+ """Invalidates proofs and pays a Lightning invoice.
+
+ Args:
+ proofs (List[Proof]): Proofs provided for paying the Lightning invoice
+ invoice (str): bolt11 Lightning invoice.
+ outputs (Optional[List[BlindedMessage]]): Blank outputs for returning overpaid fees to the wallet.
+
+ Raises:
+ e: Lightning payment unsuccessful
+
+ Returns:
+ List[BlindedMessage]: Signed outputs for returning overpaid fees to wallet.
+ """
# validate and set proofs as pending
await self._set_proofs_pending(proofs)
@@ -391,32 +584,69 @@ class Ledger:
total_provided = sum_proofs(proofs)
invoice_obj = bolt11.decode(invoice)
- amount = math.ceil(invoice_obj.amount_msat / 1000)
+ invoice_amount = math.ceil(invoice_obj.amount_msat / 1000)
fees_msat = await self.check_fees(invoice)
- assert total_provided >= amount + fees_msat / 1000, Exception(
+ assert total_provided >= invoice_amount + fees_msat / 1000, Exception(
"provided proofs not enough for Lightning payment."
)
+ # promises to return for overpaid fees
+ return_promises: List[BlindedSignature] = []
+
if settings.lightning:
- status, preimage = await self._pay_lightning_invoice(invoice, fees_msat)
+ status, preimage, fee_msat = await self._pay_lightning_invoice(
+ invoice, fees_msat
+ )
else:
- status, preimage = True, "preimage"
+ status, preimage, fee_msat = True, "preimage", 0
+
if status == True:
await self._invalidate_proofs(proofs)
+
+ # prepare change to compensate wallet for overpaid fees
+ assert fee_msat is not None, Exception("fees not valid")
+ if outputs:
+ return_promises = await self._generate_change_promises(
+ total_provided=total_provided,
+ invoice_amount=invoice_amount,
+ ln_fee_msat=fee_msat,
+ outputs=outputs,
+ )
+
except Exception as e:
raise e
finally:
# delete proofs from pending list
await self._unset_proofs_pending(proofs)
- return status, preimage
+ return status, preimage, return_promises
async def check_spendable(self, proofs: List[Proof]):
- """Checks if all provided proofs are valid and still spendable (i.e. have not been spent)."""
+ """Checks if provided proofs are valid and have not been spent yet.
+ Used by wallets to check if their proofs have been redeemed by a receiver.
+
+ Returns a list in the same order as the provided proofs. Wallet must match the list
+ to the proofs they have provided in order to figure out which proof is still spendable
+ and which isn't.
+
+ Args:
+ proofs (List[Proof]): List of proofs to check.
+
+ Returns:
+ List[bool]: List of which proof is still spendable (True if still spendable, else False)
+ """
return [self._check_spendable(p) for p in proofs]
async def check_fees(self, pr: str):
- """Returns the fees (in msat) required to pay this pr."""
+ """Returns the fee reserve (in sat) that a wallet must add to its proofs
+ in order to pay a Lightning invoice.
+
+ Args:
+ pr (str): Bolt11 encoded payment request. Lightning invoice.
+
+ Returns:
+ int: Fee in Satoshis.
+ """
# hack: check if it's internal, if it exists, it will return paid = False,
# if id does not exist (not internal), it returns paid = None
if settings.lightning:
@@ -428,7 +658,8 @@ class Ledger:
amount = 0
internal = True
fees_msat = fee_reserve(amount * 1000, internal)
- return fees_msat
+ fee_sat = math.ceil(fees_msat / 1000)
+ return fee_sat
async def split(
self,
@@ -437,7 +668,21 @@ class Ledger:
outputs: List[BlindedMessage],
keyset: Optional[MintKeyset] = None,
):
- """Consumes proofs and prepares new promises based on the amount split."""
+ """Consumes proofs and prepares new promises based on the amount split. Used for splitting tokens
+ Before sending or for redeeming tokens for new ones that have been received by another wallet.
+
+ Args:
+ proofs (List[Proof]): Proofs to be invalidated for the split.
+ amount (int): Amount at which the split should happen.
+ outputs (List[BlindedMessage]): New outputs that should be signed in return.
+ keyset (Optional[MintKeyset], optional): Keyset to use. Uses default keyset if not given. Defaults to None.
+
+ Raises:
+ Exception: Validation of proofs or outputs failed
+
+ Returns:
+ Tuple[List[BlindSignature],List[BlindSignature]]: Promises on both sides of the split.
+ """
# set proofs as pending
await self._set_proofs_pending(proofs)
diff --git a/cashu/mint/router.py b/cashu/mint/router.py
index f927d6a..2cf29d6 100644
--- a/cashu/mint/router.py
+++ b/cashu/mint/router.py
@@ -113,11 +113,13 @@ async def melt(payload: PostMeltRequest) -> Union[CashuError, GetMeltResponse]:
Requests tokens to be destroyed and sent out via Lightning.
"""
try:
- ok, preimage = await ledger.melt(payload.proofs, payload.pr)
- resp = GetMeltResponse(paid=ok, preimage=preimage)
+ ok, preimage, change_promises = await ledger.melt(
+ payload.proofs, payload.pr, payload.outputs
+ )
+ resp = GetMeltResponse(paid=ok, preimage=preimage, change=change_promises)
+ return resp
except Exception as exc:
return CashuError(code=0, error=str(exc))
- return resp
@router.post(
@@ -144,8 +146,8 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
Used by wallets for figuring out the fees they need to supply together with the payment amount.
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
"""
- fees_msat = await ledger.check_fees(payload.pr)
- return CheckFeesResponse(fee=fees_msat // 1000)
+ fees_sat = await ledger.check_fees(payload.pr)
+ return CheckFeesResponse(fee=fees_sat)
@router.post("/split", name="Split", summary="Split proofs at a specified amount")
diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py
index cf6c387..06e5cd4 100644
--- a/cashu/wallet/cli/cli.py
+++ b/cashu/wallet/cli/cli.py
@@ -113,21 +113,22 @@ async def pay(ctx: Context, invoice: str, yes: bool):
wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint()
wallet.status()
- amount, fees = await wallet.get_pay_amount_with_fees(invoice)
+ total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
if not yes:
click.confirm(
- f"Pay {amount - fees} sat ({amount} sat incl. fees)?",
+ f"Pay {total_amount - fee_reserve_sat} sat ({total_amount} sat with potential fees)?",
abort=True,
default=True,
)
print(f"Paying Lightning invoice ...")
- assert amount > 0, "amount is not positive"
- if wallet.available_balance < amount:
+ assert total_amount > 0, "amount is not positive"
+ if wallet.available_balance < total_amount:
print("Error: Balance too low.")
return
- _, send_proofs = await wallet.split_to_send(wallet.proofs, amount) # type: ignore
+ _, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount) # type: ignore
await wallet.pay_lightning(send_proofs, invoice)
+ await wallet.load_proofs()
wallet.status()
diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py
index 4ed201c..b0b0fc4 100644
--- a/cashu/wallet/wallet.py
+++ b/cashu/wallet/wallet.py
@@ -18,6 +18,7 @@ from cashu.core.base import (
CheckFeesRequest,
CheckSpendableRequest,
CheckSpendableResponse,
+ GetMeltResponse,
GetMintResponse,
Invoice,
KeysetsResponse,
@@ -425,19 +426,22 @@ class LedgerAPI:
return return_dict
@async_set_requests
- async def pay_lightning(self, proofs: List[Proof], invoice: str):
+ async def pay_lightning(
+ self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]]
+ ):
"""
Accepts proofs and a lightning invoice to pay in exchange.
"""
- payload = PostMeltRequest(proofs=proofs, pr=invoice)
+
+ payload = PostMeltRequest(proofs=proofs, pr=invoice, outputs=outputs)
def _meltrequest_include_fields(proofs):
"""strips away fields from the model that aren't necessary for the /melt"""
proofs_include = {"id", "amount", "secret", "C", "script"}
return {
- "amount": ...,
- "pr": ...,
"proofs": {i: proofs_include for i in range(len(proofs))},
+ "pr": ...,
+ "outputs": ...,
}
resp = self.s.post(
@@ -447,7 +451,7 @@ class LedgerAPI:
resp.raise_for_status()
return_dict = resp.json()
self.raise_on_error(return_dict)
- return return_dict
+ return GetMeltResponse.parse_obj(return_dict)
class Wallet(LedgerAPI):
@@ -553,10 +557,8 @@ class Wallet(LedgerAPI):
frst_proofs, scnd_proofs = await super().split(proofs, amount, scnd_secret)
if len(frst_proofs) == 0 and len(scnd_proofs) == 0:
raise Exception("received no splits.")
- used_secrets = [p["secret"] for p in proofs]
- self.proofs = list(
- filter(lambda p: p["secret"] not in used_secrets, self.proofs)
- )
+ used_secrets = [p.secret for p in proofs]
+ self.proofs = list(filter(lambda p: p.secret not in used_secrets, self.proofs))
self.proofs += frst_proofs + scnd_proofs
await self._store_proofs(frst_proofs + scnd_proofs)
for proof in proofs:
@@ -565,20 +567,41 @@ class Wallet(LedgerAPI):
async def pay_lightning(self, proofs: List[Proof], invoice: str):
"""Pays a lightning invoice"""
- status = await super().pay_lightning(proofs, invoice)
- if status["paid"] == True:
+
+ # generate outputs for the change for overpaid fees
+ # we will generate four blanked outputs that the mint will
+ # imprint with value depending on the fees we overpaid
+ n_return_outputs = 4
+ secrets = [self._generate_secret() for _ in range(n_return_outputs)]
+ outputs, rs = self._construct_outputs(n_return_outputs * [1], secrets)
+
+ status = await super().pay_lightning(proofs, invoice, outputs)
+
+ if status.paid == True:
+ # the payment was successful
await self.invalidate(proofs)
invoice_obj = Invoice(
amount=-sum_proofs(proofs),
pr=invoice,
- preimage=status.get("preimage"),
+ preimage=status.preimage,
paid=True,
time_paid=time.time(),
)
await store_lightning_invoice(db=self.db, invoice=invoice_obj)
+
+ # handle change and produce proofs
+ if status.change:
+ change_proofs = self._construct_proofs(
+ status.change,
+ secrets[: len(status.change)],
+ rs[: len(status.change)],
+ )
+ logger.debug(f"Received change: {sum_proofs(change_proofs)} sat")
+ await self._store_proofs(change_proofs)
+
else:
raise Exception("could not pay invoice.")
- return status["paid"]
+ return status.paid
async def check_spendable(self, proofs):
return await super().check_spendable(proofs)
@@ -735,6 +758,7 @@ class Wallet(LedgerAPI):
decoded_invoice: InvoiceBolt11 = bolt11.decode(invoice)
# check if it's an internal payment
fees = int((await self.check_fees(invoice))["fee"])
+ logger.debug(f"Mint wants {fees} sat as fee reserve.")
amount = math.ceil((decoded_invoice.amount_msat + fees * 1000) / 1000) # 1% fee
return amount, fees
diff --git a/pyproject.toml b/pyproject.toml
index 93fa906..5c05b26 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "cashu"
-version = "0.9.4"
+version = "0.10.0"
description = "Ecash wallet and mint."
authors = ["calle "]
license = "MIT"
diff --git a/setup.py b/setup.py
index 320c90b..86f0ef7 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
setuptools.setup(
name="cashu",
- version="0.9.4",
+ version="0.10.0",
description="Ecash wallet and mint for Bitcoin Lightning",
long_description=long_description,
long_description_content_type="text/markdown",