Skip to content

Commit

Permalink
Fix receive -a to receive all pending tokens (#578)
Browse files Browse the repository at this point in the history
* catch error if receive -a doesnt work with a certain mint

* receive pending tokens from multiple mints

* receive pending from all mints and catch exceptions
  • Loading branch information
callebtc authored Jul 11, 2024
1 parent 26b9495 commit 1660005
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 22 deletions.
27 changes: 8 additions & 19 deletions cashu/wallet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
get_unit_wallet,
print_balance,
print_mint_balances,
receive_all_pending,
verify_mint,
)
from ..helpers import (
Expand Down Expand Up @@ -601,7 +602,7 @@ async def receive_cli(
all: bool,
):
wallet: Wallet = ctx.obj["WALLET"]

# receive a specific token
if token:
token_obj = deserialize_token_from_string(token)
# verify that we trust the mint in this tokens
Expand All @@ -615,30 +616,16 @@ async def receive_cli(
await verify_mint(mint_wallet, mint_url)
receive_wallet = await receive(mint_wallet, token_obj)
ctx.obj["WALLET"] = receive_wallet
# receive tokens via nostr
elif nostr:
await receive_nostr(wallet)
# exit on keypress
input("Enter any text to exit.")
print("Exiting.")
os._exit(0)
# receive all pending outgoing tokens back to the wallet
elif all:
reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs):
for key, value in groupby(reserved_proofs, key=itemgetter("send_id")): # type: ignore
proofs = list(value)
token = await wallet.serialize_proofs(proofs)
token_obj = TokenV4.deserialize(token)
# verify that we trust the mint of this token
# ask the user if they want to trust the mint
mint_url = token_obj.mint
mint_wallet = Wallet(
mint_url,
os.path.join(settings.cashu_dir, wallet.name),
unit=token_obj.unit,
)
await verify_mint(mint_wallet, mint_url)
receive_wallet = await receive(wallet, token_obj)
ctx.obj["WALLET"] = receive_wallet
await receive_all_pending(ctx, wallet)
else:
print("Error: enter token or use either flag --nostr or --all.")
return
Expand Down Expand Up @@ -767,7 +754,9 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
)
print(f"Legacy token: {token_legacy}\n")
print("--------------------------\n")
print("To remove all spent tokens use: cashu burn -a")
print("To remove all pending tokens that are already spent use: cashu burn -a")
print("To remove a specific pending token use: cashu burn <token>")
print("To receive all pending tokens use: cashu receive -a")


@cli.command("lock", help="Generate receiving lock.")
Expand Down
49 changes: 48 additions & 1 deletion cashu/wallet/cli/cli_helpers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
#!/usr/bin/env python
import os
from itertools import groupby
from operator import itemgetter

import click
from click import Context
from loguru import logger

from ...core.base import Unit
from ...core.settings import settings
from ...wallet.crud import get_keysets
from ...wallet.crud import (
get_keysets,
get_reserved_proofs,
)
from ...wallet.wallet import Wallet as Wallet
from ..helpers import (
deserialize_token_from_string,
receive,
)


async def print_balance(ctx: Context):
Expand Down Expand Up @@ -170,3 +180,40 @@ async def verify_mint(mint_wallet: Wallet, url: str):
)
else:
logger.debug(f"We know mint {url} already")


async def receive_all_pending(ctx: Context, wallet: Wallet):
reserved_proofs = await get_reserved_proofs(wallet.db)
if not len(reserved_proofs):
print("No pending proofs to receive.")
return
for key, value in groupby(reserved_proofs, key=itemgetter("send_id")): # type: ignore
mint_url = None
token_obj = None
try:
proofs = list(value)
mint_url, unit = await wallet._get_proofs_mint_unit(proofs)
mint_wallet = await Wallet.with_db(
url=mint_url,
db=os.path.join(settings.cashu_dir, wallet.name),
name=wallet.name,
unit=unit.name,
)
# verify that we trust the mint of this token
# ask the user if they want to trust the mint
await verify_mint(mint_wallet, mint_url)

token = await mint_wallet.serialize_proofs(proofs)
token_obj = deserialize_token_from_string(token)
mint_url = token_obj.mint
receive_wallet = await receive(mint_wallet, token_obj)
ctx.obj["WALLET"] = receive_wallet
except Exception as e:
if mint_url and token_obj:
unit = Unit[token_obj.unit]
print(
f"Could not receive {unit.str(token_obj.amount)} from mint {mint_url}: {str(e)}"
)
else:
print(f"Could not receive token: {str(e)}")
continue
47 changes: 45 additions & 2 deletions cashu/wallet/proofs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from itertools import groupby
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple

from loguru import logger

Expand Down Expand Up @@ -91,6 +91,44 @@ async def _get_keyset_urls(self, keysets: List[str]) -> Dict[str, List[str]]:
)
return mint_urls

async def _get_proofs_keysets(self, proofs: List[Proof]) -> Dict[str, WalletKeyset]:
keyset_ids = self._get_proofs_keyset_ids(proofs)
keysets_dict = {}
async with self.db.get_connection() as conn:
for keyset_id in keyset_ids:
keyset = await get_keysets(id=keyset_id, db=self.db, conn=conn)
if len(keyset) == 1:
keysets_dict[keyset_id] = keyset[0]
return keysets_dict

async def _get_proofs_mint_unit(self, proofs: List[Proof]) -> Tuple[str, Unit]:
"""Helper function that extracts the mint URL and unit from a list of proofs. It raises an exception if the proofs are from multiple mints or units.
Args:
proofs (List[Proof]): List of proofs to extract the mint URL and unit from.
Raises:
Exception: If the proofs are from multiple mints or units.
Exception: If the proofs are from an unknown mint or keyset.
Returns:
Tuple[str, Unit]: Mint URL and `Unit` of the proofs
"""
proofs_keysets = await self._get_proofs_keysets(proofs)
mint_urls = [k.mint_url for k in proofs_keysets.values()]
if not mint_urls:
raise Exception("Proofs from unknown mint or keyset.")
if len(set(mint_urls)) != 1:
raise Exception("Proofs from multiple mints.")
mint_url = mint_urls[0]
if not mint_url:
raise Exception("No mint URL found for keyset")
proofs_units = [k.unit for k in proofs_keysets.values()]
if len(set(proofs_units)) != 1:
raise Exception("Proofs from multiple units.")
unit = proofs_units[0]
return mint_url, unit

async def serialize_proofs(
self,
proofs: List[Proof],
Expand Down Expand Up @@ -136,6 +174,8 @@ async def _make_tokenv3(
# extract all keysets IDs from proofs
keyset_ids = self._get_proofs_keyset_ids(proofs)
keysets = {k.id: k for k in self.keysets.values() if k.id in keyset_ids}
if not keysets:
raise ValueError("No keysets found for proofs")
assert (
len(set([k.unit for k in keysets.values()])) == 1
), "All keysets must have the same unit"
Expand Down Expand Up @@ -170,7 +210,10 @@ async def _make_tokenv4(

# get all keysets from proofs
keyset_ids = set(self._get_proofs_keyset_ids(proofs))
keysets = [self.keysets[i] for i in keyset_ids]
try:
keysets = [self.keysets[i] for i in keyset_ids]
except KeyError:
raise ValueError("Keysets of proofs are not loaded in wallet")
# we make sure that all proofs are from keysets of the same mint
if len(set([k.mint_url for k in keysets])) > 1:
raise ValueError("TokenV4 can only contain proofs from a single mint URL")
Expand Down

0 comments on commit 1660005

Please sign in to comment.