From 53a8387a0d86efb2106974a520e82c3d93227f2b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 11 Jan 2023 02:57:59 +0100 Subject: [PATCH 01/20] refactor --- cashu/core/base.py | 45 +++++++++++++++--------- cashu/core/errors.py | 27 ++++++--------- cashu/mint/router.py | 42 +++++++++++++---------- cashu/wallet/cli.py | 2 +- cashu/wallet/cli_helpers.py | 5 +-- cashu/wallet/wallet.py | 51 ++++++++++++++++------------ docs/specs/cashu_client_spec.md | 6 ++-- docs/specs/wip/4 - Minting tokens.md | 2 +- docs/specs/wip/6 - Split.md | 2 +- tests/test_tor.py | 2 +- tests/test_wallet.py | 4 +-- 11 files changed, 107 insertions(+), 81 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 78dfc86..a120572 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -67,10 +67,22 @@ class BlindedSignature(BaseModel): C_: str -class MintRequest(BaseModel): +class KeysResponse(BaseModel): + __root__: Dict[str, str] + + +class KeysetsResponse(BaseModel): + keysets: list[str] + + +class BlindedMessages(BaseModel): blinded_messages: List[BlindedMessage] = [] +class PostMintResponse(BaseModel): + promises: List[BlindedSignature] = [] + + class GetMintResponse(BaseModel): pr: str hash: str @@ -85,9 +97,9 @@ class SplitRequest(BaseModel): proofs: List[Proof] amount: int output_data: Union[ - MintRequest, None + BlindedMessages, None ] = None # backwards compatibility with clients < v0.2.2 - outputs: Union[MintRequest, None] = None + outputs: Union[BlindedMessages, None] = None def __init__(self, **data): super().__init__(**data) @@ -129,17 +141,17 @@ class KeyBase(BaseModel): class WalletKeyset: - id: str - public_keys: Dict[int, PublicKey] + id: Union[str, None] + public_keys: Union[Dict[int, PublicKey], None] mint_url: Union[str, None] = None valid_from: Union[str, None] = None valid_to: Union[str, None] = None first_seen: Union[str, None] = None - active: bool = True + active: Union[bool, None] = True def __init__( self, - pubkeys: Dict[int, PublicKey] = None, + public_keys=None, mint_url=None, id=None, valid_from=None, @@ -153,20 +165,20 @@ class WalletKeyset: self.first_seen = first_seen self.active = active self.mint_url = mint_url - if pubkeys: - self.public_keys = pubkeys + if public_keys: + self.public_keys = public_keys self.id = derive_keyset_id(self.public_keys) class MintKeyset: - id: str + id: Union[str, None] derivation_path: str private_keys: Dict[int, PrivateKey] - public_keys: Dict[int, PublicKey] = {} + public_keys: Union[Dict[int, PublicKey], None] = None valid_from: Union[str, None] = None valid_to: Union[str, None] = None first_seen: Union[str, None] = None - active: bool = True + active: Union[bool, None] = True version: Union[str, None] = None def __init__( @@ -194,13 +206,14 @@ class MintKeyset: def generate_keys(self, seed): """Generates keys of a keyset from a seed.""" self.private_keys = derive_keys(seed, self.derivation_path) - self.public_keys = derive_pubkeys(self.private_keys) - self.id = derive_keyset_id(self.public_keys) + self.public_keys = derive_pubkeys(self.private_keys) # type: ignore + self.id = derive_keyset_id(self.public_keys) # type: ignore def get_keybase(self): + assert self.id is not None return { k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex()) - for k, v in self.public_keys.items() + for k, v in self.public_keys.items() # type: ignore } @@ -208,7 +221,7 @@ class MintKeysets: keysets: Dict[str, MintKeyset] def __init__(self, keysets: List[MintKeyset]): - self.keysets: Dict[str, MintKeyset] = {k.id: k for k in keysets} + self.keysets = {k.id: k for k in keysets} # type: ignore def get_ids(self): return [k for k, _ in self.keysets.items()] diff --git a/cashu/core/errors.py b/cashu/core/errors.py index f770896..19e757d 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -2,25 +2,20 @@ from pydantic import BaseModel class CashuError(BaseModel): - code = "000" - error = "CashuError" + code: int + error: str -# class CashuError(Exception, BaseModel): -# code = "000" -# error = "CashuError" +class MintException(CashuError): + code = 100 + error = "Mint" -# class MintException(CashuError): -# code = 100 -# error = "Mint" +class LightningException(MintException): + code = 200 + error = "Lightning" -# class LightningException(MintException): -# code = 200 -# error = "Lightning" - - -# class InvoiceNotPaidException(LightningException): -# code = 201 -# error = "invoice not paid." +class InvoiceNotPaidException(LightningException): + code = 201 + error = "invoice not paid." diff --git a/cashu/mint/router.py b/cashu/mint/router.py index c7fe9f9..ab879ff 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -5,13 +5,16 @@ from secp256k1 import PublicKey from cashu.core.base import ( BlindedSignature, + KeysResponse, + KeysetsResponse, CheckFeesRequest, CheckFeesResponse, CheckRequest, GetMeltResponse, + BlindedMessages, GetMintResponse, + PostMintResponse, MeltRequest, - MintRequest, PostSplitResponse, SplitRequest, ) @@ -22,28 +25,32 @@ router: APIRouter = APIRouter() @router.get("/keys") -async def keys() -> dict[int, str]: +async def keys() -> KeysResponse: """Get the public keys of the mint of the newest keyset""" keyset = ledger.get_keyset() - return keyset + keys = KeysResponse.parse_obj(keyset) + return keys @router.get("/keys/{idBase64Urlsafe}") -async def keyset_keys(idBase64Urlsafe: str) -> dict[int, str]: +async def keyset_keys(idBase64Urlsafe: str) -> KeysResponse: """ - 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 + Get the public keys of the mint of a specific keyset id. + The id is encoded in idBase64Urlsafe 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 + keys = KeysResponse.parse_obj(keyset) + return keys @router.get("/keysets") -async def keysets() -> dict[str, list[str]]: - """Get all active keysets of the mint""" - return {"keysets": ledger.keysets.get_ids()} +async def keysets() -> KeysetsResponse: + """Get all active keyset ids of the mint""" + # keysets = {"keysets": ledger.keysets.get_ids()} + keysets = KeysetsResponse(keysets=ledger.keysets.get_ids()) + return keysets @router.get("/mint") @@ -62,9 +69,9 @@ async def request_mint(amount: int = 0) -> GetMintResponse: @router.post("/mint") async def mint( - mint_request: MintRequest, + mint_request: BlindedMessages, payment_hash: Union[str, None] = None, -) -> Union[List[BlindedSignature], CashuError]: +) -> Union[PostMintResponse, CashuError]: """ Requests the minting of tokens belonging to a paid payment request. @@ -74,9 +81,10 @@ async def mint( promises = await ledger.mint( mint_request.blinded_messages, payment_hash=payment_hash ) - return promises + blinded_signatures = PostMintResponse(promises=promises) + return blinded_signatures except Exception as exc: - return CashuError(error=str(exc)) + return CashuError(code=0, error=str(exc)) @router.post("/melt") @@ -103,7 +111,7 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse: This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu). """ fees_msat = await ledger.check_fees(payload.pr) - return CheckFeesResponse(fee=fees_msat / 1000) + return CheckFeesResponse(fee=fees_msat // 1000) @router.post("/split") @@ -124,9 +132,9 @@ async def split( try: split_return = await ledger.split(proofs, amount, outputs) except Exception as exc: - return CashuError(error=str(exc)) + return CashuError(code=0, error=str(exc)) if not split_return: - return CashuError(error="there was an error with the split") + return CashuError(code=0, error="there was an error with the split") frst_promises, scnd_promises = split_return resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises) return resp diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index a500921..10f7ef5 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -508,7 +508,7 @@ async def pending(ctx): reserved_proofs = await get_reserved_proofs(wallet.db) if len(reserved_proofs): print(f"--------------------------\n") - sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id")) + sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id")) # type: ignore for i, (key, value) in enumerate( groupby(sorted_proofs, key=itemgetter("send_id")) ): diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index 132b6fe..0d6b6e8 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -19,7 +19,7 @@ async def verify_mints(ctx, dtoken): mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"]) ) # make sure that this mint supports this keyset - mint_keysets = await keyset_wallet._get_keysets(mint_url) + mint_keysets = await keyset_wallet._get_keyset_ids(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 @@ -119,6 +119,7 @@ async def get_mint_wallet(ctx): mint_keysets: WalletKeyset = await get_keyset(mint_url=mint_url, db=mint_wallet.db) # type: ignore # load the keys + assert mint_keysets.id await mint_wallet.load_mint(keyset_id=mint_keysets.id) return mint_wallet @@ -161,7 +162,7 @@ async def proofs_to_token(wallet, proofs, url: str): # check whether we know the mint urls for these proofs for k in keysets: ks = await get_keyset(id=k, db=wallet.db) - url = ks.mint_url if ks is not None else None + url = ks.mint_url if ks and ks.mint_url else "" url = url or ( input(f"Enter mint URL (press enter for default {MINT_URL}): ") or MINT_URL diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 378308d..f5959fc 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -16,10 +16,13 @@ from cashu.core.base import ( BlindedMessage, BlindedSignature, CheckFeesRequest, + KeysetsResponse, + GetMintResponse, + PostMintResponse, CheckRequest, Invoice, MeltRequest, - MintRequest, + BlindedMessages, P2SHScript, Proof, SplitRequest, @@ -130,6 +133,8 @@ class LedgerAPI: keyset = await self._get_keys(self.url) # store current keyset + assert keyset.public_keys + assert keyset.id assert len(keyset.public_keys) > 0, "did not receive keys from mint." # check if current keyset is in db @@ -140,7 +145,7 @@ class LedgerAPI: # get all active keysets of this mint mint_keysets = [] try: - keysets_resp = await self._get_keysets(self.url) + keysets_resp = await self._get_keyset_ids(self.url) mint_keysets = keysets_resp["keysets"] # store active keysets except: @@ -160,7 +165,7 @@ class LedgerAPI: assert len(amounts) == len( secrets ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}" - payloads: MintRequest = MintRequest() + payloads: BlindedMessages = BlindedMessages() rs = [] for secret, amount in zip(secrets, amounts): B_, r = b_dhke.step1_alice(secret) @@ -198,7 +203,7 @@ class LedgerAPI: int(amt): PublicKey(bytes.fromhex(val), raw=True) for amt, val in keys.items() } - keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url) + keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url) return keyset async def _get_keyset(self, url: str, keyset_id: str): @@ -217,18 +222,19 @@ class LedgerAPI: int(amt): PublicKey(bytes.fromhex(val), raw=True) for amt, val in keys.items() } - keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url) + keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url) return keyset - async def _get_keysets(self, url: str): + async def _get_keyset_ids(self, url: str): self.s = self._set_requests() resp = self.s.get( url + "/keysets", ) resp.raise_for_status() - keysets = resp.json() - assert len(keysets), Exception("did not receive any keysets") - return keysets + keysets_dict = resp.json() + keysets = KeysetsResponse.parse_obj(keysets_dict) + assert len(keysets.keysets), Exception("did not receive any keysets") + return keysets.dict() def request_mint(self, amount): """Requests a mint from the server and returns Lightning invoice.""" @@ -237,7 +243,8 @@ class LedgerAPI: resp.raise_for_status() return_dict = resp.json() self.raise_on_error(return_dict) - return Invoice(amount=amount, pr=return_dict["pr"], hash=return_dict["hash"]) + mint_response = GetMintResponse.parse_obj(return_dict) + return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash) async def mint(self, amounts, payment_hash=None): """Mints new coins and returns a proof of promise.""" @@ -251,11 +258,10 @@ class LedgerAPI: params={"payment_hash": payment_hash}, ) resp.raise_for_status() - promises_list = resp.json() - self.raise_on_error(promises_list) - - promises = [BlindedSignature(**p) for p in promises_list] - return self._construct_proofs(promises, secrets, rs) + reponse_dict = resp.json() + self.raise_on_error(reponse_dict) + promises = PostMintResponse.parse_obj(reponse_dict) + return self._construct_proofs(promises.promises, secrets, rs) async def split(self, proofs, amount, scnd_secret: Optional[str] = None): """Consume proofs and create new promises based on amount split. @@ -304,7 +310,7 @@ class LedgerAPI: self.s = self._set_requests() resp = self.s.post( self.url + "/split", - json=split_payload.dict(include=_splitrequest_include_fields(proofs)), + json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore ) resp.raise_for_status() promises_dict = resp.json() @@ -337,7 +343,7 @@ class LedgerAPI: self.s = self._set_requests() resp = self.s.post( self.url + "/check", - json=payload.dict(include=_check_spendable_include_fields(proofs)), + json=payload.dict(include=_check_spendable_include_fields(proofs)), # type: ignore ) resp.raise_for_status() return_dict = resp.json() @@ -375,7 +381,7 @@ class LedgerAPI: self.s = self._set_requests() resp = self.s.post( self.url + "/melt", - json=payload.dict(include=_meltequest_include_fields(proofs)), + json=payload.dict(include=_meltequest_include_fields(proofs)), # type: ignore ) resp.raise_for_status() return_dict = resp.json() @@ -411,7 +417,9 @@ class Wallet(LedgerAPI): 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) + keyset_crud = await get_keyset(id=id, db=self.db) + assert keyset_crud is not None, "keyset not found" + keyset: WalletKeyset = keyset_crud if keyset.mint_url not in ret: ret[keyset.mint_url] = [p for p in proofs if p.id == id] else: @@ -504,7 +512,7 @@ class Wallet(LedgerAPI): 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: + if keyset and keyset.mint_url and keyset.id: # TODO: replace this with a mint pubkey placeholder_mint_id = keyset.mint_url if placeholder_mint_id not in mints: @@ -577,7 +585,8 @@ class Wallet(LedgerAPI): Splits proofs such that a Lightning invoice can be paid. """ amount, _ = await self.get_pay_amount_with_fees(invoice) - _, send_proofs = await self.split_to_send(self.proofs, amount) + # TODO: fix mypy asyncio return multiple values + _, send_proofs = await self.split_to_send(self.proofs, amount) # type: ignore return send_proofs async def split_to_send( diff --git a/docs/specs/cashu_client_spec.md b/docs/specs/cashu_client_spec.md index 73bb92d..c41effc 100644 --- a/docs/specs/cashu_client_spec.md +++ b/docs/specs/cashu_client_spec.md @@ -52,8 +52,8 @@ Here we see how `Alice` generates `N` blinded messages `T_i`. The following step - `Alice` remembers `r` for the construction of the proof in Step 5. ### Step 4: Request tokens -- `Alice` constructs JSON `MintRequest = {"blinded_messages" : ["amount" : , "B_" : ] }` [NOTE: rename "blinded_messages", rename "B_", rename "MintRequest"] -- `Alice` requests tokens via `POST /mint?payment_hash=` with body `MintRequest` [NOTE: rename MintRequest] +- `Alice` constructs JSON `BlindedMessages = {"blinded_messages" : ["amount" : , "B_" : ] }` [NOTE: rename "blinded_messages", rename "B_", rename "BlindedMessages"] +- `Alice` requests tokens via `POST /mint?payment_hash=` with body `BlindedMessages` [NOTE: rename BlindedMessages] - `Alice` receives from `Bob` a list of blinded signatures `List[BlindedSignature]`, one for each token, e.g. `[{"amount" : , "C_" : }, ...]` [NOTE: rename C_] - If an error occured, `Alice` receives JSON `{"error" : }}`[*TODO: Specify case of error*] @@ -121,5 +121,5 @@ Here we describe how `Alice` can request from `Bob` to make a Lightning payment # Todo: - Call subsections 1. and 1.2 etc so they can be referenced -- Define objets like `MintRequest` and `SplitRequests` once when they appear and reuse them. +- Define objets like `BlindedMessages` and `SplitRequests` once when they appear and reuse them. - Clarify whether a `TOKEN` is a single Proof or a list of Proofs diff --git a/docs/specs/wip/4 - Minting tokens.md b/docs/specs/wip/4 - Minting tokens.md index 0a7dc15..5239458 100644 --- a/docs/specs/wip/4 - Minting tokens.md +++ b/docs/specs/wip/4 - Minting tokens.md @@ -10,7 +10,7 @@ Request of `Alice`: POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a ``` -With the data being of the form `MintRequest`: +With the data being of the form `BlindedMessages`: ```json { diff --git a/docs/specs/wip/6 - Split.md b/docs/specs/wip/6 - Split.md index e6b735f..c25e831 100644 --- a/docs/specs/wip/6 - Split.md +++ b/docs/specs/wip/6 - Split.md @@ -25,7 +25,7 @@ With the data being of the form `SplitRequest`: ```json { "proofs": Proofs, - "outputs": MintRequest, + "outputs": BlindedMessages, "amount": int } ``` diff --git a/tests/test_tor.py b/tests/test_tor.py index 4d792bb..8cbc92e 100644 --- a/tests/test_tor.py +++ b/tests/test_tor.py @@ -4,7 +4,7 @@ import requests from cashu.tor.tor import TorProxy -# @pytest.mark.skip +@pytest.mark.skip def test_tor_setup(): s = requests.Session() diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 493503f..c8b186a 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -68,8 +68,8 @@ async def test_get_keyset(wallet1: Wallet): @pytest.mark.asyncio -async def test_get_keysets(wallet1: Wallet): - keyset = await wallet1._get_keysets(wallet1.url) +async def test_get_keyset_ids(wallet1: Wallet): + keyset = await wallet1._get_keyset_ids(wallet1.url) assert type(keyset) == dict assert type(keyset["keysets"]) == list assert len(keyset["keysets"]) > 0 From fc5347d47dfdd2ca783aeeb1f0dfcd653a11794a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 11 Jan 2023 03:00:02 +0100 Subject: [PATCH 02/20] make format --- cashu/mint/router.py | 8 ++++---- cashu/wallet/wallet.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index ab879ff..bc202a1 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -4,17 +4,17 @@ from fastapi import APIRouter from secp256k1 import PublicKey from cashu.core.base import ( + BlindedMessages, BlindedSignature, - KeysResponse, - KeysetsResponse, CheckFeesRequest, CheckFeesResponse, CheckRequest, GetMeltResponse, - BlindedMessages, GetMintResponse, - PostMintResponse, + KeysetsResponse, + KeysResponse, MeltRequest, + PostMintResponse, PostSplitResponse, SplitRequest, ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index f5959fc..fabc839 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -14,16 +14,16 @@ import cashu.core.b_dhke as b_dhke import cashu.core.bolt11 as bolt11 from cashu.core.base import ( BlindedMessage, + BlindedMessages, BlindedSignature, CheckFeesRequest, - KeysetsResponse, - GetMintResponse, - PostMintResponse, CheckRequest, + GetMintResponse, Invoice, + KeysetsResponse, MeltRequest, - BlindedMessages, P2SHScript, + PostMintResponse, Proof, SplitRequest, TokenJson, From 012987c356000c83b46d8ef8fe0da0bddf8e551a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 11 Jan 2023 03:03:12 +0100 Subject: [PATCH 03/20] clean --- cashu/mint/router.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index bc202a1..ecf6881 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -48,7 +48,6 @@ async def keyset_keys(idBase64Urlsafe: str) -> KeysResponse: @router.get("/keysets") async def keysets() -> KeysetsResponse: """Get all active keyset ids of the mint""" - # keysets = {"keysets": ledger.keysets.get_ids()} keysets = KeysetsResponse(keysets=ledger.keysets.get_ids()) return keysets From 0e82fa5cdd6ea957a8dbf6f5d61926395c1239d8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:18:52 +0100 Subject: [PATCH 04/20] V2 tokens: rename field tokens to proofs, add documentation --- cashu/core/base.py | 6 ++--- cashu/wallet/cli_helpers.py | 4 ++-- cashu/wallet/wallet.py | 10 ++++---- docs/specs/00.md | 48 ++++++++++++++++++++++++++++++++++--- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index a120572..26eaf9a 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -232,12 +232,12 @@ class TokenMintJson(BaseModel): ks: List[str] -class TokenJson(BaseModel): - tokens: List[Proof] +class TokenV2(BaseModel): + proofs: List[Proof] mints: Optional[Dict[str, TokenMintJson]] = None def to_dict(self): return dict( - tokens=[p.to_dict() for p in self.tokens], + proofs=[p.to_dict() for p in self.proofs], mints={k: v.dict() for k, v in self.mints.items()}, # type: ignore ) diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index 0d6b6e8..df7162f 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -3,7 +3,7 @@ import urllib.parse import click -from cashu.core.base import Proof, TokenJson, TokenMintJson, WalletKeyset +from cashu.core.base import Proof, TokenV2, TokenMintJson, WalletKeyset from cashu.core.settings import CASHU_DIR, MINT_URL from cashu.wallet.crud import get_keyset from cashu.wallet.wallet import Wallet as Wallet @@ -152,7 +152,7 @@ async def proofs_to_token(wallet, proofs, url: str): Ingests proofs and """ # and add url and keyset id to token - token: TokenJson = await wallet._make_token(proofs, include_mints=False) + token: TokenV2 = await wallet._make_token(proofs, include_mints=False) token.mints = {} # get keysets of proofs diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index fabc839..e797025 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -26,7 +26,7 @@ from cashu.core.base import ( PostMintResponse, Proof, SplitRequest, - TokenJson, + TokenV2, TokenMintJson, WalletKeyset, ) @@ -497,11 +497,11 @@ class Wallet(LedgerAPI): async def _make_token(self, proofs: List[Proof], include_mints=True): """ - Takes list of proofs and produces a TokenJson by looking up + Takes list of proofs and produces a TokenV2 by looking up the keyset id and mint URLs from the database. """ # build token - token = TokenJson(tokens=proofs) + token = TokenV2(proofs=proofs) # add mint information to the token, if requested if include_mints: @@ -530,9 +530,9 @@ class Wallet(LedgerAPI): token.mints = mints return token - async def _serialize_token_base64(self, token: TokenJson): + async def _serialize_token_base64(self, token: TokenV2): """ - Takes a TokenJson and serializes it in urlsafe_base64. + Takes a TokenV2 and serializes it in urlsafe_base64. """ # encode the token as a base64 string token_base64 = base64.urlsafe_b64encode( diff --git a/docs/specs/00.md b/docs/specs/00.md index 7957895..6761d70 100644 --- a/docs/specs/00.md +++ b/docs/specs/00.md @@ -66,9 +66,14 @@ A list of `Proof`'s. In general, this will be used for most operations instead o ## 0.2 - Methods ### Serialization of `Proofs` -To send and receive `Proofs`, wallets serialize them in a `base64_urlsafe` format. -Example: +To send and receive `Proofs`, wallets serialize them in a `base64_urlsafe` format. There are two versions of the serialization format. + +#### 0.2.1 - V1 tokens + +This token format is a list of `Proof`s. Each `Proof` contains the keyset id in the field `id` that can be used by a wallet to identify the mint of this token. A wallet that encounters an unknown `id`, it CAN ask the user to enter the mint url of this yet unknown mint. The wallet SHOULD explicity ask the user whether they trust the mint. + +##### Example JSON: ```json [ @@ -86,8 +91,45 @@ Example: } ``` -becomes +When serialized, this becomes: ``` W3siaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiRGJSS0l5YTBldGR3STVzRkFOMEFYUSIsICJDIjogIjAyZGY3ZjJmYzI5NjMxYjcxYTFkYjExYzE2M2IwYjFjYjQwNDQ0YWEyYjNkMjUzZDQzYjY4ZDc3YTcyZWQyZDYyNSJ9LCB7ImlkIjogIkRTQWw5bnZ2eWZ2YSIsICJhbW91bnQiOiAxNiwgInNlY3JldCI6ICJkX1BQYzVLcHVBQjJNNjBXWUFXNS1RIiwgIkMiOiAiMDI3MGUwYTM3ZjdhMGIyMWVhYjQzYWY3NTFkZDNjMDNmNjFmMDRjNjI2YzA0NDhmNjAzZjFkMWY1YWU1YTdkN2U2In1d +``` + +#### 0.2.2 - V2 tokens + +This token format includes information about the mint as well. The field `proofs` is like a V1 token. Additionally, the field `mints` can include a list of multiple mints from which the `proofs` are from. The `url` field is the URL of the mint. `ks` is a list of the keyset IDs belonging to this mint. All keyset IDs of the `proofs` must be present here to allow a wallet to map each proof to a mint. + +##### Example JSON: + +```json +{ + "proofs": [ + { + "id": "DSAl9nvvyfva", + "amount": 2, + "secret": "bdYCbHGONundLeYvv1P5dQ", + "C": "02e6117fb1b1633a8c1657ed34ab25ecf8d4974091179c4773ec59f85f4e3991cf" + }, + { + "id": "DSAl9nvvyfva", + "amount": 8, + "secret": "KxyUPt5Mur_-RV8pCECJ6A", + "C": "03b9dcdb7f195e07218b95b7c2dadc8289159fc44047439830f765b8c50bfb6bda" + } + ], + "mints": { + "MINT_NAME": { + "url": "http://server.host:3339", + "ks": ["DSAl9nvvyfva"] + } + } +} +``` + +When serialized, this becomes: + +``` +eyJwcm9vZnMiOlt7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50IjoyLCJzZWNyZXQiOiJiZFlDYkhHT051bmRMZVl2djFQNWRRIiwiQyI6IjAyZTYxMTdmYjFiMTYzM2E4YzE2NTdlZDM0YWIyNWVjZjhkNDk3NDA5MTE3OWM0NzczZWM1OWY4NWY0ZTM5OTFjZiJ9LHsiaWQiOiJEU0FsOW52dnlmdmEiLCJhbW91bnQiOjgsInNlY3JldCI6Ikt4eVVQdDVNdXJfLVJWOHBDRUNKNkEiLCJDIjoiMDNiOWRjZGI3ZjE5NWUwNzIxOGI5NWI3YzJkYWRjODI4OTE1OWZjNDQwNDc0Mzk4MzBmNzY1YjhjNTBiZmI2YmRhIn1dLCJtaW50cyI6eyJNSU5UX05BTUUiOnsidXJsIjoiaHR0cDovL3NlcnZlci5ob3N0OjMzMzkiLCJrcyI6WyJEU0FsOW52dnlmdmEiXX19fQ== ``` \ No newline at end of file From 0194fef619e32b56f2db69ba25856d000f5181be Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:28:29 +0100 Subject: [PATCH 05/20] legacy test --- cashu/core/base.py | 9 ++++++--- cashu/wallet/wallet.py | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 26eaf9a..5288b0c 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -40,9 +40,7 @@ class Proof(BaseModel): class Proofs(BaseModel): - """TODO: Use this model""" - - proofs: List[Proof] + __root__: List[Proof] class Invoice(BaseModel): @@ -79,6 +77,11 @@ class BlindedMessages(BaseModel): blinded_messages: List[BlindedMessage] = [] +class PostMintResponseLegacy(BaseModel): + # NOTE: Backwards compability for < 0.7.1 where we used a simple list and not a key-value dictionary + __root__: List[BlindedSignature] = [] + + class PostMintResponse(BaseModel): promises: List[BlindedSignature] = [] diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index e797025..4484805 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -24,6 +24,7 @@ from cashu.core.base import ( MeltRequest, P2SHScript, PostMintResponse, + PostMintResponseLegacy, Proof, SplitRequest, TokenV2, @@ -260,8 +261,12 @@ class LedgerAPI: resp.raise_for_status() reponse_dict = resp.json() self.raise_on_error(reponse_dict) - promises = PostMintResponse.parse_obj(reponse_dict) - return self._construct_proofs(promises.promises, secrets, rs) + try: + promises = PostMintResponseLegacy.parse_obj(reponse_dict) + return self._construct_proofs(promises, secrets, rs) + except: + promises = PostMintResponse.parse_obj(reponse_dict) + return self._construct_proofs(promises.promises, secrets, rs) async def split(self, proofs, amount, scnd_secret: Optional[str] = None): """Consume proofs and create new promises based on amount split. From 7f07c0c6fd16bdf084b4a48f69c474541f77fcf8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:28:48 +0100 Subject: [PATCH 06/20] comment --- cashu/wallet/wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 4484805..ca083e2 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -262,6 +262,7 @@ class LedgerAPI: reponse_dict = resp.json() self.raise_on_error(reponse_dict) try: + # backwards compatibility: parse promises < 0.7.1 with no "promises" field promises = PostMintResponseLegacy.parse_obj(reponse_dict) return self._construct_proofs(promises, secrets, rs) except: From ded836d750dbe8afaebe4d06a03d2012378ee4bd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:45:49 +0100 Subject: [PATCH 07/20] fix tokens to proofs --- cashu/wallet/cli.py | 4 ++-- cashu/wallet/cli_helpers.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 3b7138e..43e6744 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -387,7 +387,7 @@ async def receive(ctx, token: str, lock: str): # deserialize token dtoken = json.loads(base64.urlsafe_b64decode(token)) - assert "tokens" in dtoken, Exception("no proofs in token") + assert "proofs" 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 @@ -402,7 +402,7 @@ async def receive(ctx, token: str, lock: str): 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"]] + proofs = [Proof(**p) for p in dtoken["proofs"]] _, _ = await wallet.redeem(proofs, script, signature) wallet.status() diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index df7162f..b4ba620 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -63,7 +63,7 @@ async def redeem_multimint(ctx, dtoken, script, signature): # redeem proofs of this keyset redeem_proofs = [ - Proof(**p) for p in dtoken["tokens"] if Proof(**p).id == keyset + Proof(**p) for p in dtoken["proofs"] if Proof(**p).id == keyset ] _, _ = await keyset_wallet.redeem( redeem_proofs, scnd_script=script, scnd_siganture=signature From 20b99ee792ba7c044b71cb7decd19967e72ed855 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:46:33 +0100 Subject: [PATCH 08/20] bumop to 0.8 --- README.md | 2 +- cashu/core/base.py | 6 +++--- cashu/core/settings.py | 2 +- cashu/wallet/wallet.py | 13 ++++++++----- pyproject.toml | 2 +- setup.py | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2f40cc3..490828a 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ cashu info Returns: ```bash -Version: 0.7.0 +Version: 0.8 Debug: False Cashu dir: /home/user/.cashu Wallet: wallet diff --git a/cashu/core/base.py b/cashu/core/base.py index 5288b0c..ec7022d 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -16,7 +16,7 @@ class P2SHScript(BaseModel): class Proof(BaseModel): id: Union[ None, str - ] = "" # NOTE: None for backwards compatibility of old clients < 0.3 + ] = "" # NOTE: None for backwards compatibility for old clients that do not include the keyset id < 0.3 amount: int = 0 secret: str = "" C: str = "" @@ -78,7 +78,7 @@ class BlindedMessages(BaseModel): class PostMintResponseLegacy(BaseModel): - # NOTE: Backwards compability for < 0.7.1 where we used a simple list and not a key-value dictionary + # NOTE: Backwards compability for < 0.8 where we used a simple list and not a key-value dictionary __root__: List[BlindedSignature] = [] @@ -101,7 +101,7 @@ class SplitRequest(BaseModel): amount: int output_data: Union[ BlindedMessages, None - ] = None # backwards compatibility with clients < v0.2.2 + ] = None # backwards compatibility with clients that called this output_data and not outputs < v0.2.2 outputs: Union[BlindedMessages, None] = None def __init__(self, **data): diff --git a/cashu/core/settings.py b/cashu/core/settings.py index e22b118..a25ae87 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -57,4 +57,4 @@ 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" +VERSION = "0.8" diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index ca083e2..4f28508 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -262,12 +262,15 @@ class LedgerAPI: reponse_dict = resp.json() self.raise_on_error(reponse_dict) try: - # backwards compatibility: parse promises < 0.7.1 with no "promises" field - promises = PostMintResponseLegacy.parse_obj(reponse_dict) - return self._construct_proofs(promises, secrets, rs) + # backwards compatibility: parse promises < 0.8 with no "promises" field + promises = PostMintResponseLegacy.parse_obj(reponse_dict).__root__ + logger.warning( + "Parsing token with no promises field. Please upgrade mint to 0.8" + ) except: - promises = PostMintResponse.parse_obj(reponse_dict) - return self._construct_proofs(promises.promises, secrets, rs) + promises = PostMintResponse.parse_obj(reponse_dict).promises + + return self._construct_proofs(promises, secrets, rs) async def split(self, proofs, amount, scnd_secret: Optional[str] = None): """Consume proofs and create new promises based on amount split. diff --git a/pyproject.toml b/pyproject.toml index 8745a1b..b33cc7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.7.0" +version = "0.8" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" diff --git a/setup.py b/setup.py index 20093d0..267f57f 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.8", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown", From 7481dae63e64b293391b3a3e1e38490a8f835b11 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:46:53 +0100 Subject: [PATCH 09/20] make format --- cashu/wallet/cli_helpers.py | 2 +- cashu/wallet/wallet.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index b4ba620..0e1e57e 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -3,7 +3,7 @@ import urllib.parse import click -from cashu.core.base import Proof, TokenV2, TokenMintJson, WalletKeyset +from cashu.core.base import Proof, TokenMintJson, TokenV2, WalletKeyset from cashu.core.settings import CASHU_DIR, MINT_URL from cashu.wallet.crud import get_keyset from cashu.wallet.wallet import Wallet as Wallet diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 4f28508..a32803e 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -27,8 +27,8 @@ from cashu.core.base import ( PostMintResponseLegacy, Proof, SplitRequest, - TokenV2, TokenMintJson, + TokenV2, WalletKeyset, ) from cashu.core.bolt11 import Invoice as InvoiceBolt11 From d943e1970c75732c7db183f926afceb8f6bb623e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 14:36:21 +0100 Subject: [PATCH 10/20] show pending tokens in legacy format --- cashu/wallet/cli.py | 26 +++++- cashu/wallet/clihelpers.py | 173 +++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 cashu/wallet/clihelpers.py diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 43e6744..0eabc57 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -48,7 +48,7 @@ from cashu.wallet.crud import ( ) from cashu.wallet.wallet import Wallet as Wallet -from .cli_helpers import ( +from .clihelpers import ( get_mint_wallet, print_mint_balances, proofs_to_token, @@ -309,6 +309,7 @@ async def send(ctx, amount: int, lock: str, legacy: bool): @click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", type=str) @click.option( "--legacy", + "-l", default=False, is_flag=True, help="Print legacy token without mint information.", @@ -387,6 +388,10 @@ async def receive(ctx, token: str, lock: str): # deserialize token dtoken = json.loads(base64.urlsafe_b64decode(token)) + # backwards compatibility < 0.8: V2 tokens with "tokens" field instead of "proofs" field + if "tokens" in dtoken: + dtoken["proofs"] = dtoken.pop("tokens") + assert "proofs" in dtoken, Exception("no proofs in token") includes_mint_info: bool = "mints" in dtoken and dtoken.get("mints") is not None @@ -500,9 +505,17 @@ async def burn(ctx, token: str, all: bool, force: bool): @cli.command("pending", help="Show pending tokens.") +@click.option( + "--legacy", + "-l", + default=False, + is_flag=True, + help="Print legacy token without mint information.", + type=bool, +) @click.pass_context @coro -async def pending(ctx): +async def pending(ctx, legacy): wallet: Wallet = ctx.obj["WALLET"] reserved_proofs = await get_reserved_proofs(wallet.db) if len(reserved_proofs): @@ -513,7 +526,7 @@ async def pending(ctx): ): grouped_proofs = list(value) token = await wallet.serialize_proofs(grouped_proofs) - token_hidden_secret = await wallet.serialize_proofs(grouped_proofs) + # 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") @@ -521,6 +534,13 @@ async def pending(ctx): f"#{i} Amount: {sum_proofs(grouped_proofs)} sat Time: {reserved_date} ID: {key}\n" ) print(f"{token}\n") + + if legacy: + token_legacy = await wallet.serialize_proofs( + grouped_proofs, + legacy=True, + ) + print(f"{token_legacy}\n") print(f"--------------------------\n") print("To remove all spent tokens use: cashu burn -a") wallet.status() diff --git a/cashu/wallet/clihelpers.py b/cashu/wallet/clihelpers.py new file mode 100644 index 0000000..0e1e57e --- /dev/null +++ b/cashu/wallet/clihelpers.py @@ -0,0 +1,173 @@ +import os +import urllib.parse + +import click + +from cashu.core.base import Proof, TokenMintJson, TokenV2, WalletKeyset +from cashu.core.settings import CASHU_DIR, MINT_URL +from cashu.wallet.crud import get_keyset +from cashu.wallet.wallet import Wallet as Wallet + + +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 supports this keyset + mint_keysets = await keyset_wallet._get_keyset_ids(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 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 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 trust this mint and want to 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["proofs"] if Proof(**p).id == keyset + ] + _, _ = 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}: Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat) URL: {k}" + ) + print("") + + +async def get_mint_wallet(ctx): + wallet: Wallet = ctx.obj["WALLET"] + await wallet.load_mint() + + mint_balances = await wallet.balance_per_minturl() + + if len(mint_balances) > 1: + await print_mint_balances(ctx, wallet, show_mints=True) + + mint_nr_str = ( + input(f"Select mint [1-{len(mint_balances)}, press enter for default 1]: ") + or "1" + ) + if not mint_nr_str.isdigit(): + raise Exception("invalid input.") + mint_nr = int(mint_nr_str) + else: + mint_nr = 1 + + 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 + + # load the keys + assert mint_keysets.id + await mint_wallet.load_mint(keyset_id=mint_keysets.id) + + return mint_wallet + + +# LNbits token link parsing +# can extract minut URL from LNbits token links like: +# https://lnbits.server/cashu/wallet?mint_id=aMintId&recv_token=W3siaWQiOiJHY2... +def token_from_lnbits_link(link): + url, token = "", "" + if len(link.split("&recv_token=")) == 2: + # extract URL params + params = urllib.parse.parse_qs(link.split("?")[1]) + # extract URL + if "mint_id" in params: + url = ( + link.split("?")[0].split("/wallet")[0] + + "/api/v1/" + + params["mint_id"][0] + ) + # extract token + token = params["recv_token"][0] + return token, url + else: + return link, "" + + +async def proofs_to_token(wallet, proofs, url: str): + """ + Ingests proofs and + """ + # and add url and keyset id to token + token: TokenV2 = await wallet._make_token(proofs, include_mints=False) + token.mints = {} + + # get keysets of proofs + keysets = list(set([p.id for p in proofs])) + assert keysets is not None, "no keysets" + + # check whether we know the mint urls for these proofs + for k in keysets: + ks = await get_keyset(id=k, db=wallet.db) + url = ks.mint_url if ks and ks.mint_url else "" + + url = url or ( + input(f"Enter mint URL (press enter for default {MINT_URL}): ") or MINT_URL + ) + + token.mints[url] = TokenMintJson(url=url, ks=keysets) # type: ignore + token_serialized = await wallet._serialize_token_base64(token) + return token_serialized From 0d3ef4f8b6ad0d7a8a9d8f738723563ffd0d37be Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 14:36:36 +0100 Subject: [PATCH 11/20] rename clihelpers --- cashu/wallet/cli_helpers.py | 173 ------------------------------------ setup.py | 4 +- 2 files changed, 2 insertions(+), 175 deletions(-) delete mode 100644 cashu/wallet/cli_helpers.py diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py deleted file mode 100644 index 0e1e57e..0000000 --- a/cashu/wallet/cli_helpers.py +++ /dev/null @@ -1,173 +0,0 @@ -import os -import urllib.parse - -import click - -from cashu.core.base import Proof, TokenMintJson, TokenV2, WalletKeyset -from cashu.core.settings import CASHU_DIR, MINT_URL -from cashu.wallet.crud import get_keyset -from cashu.wallet.wallet import Wallet as Wallet - - -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 supports this keyset - mint_keysets = await keyset_wallet._get_keyset_ids(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 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 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 trust this mint and want to 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["proofs"] if Proof(**p).id == keyset - ] - _, _ = 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}: Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat) URL: {k}" - ) - print("") - - -async def get_mint_wallet(ctx): - wallet: Wallet = ctx.obj["WALLET"] - await wallet.load_mint() - - mint_balances = await wallet.balance_per_minturl() - - if len(mint_balances) > 1: - await print_mint_balances(ctx, wallet, show_mints=True) - - mint_nr_str = ( - input(f"Select mint [1-{len(mint_balances)}, press enter for default 1]: ") - or "1" - ) - if not mint_nr_str.isdigit(): - raise Exception("invalid input.") - mint_nr = int(mint_nr_str) - else: - mint_nr = 1 - - 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 - - # load the keys - assert mint_keysets.id - await mint_wallet.load_mint(keyset_id=mint_keysets.id) - - return mint_wallet - - -# LNbits token link parsing -# can extract minut URL from LNbits token links like: -# https://lnbits.server/cashu/wallet?mint_id=aMintId&recv_token=W3siaWQiOiJHY2... -def token_from_lnbits_link(link): - url, token = "", "" - if len(link.split("&recv_token=")) == 2: - # extract URL params - params = urllib.parse.parse_qs(link.split("?")[1]) - # extract URL - if "mint_id" in params: - url = ( - link.split("?")[0].split("/wallet")[0] - + "/api/v1/" - + params["mint_id"][0] - ) - # extract token - token = params["recv_token"][0] - return token, url - else: - return link, "" - - -async def proofs_to_token(wallet, proofs, url: str): - """ - Ingests proofs and - """ - # and add url and keyset id to token - token: TokenV2 = await wallet._make_token(proofs, include_mints=False) - token.mints = {} - - # get keysets of proofs - keysets = list(set([p.id for p in proofs])) - assert keysets is not None, "no keysets" - - # check whether we know the mint urls for these proofs - for k in keysets: - ks = await get_keyset(id=k, db=wallet.db) - url = ks.mint_url if ks and ks.mint_url else "" - - url = url or ( - input(f"Enter mint URL (press enter for default {MINT_URL}): ") or MINT_URL - ) - - token.mints[url] = TokenMintJson(url=url, ks=keysets) # type: ignore - token_serialized = await wallet._serialize_token_base64(token) - return token_serialized diff --git a/setup.py b/setup.py index 267f57f..b0c77a2 100644 --- a/setup.py +++ b/setup.py @@ -14,10 +14,10 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]} setuptools.setup( name="cashu", version="0.8", - description="Ecash wallet and mint with Bitcoin Lightning support", + description="Ecash wallet and mint for Bitcoin Lightning", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/callebtc/cashu", + url="https://github.com/cashubtc/cashu", author="Calle", author_email="calle@protonmail.com", license="MIT", From 482786dcc5f788873d093f8821d2fb0852a679b3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 14:40:54 +0100 Subject: [PATCH 12/20] less balance printing --- cashu/wallet/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 0eabc57..dc20331 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -499,7 +499,7 @@ async def burn(ctx, token: str, all: bool, force: bool): else: # check only the specified ones proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] - wallet.status() + await wallet.invalidate(proofs) wallet.status() @@ -543,7 +543,6 @@ async def pending(ctx, legacy): print(f"{token_legacy}\n") print(f"--------------------------\n") print("To remove all spent tokens use: cashu burn -a") - wallet.status() @cli.command("lock", help="Generate receiving lock.") From f808366eb245f108942341977a66c65c2a0dbedc Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:01:23 +0100 Subject: [PATCH 13/20] clean base models --- cashu/core/base.py | 70 ++++++++++++++++++++++++++++++++++++++-------- docs/specs/00.md | 2 +- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index ec7022d..d68e760 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -6,6 +6,8 @@ from pydantic import BaseModel from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys from cashu.core.secp import PrivateKey, PublicKey +# ------- PROOFS ------- + class P2SHScript(BaseModel): script: str @@ -40,9 +42,13 @@ class Proof(BaseModel): class Proofs(BaseModel): + # NOTE: not used in Pydantic validation __root__: List[Proof] +# ------- LIGHTNING INVOICE ------- + + class Invoice(BaseModel): amount: int pr: str @@ -54,15 +60,10 @@ class Invoice(BaseModel): time_paid: Union[None, str, int, float] = "" -class BlindedMessage(BaseModel): - amount: int - B_: str +# ------- API ------- -class BlindedSignature(BaseModel): - id: Union[str, None] = None - amount: int - C_: str +# ------- API: KEYS ------- class KeysResponse(BaseModel): @@ -73,8 +74,13 @@ class KeysetsResponse(BaseModel): keysets: list[str] -class BlindedMessages(BaseModel): - blinded_messages: List[BlindedMessage] = [] +# ------- API: MINT ------- + + +class BlindedSignature(BaseModel): + id: Union[str, None] = None + amount: int + C_: str class PostMintResponseLegacy(BaseModel): @@ -91,11 +97,31 @@ class GetMintResponse(BaseModel): hash: str +# ------- API: MELT ------- + + +class MeltRequest(BaseModel): + proofs: List[Proof] + invoice: str + + class GetMeltResponse(BaseModel): paid: Union[bool, None] preimage: Union[str, None] +# ------- API: SPLIT ------- + + +class BlindedMessage(BaseModel): + amount: int + B_: str + + +class BlindedMessages(BaseModel): + blinded_messages: List[BlindedMessage] = [] + + class SplitRequest(BaseModel): proofs: List[Proof] amount: int @@ -120,6 +146,9 @@ class PostSplitResponse(BaseModel): snd: List[BlindedSignature] +# ------- API: CHECK ------- + + class CheckRequest(BaseModel): proofs: List[Proof] @@ -132,18 +161,24 @@ class CheckFeesResponse(BaseModel): fee: Union[int, None] -class MeltRequest(BaseModel): - proofs: List[Proof] - invoice: str +# ------- KEYSETS ------- class KeyBase(BaseModel): + """ + Public key from a keyset id for a given amount. + """ + id: str amount: int pubkey: str class WalletKeyset: + """ + Contains the keyset from the wallets's perspective. + """ + id: Union[str, None] public_keys: Union[Dict[int, PublicKey], None] mint_url: Union[str, None] = None @@ -174,6 +209,10 @@ class WalletKeyset: class MintKeyset: + """ + Contains the keyset from the mint's perspective. + """ + id: Union[str, None] derivation_path: str private_keys: Dict[int, PrivateKey] @@ -221,6 +260,10 @@ class MintKeyset: class MintKeysets: + """ + Collection of keyset IDs and the corresponding keyset of the mint. + """ + keysets: Dict[str, MintKeyset] def __init__(self, keysets: List[MintKeyset]): @@ -230,6 +273,9 @@ class MintKeysets: return [k for k, _ in self.keysets.items()] +# ------- TOKEN ------- + + class TokenMintJson(BaseModel): url: str ks: List[str] diff --git a/docs/specs/00.md b/docs/specs/00.md index 6761d70..52b2776 100644 --- a/docs/specs/00.md +++ b/docs/specs/00.md @@ -61,7 +61,7 @@ A `Proof` is also called a `Token` and has the following form: ``` ### `Proofs` -A list of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` can be serialized (see Methods/Serialization [TODO: Link Serialization]) +An array (list) of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` can be serialized (see Methods/Serialization [TODO: Link Serialization]) ## 0.2 - Methods From c34636e2f38849ff7765ad63d1eca0aee122898d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:02:54 +0100 Subject: [PATCH 14/20] rename TokenMintJson to TokenMintV2 --- cashu/core/base.py | 4 ++-- cashu/wallet/clihelpers.py | 4 ++-- cashu/wallet/wallet.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d68e760..2c21a83 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -276,14 +276,14 @@ class MintKeysets: # ------- TOKEN ------- -class TokenMintJson(BaseModel): +class TokenMintV2(BaseModel): url: str ks: List[str] class TokenV2(BaseModel): proofs: List[Proof] - mints: Optional[Dict[str, TokenMintJson]] = None + mints: Optional[Dict[str, TokenMintV2]] = None def to_dict(self): return dict( diff --git a/cashu/wallet/clihelpers.py b/cashu/wallet/clihelpers.py index 0e1e57e..3d8c402 100644 --- a/cashu/wallet/clihelpers.py +++ b/cashu/wallet/clihelpers.py @@ -3,7 +3,7 @@ import urllib.parse import click -from cashu.core.base import Proof, TokenMintJson, TokenV2, WalletKeyset +from cashu.core.base import Proof, TokenMintV2, TokenV2, WalletKeyset from cashu.core.settings import CASHU_DIR, MINT_URL from cashu.wallet.crud import get_keyset from cashu.wallet.wallet import Wallet as Wallet @@ -168,6 +168,6 @@ async def proofs_to_token(wallet, proofs, url: str): input(f"Enter mint URL (press enter for default {MINT_URL}): ") or MINT_URL ) - token.mints[url] = TokenMintJson(url=url, ks=keysets) # type: ignore + token.mints[url] = TokenMintV2(url=url, ks=keysets) # type: ignore token_serialized = await wallet._serialize_token_base64(token) return token_serialized diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index a32803e..1223a8e 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -27,7 +27,7 @@ from cashu.core.base import ( PostMintResponseLegacy, Proof, SplitRequest, - TokenMintJson, + TokenMintV2, TokenV2, WalletKeyset, ) @@ -515,7 +515,7 @@ class Wallet(LedgerAPI): # add mint information to the token, if requested if include_mints: # hold information about the mint - mints: Dict[str, TokenMintJson] = dict() + mints: Dict[str, TokenMintV2] = dict() # iterate through all proofs and add their keyset to `mints` for proof in proofs: if proof.id: @@ -526,7 +526,7 @@ class Wallet(LedgerAPI): placeholder_mint_id = keyset.mint_url if placeholder_mint_id not in mints: # mint information - id = TokenMintJson( + id = TokenMintV2( url=keyset.mint_url, ks=[keyset.id], ) From af0984ccde30cc28e2eb506a0e1d867fedc99f2b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:29:09 +0100 Subject: [PATCH 15/20] clean --- cashu/core/base.py | 10 +++++----- cashu/wallet/cli.py | 2 +- cashu/wallet/wallet.py | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 2c21a83..d03ab98 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -41,11 +41,6 @@ class Proof(BaseModel): self.__setattr__(key, val) -class Proofs(BaseModel): - # NOTE: not used in Pydantic validation - __root__: List[Proof] - - # ------- LIGHTNING INVOICE ------- @@ -276,6 +271,11 @@ class MintKeysets: # ------- TOKEN ------- +class TokenV1(BaseModel): + # NOTE: not used in Pydantic validation + __root__: List[Proof] + + class TokenMintV2(BaseModel): url: str ks: List[str] diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index dc20331..18dbbf5 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -388,7 +388,7 @@ async def receive(ctx, token: str, lock: str): # deserialize token dtoken = json.loads(base64.urlsafe_b64decode(token)) - # backwards compatibility < 0.8: V2 tokens with "tokens" field instead of "proofs" field + # backwards compatibility wallet to wallet < 0.8: V2 tokens renamed "tokens" field to "proofs" if "tokens" in dtoken: dtoken["proofs"] = dtoken.pop("tokens") diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 1223a8e..02188c2 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -514,7 +514,7 @@ class Wallet(LedgerAPI): # add mint information to the token, if requested if include_mints: - # hold information about the mint + # dummy object to hold information about the mint mints: Dict[str, TokenMintV2] = dict() # iterate through all proofs and add their keyset to `mints` for proof in proofs: @@ -535,7 +535,9 @@ class Wallet(LedgerAPI): # 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: + # add dummy object to token token.mints = mints return token From e6f1f1e5d1ca463d98afd20bb4817ef8aaeb7ad8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 19:43:29 +0100 Subject: [PATCH 16/20] edit spec --- docs/specs/00.md | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/specs/00.md b/docs/specs/00.md index 52b2776..172b2fb 100644 --- a/docs/specs/00.md +++ b/docs/specs/00.md @@ -27,7 +27,9 @@ Mint: `Bob` ## 0.1 - Models ### `BlindedMessage` -A encrypted ("blinded") secret and an amount sent from `Alice` to `Bob`. +An encrypted ("blinded") secret and an amount sent from `Alice` to `Bob`. + +`Alice` sends this to `Bob` when she [mints tokens][04]. `amount` is the value of the requested token and `B_` is the encrypted secret message generated by `Alice`. ```json { @@ -39,6 +41,8 @@ A encrypted ("blinded") secret and an amount sent from `Alice` to `Bob`. ### `BlindedSignature` A signature on the `BlindedMessage` sent from `Bob` to `Alice`. +`Bob` sends this to `Alice` when she [mints tokens][04]. `amount` is the value of the blinded token, `C_` is the blinded signature on the secret message `B_` sent in the previous step. `id` is the [keyset id][02] of the mint public keys that signed the token. + ```json { "amount": int, @@ -48,7 +52,9 @@ A signature on the `BlindedMessage` sent from `Bob` to `Alice`. ``` ### `Proof` -A `Proof` is also called a `Token` and has the following form: +A `Proof` is also called a `Token` in its serialized form. `Alice` sends the serialized to `Carol` to initiate a payment. Upon receiving the token, `Carol` deserializes it and requests a split from `Bob` to exchange it for new `BlindedSignature`'s. `Carol` sends the `Proof` to `Bob` together with new `BlindedMessage`'s that she wants to have signed. + +`amount` is the value of the `Proof`, `secret` is the secret message, `C` is the unblinded signature on `secret`, `id` is the [keyset id][02] of the mint public keys that signed the token. `script` is a `P2SHScript` that specifies the spending condition for this `Proof` [TODO: P2SH documentation]. ```json { @@ -61,7 +67,7 @@ A `Proof` is also called a `Token` and has the following form: ``` ### `Proofs` -An array (list) of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` can be serialized (see Methods/Serialization [TODO: Link Serialization]) +An array (list) of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` can be serialized (see Methods/[Serialization](#serialization-of-proofs) [TODO: Link Serialization]) ## 0.2 - Methods @@ -132,4 +138,25 @@ When serialized, this becomes: ``` eyJwcm9vZnMiOlt7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50IjoyLCJzZWNyZXQiOiJiZFlDYkhHT051bmRMZVl2djFQNWRRIiwiQyI6IjAyZTYxMTdmYjFiMTYzM2E4YzE2NTdlZDM0YWIyNWVjZjhkNDk3NDA5MTE3OWM0NzczZWM1OWY4NWY0ZTM5OTFjZiJ9LHsiaWQiOiJEU0FsOW52dnlmdmEiLCJhbW91bnQiOjgsInNlY3JldCI6Ikt4eVVQdDVNdXJfLVJWOHBDRUNKNkEiLCJDIjoiMDNiOWRjZGI3ZjE5NWUwNzIxOGI5NWI3YzJkYWRjODI4OTE1OWZjNDQwNDc0Mzk4MzBmNzY1YjhjNTBiZmI2YmRhIn1dLCJtaW50cyI6eyJNSU5UX05BTUUiOnsidXJsIjoiaHR0cDovL3NlcnZlci5ob3N0OjMzMzkiLCJrcyI6WyJEU0FsOW52dnlmdmEiXX19fQ== -``` \ No newline at end of file +``` + +[00]: 00.md +[01]: 02.md +[03]: 03.md +[04]: 04.md +[05]: 05.md +[06]: 06.md +[07]: 07.md +[08]: 08.md +[09]: 09.md +[10]: 10.md +[11]: 11.md +[12]: 12.md +[13]: 13.md +[14]: 14.md +[15]: 15.md +[16]: 16.md +[17]: 17.md +[18]: 18.md +[19]: 19.md +[20]: 20.md \ No newline at end of file From 3b99be52b94cb17b22a3835460443a46a4eed650 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 19:50:46 +0100 Subject: [PATCH 17/20] update spec --- docs/specs/00.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/specs/00.md b/docs/specs/00.md index 172b2fb..a7f6c0d 100644 --- a/docs/specs/00.md +++ b/docs/specs/00.md @@ -27,9 +27,8 @@ Mint: `Bob` ## 0.1 - Models ### `BlindedMessage` -An encrypted ("blinded") secret and an amount sent from `Alice` to `Bob`. -`Alice` sends this to `Bob` when she [mints tokens][04]. `amount` is the value of the requested token and `B_` is the encrypted secret message generated by `Alice`. +An encrypted ("blinded") secret and an amount sent from `Alice` to `Bob` before [minting new tokens][04] ```json { @@ -38,10 +37,11 @@ An encrypted ("blinded") secret and an amount sent from `Alice` to `Bob`. } ``` -### `BlindedSignature` -A signature on the `BlindedMessage` sent from `Bob` to `Alice`. + `amount` is the value of the requested token and `B_` is the encrypted secret message generated by `Alice`. -`Bob` sends this to `Alice` when she [mints tokens][04]. `amount` is the value of the blinded token, `C_` is the blinded signature on the secret message `B_` sent in the previous step. `id` is the [keyset id][02] of the mint public keys that signed the token. +### `BlindedSignature` + +A signature on the `BlindedMessage` sent from `Bob` to `Alice` after [minting new tokens][04]. ```json { @@ -51,10 +51,11 @@ A signature on the `BlindedMessage` sent from `Bob` to `Alice`. } ``` -### `Proof` -A `Proof` is also called a `Token` in its serialized form. `Alice` sends the serialized to `Carol` to initiate a payment. Upon receiving the token, `Carol` deserializes it and requests a split from `Bob` to exchange it for new `BlindedSignature`'s. `Carol` sends the `Proof` to `Bob` together with new `BlindedMessage`'s that she wants to have signed. +`amount` is the value of the blinded token, `C_` is the blinded signature on the secret message `B_` sent in the previous step. `id` is the [keyset id][02] of the mint public keys that signed the token. -`amount` is the value of the `Proof`, `secret` is the secret message, `C` is the unblinded signature on `secret`, `id` is the [keyset id][02] of the mint public keys that signed the token. `script` is a `P2SHScript` that specifies the spending condition for this `Proof` [TODO: P2SH documentation]. +### `Proof` + +A `Proof` is also called a `Token` in its serialized form. `Alice` sends the serialized to `Carol` to initiate a payment. Upon receiving the token, `Carol` deserializes it and requests a split from `Bob` to exchange it for new `BlindedSignature`'s. `Carol` sends the `Proof` to `Bob` together with new `BlindedMessage`'s that she wants to have signed. ```json { @@ -66,8 +67,11 @@ A `Proof` is also called a `Token` in its serialized form. `Alice` sends the ser } ``` +`amount` is the value of the `Proof`, `secret` is the secret message, `C` is the unblinded signature on `secret`, `id` is the [keyset id][02] of the mint public keys that signed the token. `script` is a `P2SHScript` that specifies the spending condition for this `Proof` [TODO: P2SH documentation]. + ### `Proofs` -An array (list) of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` can be serialized (see Methods/[Serialization](#serialization-of-proofs) [TODO: Link Serialization]) + +An array (list) of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` must be serialized before sending between wallets (see [Serialization](#serialization-of-proofs). ## 0.2 - Methods From 19249aa5ffb058ee57d6d2030b803ec41b77c0cf Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 20:02:31 +0100 Subject: [PATCH 18/20] formatting --- docs/specs/00.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/specs/00.md b/docs/specs/00.md index a7f6c0d..5eab8d9 100644 --- a/docs/specs/00.md +++ b/docs/specs/00.md @@ -32,8 +32,8 @@ An encrypted ("blinded") secret and an amount sent from `Alice` to `Bob` before ```json { - "amount": int, - "B_": str + "amount": int, + "B_": str } ``` @@ -45,9 +45,9 @@ A signature on the `BlindedMessage` sent from `Bob` to `Alice` after [minting ne ```json { - "amount": int, - "C_": str, - "id": str | None + "amount": int, + "C_": str, + "id": str | None } ``` @@ -59,11 +59,11 @@ A `Proof` is also called a `Token` in its serialized form. `Alice` sends the ser ```json { - "amount": int, - "secret": str, - "C": str, - "id": None | str, - "script": P2SHScript | None, + "amount": int, + "secret": str, + "C": str, + "id": None | str, + "script": P2SHScript | None, } ``` From 2f19890eefbd42011f3179504fcf10a178ddc97a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 20:03:46 +0100 Subject: [PATCH 19/20] whitespace formatting --- docs/specs/03.md | 4 +-- docs/specs/04.md | 80 ++++++++++++++++++++++++------------------------ docs/specs/05.md | 34 ++++++++++---------- docs/specs/06.md | 56 ++++++++++++++++----------------- 4 files changed, 87 insertions(+), 87 deletions(-) diff --git a/docs/specs/03.md b/docs/specs/03.md index f39b4c3..0106358 100644 --- a/docs/specs/03.md +++ b/docs/specs/03.md @@ -22,8 +22,8 @@ Response of `Bob`: ```json { - "pr": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q...", - "hash": "67d1d9ea6ada225c115418671b64a..." + "pr": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q...", + "hash": "67d1d9ea6ada225c115418671b64a..." } ``` diff --git a/docs/specs/04.md b/docs/specs/04.md index 24680f1..e1c3761 100644 --- a/docs/specs/04.md +++ b/docs/specs/04.md @@ -16,11 +16,11 @@ With the data being of the form `BlindedMessages`: ```json { - "blinded_messages": - [ - BlindedMessage, - ... - ] + "blinded_messages": + [ + BlindedMessage, + ... + ] } ``` @@ -30,18 +30,18 @@ With curl: ```bash curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \ { - "blinded_messages": - [ - { - "amount": 2, - "B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239" - }, - { - "amount": 8, - "B_": "03b54ab451b15005f2c64d38fc512fca695914c8fd5094ee044e5724ad41fda247" - - } - ] + "blinded_messages": + [ + { + "amount": 2, + "B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239" + }, + { + "amount": 8, + "B_": "03b54ab451b15005f2c64d38fc512fca695914c8fd5094ee044e5724ad41fda247" + + } + ] } ``` @@ -52,18 +52,18 @@ If the invoice was successfully paid, `Bob` responds with a `PostMintResponse` w ```json { "promises": - [ - { - "id": "DSAl9nvvyfva", - "amount": 2, - "C_": "03e61daa438fc7bcc53f6920ec6c8c357c24094fb04c1fc60e2606df4910b21ffb" - }, - { - "id": "DSAl9nvvyfva", - "amount": 8, - "C_": "03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de" - }, - ] + [ + { + "id": "DSAl9nvvyfva", + "amount": 2, + "C_": "03e61daa438fc7bcc53f6920ec6c8c357c24094fb04c1fc60e2606df4910b21ffb" + }, + { + "id": "DSAl9nvvyfva", + "amount": 8, + "C_": "03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de" + }, + ] } ``` @@ -79,16 +79,16 @@ A list multiple `Proof`'s is called `Proofs` and has the form: ```json { "proofs" : - [ - { - "id": "DSAl9nvvyfva", - "amount": 2, - "secret": "S+tDfc1Lfsrb06zaRdVTed6Izg", - "C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3" - }, - { - ... - } - ] + [ + { + "id": "DSAl9nvvyfva", + "amount": 2, + "secret": "S+tDfc1Lfsrb06zaRdVTed6Izg", + "C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3" + }, + { + ... + } + ] } ``` \ No newline at end of file diff --git a/docs/specs/05.md b/docs/specs/05.md index 1cfb716..70c1415 100644 --- a/docs/specs/05.md +++ b/docs/specs/05.md @@ -16,12 +16,12 @@ With the data being of the form `MeltRequest`: ```json { - "proofs": - [ - Proof, - ... - ], - "invoice": str + "proofs": + [ + Proof, + ... + ], + "invoice": str } ``` @@ -32,17 +32,17 @@ With curl: curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \ { "proofs" : - [ - { - "id": "DSAl9nvvyfva", - "amount": 2, - "secret": "S+tDfc1Lfsrb06zaRdVTed6Izg", - "C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3" - }, - { - ... - } - ], + [ + { + "id": "DSAl9nvvyfva", + "amount": 2, + "secret": "S+tDfc1Lfsrb06zaRdVTed6Izg", + "C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3" + }, + { + ... + } + ], "invoice": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q..." } ``` diff --git a/docs/specs/06.md b/docs/specs/06.md index a5de2e7..2ad3fe8 100644 --- a/docs/specs/06.md +++ b/docs/specs/06.md @@ -26,9 +26,9 @@ With the data being of the form `SplitRequest`: ```json { - "proofs": Proofs, - "outputs": BlindedMessages, - "amount": int + "proofs": Proofs, + "outputs": BlindedMessages, + "amount": int } ``` @@ -37,31 +37,31 @@ With curl: ```bash curl -X POST https://mint.host:3338/split -d \ { - "proofs": - [ - { - "id": "DSAl9nvvyfva", - "amount": 2, - "secret": "S+tDfc1Lfsrb06zaRdVTed6Izg", - "C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3" - }, - { - ... - } - ], - "outputs":{ - "blinded_messages": - [ - { - "amount": 2, - "B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239" - }, - { - ... - } - ] - }, - "amount": 40 + "proofs": + [ + { + "id": "DSAl9nvvyfva", + "amount": 2, + "secret": "S+tDfc1Lfsrb06zaRdVTed6Izg", + "C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3" + }, + { + ... + } + ], + "outputs":{ + "blinded_messages": + [ + { + "amount": 2, + "B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239" + }, + { + ... + } + ] + }, + "amount": 40 } ``` From e05a8313b46da84428259a0a0d90ed268ef34283 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 14 Jan 2023 20:21:55 +0100 Subject: [PATCH 20/20] fix format --- docs/specs/00.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/specs/00.md b/docs/specs/00.md index 5eab8d9..6774f9c 100644 --- a/docs/specs/00.md +++ b/docs/specs/00.md @@ -5,24 +5,27 @@ Receiving user: `Carol` Mint: `Bob` ## Bob (mint) + - `k` private key of mint (one for each amount) - `K` public key of mint - `Q` promise (blinded signature) ## Alice (user) + - `x` random string (secret message), corresponds to point `Y` on curve - `r` private key (blinding factor) - `T` blinded message - `Z` proof (unblinded signature) # Blind Diffie-Hellmann key exchange (BDHKE) -- Mint `Bob` publishes `K = kG` -- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)` -- `Alice` sends to `Bob`: `T = Y + rG` with `r` being a random nonce -- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange) -- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z` -- Alice can take the pair `(x, Z)` as a token and can send it to `Carol`. -- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. + +- Mint `Bob` publishes `K = kG` +- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)` +- `Alice` sends to `Bob`: `T = Y + rG` with `r` being a random nonce +- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange) +- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z` +- Alice can take the pair `(x, Z)` as a token and can send it to `Carol`. +- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. ## 0.1 - Models @@ -99,6 +102,7 @@ This token format is a list of `Proof`s. Each `Proof` contains the keyset id in "secret": "d_PPc5KpuAB2M60WYAW5-Q", "C": "0270e0a37f7a0b21eab43af751dd3c03f61f04c626c0448f603f1d1f5ae5a7d7e6" } +] ``` When serialized, this becomes: