Skip to content

Commit

Permalink
fees for melt inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed May 25, 2024
1 parent 1c455bd commit 0fab554
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 30 deletions.
1 change: 1 addition & 0 deletions cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class MintSettings(CashuSettings):
mint_max_secret_length: int = Field(default=512)

mint_input_fee_ppk: int = Field(default=0)
mint_internal_quote_input_fee_reserve_percent: float = Field(default=1.0)


class MintBackends(MintSettings):
Expand Down
43 changes: 35 additions & 8 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import math
import time
from typing import Dict, List, Mapping, Optional, Tuple

Expand Down Expand Up @@ -568,14 +569,24 @@ async def melt_quote(
if not mint_quote.checking_id:
raise TransactionError("mint quote has no checking id")

internal_fee = Amount(
unit,
math.ceil(
mint_quote.amount
/ 100
* settings.mint_internal_quote_input_fee_reserve_percent
),
)
amount = Amount(unit, mint_quote.amount)

payment_quote = PaymentQuoteResponse(
checking_id=mint_quote.checking_id,
amount=Amount(unit, mint_quote.amount),
fee=Amount(unit, amount=0),
amount=amount,
fee=internal_fee,
)
logger.info(
f"Issuing internal melt quote: {request} ->"
f" {mint_quote.quote} ({mint_quote.amount} {mint_quote.unit})"
f" {mint_quote.quote} ({amount.str()} + {internal_fee.str()} fees)"
)
else:
# not internal, get payment quote by backend
Expand Down Expand Up @@ -671,11 +682,16 @@ async def get_melt_quote(self, quote_id: str) -> MeltQuote:

return melt_quote

async def melt_mint_settle_internally(self, melt_quote: MeltQuote) -> MeltQuote:
async def melt_mint_settle_internally(
self, melt_quote: MeltQuote, proofs: List[Proof]
) -> MeltQuote:
"""Settles a melt quote internally if there is a mint quote with the same payment request.
`proofs` are passed to determine the ecash input transaction fees for this melt quote.
Args:
melt_quote (MeltQuote): Melt quote to settle.
proofs (List[Proof]): Proofs provided for paying the Lightning invoice.
Raises:
Exception: Melt quote already paid.
Expand All @@ -691,10 +707,19 @@ async def melt_mint_settle_internally(self, melt_quote: MeltQuote) -> MeltQuote:
)
if not mint_quote:
return melt_quote

# we settle the transaction internally
if melt_quote.paid:
raise TransactionError("melt quote already paid")

# verify that the amount of the input proofs is equal to the amount of the quote
total_provided = sum_proofs(proofs)
total_needed = melt_quote.amount + melt_quote.fee_reserve
if not total_provided >= total_needed:
raise TransactionError(
f"not enough inputs provided for melt. Provided: {total_provided}, needed: {total_needed}"
)

# verify amounts from bolt11 invoice
bolt11_request = melt_quote.request
invoice_obj = bolt11.decode(bolt11_request)
Expand All @@ -719,8 +744,10 @@ async def melt_mint_settle_internally(self, melt_quote: MeltQuote) -> MeltQuote:
f" {mint_quote.quote} ({melt_quote.amount} {melt_quote.unit})"
)

# we handle this transaction internally
melt_quote.fee_paid = 0
# the internal transaction costs at least the ecash input fee
melt_quote.fee_paid = min(
self.get_fees_for_proofs(proofs), melt_quote.fee_reserve
)
melt_quote.paid = True
melt_quote.paid_time = int(time.time())
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
Expand Down Expand Up @@ -772,7 +799,7 @@ async def melt(

# verify that the amount of the input proofs is equal to the amount of the quote
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
if not total_provided >= total_needed:
raise TransactionError(
f"not enough inputs provided for melt. Provided: {total_provided}, needed: {total_needed}"
Expand All @@ -793,7 +820,7 @@ async def melt(
await self._set_proofs_pending(proofs, quote_id=melt_quote.quote)
try:
# settle the transaction internally if there is a mint quote with the same payment request
melt_quote = await self.melt_mint_settle_internally(melt_quote)
melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs)
# quote not paid yet (not internal), pay it with the backend
if not melt_quote.paid:
logger.debug(f"Lightning: pay invoice {melt_quote.request}")
Expand Down
6 changes: 3 additions & 3 deletions cashu/wallet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ async def pay(
if wallet.available_balance < total_amount:
print(" Error: Balance too low.")
return
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
send_proofs, fees = await wallet.select_to_send(wallet.proofs, total_amount)
try:
melt_response = await wallet.melt(
send_proofs, invoice, quote.fee_reserve, quote.quote
Expand Down Expand Up @@ -348,7 +348,7 @@ async def swap(ctx: Context):
total_amount = quote.amount + quote.fee_reserve
if outgoing_wallet.available_balance < total_amount:
raise Exception("balance too low")
_, send_proofs = await outgoing_wallet.split_to_send(
send_proofs, fees = await outgoing_wallet.select_to_send(
outgoing_wallet.proofs, total_amount, set_reserved=True
)
await outgoing_wallet.melt(
Expand Down Expand Up @@ -977,7 +977,7 @@ async def selfpay(ctx: Context, all: bool = False):
mint_balance_dict = await wallet.balance_per_minturl()
mint_balance = int(mint_balance_dict[wallet.url]["available"])
# send balance once to mark as reserved
await wallet.split_to_send(wallet.proofs, mint_balance, None, set_reserved=True)
await wallet.select_to_send(wallet.proofs, mint_balance, set_reserved=True)
# load all reserved proofs (including the one we just sent)
reserved_proofs = await get_reserved_proofs(wallet.db)
if not len(reserved_proofs):
Expand Down
10 changes: 2 additions & 8 deletions cashu/wallet/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,13 @@ async def _init_private_key(self, from_mnemonic: Optional[str] = None) -> None:
except Exception as e:
logger.error(e)

async def _generate_secret(self) -> str:
async def _generate_random_secret(self) -> str:
"""Returns base64 encoded deterministic random string.
NOTE: This method should probably retire after `deterministic_secrets`. We are
deriving secrets from a counter but don't store the respective blinding factor.
We won't be able to restore any ecash generated with these secrets.
"""
# secret_counter = await bump_secret_derivation(db=self.db, keyset_id=keyset_id)
# logger.trace(f"secret_counter: {secret_counter}")
# s, _, _ = await self.generate_determinstic_secret(secret_counter, keyset_id)
# # return s.decode("utf-8")
# return hashlib.sha256(s).hexdigest()

# return random 32 byte hex string
return hashlib.sha256(os.urandom(32)).hexdigest()

Expand Down Expand Up @@ -230,7 +224,7 @@ async def generate_locked_secrets(
# append predefined secrets (to send) to random secrets (to keep)
# generate secrets to keep
secrets = [
await self._generate_secret() for s in range(len(keep_outputs))
await self._generate_random_secret() for s in range(len(keep_outputs))
] + secret_locks
# TODO: derive derivation paths from secrets
derivation_paths = ["custom"] * len(secrets)
Expand Down
38 changes: 27 additions & 11 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ async def select_to_send(
tolerance: int = 0,
) -> Tuple[List[Proof], int]:
"""
Selects proofs such that a certain amount can be sent.
Selects proofs such that a desired amount can be sent.
Args:
proofs (List[Proof]): Proofs to split
Expand All @@ -886,18 +886,18 @@ async def select_to_send(
int: Fees for the transaction
"""

# select proofs that are not reserved
proofs = [p for p in proofs if not p.reserved]
# select proofs that are in the keysets of the mint
proofs = [p for p in proofs if p.id in self.keysets]
# select proofs that are not reserved and are in the active keysets of the mint
proofs = self.active_proofs(proofs)

# coin selection for potentially offline sending
send_proofs, fees = await self._select_proofs_to_send(proofs, amount, tolerance)
if not send_proofs and offline:
raise Exception(
"Could not select proofs in offline mode. Available amounts:"
f" {', '.join([Amount(self.unit, p.amount).str() for p in proofs])}"
)

# offline coin selection unsuccessful, we need to swap proofs before we can send
if not send_proofs and not offline:
# we set the proofs as reserved later
_, send_proofs = await self.split_to_send(
Expand All @@ -916,7 +916,10 @@ async def split_to_send(
set_reserved: bool = False,
) -> Tuple[List[Proof], List[Proof]]:
"""
Splits proofs such that a desired amount can be sent.
Swaps a set of proofs with the mint to get a set that sums up to a desired amount that can be sent. The remaining
proofs are returned to be kept. All newly created proofs will be stored in the database but if `set_reserved` is set
to True, the proofs to be sent (which sum up to `amount`) will be marked as reserved so they aren't used in other
transactions.
Args:
proofs (List[Proof]): Proofs to split
Expand All @@ -929,12 +932,10 @@ async def split_to_send(
Returns:
Tuple[List[Proof], List[Proof]]: Tuple of proofs to keep and proofs to send
"""
# select proofs that are not reserved
proofs = [p for p in proofs if not p.reserved]

# select proofs that are in the active keysets of the mint
proofs = [p for p in proofs if p.id in self.keysets]
# select proofs that are not reserved and are in the active keysets of the mint
proofs = self.active_proofs(proofs)

# coin selection for swapping
spendable_proofs, fees = await self._select_proofs_to_split(proofs, amount)
logger.debug(
f"Amount to send: {self.unit.str(amount)} (+ {self.unit.str(fees)} fees)"
Expand Down Expand Up @@ -963,6 +964,21 @@ def proof_amounts(self):
"""Returns a sorted list of amounts of all proofs"""
return [p.amount for p in sorted(self.proofs, key=lambda p: p.amount)]

def active_proofs(self, proofs: List[Proof]):
"""Returns a list of proofs that
- have an id that is in the current `self.keysets` which have the unit in `self.unit`
- are not reserved
"""

def is_active_proof(p: Proof) -> bool:
return (
p.id in self.keysets
and self.keysets[p.id].unit == self.unit
and not p.reserved
)

return [p for p in proofs if is_active_proof(p)]

def balance_per_keyset(self) -> Dict[str, Dict[str, Union[int, str]]]:
ret: Dict[str, Dict[str, Union[int, str]]] = {
key: {
Expand Down

0 comments on commit 0fab554

Please sign in to comment.