From 10ed146c6c0c93d5699ab1c01d1229c2bb4dc0d3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:32:38 +0200 Subject: [PATCH 1/5] Wallet: add flag --force-swap to send command --- cashu/wallet/api/router.py | 2 +- cashu/wallet/cli/cli.py | 10 +++ cashu/wallet/helpers.py | 6 +- cashu/wallet/lightning/lightning.py | 2 +- cashu/wallet/nostr.py | 2 +- cashu/wallet/proofs.py | 1 + cashu/wallet/transactions.py | 105 +++++++++++++++------------- cashu/wallet/wallet.py | 4 +- tests/test_mint_api.py | 2 +- tests/test_mint_init.py | 6 +- tests/test_mint_operations.py | 12 ++-- tests/test_mint_regtest.py | 2 +- tests/test_wallet.py | 12 ++-- tests/test_wallet_htlc.py | 16 ++--- tests/test_wallet_p2pk.py | 26 +++---- tests/test_wallet_regtest.py | 4 +- tests/test_wallet_restore.py | 16 ++--- tests/test_wallet_subscription.py | 2 +- 18 files changed, 123 insertions(+), 107 deletions(-) diff --git a/cashu/wallet/api/router.py b/cashu/wallet/api/router.py index 0065a50d..f5c771f6 100644 --- a/cashu/wallet/api/router.py +++ b/cashu/wallet/api/router.py @@ -194,7 +194,7 @@ async def swap( if outgoing_wallet.available_balance < total_amount: raise Exception("balance too low") - _, send_proofs = await outgoing_wallet.split_to_send( + _, send_proofs = await outgoing_wallet.swap_to_send( outgoing_wallet.proofs, total_amount, set_reserved=True ) await outgoing_wallet.melt( diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 2a39a41f..c9b81cc3 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -548,6 +548,14 @@ async def balance(ctx: Context, verbose): help="Include fees for receiving token.", type=bool, ) +@click.option( + "--force-swap", + "-s", + default=False, + is_flag=True, + help="Force swap token.", + type=bool, +) @click.pass_context @coro async def send_command( @@ -562,6 +570,7 @@ async def send_command( yes: bool, offline: bool, include_fees: bool, + force_swap: bool, ): wallet: Wallet = ctx.obj["WALLET"] amount = int(amount * 100) if wallet.unit in [Unit.usd, Unit.eur] else int(amount) @@ -575,6 +584,7 @@ async def send_command( include_dleq=dleq, include_fees=include_fees, memo=memo, + force_swap=force_swap, ) else: await send_nostr(wallet, amount=amount, pubkey=nostr, verbose=verbose, yes=yes) diff --git a/cashu/wallet/helpers.py b/cashu/wallet/helpers.py index 834be839..627423d4 100644 --- a/cashu/wallet/helpers.py +++ b/cashu/wallet/helpers.py @@ -116,6 +116,7 @@ async def send( include_dleq: bool = False, include_fees: bool = False, memo: Optional[str] = None, + force_swap: bool = False, ): """ Prints token to send to stdout. @@ -144,13 +145,12 @@ async def send( await wallet.load_proofs() await wallet.load_mint() - if secret_lock: - _, send_proofs = await wallet.split_to_send( + if secret_lock or force_swap: + _, send_proofs = await wallet.swap_to_send( wallet.proofs, amount, set_reserved=False, # we set reserved later secret_lock=secret_lock, - include_fees=include_fees, ) else: send_proofs, fees = await wallet.select_to_send( diff --git a/cashu/wallet/lightning/lightning.py b/cashu/wallet/lightning/lightning.py index be6ef103..56ab06aa 100644 --- a/cashu/wallet/lightning/lightning.py +++ b/cashu/wallet/lightning/lightning.py @@ -61,7 +61,7 @@ async def pay_invoice(self, pr: str) -> PaymentResponse: if self.available_balance < total_amount: print("Error: Balance too low.") return PaymentResponse(ok=False) - _, send_proofs = await self.split_to_send(self.proofs, total_amount) + _, send_proofs = await self.swap_to_send(self.proofs, total_amount) try: resp = await self.melt(send_proofs, pr, quote.fee_reserve, quote.quote) if resp.change: diff --git a/cashu/wallet/nostr.py b/cashu/wallet/nostr.py index 217ed95a..bb989d03 100644 --- a/cashu/wallet/nostr.py +++ b/cashu/wallet/nostr.py @@ -62,7 +62,7 @@ async def send_nostr( pubkey = await nip5_to_pubkey(wallet, pubkey) await wallet.load_mint() await wallet.load_proofs() - _, send_proofs = await wallet.split_to_send( + _, send_proofs = await wallet.swap_to_send( wallet.proofs, amount, set_reserved=True, include_fees=False ) token = await wallet.serialize_proofs(send_proofs, include_dleq=include_dleq) diff --git a/cashu/wallet/proofs.py b/cashu/wallet/proofs.py index ae8b839c..c0169e18 100644 --- a/cashu/wallet/proofs.py +++ b/cashu/wallet/proofs.py @@ -148,6 +148,7 @@ async def serialize_proofs( try: _ = [bytes.fromhex(p.id) for p in proofs] except ValueError: + logger.debug("Proof with base64 keyset, using legacy token serialization") legacy = True if legacy: diff --git a/cashu/wallet/transactions.py b/cashu/wallet/transactions.py index 94c47c54..78251488 100644 --- a/cashu/wallet/transactions.py +++ b/cashu/wallet/transactions.py @@ -36,55 +36,63 @@ 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_( - self, proofs: List[Proof], amount_to_send: int, tolerance: int = 0 - ) -> List[Proof]: - send_proofs: List[Proof] = [] - NO_SELECTION: List[Proof] = [] - - logger.trace(f"proofs: {[p.amount for p in proofs]}") - # sort proofs by amount (descending) - sorted_proofs = sorted(proofs, key=lambda p: p.amount, reverse=True) - # only consider proofs smaller than the amount we want to send (+ tolerance) for coin selection - fee_for_single_proof = self.get_fees_for_proofs([sorted_proofs[0]]) - sorted_proofs = [ - p - for p in sorted_proofs - if p.amount <= amount_to_send + tolerance + fee_for_single_proof - ] - if not sorted_proofs: - logger.info( - f"no small-enough proofs to send. Have: {[p.amount for p in proofs]}" - ) - return NO_SELECTION - - target_amount = amount_to_send - - # compose the target amount from the remaining_proofs - logger.debug(f"sorted_proofs: {[p.amount for p in sorted_proofs]}") - for p in sorted_proofs: - # logger.debug(f"send_proofs: {[p.amount for p in send_proofs]}") - # logger.debug(f"target_amount: {target_amount}") - # logger.debug(f"p.amount: {p.amount}") - if sum_proofs(send_proofs) + p.amount <= target_amount + tolerance: - send_proofs.append(p) - target_amount = amount_to_send + self.get_fees_for_proofs(send_proofs) - - if sum_proofs(send_proofs) < amount_to_send: - logger.info("could not select proofs to reach target amount (too little).") - return NO_SELECTION - - fees = self.get_fees_for_proofs(send_proofs) - logger.debug(f"Selected sum of proofs: {sum_proofs(send_proofs)}, fees: {fees}") - return send_proofs + # async def _select_proofs_to_send_legacy( + # self, proofs: List[Proof], amount_to_send: int, tolerance: int = 0 + # ) -> List[Proof]: + # send_proofs: List[Proof] = [] + # NO_SELECTION: List[Proof] = [] + + # logger.trace(f"proofs: {[p.amount for p in proofs]}") + # # sort proofs by amount (descending) + # sorted_proofs = sorted(proofs, key=lambda p: p.amount, reverse=True) + # # only consider proofs smaller than the amount we want to send (+ tolerance) for coin selection + # fee_for_single_proof = self.get_fees_for_proofs([sorted_proofs[0]]) + # sorted_proofs = [ + # p + # for p in sorted_proofs + # if p.amount <= amount_to_send + tolerance + fee_for_single_proof + # ] + # if not sorted_proofs: + # logger.info( + # f"no small-enough proofs to send. Have: {[p.amount for p in proofs]}" + # ) + # return NO_SELECTION + + # target_amount = amount_to_send + + # # compose the target amount from the remaining_proofs + # logger.debug(f"sorted_proofs: {[p.amount for p in sorted_proofs]}") + # for p in sorted_proofs: + # if sum_proofs(send_proofs) + p.amount <= target_amount + tolerance: + # send_proofs.append(p) + # target_amount = amount_to_send + self.get_fees_for_proofs(send_proofs) + + # if sum_proofs(send_proofs) < amount_to_send: + # logger.info("could not select proofs to reach target amount (too little).") + # return NO_SELECTION + + # fees = self.get_fees_for_proofs(send_proofs) + # logger.debug(f"Selected sum of proofs: {sum_proofs(send_proofs)}, fees: {fees}") + # return send_proofs async def _select_proofs_to_send( self, proofs: List[Proof], amount_to_send: Union[int, float], *, - include_fees: bool = True, + include_fees: bool = False, ) -> List[Proof]: + """Select proofs to send based on the amount to send and the proofs available. Implements a simple coin selection algorithm. + Can be used for selecting proofs to send an offline transaction. + + Args: + proofs (List[Proof]): List of proofs to select from + amount_to_send (Union[int, float]): Amount to select proofs for + include_fees (bool, optional): Whether to include fees necessary to redeem the tokens in the selection. Defaults to False. + + Returns: + List[Proof]: _description_ + """ # check that enough spendable proofs exist if sum_proofs(proofs) < amount_to_send: return [] @@ -147,9 +155,8 @@ async def _select_proofs_to_split( Rules: 1) Proofs that are not marked as reserved - 2) Proofs that have a different keyset than the activated keyset_id of the mint - 3) Include all proofs that have an older keyset than the current keyset of the mint (to get rid of old epochs). - 4) If the target amount is not reached, add proofs of the current keyset until it is. + 2) Include all proofs from inactive keysets (old epochs) to get rid of them + 3) If the target amount is not reached, add proofs of the current keyset until it is. Args: proofs (List[Proof]): List of proofs to select from @@ -171,11 +178,9 @@ async def _select_proofs_to_split( if sum_proofs(proofs) < amount_to_send: raise Exception("balance too low.") - # add all proofs that have an older keyset than the current keyset of the mint - proofs_old_epochs = [ - p for p in proofs if p.id != self.keysets[self.keyset_id].id - ] - send_proofs += proofs_old_epochs + # add all proofs from inactive keysets + proofs_inactive_keysets = [p for p in proofs if not self.keysets[p.id].active] + send_proofs += proofs_inactive_keysets # coinselect based on amount only from the current keyset # start with the proofs with the largest amount and add them until the target amount is reached diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index ad0c2a97..d09bdcb8 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -1049,7 +1049,7 @@ async def select_to_send( if not offline: logger.debug("Offline coin selection unsuccessful. Splitting proofs.") # we set the proofs as reserved later - _, send_proofs = await self.split_to_send( + _, send_proofs = await self.swap_to_send( proofs, amount, set_reserved=False ) else: @@ -1061,7 +1061,7 @@ async def select_to_send( await self.set_reserved(send_proofs, reserved=True) return send_proofs, fees - async def split_to_send( + async def swap_to_send( self, proofs: List[Proof], amount: int, diff --git a/tests/test_mint_api.py b/tests/test_mint_api.py index e1f69e29..494c27c5 100644 --- a/tests/test_mint_api.py +++ b/tests/test_mint_api.py @@ -422,7 +422,7 @@ async def test_melt_external(ledger: Ledger, wallet: Wallet): assert quote.amount == 62 assert quote.fee_reserve == 2 - keep, send = await wallet.split_to_send(wallet.proofs, 64) + keep, send = await wallet.swap_to_send(wallet.proofs, 64) inputs_payload = [p.to_dict() for p in send] # outputs for change diff --git a/tests/test_mint_init.py b/tests/test_mint_init.py index a33a6cf9..9c9ae7fa 100644 --- a/tests/test_mint_init.py +++ b/tests/test_mint_init.py @@ -252,7 +252,7 @@ async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Led # wallet pays the invoice quote = await wallet.melt_quote(invoice_payment_request) total_amount = quote.amount + quote.fee_reserve - _, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount) + _, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount) asyncio.create_task( wallet.melt( proofs=send_proofs, @@ -297,7 +297,7 @@ async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Led # wallet pays the invoice quote = await wallet.melt_quote(invoice_payment_request) total_amount = quote.amount + quote.fee_reserve - _, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount) + _, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount) asyncio.create_task( wallet.melt( proofs=send_proofs, @@ -347,7 +347,7 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led # wallet pays the invoice quote = await wallet.melt_quote(invoice_payment_request) total_amount = quote.amount + quote.fee_reserve - _, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount) + _, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount) asyncio.create_task( wallet.melt( proofs=send_proofs, diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index 518ae24a..23005653 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -60,7 +60,7 @@ async def test_melt_internal(wallet1: Wallet, ledger: Ledger): assert not melt_quote_pre_payment.paid, "melt quote should not be paid" assert melt_quote_pre_payment.state == MeltQuoteState.unpaid - keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 64) + keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 64) await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) @@ -85,7 +85,7 @@ async def test_melt_external(wallet1: Wallet, ledger: Ledger): assert mint_quote.state == MeltQuoteState.unpaid.value total_amount = mint_quote.amount + mint_quote.fee_reserve - keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) + keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, total_amount) melt_quote = await ledger.melt_quote( PostMeltQuoteRequest(request=invoice_payment_request, unit="sat") ) @@ -169,7 +169,7 @@ async def test_split(wallet1: Wallet, ledger: Ledger): await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) - keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) + keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10) secrets, rs, derivation_paths = await wallet1.generate_n_secrets(len(send_proofs)) outputs, rs = wallet1._construct_outputs( [p.amount for p in send_proofs], secrets, rs @@ -185,7 +185,7 @@ async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(64) await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10, set_reserved=False) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10, set_reserved=False) await assert_err( ledger.split(proofs=send_proofs, outputs=[]), "no outputs provided", @@ -198,7 +198,7 @@ async def test_split_with_input_less_than_outputs(wallet1: Wallet, ledger: Ledge await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) - keep_proofs, send_proofs = await wallet1.split_to_send( + keep_proofs, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 10, set_reserved=False ) @@ -396,7 +396,7 @@ async def test_check_proof_state(wallet1: Wallet, ledger: Ledger): await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) - keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) + keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10) proof_states = await ledger.db_read.get_proofs_states(Ys=[p.Y for p in send_proofs]) assert all([p.state.value == "UNSPENT" for p in proof_states]) diff --git a/tests/test_mint_regtest.py b/tests/test_mint_regtest.py index 8a934f0f..726dfcd5 100644 --- a/tests/test_mint_regtest.py +++ b/tests/test_mint_regtest.py @@ -43,7 +43,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): # wallet pays the invoice quote = await wallet.melt_quote(invoice_payment_request) total_amount = quote.amount + quote.fee_reserve - _, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount) + _, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount) asyncio.create_task(ledger.melt(proofs=send_proofs, quote=quote.quote)) # asyncio.create_task( # wallet.melt( diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 78960908..8bb1a950 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -241,14 +241,14 @@ async def test_split(wallet1: Wallet): @pytest.mark.asyncio -async def test_split_to_send(wallet1: Wallet): +async def test_swap_to_send(wallet1: Wallet): invoice = await wallet1.request_mint(64) await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 # this will select 32 sats and them (nothing to keep) - keep_proofs, send_proofs = await wallet1.split_to_send( + keep_proofs, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 32, set_reserved=True ) assert_amt(send_proofs, 32) @@ -307,7 +307,7 @@ async def test_melt(wallet1: Wallet): assert total_amount == 64 assert quote.fee_reserve == 0 - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, total_amount) melt_response = await wallet1.melt( proofs=send_proofs, @@ -343,12 +343,12 @@ async def test_melt(wallet1: Wallet): @pytest.mark.asyncio -async def test_split_to_send_more_than_balance(wallet1: Wallet): +async def test_swap_to_send_more_than_balance(wallet1: Wallet): invoice = await wallet1.request_mint(64) await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await assert_err( - wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True), + wallet1.swap_to_send(wallet1.proofs, 128, set_reserved=True), "balance too low.", ) assert wallet1.balance == 64 @@ -405,7 +405,7 @@ async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) - _, spendable_proofs = await wallet1.split_to_send( + _, spendable_proofs = await wallet1.swap_to_send( wallet1.proofs, 32, set_reserved=True ) await wallet2.redeem(spendable_proofs) diff --git a/tests/test_wallet_htlc.py b/tests/test_wallet_htlc.py index bda57263..0a36ecd1 100644 --- a/tests/test_wallet_htlc.py +++ b/tests/test_wallet_htlc.py @@ -74,7 +74,7 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): preimage = "00000000000000000000000000000000" preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock(preimage=preimage) - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) for p in send_proofs: assert HTLCSecret.deserialize(p.secret).data == preimage_hash @@ -87,7 +87,7 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): preimage = "00000000000000000000000000000000" # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock(preimage=preimage) - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) for p in send_proofs: p.witness = HTLCWitness(preimage=preimage).json() await wallet2.redeem(send_proofs) @@ -103,7 +103,7 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet) secret = await wallet1.create_htlc_lock( preimage=preimage[:-5] + "11111" ) # wrong preimage - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) for p in send_proofs: p.witness = HTLCWitness(preimage=preimage).json() await assert_err( @@ -122,7 +122,7 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): secret = await wallet1.create_htlc_lock( preimage=preimage, hashlock_pubkey=pubkey_wallet1 ) - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) for p in send_proofs: p.witness = HTLCWitness(preimage=preimage).json() await assert_err( @@ -142,7 +142,7 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet secret = await wallet1.create_htlc_lock( preimage=preimage, hashlock_pubkey=pubkey_wallet1 ) - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) signatures = await wallet1.sign_p2pk_proofs(send_proofs) for p, s in zip(send_proofs, signatures): p.witness = HTLCWitness( @@ -166,7 +166,7 @@ async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wall secret = await wallet1.create_htlc_lock( preimage=preimage, hashlock_pubkey=pubkey_wallet1 ) - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) signatures = await wallet1.sign_p2pk_proofs(send_proofs) for p, s in zip(send_proofs, signatures): @@ -192,7 +192,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( locktime_seconds=2, locktime_pubkey=pubkey_wallet1, ) - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) signatures = await wallet1.sign_p2pk_proofs(send_proofs) for p, s in zip(send_proofs, signatures): @@ -226,7 +226,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature( locktime_seconds=2, locktime_pubkey=pubkey_wallet1, ) - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) signatures = await wallet1.sign_p2pk_proofs(send_proofs) for p, s in zip(send_proofs, signatures): diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index ac236d5c..d3dcde53 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -74,7 +74,7 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet): pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) await wallet2.redeem(send_proofs) @@ -100,7 +100,7 @@ async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet): secret_lock = await wallet1.create_p2pk_lock( pubkey_wallet2, sig_all=True ) # sender side - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) await wallet2.redeem(send_proofs) @@ -114,7 +114,7 @@ async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wal pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) # receiver side: wrong private key @@ -137,7 +137,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key( secret_lock = await wallet1.create_p2pk_lock( pubkey_wallet2, locktime_seconds=2 ) # sender side - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) # receiver side: wrong private key @@ -167,7 +167,7 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet locktime_seconds=2, # locktime tags=Tags([["refund", pubkey_wallet2]]), # refund pubkey ) # sender side - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) send_proofs_copy = copy.deepcopy(send_proofs) @@ -198,7 +198,7 @@ async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: locktime_seconds=2, # locktime tags=Tags([["refund", garbage_pubkey_2.serialize().hex()]]), # refund pubkey ) # sender side - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) send_proofs_copy = copy.deepcopy(send_proofs) @@ -235,7 +235,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey( [["refund", pubkey_wallet2, pubkey_wallet1]] ), # multiple refund pubkeys ) # sender side - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) send_proofs_copy = copy.deepcopy(send_proofs) @@ -263,7 +263,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 ) - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) # add signatures of wallet1 @@ -285,7 +285,7 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 ) - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) # add signatures of wallet2 – this is a duplicate signature @@ -308,7 +308,7 @@ async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wal secret_lock = await wallet1.create_p2pk_lock( pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 ) - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) await assert_err( @@ -330,7 +330,7 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=3 ) - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) # add signatures of wallet1 @@ -352,7 +352,7 @@ async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: secret_lock = await wallet1.create_p2pk_lock( pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet2]]), n_sigs=2 ) - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) await assert_err(wallet2.redeem(send_proofs), "Mint Error: pubkeys must be unique.") @@ -377,7 +377,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key( secret_lock = await wallet1.create_p2pk_lock( pubkey_wallet2, tags=Tags([["pubkeys", wrong_public_key_hex]]), n_sigs=2 ) - _, send_proofs = await wallet1.split_to_send( + _, send_proofs = await wallet1.swap_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) # add signatures of wallet1 diff --git a/tests/test_wallet_regtest.py b/tests/test_wallet_regtest.py index 3e6d31bd..526fa119 100644 --- a/tests/test_wallet_regtest.py +++ b/tests/test_wallet_regtest.py @@ -45,7 +45,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): # wallet pays the invoice quote = await wallet.melt_quote(invoice_payment_request) total_amount = quote.amount + quote.fee_reserve - _, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount) + _, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount) asyncio.create_task( wallet.melt( proofs=send_proofs, @@ -85,7 +85,7 @@ async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger): # wallet pays the invoice quote = await wallet.melt_quote(invoice_payment_request) total_amount = quote.amount + quote.fee_reserve - _, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount) + _, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount) asyncio.create_task( wallet.melt( proofs=send_proofs, diff --git a/tests/test_wallet_restore.py b/tests/test_wallet_restore.py index 4b92ca76..837d9657 100644 --- a/tests/test_wallet_restore.py +++ b/tests/test_wallet_restore.py @@ -185,7 +185,7 @@ async def test_restore_wallet_with_invalid_mnemonic(wallet3: Wallet): @pytest.mark.asyncio -async def test_restore_wallet_after_split_to_send(wallet3: Wallet): +async def test_restore_wallet_after_swap_to_send(wallet3: Wallet): await wallet3._init_private_key( "half depart obvious quality work element tank gorilla view sugar picture" " humble" @@ -197,7 +197,7 @@ async def test_restore_wallet_after_split_to_send(wallet3: Wallet): await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 - _, spendable_proofs = await wallet3.split_to_send( + _, spendable_proofs = await wallet3.swap_to_send( wallet3.proofs, 32, set_reserved=True ) # type: ignore @@ -222,7 +222,7 @@ async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: W await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 - _, spendable_proofs = await wallet3.split_to_send( + _, spendable_proofs = await wallet3.swap_to_send( wallet3.proofs, 32, set_reserved=True ) # type: ignore @@ -265,7 +265,7 @@ async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet): await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 - _, spendable_proofs = await wallet3.split_to_send( + _, spendable_proofs = await wallet3.swap_to_send( wallet3.proofs, 32, set_reserved=True ) # type: ignore @@ -295,7 +295,7 @@ async def test_restore_wallet_after_send_twice( box.add(wallet3.proofs) assert wallet3.balance == 2 - keep_proofs, spendable_proofs = await wallet3.split_to_send( + keep_proofs, spendable_proofs = await wallet3.swap_to_send( wallet3.proofs, 1, set_reserved=True ) # type: ignore box.add(wallet3.proofs) @@ -317,7 +317,7 @@ async def test_restore_wallet_after_send_twice( # again - _, spendable_proofs = await wallet3.split_to_send( + _, spendable_proofs = await wallet3.swap_to_send( wallet3.proofs, 1, set_reserved=True ) # type: ignore box.add(wallet3.proofs) @@ -354,7 +354,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value( box.add(wallet3.proofs) assert wallet3.balance == 64 - keep_proofs, spendable_proofs = await wallet3.split_to_send( + keep_proofs, spendable_proofs = await wallet3.swap_to_send( wallet3.proofs, 10, set_reserved=True ) # type: ignore box.add(wallet3.proofs) @@ -376,7 +376,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value( # again - _, spendable_proofs = await wallet3.split_to_send( + _, spendable_proofs = await wallet3.swap_to_send( wallet3.proofs, 12, set_reserved=True ) # type: ignore diff --git a/tests/test_wallet_subscription.py b/tests/test_wallet_subscription.py index ce77a16f..ac4711fa 100644 --- a/tests/test_wallet_subscription.py +++ b/tests/test_wallet_subscription.py @@ -86,7 +86,7 @@ def callback(msg: JSONRPCNotficationParams): wallet.proofs, callback=callback ) - _ = await wallet.split_to_send(wallet.proofs, 64) + _ = await wallet.swap_to_send(wallet.proofs, 64) wait = 1 await asyncio.sleep(wait) From 1415077747bfc04e240f3ef499ef0272d472418e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:40:36 +0200 Subject: [PATCH 2/5] Reame split to swap across codebase --- cashu/core/models.py | 10 ++-- cashu/mint/ledger.py | 14 +++--- cashu/mint/router.py | 20 ++++---- cashu/mint/router_deprecated.py | 18 +++---- cashu/wallet/transactions.py | 82 +++++++++++++++---------------- cashu/wallet/v1_api.py | 8 +-- cashu/wallet/wallet.py | 2 +- cashu/wallet/wallet_deprecated.py | 8 +-- 8 files changed, 81 insertions(+), 81 deletions(-) diff --git a/cashu/core/models.py b/cashu/core/models.py index a9743144..1fb6067a 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -222,19 +222,19 @@ class PostMeltRequest_deprecated(BaseModel): # ------- API: SPLIT ------- -class PostSplitRequest(BaseModel): +class PostSwapRequest(BaseModel): inputs: List[Proof] = Field(..., max_items=settings.mint_max_request_length) outputs: List[BlindedMessage] = Field( ..., max_items=settings.mint_max_request_length ) -class PostSplitResponse(BaseModel): +class PostSwapResponse(BaseModel): signatures: List[BlindedSignature] # deprecated since 0.13.0 -class PostSplitRequest_Deprecated(BaseModel): +class PostSwapRequest_Deprecated(BaseModel): proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length) amount: Optional[int] = None outputs: List[BlindedMessage_Deprecated] = Field( @@ -242,11 +242,11 @@ class PostSplitRequest_Deprecated(BaseModel): ) -class PostSplitResponse_Deprecated(BaseModel): +class PostSwapResponse_Deprecated(BaseModel): promises: List[BlindedSignature] = [] -class PostSplitResponse_Very_Deprecated(BaseModel): +class PostSwapResponse_Very_Deprecated(BaseModel): fst: List[BlindedSignature] = [] snd: List[BlindedSignature] = [] deprecated: str = "The amount field is deprecated since 0.13.0" diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 5ff9b3bc..5d11c404 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -961,18 +961,18 @@ async def melt( return PostMeltQuoteResponse.from_melt_quote(melt_quote) - async def split( + async def swap( self, *, proofs: List[Proof], outputs: List[BlindedMessage], keyset: Optional[MintKeyset] = None, ): - """Consumes proofs and prepares new promises based on the amount split. Used for splitting tokens + """Consumes proofs and prepares new promises based on the amount swap. Used for swapping 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. + proofs (List[Proof]): Proofs to be invalidated for the swap. 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. @@ -980,9 +980,9 @@ async def split( Exception: Validation of proofs or outputs failed Returns: - Tuple[List[BlindSignature],List[BlindSignature]]: Promises on both sides of the split. + List[BlindedSignature]: New promises (signatures) for the outputs. """ - logger.trace("split called") + logger.trace("swap called") # verify spending inputs, outputs, and spending conditions await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs) await self.db_write._set_proofs_pending(proofs) @@ -991,13 +991,13 @@ async def split( await self._invalidate_proofs(proofs=proofs, conn=conn) promises = await self._generate_promises(outputs, keyset, conn) except Exception as e: - logger.trace(f"split failed: {e}") + logger.trace(f"swap failed: {e}") raise e finally: # delete proofs from pending list await self.db_write._unset_proofs_pending(proofs) - logger.trace("split successful") + logger.trace("swap successful") return promises async def restore( diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 9a3c26d7..d45ea69e 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -21,8 +21,8 @@ PostMintResponse, PostRestoreRequest, PostRestoreResponse, - PostSplitRequest, - PostSplitResponse, + PostSwapRequest, + PostSwapResponse, ) from ..core.settings import settings from ..mint.startup import ledger @@ -312,7 +312,7 @@ async def melt(request: Request, payload: PostMeltRequest) -> PostMeltQuoteRespo "/v1/swap", name="Swap tokens", summary="Swap inputs for outputs of the same value", - response_model=PostSplitResponse, + response_model=PostSwapResponse, response_description=( "An array of blinded signatures that can be used to create proofs." ), @@ -320,20 +320,20 @@ async def melt(request: Request, payload: PostMeltRequest) -> PostMeltQuoteRespo @limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def swap( request: Request, - payload: PostSplitRequest, -) -> PostSplitResponse: + payload: PostSwapRequest, +) -> PostSwapResponse: """ - Requests a set of Proofs to be split into two a new set of BlindedSignatures. + Requests a set of Proofs to be swapped for another set of BlindSignatures. - This endpoint is used by Alice to split a set of proofs before making a payment to Carol. - It is then used by Carol (by setting split=total) to redeem the tokens. + This endpoint can be used by Alice to swap a set of proofs before making a payment to Carol. + It can then used by Carol to redeem the tokens for new proofs. """ logger.trace(f"> POST /v1/swap: {payload}") assert payload.outputs, Exception("no outputs provided.") - signatures = await ledger.split(proofs=payload.inputs, outputs=payload.outputs) + signatures = await ledger.swap(proofs=payload.inputs, outputs=payload.outputs) - return PostSplitResponse(signatures=signatures) + return PostSwapResponse(signatures=signatures) @router.post( diff --git a/cashu/mint/router_deprecated.py b/cashu/mint/router_deprecated.py index 74d87c7e..95d6ee26 100644 --- a/cashu/mint/router_deprecated.py +++ b/cashu/mint/router_deprecated.py @@ -22,9 +22,9 @@ PostMintResponse_deprecated, PostRestoreRequest_Deprecated, PostRestoreResponse, - PostSplitRequest_Deprecated, - PostSplitResponse_Deprecated, - PostSplitResponse_Very_Deprecated, + PostSwapRequest_Deprecated, + PostSwapResponse_Deprecated, + PostSwapResponse_Very_Deprecated, ) from ..core.settings import settings from .limit import limiter @@ -270,7 +270,7 @@ async def check_fees( name="Split", summary="Split proofs at a specified amount", # response_model=Union[ - # PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated + # PostSwapResponse_Very_Deprecated, PostSwapResponse_Deprecated # ], response_description=( "A list of blinded signatures that can be used to create proofs." @@ -280,8 +280,8 @@ async def check_fees( @limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def split_deprecated( request: Request, - payload: PostSplitRequest_Deprecated, - # ) -> Union[PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated]: + payload: PostSwapRequest_Deprecated, + # ) -> Union[PostSwapResponse_Very_Deprecated, PostSwapResponse_Deprecated]: ): """ Requests a set of Proofs to be split into two a new set of BlindedSignatures. @@ -297,7 +297,7 @@ async def split_deprecated( for o in payload.outputs ] # END BACKWARDS COMPATIBILITY < 0.14 - promises = await ledger.split(proofs=payload.proofs, outputs=outputs) + promises = await ledger.swap(proofs=payload.proofs, outputs=outputs) if payload.amount: # BEGIN backwards compatibility < 0.13 @@ -319,10 +319,10 @@ async def split_deprecated( f" {sum([p.amount for p in frst_promises])} sat and send:" f" {len(scnd_promises)}: {sum([p.amount for p in scnd_promises])} sat" ) - return PostSplitResponse_Very_Deprecated(fst=frst_promises, snd=scnd_promises) + return PostSwapResponse_Very_Deprecated(fst=frst_promises, snd=scnd_promises) # END backwards compatibility < 0.13 else: - return PostSplitResponse_Deprecated(promises=promises) + return PostSwapResponse_Deprecated(promises=promises) @router_deprecated.post( diff --git a/cashu/wallet/transactions.py b/cashu/wallet/transactions.py index 78251488..6af54dc4 100644 --- a/cashu/wallet/transactions.py +++ b/cashu/wallet/transactions.py @@ -36,44 +36,44 @@ 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_legacy( - # self, proofs: List[Proof], amount_to_send: int, tolerance: int = 0 - # ) -> List[Proof]: - # send_proofs: List[Proof] = [] - # NO_SELECTION: List[Proof] = [] - - # logger.trace(f"proofs: {[p.amount for p in proofs]}") - # # sort proofs by amount (descending) - # sorted_proofs = sorted(proofs, key=lambda p: p.amount, reverse=True) - # # only consider proofs smaller than the amount we want to send (+ tolerance) for coin selection - # fee_for_single_proof = self.get_fees_for_proofs([sorted_proofs[0]]) - # sorted_proofs = [ - # p - # for p in sorted_proofs - # if p.amount <= amount_to_send + tolerance + fee_for_single_proof - # ] - # if not sorted_proofs: - # logger.info( - # f"no small-enough proofs to send. Have: {[p.amount for p in proofs]}" - # ) - # return NO_SELECTION - - # target_amount = amount_to_send - - # # compose the target amount from the remaining_proofs - # logger.debug(f"sorted_proofs: {[p.amount for p in sorted_proofs]}") - # for p in sorted_proofs: - # if sum_proofs(send_proofs) + p.amount <= target_amount + tolerance: - # send_proofs.append(p) - # target_amount = amount_to_send + self.get_fees_for_proofs(send_proofs) - - # if sum_proofs(send_proofs) < amount_to_send: - # logger.info("could not select proofs to reach target amount (too little).") - # return NO_SELECTION - - # fees = self.get_fees_for_proofs(send_proofs) - # logger.debug(f"Selected sum of proofs: {sum_proofs(send_proofs)}, fees: {fees}") - # return send_proofs + async def _select_proofs_to_send_legacy( + self, proofs: List[Proof], amount_to_send: int, tolerance: int = 0 + ) -> List[Proof]: + send_proofs: List[Proof] = [] + NO_SELECTION: List[Proof] = [] + + logger.trace(f"proofs: {[p.amount for p in proofs]}") + # sort proofs by amount (descending) + sorted_proofs = sorted(proofs, key=lambda p: p.amount, reverse=True) + # only consider proofs smaller than the amount we want to send (+ tolerance) for coin selection + fee_for_single_proof = self.get_fees_for_proofs([sorted_proofs[0]]) + sorted_proofs = [ + p + for p in sorted_proofs + if p.amount <= amount_to_send + tolerance + fee_for_single_proof + ] + if not sorted_proofs: + logger.info( + f"no small-enough proofs to send. Have: {[p.amount for p in proofs]}" + ) + return NO_SELECTION + + target_amount = amount_to_send + + # compose the target amount from the remaining_proofs + logger.debug(f"sorted_proofs: {[p.amount for p in sorted_proofs]}") + for p in sorted_proofs: + if sum_proofs(send_proofs) + p.amount <= target_amount + tolerance: + send_proofs.append(p) + target_amount = amount_to_send + self.get_fees_for_proofs(send_proofs) + + if sum_proofs(send_proofs) < amount_to_send: + logger.info("could not select proofs to reach target amount (too little).") + return NO_SELECTION + + fees = self.get_fees_for_proofs(send_proofs) + logger.debug(f"Selected sum of proofs: {sum_proofs(send_proofs)}, fees: {fees}") + return send_proofs async def _select_proofs_to_send( self, @@ -144,7 +144,7 @@ async def _select_proofs_to_send( ) return selected_proofs - async def _select_proofs_to_split( + async def _select_proofs_to_swap( self, proofs: List[Proof], amount_to_send: int ) -> Tuple[List[Proof], int]: """ @@ -170,7 +170,7 @@ async def _select_proofs_to_split( Exception: If the balance is too low to send the amount """ logger.debug( - f"_select_proofs_to_split - amounts we have: {amount_summary(proofs, self.unit)}" + f"_select_proofs_to_swap - amounts we have: {amount_summary(proofs, self.unit)}" ) send_proofs: List[Proof] = [] @@ -198,7 +198,7 @@ async def _select_proofs_to_split( send_proofs.append(proof_to_add) logger.trace( - f"_select_proofs_to_split – selected proof amounts: {[p.amount for p in send_proofs]}" + f"_select_proofs_to_swap – selected proof amounts: {[p.amount for p in send_proofs]}" ) fees = self.get_fees_for_proofs(send_proofs) return send_proofs, fees diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index 5acb7a90..d5d9ce50 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -39,8 +39,8 @@ PostMintRequest, PostMintResponse, PostRestoreResponse, - PostSplitRequest, - PostSplitResponse, + PostSwapRequest, + PostSwapResponse, ) from ..core.settings import settings from ..tor.tor import TorProxy @@ -465,7 +465,7 @@ async def split( ) -> List[BlindedSignature]: """Consume proofs and create new promises based on amount split.""" logger.debug("Calling split. POST /v1/swap") - split_payload = PostSplitRequest(inputs=proofs, outputs=outputs) + split_payload = PostSwapRequest(inputs=proofs, outputs=outputs) # construct payload def _splitrequest_include_fields(proofs: List[Proof]): @@ -494,7 +494,7 @@ def _splitrequest_include_fields(proofs: List[Proof]): # END backwards compatibility < 0.15.0 self.raise_on_error_request(resp) promises_dict = resp.json() - mint_response = PostSplitResponse.parse_obj(promises_dict) + mint_response = PostSwapResponse.parse_obj(promises_dict) promises = [BlindedSignature(**p.dict()) for p in mint_response.signatures] if len(promises) == 0: diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index d09bdcb8..ab49f88b 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -1093,7 +1093,7 @@ async def swap_to_send( raise Exception("balance too low.") # coin selection for swapping - # spendable_proofs, fees = await self._select_proofs_to_split(proofs, amount) + # spendable_proofs, fees = await self._select_proofs_to_swap(proofs, amount) swap_proofs = await self._select_proofs_to_send( proofs, amount, include_fees=True ) diff --git a/cashu/wallet/wallet_deprecated.py b/cashu/wallet/wallet_deprecated.py index e5b953a4..4efccde3 100644 --- a/cashu/wallet/wallet_deprecated.py +++ b/cashu/wallet/wallet_deprecated.py @@ -31,8 +31,8 @@ PostMintRequest_deprecated, PostMintResponse_deprecated, PostRestoreResponse, - PostSplitRequest_Deprecated, - PostSplitResponse_Deprecated, + PostSwapRequest_Deprecated, + PostSwapResponse_Deprecated, ) from ..core.settings import settings from ..tor.tor import TorProxy @@ -348,7 +348,7 @@ async def split_deprecated( """Consume proofs and create new promises based on amount split.""" logger.warning("Using deprecated API call: Calling split. POST /split") outputs_deprecated = [BlindedMessage_Deprecated(**o.dict()) for o in outputs] - split_payload = PostSplitRequest_Deprecated( + split_payload = PostSwapRequest_Deprecated( proofs=proofs, outputs=outputs_deprecated ) @@ -373,7 +373,7 @@ def _splitrequest_include_fields(proofs: List[Proof]): ) self.raise_on_error(resp) promises_dict = resp.json() - mint_response = PostSplitResponse_Deprecated.parse_obj(promises_dict) + mint_response = PostSwapResponse_Deprecated.parse_obj(promises_dict) promises = [BlindedSignature(**p.dict()) for p in mint_response.promises] if len(promises) == 0: From 29bc4f1d4fdc24cdb57635893898507b7a4ae013 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:47:55 +0200 Subject: [PATCH 3/5] rename remaining splits to swap --- tests/test_mint_fees.py | 6 +++--- tests/test_mint_operations.py | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_mint_fees.py b/tests/test_mint_fees.py index f861a12d..3c988417 100644 --- a/tests/test_mint_fees.py +++ b/tests/test_mint_fees.py @@ -123,7 +123,7 @@ async def test_split_with_fees(wallet1: Wallet, ledger: Ledger): assert fees == 1 outputs = await wallet1.construct_outputs(amount_split(9)) - promises = await ledger.split(proofs=send_proofs, outputs=outputs) + promises = await ledger.swap(proofs=send_proofs, outputs=outputs) assert len(promises) == len(outputs) assert [p.amount for p in promises] == [p.amount for p in outputs] @@ -141,7 +141,7 @@ async def test_split_with_high_fees(wallet1: Wallet, ledger: Ledger): assert fees == 3 outputs = await wallet1.construct_outputs(amount_split(7)) - promises = await ledger.split(proofs=send_proofs, outputs=outputs) + promises = await ledger.swap(proofs=send_proofs, outputs=outputs) assert len(promises) == len(outputs) assert [p.amount for p in promises] == [p.amount for p in outputs] @@ -161,7 +161,7 @@ async def test_split_not_enough_fees(wallet1: Wallet, ledger: Ledger): outputs = await wallet1.construct_outputs(amount_split(10)) await assert_err( - ledger.split(proofs=send_proofs, outputs=outputs), "are not balanced" + ledger.swap(proofs=send_proofs, outputs=outputs), "are not balanced" ) diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index 23005653..acfd9134 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -175,7 +175,7 @@ async def test_split(wallet1: Wallet, ledger: Ledger): [p.amount for p in send_proofs], secrets, rs ) - promises = await ledger.split(proofs=send_proofs, outputs=outputs) + promises = await ledger.swap(proofs=send_proofs, outputs=outputs) assert len(promises) == len(outputs) assert [p.amount for p in promises] == [p.amount for p in outputs] @@ -187,7 +187,7 @@ async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger): await wallet1.mint(64, id=invoice.id) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10, set_reserved=False) await assert_err( - ledger.split(proofs=send_proofs, outputs=[]), + ledger.swap(proofs=send_proofs, outputs=[]), "no outputs provided", ) @@ -213,7 +213,7 @@ async def test_split_with_input_less_than_outputs(wallet1: Wallet, ledger: Ledge ) await assert_err( - ledger.split(proofs=send_proofs, outputs=outputs), + ledger.swap(proofs=send_proofs, outputs=outputs), "are not balanced", ) @@ -237,7 +237,7 @@ async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledge outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs) await assert_err( - ledger.split(proofs=inputs, outputs=outputs), + ledger.swap(proofs=inputs, outputs=outputs), "are not balanced", ) @@ -262,11 +262,11 @@ async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger): ) outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs) - await ledger.split(proofs=inputs1, outputs=outputs) + await ledger.swap(proofs=inputs1, outputs=outputs) # try to spend other proofs with the same outputs again await assert_err( - ledger.split(proofs=inputs2, outputs=outputs), + ledger.swap(proofs=inputs2, outputs=outputs), "outputs have already been signed before.", ) @@ -277,7 +277,7 @@ async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger): ) outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs) - await ledger.split(proofs=inputs2, outputs=outputs) + await ledger.swap(proofs=inputs2, outputs=outputs) @pytest.mark.asyncio From 9f3f2973d0b2c9745130f06533eeb8029ec4a2f3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 11 Jul 2024 19:10:23 +0200 Subject: [PATCH 4/5] fix restore index with multiple keysets --- cashu/wallet/secrets.py | 15 +++--- cashu/wallet/wallet.py | 101 ++++++++++++++++++++++++----------- tests/test_wallet_restore.py | 16 +++--- 3 files changed, 86 insertions(+), 46 deletions(-) diff --git a/cashu/wallet/secrets.py b/cashu/wallet/secrets.py index 3e6e5722..ef35f02f 100644 --- a/cashu/wallet/secrets.py +++ b/cashu/wallet/secrets.py @@ -105,27 +105,28 @@ async def _generate_random_secret(self) -> str: return hashlib.sha256(os.urandom(32)).hexdigest() async def generate_determinstic_secret( - self, counter: int + self, counter: int, keyset_id: Optional[str] = None ) -> Tuple[bytes, bytes, str]: """ Determinstically generates two secrets (one as the secret message, one as the blinding factor). """ assert self.bip32, "BIP32 not initialized yet." + keyset_id = keyset_id or self.keyset_id # integer keyset id modulo max number of bip32 child keys try: - keyest_id_int = int.from_bytes(bytes.fromhex(self.keyset_id), "big") % ( + keyest_id_int = int.from_bytes(bytes.fromhex(keyset_id), "big") % ( 2**31 - 1 ) except ValueError: # BEGIN: BACKWARDS COMPATIBILITY < 0.15.0 keyset id is not hex # calculate an integer keyset id from the base64 encoded keyset id - keyest_id_int = int.from_bytes(base64.b64decode(self.keyset_id), "big") % ( + keyest_id_int = int.from_bytes(base64.b64decode(keyset_id), "big") % ( 2**31 - 1 ) # END: BACKWARDS COMPATIBILITY < 0.15.0 keyset id is not hex - logger.trace(f"keyset id: {self.keyset_id} becomes {keyest_id_int}") + logger.trace(f"keyset id: {keyset_id} becomes {keyest_id_int}") token_derivation_path = f"m/129372'/0'/{keyest_id_int}'/{counter}'" # for secret secret_derivation_path = f"{token_derivation_path}/0" @@ -177,13 +178,14 @@ async def generate_n_secrets( return secrets, rs, derivation_paths async def generate_secrets_from_to( - self, from_counter: int, to_counter: int + self, from_counter: int, to_counter: int, keyset_id: Optional[str] = None ) -> Tuple[List[str], List[PrivateKey], List[str]]: """Generates secrets and blinding factors from `from_counter` to `to_counter` Args: from_counter (int): Start counter to_counter (int): End counter + keyset_id (Optional[str], optional): Keyset id. Defaults to None. Returns: Tuple[List[str], List[PrivateKey], List[str]]: Secrets, blinding factors, derivation paths @@ -196,7 +198,8 @@ async def generate_secrets_from_to( ), "from_counter must be smaller than to_counter" secret_counters = [c for c in range(from_counter, to_counter + 1)] secrets_rs_derivationpaths = [ - await self.generate_determinstic_secret(s) for s in secret_counters + await self.generate_determinstic_secret(s, keyset_id) + for s in secret_counters ] # secrets are supplied as str secrets = [s[0].hex() for s in secrets_rs_derivationpaths] diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index ab49f88b..e97dc2b8 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -85,7 +85,7 @@ class Wallet( """ keyset_id: str # holds current keyset id - keysets: Dict[str, WalletKeyset] # holds keysets + keysets: Dict[str, WalletKeyset] = {} # holds keysets # mint_keyset_ids: List[str] # holds active keyset ids of the mint unit: Unit mint_info: MintInfo # holds info about mint @@ -178,7 +178,7 @@ async def load_mint_info(self) -> MintInfo: logger.debug(f"Mint info: {self.mint_info}") return self.mint_info - async def load_mint_keysets(self): + async def load_mint_keysets(self, force_old_keysets=False): """Loads all keyset of the mint and makes sure we have them all in the database. Then loads all keysets from the database for the active mint and active unit into self.keysets. @@ -237,7 +237,7 @@ async def load_mint_keysets(self): ) # BEGIN backwards compatibility: phase out keysets with base64 ID by treating them as inactive - if settings.wallet_inactivate_legacy_keysets: + if settings.wallet_inactivate_legacy_keysets and not force_old_keysets: keysets_in_db = await get_keysets(mint_url=self.url, db=self.db) for keyset in keysets_in_db: if not keyset.active: @@ -306,14 +306,19 @@ async def activate_keyset(self, keyset_id: Optional[str] = None) -> None: logger.debug(f"Activated keyset {self.keyset_id}") - async def load_mint(self, keyset_id: str = "") -> None: + async def load_mint(self, keyset_id: str = "", force_old_keysets=False) -> None: """ Loads the public keys of the mint. Either gets the keys for the specified `keyset_id` or gets the keys of the active keyset from the mint. Gets the active keyset ids of the mint and stores in `self.mint_keyset_ids`. + + Args: + keyset_id (str, optional): Keyset id to load. Defaults to "". + force_old_keysets (bool, optional): If true, old deprecated base64 keysets are not ignored. This is necessary for restoring tokens from old base64 keysets. + Defaults to False. """ logger.trace("Loading mint.") - await self.load_mint_keysets() + await self.load_mint_keysets(force_old_keysets) await self.activate_keyset(keyset_id) try: await self.load_mint_info() @@ -902,6 +907,7 @@ def _construct_outputs( amounts: List[int], secrets: List[str], rs: List[PrivateKey] = [], + keyset_id: Optional[str] = None, ) -> Tuple[List[BlindedMessage], List[PrivateKey]]: """Takes a list of amounts and secrets and returns outputs. Outputs are blinded messages `outputs` and blinding factors `rs` @@ -921,8 +927,8 @@ def _construct_outputs( assert len(amounts) == len( secrets ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}" + keyset_id = keyset_id or self.keyset_id outputs: List[BlindedMessage] = [] - rs_ = [None] * len(amounts) if not rs else rs rs_return: List[PrivateKey] = [] for secret, amount, r in zip(secrets, amounts, rs_): @@ -935,7 +941,7 @@ def _construct_outputs( rs_return.append(r) output = BlindedMessage( - amount=amount, B_=B_.serialize().hex(), id=self.keyset_id + amount=amount, B_=B_.serialize().hex(), id=keyset_id ) outputs.append(output) logger.trace(f"Constructing output: {output}, r: {r.serialize()}") @@ -1195,31 +1201,41 @@ async def restore_tokens_for_keyset( to (int, optional): The number of consecutive empty responses to stop restoring. Defaults to 2. batch (int, optional): The number of proofs to restore in one batch. Defaults to 25. """ - stop_counter = 0 + empty_batches = 0 # we get the current secret counter and restore from there on spendable_proofs = [] counter_before = await bump_secret_derivation( db=self.db, keyset_id=keyset_id, by=0 ) if counter_before != 0: - print("Keyset has already been used. Restoring from it's last state.") + print("Keyset has already been used. Restoring from its last state.") i = counter_before - n_last_restored_proofs = 0 - while stop_counter < to: - print(f"Restoring token {i} to {i + batch}...") - restored_proofs = await self.restore_promises_from_to(i, i + batch - 1) + last_restore_count = 0 + while empty_batches < to: + print(f"Restoring counter {i} to {i + batch} for keyset {keyset_id} ...") + ( + next_restored_output_index, + restored_proofs, + ) = await self.restore_promises_from_to(keyset_id, i, i + batch - 1) + last_restore_count += next_restored_output_index + i += batch if len(restored_proofs) == 0: - stop_counter += 1 + empty_batches += 1 + continue spendable_proofs = await self.invalidate( restored_proofs, check_spendable=True ) if len(spendable_proofs): - n_last_restored_proofs = len(spendable_proofs) - print(f"Restored {sum_proofs(restored_proofs)} sat") - i += batch + print( + f"Restored {sum_proofs(spendable_proofs)} sat for keyset {keyset_id}." + ) + else: + logger.debug( + f"None of the {len(restored_proofs)} restored proofs are spendable." + ) # restore the secret counter to its previous value for the last round - revert_counter_by = batch * to + n_last_restored_proofs + revert_counter_by = i - last_restore_count logger.debug(f"Reverting secret counter by {revert_counter_by}") before = await bump_secret_derivation( db=self.db, @@ -1229,8 +1245,8 @@ async def restore_tokens_for_keyset( logger.debug( f"Secret counter reverted from {before} to {before - revert_counter_by}" ) - if n_last_restored_proofs == 0: - print("No tokens restored for keyset.") + if last_restore_count == 0: + print(f"No tokens restored for keyset {keyset_id}.") return async def restore_wallet_from_mnemonic( @@ -1245,14 +1261,14 @@ async def restore_wallet_from_mnemonic( batch (int, optional): The number of proofs to restore in one batch. Defaults to 25. """ await self._init_private_key(mnemonic) - await self.load_mint() + await self.load_mint(force_old_keysets=False) print("Restoring tokens...") for keyset_id in self.keysets.keys(): await self.restore_tokens_for_keyset(keyset_id, to, batch) async def restore_promises_from_to( - self, from_counter: int, to_counter: int - ) -> List[Proof]: + self, keyset_id: str, from_counter: int, to_counter: int + ) -> Tuple[int, List[Proof]]: """Restores promises from a given range of counters. This is for restoring a wallet from a mnemonic. Args: @@ -1260,18 +1276,20 @@ async def restore_promises_from_to( to_counter (int): Counter for the secret derivation to end at Returns: - List[Proof]: List of restored proofs + Tuple[int, List[Proof]]: Index of the last restored output and list of restored proofs """ # we regenerate the secrets and rs for the given range secrets, rs, derivation_paths = await self.generate_secrets_from_to( - from_counter, to_counter + from_counter, to_counter, keyset_id=keyset_id ) # we don't know the amount but luckily the mint will tell us so we use a dummy amount here amounts_dummy = [1] * len(secrets) # we generate outputs from deterministic secrets and rs - regenerated_outputs, _ = self._construct_outputs(amounts_dummy, secrets, rs) + regenerated_outputs, _ = self._construct_outputs( + amounts_dummy, secrets, rs, keyset_id=keyset_id + ) # we ask the mint to reissue the promises - proofs = await self.restore_promises( + next_restored_output_index, proofs = await self.restore_promises( outputs=regenerated_outputs, secrets=secrets, rs=rs, @@ -1279,9 +1297,9 @@ async def restore_promises_from_to( ) await set_secret_derivation( - db=self.db, keyset_id=self.keyset_id, counter=to_counter + 1 + db=self.db, keyset_id=keyset_id, counter=to_counter + 1 ) - return proofs + return next_restored_output_index, proofs async def restore_promises( self, @@ -1289,7 +1307,7 @@ async def restore_promises( secrets: List[str], rs: List[PrivateKey], derivation_paths: List[str], - ) -> List[Proof]: + ) -> Tuple[int, List[Proof]]: """Restores proofs from a list of outputs, secrets, rs and derivation paths. Args: @@ -1299,10 +1317,26 @@ async def restore_promises( derivation_paths (List[str]): Derivation paths used for the secrets necessary to unblind the promises Returns: - List[Proof]: List of restored proofs + Tuple[int, List[Proof]]: Index of the last restored output and list of restored proofs """ # restored_outputs is there so we can match the promises to the secrets and rs restored_outputs, restored_promises = await super().restore_promises(outputs) + # determine the index in `outputs` of the last restored output from restored_outputs[-1].B_ + if not restored_outputs: + next_restored_output_index = 0 + else: + next_restored_output_index = ( + next( + ( + idx + for idx, val in enumerate(outputs) + if val.B_ == restored_outputs[-1].B_ + ), + 0, + ) + + 1 + ) + logger.trace(f"Last restored output index: {next_restored_output_index}") # now we need to filter out the secrets and rs that had a match matching_indices = [ idx @@ -1311,9 +1345,12 @@ async def restore_promises( ] secrets = [secrets[i] for i in matching_indices] rs = [rs[i] for i in matching_indices] + logger.debug( + f"Restored {len(restored_promises)} promises. Constructing proofs." + ) # now we can construct the proofs with the secrets and rs proofs = await self._construct_proofs( restored_promises, secrets, rs, derivation_paths ) logger.debug(f"Restored {len(restored_promises)} promises") - return proofs + return next_restored_output_index, proofs diff --git a/tests/test_wallet_restore.py b/tests/test_wallet_restore.py index 837d9657..e245d492 100644 --- a/tests/test_wallet_restore.py +++ b/tests/test_wallet_restore.py @@ -164,7 +164,7 @@ async def test_restore_wallet_after_mint(wallet3: Wallet): await wallet3.load_proofs() wallet3.proofs = [] assert wallet3.balance == 0 - await wallet3.restore_promises_from_to(0, 20) + await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 20) assert wallet3.balance == 64 # expect that DLEQ proofs are restored @@ -205,7 +205,7 @@ async def test_restore_wallet_after_swap_to_send(wallet3: Wallet): await wallet3.load_proofs() wallet3.proofs = [] assert wallet3.balance == 0 - await wallet3.restore_promises_from_to(0, 100) + await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 100) assert wallet3.balance == 96 await wallet3.invalidate(wallet3.proofs, check_spendable=True) assert wallet3.balance == 64 @@ -232,7 +232,7 @@ async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: W await wallet3.load_proofs(reload=True) assert wallet3.proofs == [] assert wallet3.balance == 0 - await wallet3.restore_promises_from_to(0, 100) + await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 100) assert wallet3.balance == 96 await wallet3.invalidate(wallet3.proofs, check_spendable=True) assert wallet3.balance == 32 @@ -275,7 +275,7 @@ async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet): await wallet3.load_proofs(reload=True) assert wallet3.proofs == [] assert wallet3.balance == 0 - await wallet3.restore_promises_from_to(0, 100) + await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 100) assert wallet3.balance == 128 await wallet3.invalidate(wallet3.proofs, check_spendable=True) assert wallet3.balance == 64 @@ -309,7 +309,7 @@ async def test_restore_wallet_after_send_twice( await wallet3.load_proofs(reload=True) assert wallet3.proofs == [] assert wallet3.balance == 0 - await wallet3.restore_promises_from_to(0, 10) + await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 10) box.add(wallet3.proofs) assert wallet3.balance == 4 await wallet3.invalidate(wallet3.proofs, check_spendable=True) @@ -331,7 +331,7 @@ async def test_restore_wallet_after_send_twice( await wallet3.load_proofs(reload=True) assert wallet3.proofs == [] assert wallet3.balance == 0 - await wallet3.restore_promises_from_to(0, 15) + await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 15) box.add(wallet3.proofs) assert wallet3.balance == 6 await wallet3.invalidate(wallet3.proofs, check_spendable=True) @@ -368,7 +368,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value( await wallet3.load_proofs(reload=True) assert wallet3.proofs == [] assert wallet3.balance == 0 - await wallet3.restore_promises_from_to(0, 20) + await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 20) box.add(wallet3.proofs) assert wallet3.balance == 84 await wallet3.invalidate(wallet3.proofs, check_spendable=True) @@ -388,7 +388,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value( await wallet3.load_proofs(reload=True) assert wallet3.proofs == [] assert wallet3.balance == 0 - await wallet3.restore_promises_from_to(0, 50) + await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 50) assert wallet3.balance == 108 await wallet3.invalidate(wallet3.proofs, check_spendable=True) assert wallet3.balance == 64 From a3e4701ca0dc735dc0d7de6105fb01c56bd4f0e9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 11 Jul 2024 19:30:26 +0200 Subject: [PATCH 5/5] fix wallet api restore --- cashu/wallet/api/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/wallet/api/router.py b/cashu/wallet/api/router.py index f5c771f6..05ec5a62 100644 --- a/cashu/wallet/api/router.py +++ b/cashu/wallet/api/router.py @@ -433,7 +433,7 @@ async def restore( if to < 0: raise Exception("Counter must be positive") await wallet.load_mint() - await wallet.restore_promises_from_to(0, to) + await wallet.restore_promises_from_to(wallet.keyset_id, 0, to) await wallet.invalidate(wallet.proofs, check_spendable=True) return RestoreResponse(balance=wallet.available_balance)