diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 1f994d53..0c8725e3 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -203,10 +203,14 @@ async def pay( quote = await wallet.melt_quote(invoice, amount) logger.debug(f"Quote: {quote}") total_amount = quote.amount + quote.fee_reserve + # we need to include fees so we can use the proofs for melting the `total_amount` + send_proofs, ecash_fees = await wallet.select_to_send( + wallet.proofs, total_amount, include_fees=True, set_reserved=True + ) if not yes: potential = ( - f" ({wallet.unit.str(total_amount)} with potential fees)" - if quote.fee_reserve + f" ({wallet.unit.str(total_amount + ecash_fees)} with potential fees)" + if quote.fee_reserve or ecash_fees else "" ) message = f"Pay {wallet.unit.str(quote.amount)}{potential}?" @@ -221,9 +225,7 @@ async def pay( if wallet.available_balance < total_amount: print(" Error: Balance too low.") return - send_proofs, fees = await wallet.select_to_send( - wallet.proofs, total_amount, include_fees=True, set_reserved=True - ) + try: melt_response = await wallet.melt( send_proofs, invoice, quote.fee_reserve, quote.quote diff --git a/cashu/wallet/transactions.py b/cashu/wallet/transactions.py index 9d72b72f..084d95c6 100644 --- a/cashu/wallet/transactions.py +++ b/cashu/wallet/transactions.py @@ -36,7 +36,7 @@ def get_fees_for_proofs(self, proofs: List[Proof]) -> int: def get_fees_for_proofs_ppk(self, proofs: List[Proof]) -> int: return sum([self.keysets[p.id].input_fee_ppk for p in proofs]) - async def _select_proofs_to_send( + async def coinselect( self, proofs: List[Proof], amount_to_send: Union[int, float], @@ -59,7 +59,7 @@ async def _select_proofs_to_send( return [] logger.trace( - f"_select_proofs_to_send – amount_to_send: {amount_to_send} – amounts we have: {amount_summary(proofs, self.unit)} (sum: {sum_proofs(proofs)})" + f"coinselect – amount_to_send: {amount_to_send} – amounts we have: {amount_summary(proofs, self.unit)} (sum: {sum_proofs(proofs)})" ) sorted_proofs = sorted(proofs, key=lambda p: p.amount) @@ -91,7 +91,7 @@ async def _select_proofs_to_send( logger.trace( f"> selecting more proofs from {amount_summary(smaller_proofs[1:], self.unit)} sum: {sum_proofs(smaller_proofs[1:])} to reach {remainder}" ) - selected_proofs += await self._select_proofs_to_send( + selected_proofs += await self.coinselect( smaller_proofs[1:], remainder, include_fees=include_fees ) sum_selected_proofs = sum_proofs(selected_proofs) @@ -101,7 +101,7 @@ async def _select_proofs_to_send( return [next_bigger] logger.trace( - f"_select_proofs_to_send - selected proof amounts: {amount_summary(selected_proofs, self.unit)} (sum: {sum_proofs(selected_proofs)})" + f"coinselect - selected proof amounts: {amount_summary(selected_proofs, self.unit)} (sum: {sum_proofs(selected_proofs)})" ) return selected_proofs diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 8c4c6893..43f10b9c 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -589,21 +589,24 @@ def determine_output_amounts( self, proofs: List[Proof], amount: int, - include_fees_to_send: bool = False, + include_fees: bool = False, keyset_id_outputs: Optional[str] = None, ) -> Tuple[List[int], List[int]]: """This function generates a suitable amount split for the outputs to keep and the outputs to send. It calculates the amount to keep based on the wallet state and the amount to send based on the amount provided. + Amount to keep is based on the proofs we have in the wallet + Amount to send is optimally split based on the amount provided plus optionally the fees required to receive them. + Args: proofs (List[Proof]): Proofs to be split. amount (int): Amount to be sent. - include_fees_to_send (bool, optional): If True, the fees are included in the amount to send (output of + include_fees (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. keyset_id_outputs (str, optional): The keyset ID of the outputs to be produced, used to determine the - fee if `include_fees_to_send` is set. + fee if `include_fees` is set. Returns: Tuple[List[int], List[int]]: Two lists of amounts, one for keeping and one for sending. @@ -612,7 +615,7 @@ def determine_output_amounts( total = sum_proofs(proofs) keep_amt, send_amt = total - amount, amount - if include_fees_to_send: + if include_fees: keyset_id = keyset_id_outputs or self.keyset_id tmp_proofs = [Proof(id=keyset_id) for _ in amount_split(send_amt)] fee = self.get_fees_for_proofs(tmp_proofs) @@ -638,7 +641,7 @@ async def split( proofs: List[Proof], amount: int, secret_lock: Optional[Secret] = None, - include_fees_to_send: bool = False, + include_fees: 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. @@ -650,9 +653,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 + include_fees (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. + `proofs` (input to this method) which must already be included. Defaults to False. Returns: Tuple[List[Proof], List[Proof]]: Two lists of proofs, one for keeping and one for sending. @@ -668,21 +671,16 @@ async def split( input_fees = self.get_fees_for_proofs(proofs) logger.debug(f"Input fees: {input_fees}") - # create a suitable amount lists to keep and send based on the proofs - # provided and the state of the wallet + # create a suitable amounts to keep and send. keep_outputs, send_outputs = self.determine_output_amounts( proofs, amount, - include_fees_to_send=include_fees_to_send, + include_fees=include_fees, keyset_id_outputs=self.keyset_id, ) amounts = keep_outputs + send_outputs - if not amounts: - logger.warning("Swap has no outputs") - return [], [] - # generate secrets for new outputs if secret_lock is None: secrets, rs, derivation_paths = await self.generate_n_secrets(len(amounts)) @@ -1060,7 +1058,7 @@ async def select_to_send( If `set_reserved` is set to True, the proofs are marked as reserved so they aren't used in other transactions. - If `include_fees` is set to False, the swap fees are not included in the amount to be selected. + If `include_fees` is set to True, the selection includes the swap fees to receive the selected proofs. Args: proofs (List[Proof]): Proofs to split @@ -1080,9 +1078,7 @@ async def select_to_send( raise Exception("balance too low.") # coin selection for potentially offline sending - send_proofs = await self._select_proofs_to_send( - proofs, amount, include_fees=include_fees - ) + send_proofs = await self.coinselect(proofs, amount, include_fees=include_fees) fees = self.get_fees_for_proofs(send_proofs) logger.trace( f"select_to_send: selected: {self.unit.str(sum_proofs(send_proofs))} (+ {self.unit.str(fees)} fees) – wanted: {self.unit.str(amount)}" @@ -1139,11 +1135,10 @@ async def swap_to_send( if sum_proofs(proofs) < amount: raise Exception("balance too low.") - # coin selection for swapping - swap_proofs = await self._select_proofs_to_send( - proofs, amount, include_fees=True - ) - # add proofs from inactive keysets to swap_proofs to get rid of them + # coin selection for swapping, needs to include fees + swap_proofs = await self.coinselect(proofs, amount, include_fees=True) + + # Extra rule: add proofs from inactive keysets to swap_proofs to get rid of them swap_proofs += [ p for p in proofs @@ -1155,7 +1150,7 @@ async def swap_to_send( 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, include_fees + swap_proofs, amount, secret_lock, include_fees=include_fees ) if set_reserved: await self.set_reserved(send_proofs, reserved=True) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 7da914cf..29dc7c82 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -254,7 +254,7 @@ async def test_swap_to_send(wallet1: Wallet): assert_amt(send_proofs, 32) assert_amt(keep_proofs, 0) - spendable_proofs = await wallet1._select_proofs_to_send(wallet1.proofs, 32) + spendable_proofs = await wallet1.coinselect(wallet1.proofs, 32) assert sum_proofs(spendable_proofs) == 32 assert sum_proofs(send_proofs) == 32