From bea17fbd1a2bffef68c98a3b6c57689584915a79 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 01:53:58 +0100 Subject: [PATCH 01/25] feat: early multimint support --- cashu/mint/router.py | 14 ++++- cashu/mint/startup.py | 2 +- cashu/wallet/cli.py | 98 ++++++++++++++++++++++++++------ cashu/wallet/crud.py | 4 +- cashu/wallet/wallet.py | 124 +++++++++++++++++++++++++++++++++-------- 5 files changed, 198 insertions(+), 44 deletions(-) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 09ca2e2..c7fe9f9 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -23,11 +23,23 @@ router: APIRouter = APIRouter() @router.get("/keys") async def keys() -> dict[int, str]: - """Get the public keys of the mint""" + """Get the public keys of the mint of the newest keyset""" keyset = ledger.get_keyset() return keyset +@router.get("/keys/{idBase64Urlsafe}") +async def keyset_keys(idBase64Urlsafe: str) -> dict[int, str]: + """ + Get the public keys of the mint of a specificy keyset id. + The id is encoded in base64_urlsafe and needs to be converted back to + normal base64 before it can be processed. + """ + id = idBase64Urlsafe.replace("-", "+").replace("_", "/") + keyset = ledger.get_keyset(keyset_id=id) + return keyset + + @router.get("/keysets") async def keysets() -> dict[str, list[str]]: """Get all active keysets of the mint""" diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index a006e07..279fa3b 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -16,7 +16,7 @@ ledger = Ledger( db=Database("mint", "data/mint"), seed=MINT_PRIVATE_KEY, # seed="asd", - derivation_path="0/0/0/0", + derivation_path="0/0/0/1", lightning=LNbitsWallet() if LIGHTNING else None, ) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 09ff7d9..3fdfa30 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -36,6 +36,7 @@ from cashu.wallet.crud import ( get_lightning_invoices, get_reserved_proofs, get_unused_locks, + get_keyset, ) from cashu.wallet.wallet import Wallet as Wallet @@ -64,14 +65,6 @@ class NaturalOrderGroup(click.Group): ) @click.pass_context def cli(ctx, host: str, walletname: str): - # configure logger - logger.remove() - logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO") - ctx.ensure_object(dict) - ctx.obj["HOST"] = host - ctx.obj["WALLET_NAME"] = walletname - wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, walletname)) - if TOR and not TorProxy().check_platform(): error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. Please install Tor manually and set TOR=false and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended) or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly." error_str += "\n\n" @@ -83,6 +76,13 @@ def cli(ctx, host: str, walletname: str): ) raise Exception(error_str) + # configure logger + logger.remove() + logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO") + ctx.ensure_object(dict) + ctx.obj["HOST"] = host + ctx.obj["WALLET_NAME"] = walletname + wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, walletname)) ctx.obj["WALLET"] = wallet asyncio.run(init_wallet(wallet)) pass @@ -185,13 +185,22 @@ async def invoice(ctx, amount: int, hash: str): @coro async def balance(ctx, verbose): wallet: Wallet = ctx.obj["WALLET"] - keyset_balances = wallet.balance_per_keyset() - if len(keyset_balances) > 1: - print(f"You have balances in {len(keyset_balances)} keysets:") + # keyset_balances = wallet.balance_per_keyset() + # if len(keyset_balances) > 1: + # print(f"You have balances in {len(keyset_balances)} keysets:") + # print("") + # for k, v in keyset_balances.items(): + # print( + # f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)" + # ) + # print("") + mint_balances = await wallet.balance_per_minturl() + if len(mint_balances) > 1: + print(f"You have balances in {len(mint_balances)} mints:") print("") - for k, v in keyset_balances.items(): + for k, v in mint_balances.items(): print( - f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)" + f"Mint: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)" ) print("") if verbose: @@ -221,9 +230,10 @@ async def send(ctx, amount: int, lock: str): wallet.proofs, amount, lock, set_reserved=True ) token = await wallet.serialize_proofs( - send_proofs, hide_secrets=True if lock and not p2sh else False + send_proofs, + hide_secrets=True if lock and not p2sh else False, + include_mints=True, ) - print(token) wallet.status() @@ -249,8 +259,62 @@ async def receive(ctx, token: str, lock: str): signature = p2shscripts[0].signature else: script, signature = None, None - proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] - _, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature) + + try: + # backwards compatibility < 0.7.0: tokens without mint information + proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] + _, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature) + except: + dtoken = json.loads(base64.urlsafe_b64decode(token)) + assert "tokens" in dtoken, Exception("no proofs in token") + # if there is a `mints` field in the token + # we get the mint information in the token and load the keys of each mint + # we then redeem the tokens for each keyset individually + if "mints" in dtoken: + for mint_id in dtoken.get("mints"): + for keyset in set(dtoken["mints"][mint_id]["ks"]): + mint_url = dtoken["mints"][mint_id]["url"] + # init a temporary wallet object + keyset_wallet = Wallet( + mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"]) + ) + + # first we check whether we know this mint already and ask the user + mint_keysets = await get_keyset(id=keyset, db=keyset_wallet.db) + if mint_keysets is None: + click.confirm( + f"Do you want to receive tokens from mint {mint_url}?", + abort=True, + default=True, + ) + + # make sure that this mint indeed supports this keyset + mint_keysets = await keyset_wallet._get_keysets(mint_url) + if keyset in mint_keysets["keysets"]: + # load the keys + await keyset_wallet.load_mint(keyset_id=keyset) + + # redeem proofs of this keyset + redeem_proofs = [ + Proof(**p) + for p in dtoken["tokens"] + if Proof(**p).id == keyset + ] + _, _ = await keyset_wallet.redeem( + redeem_proofs, scnd_script=script, scnd_siganture=signature + ) + keyset_wallet.db.connect() + + # reload proofs to update the main wallet's balance + await wallet.load_proofs() + + else: + # no mint information present, assume default mint + proofs = [Proof(**p) for p in dtoken["tokens"]] + _, _ = await wallet.redeem( + proofs, scnd_script=script, scnd_siganture=signature + ) + wallet.status() diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index adfe5cb..3c1c883 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -207,8 +207,8 @@ async def store_keyset( async def get_keyset( - id: str = None, - mint_url: str = None, + id: str = "", + mint_url: str = "", db: Database = None, conn: Optional[Connection] = None, ): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 5660dff..68a0d02 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -107,24 +107,22 @@ class LedgerAPI: """Returns base64 encoded random string.""" return scrts.token_urlsafe(randombits // 8) - async def _load_mint(self): + async def _load_mint(self, keyset_id: str = ""): """ - Loads the current keys and the active keyset of the map. + Loads the public keys of the mint. Either gets the keys for the specified + `keyset_id` or loads the most recent one from the mint. + Gets and the active keyset ids of the mint and stores in `self.keysets`. """ assert len( self.url ), "Ledger not initialized correctly: mint URL not specified yet. " - # get current keyset - keyset = await self._get_keys(self.url) - # get all active keysets - mint_keysets = [] - try: - keysets_resp = await self._get_keysets(self.url) - mint_keysets = keysets_resp["keysets"] - # store active keysets - except: - pass - self.keysets = mint_keysets if len(mint_keysets) else [keyset.id] + + if keyset_id: + # get requested keyset + keyset = await self._get_keyset(self.url, keyset_id) + else: + # get current keyset + keyset = await self._get_keys(self.url) # store current keyset assert len(keyset.public_keys) > 0, "did not receive keys from mint." @@ -134,6 +132,16 @@ class LedgerAPI: if keyset_local is None: await store_keyset(keyset=keyset, db=self.db) + # get all active keysets of this mint + mint_keysets = [] + try: + keysets_resp = await self._get_keysets(self.url) + mint_keysets = keysets_resp["keysets"] + # store active keysets + except: + pass + self.keysets = mint_keysets if len(mint_keysets) else [keyset.id] + logger.debug(f"Mint keysets: {self.keysets}") logger.debug(f"Current mint keyset: {keyset.id}") @@ -173,7 +181,7 @@ class LedgerAPI: ENDPOINTS """ - async def _get_keys(self, url): + async def _get_keys(self, url: str): self.s = self._set_requests() resp = self.s.get( url + "/keys", @@ -188,7 +196,26 @@ class LedgerAPI: keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url) return keyset - async def _get_keysets(self, url): + async def _get_keyset(self, url: str, keyset_id: str): + """ + keyset_id is base64, needs to be urlsafe-encoded. + """ + self.s = self._set_requests() + keyset_id_urlsafe = keyset_id.replace("-", "+").replace("_", "/") + resp = self.s.get( + url + f"/keys/{keyset_id_urlsafe}", + ) + resp.raise_for_status() + keys = resp.json() + assert len(keys), Exception("did not receive any keys") + keyset_keys = { + int(amt): PublicKey(bytes.fromhex(val), raw=True) + for amt, val in keys.items() + } + keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url) + return keyset + + async def _get_keysets(self, url: str): self.s = self._set_requests() resp = self.s.get( url + "/keysets", @@ -359,8 +386,8 @@ class Wallet(LedgerAPI): self.proofs: List[Proof] = [] self.name = name - async def load_mint(self): - await super()._load_mint() + async def load_mint(self, keyset_id: str = ""): + await super()._load_mint(keyset_id) async def load_proofs(self): self.proofs = await get_proofs(db=self.db) @@ -373,6 +400,16 @@ class Wallet(LedgerAPI): def _get_proofs_per_keyset(proofs: List[Proof]): return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)} + async def _get_proofs_per_minturl(self, proofs: List[Proof]): + ret = {} + for id in set([p.id for p in proofs]): + keyset: WalletKeyset = await get_keyset(id=id, db=self.db) + if keyset.mint_url not in ret: + ret[keyset.mint_url] = [p for p in proofs if p.id == id] + else: + ret[keyset.mint_url].extend([p for p in proofs if p.id == id]) + return ret + async def request_mint(self, amount): invoice = super().request_mint(amount) invoice.time_created = int(time.time()) @@ -442,16 +479,47 @@ class Wallet(LedgerAPI): raise Exception("could not pay invoice.") return status["paid"] - @staticmethod - async def serialize_proofs(proofs: List[Proof], hide_secrets=False): + async def serialize_proofs( + self, proofs: List[Proof], hide_secrets=False, include_mints=False + ): + """ + Produces sharable token with proofs and mint information. + """ + # serialize the list of proofs, either with secrets included or without secrets if hide_secrets: proofs_serialized = [p.to_dict_no_secret() for p in proofs] else: proofs_serialized = [p.to_dict() for p in proofs] - token = base64.urlsafe_b64encode( - json.dumps(proofs_serialized).encode() - ).decode() - return token + + token = dict(tokens=proofs_serialized) + + # add mint information to the token, if requested + if include_mints: + + mints = dict() + + # iterate through all proofs and add their keyset to `mints` + for proof in proofs: + if proof.id: + keyset = await get_keyset(id=proof.id, db=self.db) + if keyset: + # TODO: replace this with a mint pubkey + placeholder_mint_id = keyset.mint_url + if placeholder_mint_id not in mints: + id = dict( + url=keyset.mint_url, + ks=[keyset.id], + ) + mints[placeholder_mint_id] = id + else: + if keyset.id not in mints[placeholder_mint_id]["ks"]: + mints[placeholder_mint_id]["ks"].append(keyset.id) + if mints: + token["mints"] = mints + + # encode the token as a base64 string + token_base64 = base64.urlsafe_b64encode(json.dumps(token).encode()).decode() + return token_base64 async def _get_spendable_proofs(self, proofs: List[Proof]): """ @@ -568,5 +636,15 @@ class Wallet(LedgerAPI): for key, proofs in self._get_proofs_per_keyset(self.proofs).items() } + async def balance_per_minturl(self): + balances = await self._get_proofs_per_minturl(self.proofs) + return { + key: { + "balance": sum_proofs(proofs), + "available": sum_proofs([p for p in proofs if not p.reserved]), + } + for key, proofs in balances.items() + } + def proof_amounts(self): return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])] From aa7b0412d67f8f52b6523bc24a13d75e085e6c6b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 03:51:42 +0100 Subject: [PATCH 02/25] multimint works --- cashu/wallet/cli.py | 12 +++++++++++- cashu/wallet/wallet.py | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 3fdfa30..11b5467 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -234,6 +234,16 @@ async def send(ctx, amount: int, lock: str): hide_secrets=True if lock and not p2sh else False, include_mints=True, ) + print(token) + + # print("") + # print("Legacy:") + # token = await wallet.serialize_proofs( + # send_proofs, + # hide_secrets=True if lock and not p2sh else False, + # include_mints=False, + # ) + # print(token) wallet.status() @@ -495,5 +505,5 @@ async def info(ctx): print(f"Tor enabled: {TOR}") if SOCKS_HOST: print(f"Socks proxy: {SOCKS_HOST}:{SOCKS_PORT}") - print(f"Mint URL: {MINT_URL}") + print(f"Mint URL: {ctx.obj['HOST']}") return diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 68a0d02..60f0806 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -63,6 +63,8 @@ class LedgerAPI: def _set_requests(self): s = requests.Session() s.headers.update({"Client-version": VERSION}) + if DEBUG: + s.verify = False socks_host, socks_port = None, None if TOR and TorProxy().check_platform(): self.tor = TorProxy(timeout=True) From d9bca8e6e291993cdf66c9413ac98324962cf7c0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 11:32:14 +0100 Subject: [PATCH 03/25] keyset logging as trace level --- cashu/mint/ledger.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index afa1f99..af4129b 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -57,12 +57,12 @@ class Ledger: seed=self.master_key, derivation_path=derivation_path, version=VERSION ) # check if current keyset is stored in db and store if not - logger.debug(f"Loading keyset {keyset.id} from db.") + logger.trace(f"Loading keyset {keyset.id} from db.") tmp_keyset_local: List[MintKeyset] = await self.crud.get_keyset( id=keyset.id, db=self.db ) if not len(tmp_keyset_local) and autosave: - logger.debug(f"Storing keyset {keyset.id}.") + logger.trace(f"Storing keyset {keyset.id}.") await self.crud.store_keyset(keyset=keyset, db=self.db) # store the new keyset in the current keysets @@ -74,10 +74,10 @@ class Ledger: # load all past keysets from db tmp_keysets: List[MintKeyset] = await self.crud.get_keyset(db=self.db) self.keysets = MintKeysets(tmp_keysets) - logger.debug(f"Loading {len(self.keysets.keysets)} keysets form db.") + logger.trace(f"Loading {len(self.keysets.keysets)} keysets form db.") # generate all derived keys from stored derivation paths of past keysets for _, v in self.keysets.keysets.items(): - logger.debug(f"Generating keys for keyset {v.id}") + logger.trace(f"Generating keys for keyset {v.id}") v.generate_keys(self.master_key) # load the current keyset self.keyset = await self.load_keyset(self.derivation_path, autosave) From 398e98d84711f1ca1c305a7a98f61cb43af88b82 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 12:44:47 +0100 Subject: [PATCH 04/25] feat: add legacy token serialization for backwards compatibility with old clients --- cashu/wallet/cli.py | 53 ++++++++++++++++++++++++++++-------------- cashu/wallet/wallet.py | 7 +++++- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 11b5467..c8beccc 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -185,17 +185,21 @@ async def invoice(ctx, amount: int, hash: str): @coro async def balance(ctx, verbose): wallet: Wallet = ctx.obj["WALLET"] - # keyset_balances = wallet.balance_per_keyset() - # if len(keyset_balances) > 1: - # print(f"You have balances in {len(keyset_balances)} keysets:") - # print("") - # for k, v in keyset_balances.items(): - # print( - # f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)" - # ) - # print("") + if verbose: + # show balances per keyset + keyset_balances = wallet.balance_per_keyset() + if len(keyset_balances) > 1: + print(f"You have balances in {len(keyset_balances)} keysets:") + print("") + for k, v in keyset_balances.items(): + print( + f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)" + ) + print("") + mint_balances = await wallet.balance_per_minturl() if len(mint_balances) > 1: + # show balances per mint print(f"You have balances in {len(mint_balances)} mints:") print("") for k, v in mint_balances.items(): @@ -203,6 +207,7 @@ async def balance(ctx, verbose): f"Mint: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)" ) print("") + if verbose: print( f"Balance: {wallet.balance} sat (available: {wallet.available_balance} sat in {len([p for p in wallet.proofs if not p.reserved])} tokens)" @@ -214,9 +219,16 @@ async def balance(ctx, verbose): @cli.command("send", help="Send tokens.") @click.argument("amount", type=int) @click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", type=str) +@click.option( + "--legacy", + default=False, + is_flag=True, + help="Print legacy token without mint information.", + type=bool, +) @click.pass_context @coro -async def send(ctx, amount: int, lock: str): +async def send(ctx, amount: int, lock: str, legacy: bool): if lock and len(lock) < 22: print("Error: lock has to be at least 22 characters long.") return @@ -236,14 +248,19 @@ async def send(ctx, amount: int, lock: str): ) print(token) - # print("") - # print("Legacy:") - # token = await wallet.serialize_proofs( - # send_proofs, - # hide_secrets=True if lock and not p2sh else False, - # include_mints=False, - # ) - # print(token) + if legacy: + print("") + print( + "Legacy token without mint information for older clients. This token can only be be received by wallets who use the mint the token is issued from:" + ) + print("") + token = await wallet.serialize_proofs( + send_proofs, + hide_secrets=True if lock and not p2sh else False, + legacy=True, + ) + print(token) + wallet.status() diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 60f0806..205d689 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -482,7 +482,7 @@ class Wallet(LedgerAPI): return status["paid"] async def serialize_proofs( - self, proofs: List[Proof], hide_secrets=False, include_mints=False + self, proofs: List[Proof], hide_secrets=False, include_mints=False, legacy=False ): """ Produces sharable token with proofs and mint information. @@ -493,6 +493,11 @@ class Wallet(LedgerAPI): else: proofs_serialized = [p.to_dict() for p in proofs] + if legacy: + return base64.urlsafe_b64encode( + json.dumps(proofs_serialized).encode() + ).decode() + token = dict(tokens=proofs_serialized) # add mint information to the token, if requested From 25163f38a71ffea4e56056c52b0e667a23cace39 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 14:44:47 +0100 Subject: [PATCH 05/25] reset derivation path --- cashu/mint/startup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index 279fa3b..a006e07 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -16,7 +16,7 @@ ledger = Ledger( db=Database("mint", "data/mint"), seed=MINT_PRIVATE_KEY, # seed="asd", - derivation_path="0/0/0/1", + derivation_path="0/0/0/0", lightning=LNbitsWallet() if LIGHTNING else None, ) From 1181b64615bb945a036fd0ac7497ac6b81c50017 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 15:15:40 +0100 Subject: [PATCH 06/25] introduce basemodel for token --- cashu/core/base.py | 12 +++++++++++- cashu/wallet/cli.py | 10 +++------- cashu/wallet/wallet.py | 33 ++++++++++++++++++--------------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d39db5e..7c90330 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Union, TypedDict, Optional from pydantic import BaseModel @@ -212,3 +212,13 @@ class MintKeysets: def get_ids(self): return [k for k, _ in self.keysets.items()] + + +class TokenMintJson(BaseModel): + url: str + ks: List[str] + + +class TokenJson(BaseModel): + tokens: List[Proof] + mints: Optional[Dict[str, TokenMintJson]] = None diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index c8beccc..0759c26 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -243,7 +243,6 @@ async def send(ctx, amount: int, lock: str, legacy: bool): ) token = await wallet.serialize_proofs( send_proofs, - hide_secrets=True if lock and not p2sh else False, include_mints=True, ) print(token) @@ -256,7 +255,6 @@ async def send(ctx, amount: int, lock: str, legacy: bool): print("") token = await wallet.serialize_proofs( send_proofs, - hide_secrets=True if lock and not p2sh else False, legacy=True, ) print(token) @@ -297,7 +295,7 @@ async def receive(ctx, token: str, lock: str): # if there is a `mints` field in the token # we get the mint information in the token and load the keys of each mint # we then redeem the tokens for each keyset individually - if "mints" in dtoken: + if "mints" in dtoken and dtoken.get("mints") is not None: for mint_id in dtoken.get("mints"): for keyset in set(dtoken["mints"][mint_id]["ks"]): mint_url = dtoken["mints"][mint_id]["url"] @@ -390,16 +388,14 @@ async def pending(ctx): ): grouped_proofs = list(value) token = await wallet.serialize_proofs(grouped_proofs) - token_hidden_secret = await wallet.serialize_proofs( - grouped_proofs, hide_secrets=True - ) + token_hidden_secret = await wallet.serialize_proofs(grouped_proofs) reserved_date = datetime.utcfromtimestamp( int(grouped_proofs[0].time_reserved) ).strftime("%Y-%m-%d %H:%M:%S") print( f"#{i} Amount: {sum_proofs(grouped_proofs)} sat Time: {reserved_date} ID: {key}\n" ) - print(f"With secret: {token}\n\nSecretless: {token_hidden_secret}\n") + print(f"{token}\n") print(f"--------------------------\n") wallet.status() diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 205d689..e81ec84 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -24,6 +24,8 @@ from cashu.core.base import ( Proof, SplitRequest, WalletKeyset, + TokenJson, + TokenMintJson, ) from cashu.core.bolt11 import Invoice as InvoiceBolt11 from cashu.core.db import Database @@ -405,6 +407,8 @@ class Wallet(LedgerAPI): async def _get_proofs_per_minturl(self, proofs: List[Proof]): ret = {} for id in set([p.id for p in proofs]): + if id is None: + continue keyset: WalletKeyset = await get_keyset(id=id, db=self.db) if keyset.mint_url not in ret: ret[keyset.mint_url] = [p for p in proofs if p.id == id] @@ -482,50 +486,49 @@ class Wallet(LedgerAPI): return status["paid"] async def serialize_proofs( - self, proofs: List[Proof], hide_secrets=False, include_mints=False, legacy=False + self, proofs: List[Proof], include_mints=False, legacy=False ): """ Produces sharable token with proofs and mint information. """ - # serialize the list of proofs, either with secrets included or without secrets - if hide_secrets: - proofs_serialized = [p.to_dict_no_secret() for p in proofs] - else: - proofs_serialized = [p.to_dict() for p in proofs] if legacy: + proofs_serialized = [p.to_dict() for p in proofs] return base64.urlsafe_b64encode( json.dumps(proofs_serialized).encode() ).decode() - token = dict(tokens=proofs_serialized) + token = TokenJson(tokens=proofs) + # token = dict(tokens=proofs_serialized) # add mint information to the token, if requested if include_mints: - mints = dict() + mints: Dict[str, TokenMintJson] = dict() # iterate through all proofs and add their keyset to `mints` for proof in proofs: if proof.id: keyset = await get_keyset(id=proof.id, db=self.db) - if keyset: + if keyset and keyset.mint_url: # TODO: replace this with a mint pubkey placeholder_mint_id = keyset.mint_url if placeholder_mint_id not in mints: - id = dict( + id = TokenMintJson( url=keyset.mint_url, ks=[keyset.id], ) mints[placeholder_mint_id] = id else: - if keyset.id not in mints[placeholder_mint_id]["ks"]: - mints[placeholder_mint_id]["ks"].append(keyset.id) - if mints: - token["mints"] = mints + if keyset.id not in mints[placeholder_mint_id].ks: + mints[placeholder_mint_id].ks.append(keyset.id) + if len(mints) > 0: + token.mints = mints # encode the token as a base64 string - token_base64 = base64.urlsafe_b64encode(json.dumps(token).encode()).decode() + token_base64 = base64.urlsafe_b64encode( + json.dumps(token.dict()).encode() + ).decode() return token_base64 async def _get_spendable_proofs(self, proofs: List[Proof]): From 86e8635a16ef198f31a30c632d6f1edb47a9dcdd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 15:17:09 +0100 Subject: [PATCH 07/25] make format --- cashu/core/base.py | 2 +- cashu/wallet/cli.py | 2 +- cashu/wallet/wallet.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 7c90330..2f2e3dd 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import Any, Dict, List, Union, TypedDict, Optional +from typing import Any, Dict, List, Optional, TypedDict, Union from pydantic import BaseModel diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 0759c26..ae691d4 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -33,10 +33,10 @@ from cashu.core.settings import ( from cashu.tor.tor import TorProxy from cashu.wallet import migrations from cashu.wallet.crud import ( + get_keyset, get_lightning_invoices, get_reserved_proofs, get_unused_locks, - get_keyset, ) from cashu.wallet.wallet import Wallet as Wallet diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index e81ec84..c9b4f2c 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -23,9 +23,9 @@ from cashu.core.base import ( P2SHScript, Proof, SplitRequest, - WalletKeyset, TokenJson, TokenMintJson, + WalletKeyset, ) from cashu.core.bolt11 import Invoice as InvoiceBolt11 from cashu.core.db import Database @@ -58,6 +58,7 @@ class LedgerAPI: keys: Dict[int, str] keyset: str tor: TorProxy + db: Database def __init__(self, url): self.url = url From 83997d5b608313d311aa30565a704940532f534b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 15:37:59 +0100 Subject: [PATCH 08/25] comment --- cashu/wallet/wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index c9b4f2c..5d4e31a 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -291,6 +291,7 @@ class LedgerAPI: payloads, rs = self._construct_outputs(amounts, secrets) split_payload = SplitRequest(proofs=proofs, amount=amount, outputs=payloads) + # construct payload def _splitrequest_include_fields(proofs): """strips away fields from the model that aren't necessary for the /split""" proofs_include = {"id", "amount", "secret", "C", "script"} From 0840ee184cd78ddd21263e48477e0d1d352b4b28 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 15:43:48 +0100 Subject: [PATCH 09/25] fix typing --- cashu/wallet/wallet.py | 18 +++++++++--------- tests/test_wallet.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 5d4e31a..1319448 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -5,7 +5,7 @@ import secrets as scrts import time import uuid from itertools import groupby -from typing import Dict, List +from typing import Dict, List, Optional import requests from loguru import logger @@ -55,7 +55,7 @@ from cashu.wallet.crud import ( class LedgerAPI: - keys: Dict[int, str] + keys: Dict[int, PublicKey] keyset: str tor: TorProxy db: Database @@ -133,7 +133,7 @@ class LedgerAPI: assert len(keyset.public_keys) > 0, "did not receive keys from mint." # check if current keyset is in db - keyset_local: WalletKeyset = await get_keyset(keyset.id, db=self.db) + keyset_local: Optional[WalletKeyset] = await get_keyset(keyset.id, db=self.db) if keyset_local is None: await store_keyset(keyset=keyset, db=self.db) @@ -257,7 +257,7 @@ class LedgerAPI: promises = [BlindedSignature(**p) for p in promises_list] return self._construct_proofs(promises, secrets, rs) - async def split(self, proofs, amount, scnd_secret: str = None): + async def split(self, proofs, amount, scnd_secret: Optional[str] = None): """Consume proofs and create new promises based on amount split. If scnd_secret is None, random secrets will be generated for the tokens to keep (frst_outputs) and the promises to send (scnd_outputs). @@ -424,7 +424,7 @@ class Wallet(LedgerAPI): await store_lightning_invoice(db=self.db, invoice=invoice) return invoice - async def mint(self, amount: int, payment_hash: str = None): + async def mint(self, amount: int, payment_hash: Optional[str] = None): split = amount_split(amount) proofs = await super().mint(split, payment_hash) if proofs == []: @@ -440,8 +440,8 @@ class Wallet(LedgerAPI): async def redeem( self, proofs: List[Proof], - scnd_script: str = None, - scnd_siganture: str = None, + scnd_script: Optional[str] = None, + scnd_siganture: Optional[str] = None, ): if scnd_script and scnd_siganture: logger.debug(f"Unlock script: {scnd_script}") @@ -454,7 +454,7 @@ class Wallet(LedgerAPI): self, proofs: List[Proof], amount: int, - scnd_secret: str = None, + scnd_secret: Optional[str] = None, ): assert len(proofs) > 0, ValueError("no proofs provided.") frst_proofs, scnd_proofs = await super().split(proofs, amount, scnd_secret) @@ -569,7 +569,7 @@ class Wallet(LedgerAPI): self, proofs: List[Proof], amount, - scnd_secret: str = None, + scnd_secret: Optional[str] = None, set_reserved: bool = False, ): """Like self.split but only considers non-reserved tokens.""" diff --git a/tests/test_wallet.py b/tests/test_wallet.py index f06e530..ab9330d 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -63,7 +63,7 @@ async def test_get_keysets(wallet1: Wallet): assert type(keyset) == dict assert type(keyset["keysets"]) == list assert len(keyset["keysets"]) > 0 - assert keyset["keysets"][0] == wallet1.keyset_id + assert keyset["keysets"][-1] == wallet1.keyset_id @pytest.mark.asyncio From cc06a33d98334793ddf1dc661cf9f9f9ca007f82 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 15:47:32 +0100 Subject: [PATCH 10/25] comments --- cashu/wallet/wallet.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 1319448..0c939e7 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -488,7 +488,7 @@ class Wallet(LedgerAPI): return status["paid"] async def serialize_proofs( - self, proofs: List[Proof], include_mints=False, legacy=False + self, proofs: List[Proof], include_mints=True, legacy=False ): """ Produces sharable token with proofs and mint information. @@ -500,28 +500,30 @@ class Wallet(LedgerAPI): json.dumps(proofs_serialized).encode() ).decode() + # build token token = TokenJson(tokens=proofs) - # token = dict(tokens=proofs_serialized) # add mint information to the token, if requested if include_mints: - + # hold information about the mint mints: Dict[str, TokenMintJson] = dict() - # iterate through all proofs and add their keyset to `mints` for proof in proofs: if proof.id: + # load the keyset from the db keyset = await get_keyset(id=proof.id, db=self.db) if keyset and keyset.mint_url: # TODO: replace this with a mint pubkey placeholder_mint_id = keyset.mint_url if placeholder_mint_id not in mints: + # mint information id = TokenMintJson( url=keyset.mint_url, ks=[keyset.id], ) mints[placeholder_mint_id] = id else: + # if a mint has multiple keysets, append to the existing list if keyset.id not in mints[placeholder_mint_id].ks: mints[placeholder_mint_id].ks.append(keyset.id) if len(mints) > 0: From d198a7dc503b7c480a6c0a6cda7af4d63763fec0 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sat, 24 Dec 2022 18:55:30 +0100 Subject: [PATCH 11/25] fix b64 urlsave keyset_id --- cashu/wallet/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 0c939e7..a207f9e 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -206,7 +206,7 @@ class LedgerAPI: keyset_id is base64, needs to be urlsafe-encoded. """ self.s = self._set_requests() - keyset_id_urlsafe = keyset_id.replace("-", "+").replace("_", "/") + keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_") resp = self.s.get( url + f"/keys/{keyset_id_urlsafe}", ) From 5fce0cf6e8216311d475ecdec68b3dfa24710931 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 25 Dec 2022 11:31:46 +0100 Subject: [PATCH 12/25] test: GET /keys/{id} --- tests/test_wallet.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index ab9330d..493503f 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -57,6 +57,16 @@ async def test_get_keys(wallet1: Wallet): assert len(keyset.id) > 0 +@pytest.mark.asyncio +async def test_get_keyset(wallet1: Wallet): + assert len(wallet1.keys) == MAX_ORDER + # ket's get the keys first so we can get a keyset ID that we use later + keys1 = await wallet1._get_keys(wallet1.url) + # gets the keys of a specific keyset + keys2 = await wallet1._get_keyset(wallet1.url, keys1.id) + assert len(keys1.public_keys) == len(keys2.public_keys) + + @pytest.mark.asyncio async def test_get_keysets(wallet1: Wallet): keyset = await wallet1._get_keysets(wallet1.url) From 0e3d5c4fd3a9c84abe290d2d7a3cdec25d5fa79f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 25 Dec 2022 13:22:53 +0100 Subject: [PATCH 13/25] refactor --- cashu/wallet/cli.py | 79 ++++++++++++++++--------------------- cashu/wallet/cli_helpers.py | 67 +++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 45 deletions(-) create mode 100644 cashu/wallet/cli_helpers.py diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index ae691d4..f77871e 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -40,6 +40,8 @@ from cashu.wallet.crud import ( ) from cashu.wallet.wallet import Wallet as Wallet +from .cli_helpers import verify_mints, redeem_multimint + async def init_wallet(wallet: Wallet): """Performs migrations and loads proofs from db.""" @@ -193,18 +195,28 @@ async def balance(ctx, verbose): print("") for k, v in keyset_balances.items(): print( - f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)" + f"Keyset: {k or 'undefined'} - Balance: {v['available']} sat (with pending: {v['balance']} sat)" ) print("") mint_balances = await wallet.balance_per_minturl() - if len(mint_balances) > 1: - # show balances per mint + + # if we have a balance on a non-default mint + show_mints = False + keysets = [k for k, v in wallet.balance_per_keyset().items()] + for k in keysets: + ks = await get_keyset(id=str(k), db=wallet.db) + if ks and ks.mint_url != ctx.obj["HOST"]: + show_mints = True + + # or we have a balance on more than one mint + # show balances per mint + if len(mint_balances) > 1 or show_mints: print(f"You have balances in {len(mint_balances)} mints:") print("") for k, v in mint_balances.items(): print( - f"Mint: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)" + f"Mint: {k or 'undefined'} - Balance: {v['available']} sat (with pending: {v['balance']} sat)" ) print("") @@ -271,6 +283,8 @@ async def receive(ctx, token: str, lock: str): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() wallet.status() + + # extract script and signature from P2SH lock if lock: # load the script and signature of this address from the database assert len(lock.split("P2SH:")) == 2, Exception( @@ -285,56 +299,31 @@ async def receive(ctx, token: str, lock: str): else: script, signature = None, None + # we support old tokens (< 0.7) without mint information and (W3siaWQ...) + # new tokens (>= 0.7) with multiple mint support (eyJ0b2...) try: - # backwards compatibility < 0.7.0: tokens without mint information + # backwards compatibility: tokens without mint information proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] _, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature) except: + # assume token with mint information dtoken = json.loads(base64.urlsafe_b64decode(token)) assert "tokens" in dtoken, Exception("no proofs in token") + includes_mint_info: bool = "mints" in dtoken and dtoken.get("mints") is not None + # if there is a `mints` field in the token - # we get the mint information in the token and load the keys of each mint - # we then redeem the tokens for each keyset individually - if "mints" in dtoken and dtoken.get("mints") is not None: - for mint_id in dtoken.get("mints"): - for keyset in set(dtoken["mints"][mint_id]["ks"]): - mint_url = dtoken["mints"][mint_id]["url"] - # init a temporary wallet object - keyset_wallet = Wallet( - mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"]) - ) - - # first we check whether we know this mint already and ask the user - mint_keysets = await get_keyset(id=keyset, db=keyset_wallet.db) - if mint_keysets is None: - click.confirm( - f"Do you want to receive tokens from mint {mint_url}?", - abort=True, - default=True, - ) - - # make sure that this mint indeed supports this keyset - mint_keysets = await keyset_wallet._get_keysets(mint_url) - if keyset in mint_keysets["keysets"]: - # load the keys - await keyset_wallet.load_mint(keyset_id=keyset) - - # redeem proofs of this keyset - redeem_proofs = [ - Proof(**p) - for p in dtoken["tokens"] - if Proof(**p).id == keyset - ] - _, _ = await keyset_wallet.redeem( - redeem_proofs, scnd_script=script, scnd_siganture=signature - ) - keyset_wallet.db.connect() - - # reload proofs to update the main wallet's balance - await wallet.load_proofs() + # we check whether the token has mints that we don't know yet + # and ask the user if they want to trust the new mitns + if includes_mint_info: + # we ask the user to confirm any new mints the tokens may include + await verify_mints(ctx, dtoken) + # proceed with redemption + await redeem_multimint(ctx, dtoken, script, signature) + # reload main wallet so the balance updates + await wallet.load_proofs() else: - # no mint information present, assume default mint + # no mint information present, use wallet's default mint proofs = [Proof(**p) for p in dtoken["tokens"]] _, _ = await wallet.redeem( proofs, scnd_script=script, scnd_siganture=signature diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py new file mode 100644 index 0000000..37dec55 --- /dev/null +++ b/cashu/wallet/cli_helpers.py @@ -0,0 +1,67 @@ +import click +import os +from cashu.core.settings import CASHU_DIR +from cashu.wallet.crud import get_keyset +from cashu.wallet.wallet import Wallet as Wallet +from cashu.core.base import Proof + + +async def verify_mints(ctx, dtoken): + trust_token_mints = True + for mint_id in dtoken.get("mints"): + for keyset in set(dtoken["mints"][mint_id]["ks"]): + mint_url = dtoken["mints"][mint_id]["url"] + # init a temporary wallet object + keyset_wallet = Wallet( + mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"]) + ) + # make sure that this mint indeed supports this keyset + mint_keysets = await keyset_wallet._get_keysets(mint_url) + assert keyset in mint_keysets["keysets"], "mint does not have this keyset." + + # we validate the keyset id by fetching the keys from the mint + mint_keyset = await keyset_wallet._get_keyset(mint_url, keyset) + assert keyset == mint_keyset.id, Exception("keyset not valid.") + + # we check the db whether we know this keyset already and ask the user + mint_keysets = await get_keyset(id=keyset, db=keyset_wallet.db) + if mint_keysets is None: + # we encountered a new mint and ask for a user confirmation + trust_token_mints = False + print("") + print("Warning: Tokens are from a mint or keyset you don't know yet.") + print("\n") + print(f"Mint URL: {mint_url}") + print(f"Mint keyset: {keyset}") + print("\n") + click.confirm( + f"Do you want to trust this mint and receive the tokens?", + abort=True, + default=True, + ) + trust_token_mints = True + + assert trust_token_mints, Exception("Aborted!") + + +async def redeem_multimint(ctx, dtoken, script, signature): + # we get the mint information in the token and load the keys of each mint + # we then redeem the tokens for each keyset individually + for mint_id in dtoken.get("mints"): + for keyset in set(dtoken["mints"][mint_id]["ks"]): + mint_url = dtoken["mints"][mint_id]["url"] + # init a temporary wallet object + keyset_wallet = Wallet( + mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"]) + ) + + # load the keys + await keyset_wallet.load_mint(keyset_id=keyset) + + # redeem proofs of this keyset + redeem_proofs = [ + Proof(**p) for p in dtoken["tokens"] if Proof(**p).id == keyset + ] + _, _ = await keyset_wallet.redeem( + redeem_proofs, scnd_script=script, scnd_siganture=signature + ) From 829dd908d83670e86bf329e08cefa5a503b2ec59 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 19:44:39 +0100 Subject: [PATCH 14/25] fix --- cashu/wallet/cli.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 043f03a..29aa9e5 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -285,12 +285,6 @@ async def send(ctx, amount: int, lock: str, legacy: bool): async def receive(ctx, token: str, lock: str): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() -<<<<<<< HEAD - wallet.status() - - # extract script and signature from P2SH lock -======= ->>>>>>> main if lock: # load the script and signature of this address from the database assert len(lock.split("P2SH:")) == 2, Exception( From eabe19e6759573a4f5db978ff1bbfa00ba8a82b7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 19:45:20 +0100 Subject: [PATCH 15/25] bump to 0.7.1 --- README.md | 2 +- cashu/core/settings.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aad816c..9963206 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ cashu info Returns: ```bash -Version: 0.7.0 +Version: 0.7.1 Debug: False Cashu dir: /home/user/.cashu Wallet: wallet diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 5865f51..31c8b84 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -55,4 +55,4 @@ LNBITS_KEY = env.str("LNBITS_KEY", default=None) NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None) MAX_ORDER = 64 -VERSION = "0.7.0" +VERSION = "0.7.1" diff --git a/pyproject.toml b/pyproject.toml index 8745a1b..861f4a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.7.0" +version = "0.7.1" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" diff --git a/setup.py b/setup.py index 20093d0..7ab643e 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]} setuptools.setup( name="cashu", - version="0.7.0", + version="0.7.1", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown", From 9d5dbc97d51fadddf8f823744a091e4be76b3bb3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 21:00:30 +0100 Subject: [PATCH 16/25] fix: use mint url for check --- cashu/wallet/cli.py | 13 +++++++------ cashu/wallet/cli_helpers.py | 12 +++++++----- cashu/wallet/wallet.py | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 29aa9e5..9f586a6 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -73,7 +73,7 @@ class NaturalOrderGroup(click.Group): @click.pass_context def cli(ctx, host: str, walletname: str): if TOR and not TorProxy().check_platform(): - error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. Please install Tor manually and set TOR=false and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended) or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly." + error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. You have two options: Either install Tor manually and set TOR=FALSE and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended). Or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly." error_str += "\n\n" if ENV_FILE: error_str += f"Edit your Cashu config file here: {ENV_FILE}" @@ -83,7 +83,7 @@ def cli(ctx, host: str, walletname: str): f"Ceate a new Cashu config file here: {os.path.join(CASHU_DIR, '.env')}" ) env_path = os.path.join(CASHU_DIR, ".env") - error_str += f'\n\nYou can turn off Tor with this command: echo "TOR=false" >> {env_path}' + error_str += f'\n\nYou can turn off Tor with this command: echo "TOR=FALSE" >> {env_path}' raise Exception(error_str) # configure logger @@ -203,13 +203,14 @@ async def balance(ctx, verbose): print("") for k, v in keyset_balances.items(): print( - f"Keyset: {k or 'undefined'} - Balance: {v['available']} sat (with pending: {v['balance']} sat)" + f"Keyset: {k} - Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat)" ) print("") + # get balances per mint mint_balances = await wallet.balance_per_minturl() - # if we have a balance on a non-default mint + # if we have a balance on a non-default mint, we show its URL show_mints = False keysets = [k for k, v in wallet.balance_per_keyset().items()] for k in keysets: @@ -224,13 +225,13 @@ async def balance(ctx, verbose): print("") for k, v in mint_balances.items(): print( - f"Mint: {k or 'undefined'} - Balance: {v['available']} sat (with pending: {v['balance']} sat)" + f"Mint: {k} - Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat)" ) print("") if verbose: print( - f"Balance: {wallet.balance} sat (available: {wallet.available_balance} sat in {len([p for p in wallet.proofs if not p.reserved])} tokens)" + f"Balance: {wallet.available_balance} sat (pending: {wallet.balance-wallet.available_balance} sat) in {len([p for p in wallet.proofs if not p.reserved])} tokens" ) else: print(f"Balance: {wallet.available_balance} sat") diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index 37dec55..0fe3a8a 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -15,7 +15,7 @@ async def verify_mints(ctx, dtoken): keyset_wallet = Wallet( mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"]) ) - # make sure that this mint indeed supports this keyset + # make sure that this mint supports this keyset mint_keysets = await keyset_wallet._get_keysets(mint_url) assert keyset in mint_keysets["keysets"], "mint does not have this keyset." @@ -23,19 +23,21 @@ async def verify_mints(ctx, dtoken): mint_keyset = await keyset_wallet._get_keyset(mint_url, keyset) assert keyset == mint_keyset.id, Exception("keyset not valid.") - # we check the db whether we know this keyset already and ask the user - mint_keysets = await get_keyset(id=keyset, db=keyset_wallet.db) + # we check the db whether we know this mint already and ask the user if not + mint_keysets = await get_keyset(mint_url=mint_url, db=keyset_wallet.db) if mint_keysets is None: # we encountered a new mint and ask for a user confirmation trust_token_mints = False print("") - print("Warning: Tokens are from a mint or keyset you don't know yet.") + print( + "Warning: Tokens are from a mint you don't know yet. Make sure that you know this mint." + ) print("\n") print(f"Mint URL: {mint_url}") print(f"Mint keyset: {keyset}") print("\n") click.confirm( - f"Do you want to trust this mint and receive the tokens?", + f"Do you trust this mint and want to receive the tokens?", abort=True, default=True, ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index a207f9e..055cbd8 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -66,8 +66,8 @@ class LedgerAPI: def _set_requests(self): s = requests.Session() s.headers.update({"Client-version": VERSION}) - if DEBUG: - s.verify = False + # if DEBUG: + # s.verify = False socks_host, socks_port = None, None if TOR and TorProxy().check_platform(): self.tor = TorProxy(timeout=True) From 3d18e53cb73511adb6a48b115ac232f6a10163a1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 21:14:00 +0100 Subject: [PATCH 17/25] test --- cashu/wallet/cli.py | 61 ++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 9f586a6..3e681d1 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -286,49 +286,58 @@ async def send(ctx, amount: int, lock: str, legacy: bool): async def receive(ctx, token: str, lock: str): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() + + # check for P2SH locks if lock: # load the script and signature of this address from the database assert len(lock.split("P2SH:")) == 2, Exception( "lock has wrong format. Expected P2SH:
." ) address_split = lock.split("P2SH:")[1] - p2shscripts = await get_unused_locks(address_split, db=wallet.db) assert len(p2shscripts) == 1, Exception("lock not found.") - script = p2shscripts[0].script - signature = p2shscripts[0].signature + script, signature = p2shscripts[0].script, p2shscripts[0].signature else: script, signature = None, None + # deserialize token + # we support old tokens (< 0.7) without mint information and (W3siaWQ...) # new tokens (>= 0.7) with multiple mint support (eyJ0b2...) try: # backwards compatibility: tokens without mint information + # trows an error if the desirialization with the old format doesn't work proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] + token = await wallet.serialize_proofs( + proofs, + include_mints=False, + ) + # _, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature) + except Exception as e: + print(e) + print(token) + + # deserialize token + dtoken = json.loads(base64.urlsafe_b64decode(token)) + + assert "tokens" in dtoken, Exception("no proofs in token") + includes_mint_info: bool = "mints" in dtoken and dtoken.get("mints") is not None + + # if there is a `mints` field in the token + # we check whether the token has mints that we don't know yet + # and ask the user if they want to trust the new mitns + if includes_mint_info: + # we ask the user to confirm any new mints the tokens may include + await verify_mints(ctx, dtoken) + # redeem tokens with new wallet instances + await redeem_multimint(ctx, dtoken, script, signature) + # reload main wallet so the balance updates + await wallet.load_proofs() + + else: + # no mint information present, we extract the proofs and use wallet's default mint + proofs = [Proof(**p) for p in dtoken["tokens"]] _, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature) - except: - # assume token with mint information - dtoken = json.loads(base64.urlsafe_b64decode(token)) - assert "tokens" in dtoken, Exception("no proofs in token") - includes_mint_info: bool = "mints" in dtoken and dtoken.get("mints") is not None - - # if there is a `mints` field in the token - # we check whether the token has mints that we don't know yet - # and ask the user if they want to trust the new mitns - if includes_mint_info: - # we ask the user to confirm any new mints the tokens may include - await verify_mints(ctx, dtoken) - # proceed with redemption - await redeem_multimint(ctx, dtoken, script, signature) - # reload main wallet so the balance updates - await wallet.load_proofs() - - else: - # no mint information present, use wallet's default mint - proofs = [Proof(**p) for p in dtoken["tokens"]] - _, _ = await wallet.redeem( - proofs, scnd_script=script, scnd_siganture=signature - ) wallet.status() From ac2f546274cdf464636d4548df1196478fcae089 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 22:00:30 +0100 Subject: [PATCH 18/25] can receive lnbits link tokens --- cashu/wallet/cli.py | 51 ++++++++++++++++++++++++++++++++----- cashu/wallet/cli_helpers.py | 6 +++-- cashu/wallet/wallet.py | 39 ++++++++++++++++++---------- 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 3e681d1..020972b 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -7,17 +7,19 @@ import os import sys import threading import time +import urllib.parse from datetime import datetime from functools import wraps from itertools import groupby from operator import itemgetter from os import listdir from os.path import isdir, join +from typing import Dict, List import click from loguru import logger -from cashu.core.base import Proof +from cashu.core.base import Proof, TokenJson, TokenMintJson from cashu.core.helpers import sum_proofs from cashu.core.migrations import migrate_databases from cashu.core.settings import ( @@ -45,7 +47,7 @@ from cashu.wallet.crud import ( ) from cashu.wallet.wallet import Wallet as Wallet -from .cli_helpers import verify_mints, redeem_multimint +from .cli_helpers import redeem_multimint, verify_mints async def init_wallet(wallet: Wallet): @@ -302,20 +304,56 @@ async def receive(ctx, token: str, lock: str): # deserialize token + # ----- backwards compatibility ----- + # we support old tokens (< 0.7) without mint information and (W3siaWQ...) # new tokens (>= 0.7) with multiple mint support (eyJ0b2...) try: # backwards compatibility: tokens without mint information + # supports tokens of the form W3siaWQiOiJH + + # LNbits token link parsing + # can extract minut URL from LNbits token links like: + # https://lnbits.server/cashu/wallet?mint_id=aMintId&recv_token=W3siaWQiOiJHY2... + url = None + if len(token.split("&recv_token=")) == 2: + # extract URL params + params = urllib.parse.parse_qs(token.split("?")[1]) + # extract URL + if "mint_id" in params: + url = ( + token.split("?")[0].split("/wallet")[0] + + "/api/v1/" + + params["mint_id"][0] + ) + # extract token + token = params["recv_token"][0] + + # assume W3siaWQiOiJH.. token # trows an error if the desirialization with the old format doesn't work proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] token = await wallet.serialize_proofs( proofs, include_mints=False, ) - # _, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature) + + # if it was an LNbits link + # and add url and keyset id to token from link extraction above + if url: + token_object: TokenJson = await wallet._make_token( + proofs, include_mints=False + ) + token_object.mints = {} + keysets = list(set([p.id for p in proofs])) + assert keysets is not None, "no keysets" + token_object.mints[url] = TokenMintJson(url=url, ks=keysets) # type: ignore + token = await wallet._serialize_token_base64(token_object) + except Exception as e: - print(e) - print(token) + print(f"error decoding token: {str(e)}") + raise e + + # ----- receive token ----- # deserialize token dtoken = json.loads(base64.urlsafe_b64decode(token)) @@ -333,11 +371,10 @@ async def receive(ctx, token: str, lock: str): await redeem_multimint(ctx, dtoken, script, signature) # reload main wallet so the balance updates await wallet.load_proofs() - else: # no mint information present, we extract the proofs and use wallet's default mint proofs = [Proof(**p) for p in dtoken["tokens"]] - _, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature) + _, _ = await wallet.redeem(proofs, script, signature) wallet.status() diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index 0fe3a8a..15a5b7b 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -1,9 +1,11 @@ -import click import os + +import click + +from cashu.core.base import Proof from cashu.core.settings import CASHU_DIR from cashu.wallet.crud import get_keyset from cashu.wallet.wallet import Wallet as Wallet -from cashu.core.base import Proof async def verify_mints(ctx, dtoken): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 055cbd8..e0124eb 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -66,8 +66,8 @@ class LedgerAPI: def _set_requests(self): s = requests.Session() s.headers.update({"Client-version": VERSION}) - # if DEBUG: - # s.verify = False + if DEBUG: + s.verify = False socks_host, socks_port = None, None if TOR and TorProxy().check_platform(): self.tor = TorProxy(timeout=True) @@ -487,19 +487,11 @@ class Wallet(LedgerAPI): raise Exception("could not pay invoice.") return status["paid"] - async def serialize_proofs( - self, proofs: List[Proof], include_mints=True, legacy=False - ): + async def _make_token(self, proofs: List[Proof], include_mints=True): """ - Produces sharable token with proofs and mint information. + Takes list of proofs and produces a TokenJson by looking up + the keyset id and mint URLs from the database. """ - - if legacy: - proofs_serialized = [p.to_dict() for p in proofs] - return base64.urlsafe_b64encode( - json.dumps(proofs_serialized).encode() - ).decode() - # build token token = TokenJson(tokens=proofs) @@ -528,13 +520,34 @@ class Wallet(LedgerAPI): mints[placeholder_mint_id].ks.append(keyset.id) if len(mints) > 0: token.mints = mints + return token + async def _serialize_token_base64(self, token: TokenJson): + """ + Takes a TokenJson and serializes it in urlsafe_base64. + """ # encode the token as a base64 string token_base64 = base64.urlsafe_b64encode( json.dumps(token.dict()).encode() ).decode() return token_base64 + async def serialize_proofs( + self, proofs: List[Proof], include_mints=True, legacy=False + ): + """ + Produces sharable token with proofs and mint information. + """ + + if legacy: + proofs_serialized = [p.to_dict() for p in proofs] + return base64.urlsafe_b64encode( + json.dumps(proofs_serialized).encode() + ).decode() + + token = await self._make_token(proofs, include_mints) + return await self._serialize_token_base64(token) + async def _get_spendable_proofs(self, proofs: List[Proof]): """ Selects proofs that can be used with the current mint. From fcd46eef4147a3632c0a5ecefecf39a62af09f23 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 22:13:19 +0100 Subject: [PATCH 19/25] fix help --- cashu/wallet/cli_helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index 15a5b7b..d1084da 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -31,9 +31,7 @@ async def verify_mints(ctx, dtoken): # we encountered a new mint and ask for a user confirmation trust_token_mints = False print("") - print( - "Warning: Tokens are from a mint you don't know yet. Make sure that you know this mint." - ) + print("Warning: Tokens are from a mint you don't know yet.") print("\n") print(f"Mint URL: {mint_url}") print(f"Mint keyset: {keyset}") From bdd3f3ae1f23529f74a04f10a07b30754b299530 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 22:41:21 +0100 Subject: [PATCH 20/25] fix return --- cashu/wallet/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 020972b..e389207 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -91,6 +91,7 @@ def cli(ctx, host: str, walletname: str): # configure logger logger.remove() logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO") + ctx.ensure_object(dict) ctx.obj["HOST"] = host ctx.obj["WALLET_NAME"] = walletname @@ -349,9 +350,8 @@ async def receive(ctx, token: str, lock: str): token_object.mints[url] = TokenMintJson(url=url, ks=keysets) # type: ignore token = await wallet._serialize_token_base64(token_object) - except Exception as e: - print(f"error decoding token: {str(e)}") - raise e + except: + pass # ----- receive token ----- From 4c045abadddf6ae5b23f7502ca94ffa86b3e2ee9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 23:20:42 +0100 Subject: [PATCH 21/25] choose which mint to send from --- cashu/wallet/cli.py | 42 ++++++++++------------------ cashu/wallet/cli_helpers.py | 55 ++++++++++++++++++++++++++++++++++++- cashu/wallet/wallet.py | 3 +- 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index e389207..e5543c2 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -47,7 +47,12 @@ from cashu.wallet.crud import ( ) from cashu.wallet.wallet import Wallet as Wallet -from .cli_helpers import redeem_multimint, verify_mints +from .cli_helpers import ( + redeem_multimint, + verify_mints, + print_mint_balances, + get_mint_wallet, +) async def init_wallet(wallet: Wallet): @@ -210,27 +215,7 @@ async def balance(ctx, verbose): ) print("") - # get balances per mint - mint_balances = await wallet.balance_per_minturl() - - # if we have a balance on a non-default mint, we show its URL - show_mints = False - keysets = [k for k, v in wallet.balance_per_keyset().items()] - for k in keysets: - ks = await get_keyset(id=str(k), db=wallet.db) - if ks and ks.mint_url != ctx.obj["HOST"]: - show_mints = True - - # or we have a balance on more than one mint - # show balances per mint - if len(mint_balances) > 1 or show_mints: - print(f"You have balances in {len(mint_balances)} mints:") - print("") - for k, v in mint_balances.items(): - print( - f"Mint: {k} - Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat)" - ) - print("") + await print_mint_balances(ctx, wallet) if verbose: print( @@ -259,9 +244,10 @@ async def send(ctx, amount: int, lock: str, legacy: bool): p2sh = False if lock and len(lock.split("P2SH:")) == 2: p2sh = True - wallet: Wallet = ctx.obj["WALLET"] - await wallet.load_mint() - wallet.status() + + wallet = await get_mint_wallet(ctx) + await wallet.load_proofs() + _, send_proofs = await wallet.split_to_send( wallet.proofs, amount, lock, set_reserved=True ) @@ -444,6 +430,7 @@ async def pending(ctx): ) print(f"{token}\n") print(f"--------------------------\n") + print("To remove all spent tokens use: cashu burn -a") wallet.status() @@ -545,9 +532,8 @@ async def invoices(ctx): @click.pass_context @coro async def nsend(ctx, amount: int, pubkey: str, verbose: bool, yes: bool): - wallet: Wallet = ctx.obj["WALLET"] - await wallet.load_mint() - wallet.status() + wallet = await get_mint_wallet(ctx) + await wallet.load_proofs() _, send_proofs = await wallet.split_to_send( wallet.proofs, amount, set_reserved=True ) diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index d1084da..145dde7 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -2,7 +2,7 @@ import os import click -from cashu.core.base import Proof +from cashu.core.base import Proof, WalletKeyset from cashu.core.settings import CASHU_DIR from cashu.wallet.crud import get_keyset from cashu.wallet.wallet import Wallet as Wallet @@ -67,3 +67,56 @@ async def redeem_multimint(ctx, dtoken, script, signature): _, _ = await keyset_wallet.redeem( redeem_proofs, scnd_script=script, scnd_siganture=signature ) + + +async def print_mint_balances(ctx, wallet, show_mints=False): + # get balances per mint + mint_balances = await wallet.balance_per_minturl() + + # if we have a balance on a non-default mint, we show its URL + keysets = [k for k, v in wallet.balance_per_keyset().items()] + for k in keysets: + ks = await get_keyset(id=str(k), db=wallet.db) + if ks and ks.mint_url != ctx.obj["HOST"]: + show_mints = True + + # or we have a balance on more than one mint + # show balances per mint + if len(mint_balances) > 1 or show_mints: + print(f"You have balances in {len(mint_balances)} mints:") + print("") + for i, (k, v) in enumerate(mint_balances.items()): + print( + f"Mint {i+1}: {k} - Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat)" + ) + print("") + + +async def get_mint_wallet(ctx): + wallet: Wallet = ctx.obj["WALLET"] + await wallet.load_mint() + + mint_balances = await wallet.balance_per_minturl() + print(mint_balances) + if len(mint_balances) == 1: + return wallet + + await print_mint_balances(ctx, wallet, show_mints=True) + + mint_nr = input( + f"Which mint do you want to use? [1-{len(mint_balances)}, default: 1] " + ) + mint_nr = "1" if mint_nr == "" else mint_nr + if not mint_nr.isdigit(): + raise Exception("invalid input.") + mint_nr = int(mint_nr) + + mint_url = list(mint_balances.keys())[mint_nr - 1] + + mint_wallet = Wallet(mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])) + mint_keysets: WalletKeyset = await get_keyset(mint_url=mint_url, db=mint_wallet.db) # type: ignore + print(mint_keysets.id) + # load the keys + await mint_wallet.load_mint(keyset_id=mint_keysets.id) + + return mint_wallet diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index e0124eb..76ada45 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -665,13 +665,14 @@ class Wallet(LedgerAPI): async def balance_per_minturl(self): balances = await self._get_proofs_per_minturl(self.proofs) - return { + balances_return = { key: { "balance": sum_proofs(proofs), "available": sum_proofs([p for p in proofs if not p.reserved]), } for key, proofs in balances.items() } + return dict(sorted(balances_return.items(), key=lambda item: item[1]["available"], reverse=True)) # type: ignore def proof_amounts(self): return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])] From 514cbd8cff592a18c29f05ddb14c446b8dd49e4f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 23:21:07 +0100 Subject: [PATCH 22/25] make format --- cashu/wallet/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index e5543c2..93f6b6d 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -48,10 +48,10 @@ from cashu.wallet.crud import ( from cashu.wallet.wallet import Wallet as Wallet from .cli_helpers import ( + get_mint_wallet, + print_mint_balances, redeem_multimint, verify_mints, - print_mint_balances, - get_mint_wallet, ) From c1c8a00c64cc4368944ebc48b2a3260161e471b1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 23:24:40 +0100 Subject: [PATCH 23/25] v 0.7.0 --- README.md | 2 +- cashu/core/settings.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9963206..aad816c 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ cashu info Returns: ```bash -Version: 0.7.1 +Version: 0.7.0 Debug: False Cashu dir: /home/user/.cashu Wallet: wallet diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 31c8b84..5865f51 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -55,4 +55,4 @@ LNBITS_KEY = env.str("LNBITS_KEY", default=None) NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None) MAX_ORDER = 64 -VERSION = "0.7.1" +VERSION = "0.7.0" diff --git a/pyproject.toml b/pyproject.toml index 861f4a9..8745a1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.7.1" +version = "0.7.0" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" diff --git a/setup.py b/setup.py index 7ab643e..20093d0 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]} setuptools.setup( name="cashu", - version="0.7.1", + version="0.7.0", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown", From 2defdf2e1a9ff2219574a8f23ff7b31fd0bbee41 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 23:30:49 +0100 Subject: [PATCH 24/25] clean up --- cashu/wallet/cli_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index 145dde7..3217631 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -97,7 +97,7 @@ async def get_mint_wallet(ctx): await wallet.load_mint() mint_balances = await wallet.balance_per_minturl() - print(mint_balances) + # if there is only one mint, use it if len(mint_balances) == 1: return wallet @@ -113,9 +113,10 @@ async def get_mint_wallet(ctx): mint_url = list(mint_balances.keys())[mint_nr - 1] + # load this mint_url into a wallet mint_wallet = Wallet(mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])) mint_keysets: WalletKeyset = await get_keyset(mint_url=mint_url, db=mint_wallet.db) # type: ignore - print(mint_keysets.id) + # load the keys await mint_wallet.load_mint(keyset_id=mint_keysets.id) From 542f4c714193b9a5e011658fa658911a51a08902 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 27 Dec 2022 15:39:06 +0100 Subject: [PATCH 25/25] nostr relay setting --- cashu/core/settings.py | 6 ++++-- cashu/wallet/cli.py | 6 ++++-- cashu/wallet/wallet_live/.DS_Store | Bin 6148 -> 0 bytes cashu/wallet/wallet_live/.placeholder | 0 cashu/wallet/wallet_live/wallet.sqlite3 | Bin 36864 -> 0 bytes 5 files changed, 8 insertions(+), 4 deletions(-) delete mode 100644 cashu/wallet/wallet_live/.DS_Store delete mode 100644 cashu/wallet/wallet_live/.placeholder delete mode 100644 cashu/wallet/wallet_live/wallet.sqlite3 diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 5865f51..e22b118 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -7,9 +7,10 @@ from environs import Env # type: ignore env = Env() -ENV_FILE = os.path.join(str(Path.home()), ".cashu", ".env") +# env file: default to current dir, else home dir +ENV_FILE = os.path.join(os.getcwd(), ".env") if not os.path.isfile(ENV_FILE): - ENV_FILE = os.path.join(os.getcwd(), ".env") + ENV_FILE = os.path.join(str(Path.home()), ".cashu", ".env") if os.path.isfile(ENV_FILE): env.read_env(ENV_FILE) else: @@ -53,6 +54,7 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None) LNBITS_KEY = env.str("LNBITS_KEY", default=None) NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None) +NOSTR_RELAYS = env.list("NOSTR_RELAYS", default=["wss://nostr-pub.wellorder.net"]) MAX_ORDER = 64 VERSION = "0.7.0" diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 93f6b6d..2330200 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -29,6 +29,7 @@ from cashu.core.settings import ( LIGHTNING, MINT_URL, NOSTR_PRIVATE_KEY, + NOSTR_RELAYS, SOCKS_HOST, SOCKS_PORT, TOR, @@ -551,7 +552,7 @@ async def nsend(ctx, amount: int, pubkey: str, verbose: bool, yes: bool): ) # we only use ephemeral private keys for sending - client = NostrClient() + client = NostrClient(relays=NOSTR_RELAYS) if verbose: print(f"Your ephemeral nostr private key: {client.private_key.hex()}") await asyncio.sleep(1) @@ -577,7 +578,7 @@ async def nreceive(ctx, verbose: bool): "Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it." ) print("") - client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) + client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY, relays=NOSTR_RELAYS) print(f"Your nostr public key: {client.public_key.hex()}") if verbose: print(f"Your nostr private key (do not share!): {client.private_key.hex()}") @@ -646,6 +647,7 @@ async def info(ctx): if NOSTR_PRIVATE_KEY: client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY, connect=False) print(f"Nostr public key: {client.public_key.hex()}") + print(f"Nostr relays: {NOSTR_RELAYS}") if SOCKS_HOST: print(f"Socks proxy: {SOCKS_HOST}:{SOCKS_PORT}") print(f"Mint URL: {ctx.obj['HOST']}") diff --git a/cashu/wallet/wallet_live/.DS_Store b/cashu/wallet/wallet_live/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 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 k0+;Xa-`2E`e^hn*zTW@8Z+~!fc5m