diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 8910ac21..d1c1ae5c 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -624,7 +624,7 @@ def determine_output_amounts( # generate optimal split for outputs to send send_amounts = amount_split(send_amt) - # we subtract the fee for the entire transaction from the amount to keep + # we subtract the input fee for the entire transaction from the amount to keep keep_amt -= self.get_fees_for_proofs(proofs) logger.trace(f"Keep amount: {keep_amt}") @@ -638,6 +638,7 @@ async def split( proofs: List[Proof], amount: int, secret_lock: Optional[Secret] = None, + include_fees_to_send: bool = False, ) -> Tuple[List[Proof], List[Proof]]: """Calls the swap API to split the proofs into two sets of proofs, one for keeping and one for sending. @@ -649,6 +650,9 @@ async def split( proofs (List[Proof]): Proofs to be split. amount (int): Amount to be sent. secret_lock (Optional[Secret], optional): Secret to lock the tokens to be sent. Defaults to None. + include_fees_to_send (bool, optional): If True, the fees are included in the amount to send (output of + this method, to be sent in the future). This is not the fee that is required to swap the + `proofs` (input to this method). Defaults to False. Returns: Tuple[List[Proof], List[Proof]]: Two lists of proofs, one for keeping and one for sending. @@ -667,7 +671,10 @@ async def split( # create a suitable amount lists to keep and send based on the proofs # provided and the state of the wallet keep_outputs, send_outputs = self.determine_output_amounts( - proofs, amount, include_fees_to_send=True, keyset_id_outputs=self.keyset_id + proofs, + amount, + include_fees_to_send=include_fees_to_send, + keyset_id_outputs=self.keyset_id, ) amounts = keep_outputs + send_outputs @@ -1060,7 +1067,8 @@ async def select_to_send( amount (int): Amount to split to set_reserved (bool, optional): If set, the proofs are marked as reserved. Defaults to False. offline (bool, optional): If set, the coin selection is done offline. Defaults to False. - include_fees (bool, optional): If set, the fees are included in the amount to be selected. Defaults to False. + include_fees (bool, optional): If set, the fees for spending the proofs later are included in the + amount to be selected. Defaults to False. Returns: List[Proof]: Proofs to send @@ -1085,7 +1093,10 @@ async def select_to_send( logger.debug("Offline coin selection unsuccessful. Splitting proofs.") # we set the proofs as reserved later _, send_proofs = await self.swap_to_send( - proofs, amount, set_reserved=False, include_fees=include_fees + proofs, + amount, + set_reserved=False, + include_fees_to_send=include_fees, ) else: raise Exception( @@ -1103,7 +1114,7 @@ async def swap_to_send( *, secret_lock: Optional[Secret] = None, set_reserved: bool = False, - include_fees: bool = True, + include_fees_to_send: bool = True, ) -> Tuple[List[Proof], List[Proof]]: """ 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 @@ -1116,8 +1127,8 @@ async def swap_to_send( amount (int): Amount to split to secret_lock (Optional[str], optional): If set, a custom secret is used to lock new outputs. Defaults to None. set_reserved (bool, optional): If set, the proofs are marked as reserved. Should be set to False if a payment attempt - is made with the split that could fail (like a Lightning payment). Should be set to True if the token to be sent is - displayed to the user to be then sent to someone else. Defaults to False. + is made with the split that could fail (like a Lightning payment). Should be set to True if the token to be sent is + displayed to the user to be then sent to someone else. Defaults to False. Returns: Tuple[List[Proof], List[Proof]]: Tuple of proofs to keep and proofs to send @@ -1142,7 +1153,9 @@ async def swap_to_send( logger.debug( f"Amount to send: {self.unit.str(amount)} (+ {self.unit.str(fees)} fees)" ) - keep_proofs, send_proofs = await self.split(swap_proofs, amount, secret_lock) + keep_proofs, send_proofs = await self.split( + swap_proofs, amount, secret_lock, include_fees_to_send + ) if set_reserved: await self.set_reserved(send_proofs, reserved=True) return keep_proofs, send_proofs diff --git a/tests/test_mint_fees.py b/tests/test_mint_fees.py index 85a70f3f..8a5468f8 100644 --- a/tests/test_mint_fees.py +++ b/tests/test_mint_fees.py @@ -96,19 +96,45 @@ async def test_get_fees_for_proofs(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio @pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") -async def test_wallet_fee(wallet1: Wallet, ledger: Ledger): - # THIS TEST IS A FAKE, WE SET THE WALLET FEES MANUALLY IN set_ledger_keyset_fees - # It would be better to test if the wallet can get the fees from the mint itself - # but the ledger instance does not update the responses from the `mint` that is running in the background - # so we just pretend here and test really nothing... - +async def test_wallet_selection_with_fee(wallet1: Wallet, ledger: Ledger): # set fees to 100 ppk set_ledger_keyset_fees(100, ledger, wallet1) + # THIS TEST IS A FAKE, WE SET THE WALLET FEES MANUALLY IN set_ledger_keyset_fees # check if all wallet keysets have the correct fees for keyset in wallet1.keysets.values(): assert keyset.input_fee_ppk == 100 + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id) + + send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10) + assert sum_proofs(send_proofs) == 10 + + send_proofs_with_fees, _ = await wallet1.select_to_send( + wallet1.proofs, 10, include_fees=True + ) + assert sum_proofs(send_proofs_with_fees) == 11 + + +@pytest.mark.asyncio +@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") +async def test_wallet_swap_to_send_with_fee(wallet1: Wallet, ledger: Ledger): + # set fees to 100 ppk + set_ledger_keyset_fees(100, ledger, wallet1) + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id, split=[32, 32]) # make sure we need to swap + + # quirk: this should call a `/v1/swap` with the mint but the mint will + # throw an error since the fees are only changed in the `ledger` instance, not in the uvicorn API server + # this *should* succeed normally + await assert_err( + wallet1.select_to_send(wallet1.proofs, 10), + "Mint Error: inputs (32) - fees (0) vs outputs (31) are not balanced.", + ) + @pytest.mark.asyncio async def test_split_with_fees(wallet1: Wallet, ledger: Ledger): @@ -244,38 +270,3 @@ async def test_melt_external_with_fees(wallet1: Wallet, ledger: Ledger): melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) assert melt_quote_post_payment.paid, "melt quote should be paid" - - -@pytest.mark.asyncio -@pytest.mark.skipif(is_fake, reason="only works with Regtest") -async def test_melt_external_with_fees_with_swap(wallet1: Wallet, ledger: Ledger): - # set fees to 100 ppk - set_ledger_keyset_fees(100, ledger, wallet1) - - # mint twice so we have enough to pay the second invoice back - invoice = await wallet1.request_mint(128) - await pay_if_regtest(invoice.bolt11) - # we create a split so that the payment later of 32 sat will require a swap before we can melt - await wallet1.mint(128, id=invoice.id, split=[64, 64]) - assert wallet1.balance == 128 - - invoice_dict = get_real_invoice(32) - invoice_payment_request = invoice_dict["payment_request"] - - mint_quote = await wallet1.melt_quote(invoice_payment_request) - total_amount = mint_quote.amount + mint_quote.fee_reserve - send_proofs, fee = await wallet1.select_to_send( - wallet1.proofs, total_amount, include_fees=True - ) - melt_quote = await ledger.melt_quote( - PostMeltQuoteRequest(request=invoice_payment_request, unit="sat") - ) - - melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote) - assert not melt_quote_pre_payment.paid, "melt quote should not be paid" - - assert not melt_quote.paid, "melt quote should not be paid" - await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) - - melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) - assert melt_quote_post_payment.paid, "melt quote should be paid"