Fix/tokenv2mints (#98)

* adjust tokenv2 and make it backwards compatible

* fix dict to list

* use pydantic object and not the dtoken

* make format

* fix typo in _meltrequest_include_fields

* reorder functions

* fixes and tests working

* bump version to 0.8.3
This commit is contained in:
calle
2023-01-19 08:35:32 +01:00
committed by GitHub
parent 68b5b54c9b
commit 2dd9fd356f
13 changed files with 285 additions and 177 deletions

View File

@@ -115,7 +115,7 @@ cashu info
Returns:
```bash
Version: 0.8.2
Version: 0.8.3
Debug: False
Cashu dir: /home/user/.cashu
Wallet: wallet

View File

@@ -10,28 +10,42 @@ from cashu.core.secp import PrivateKey, PublicKey
class P2SHScript(BaseModel):
"""
Describes spending condition of a Proof
"""
script: str
signature: str
address: Union[str, None] = None
class Proof(BaseModel):
"""
Value token
"""
id: Union[
None, str
] = "" # NOTE: None for backwards compatibility for old clients that do not include the keyset id < 0.3
amount: int = 0
secret: str = ""
C: str = ""
script: Union[P2SHScript, None] = None
reserved: Union[None, bool] = False # whether this proof is reserved for sending
send_id: Union[None, str] = "" # unique ID of send attempt
secret: str = "" # secret or message to be blinded and signed
C: str = "" # signature on secret, unblinded by wallet
script: Union[P2SHScript, None] = None # P2SH spending condition
reserved: Union[
None, bool
] = False # whether this proof is reserved for sending, used for coin management in the wallet
send_id: Union[
None, str
] = "" # unique ID of send attempt, used for grouping pending tokens in the wallet
time_created: Union[None, str] = ""
time_reserved: Union[None, str] = ""
def to_dict(self):
# dictionary without the fields that don't need to be send to Carol
return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C)
def to_dict_no_secret(self):
# dictionary but without the secret itself
return dict(id=self.id, amount=self.amount, C=self.C)
def __getitem__(self, key):
@@ -47,14 +61,22 @@ class Proofs(BaseModel):
class BlindedMessage(BaseModel):
"""
Blinded message or blinded secret or "output" which is to be signed by the mint
"""
amount: int
B_: str
B_: str # Hex-encoded blinded message
class BlindedSignature(BaseModel):
"""
Blinded signature or "promise" which is the signature on a `BlindedMessage`
"""
id: Union[str, None] = None
amount: int
C_: str
C_: str # Hex-encoded signature
class BlindedMessages(BaseModel):
@@ -114,7 +136,7 @@ class GetMintResponse(BaseModel):
# ------- API: MELT -------
class MeltRequest(BaseModel):
class PostMeltRequest(BaseModel):
proofs: List[Proof]
invoice: str
@@ -127,7 +149,7 @@ class GetMeltResponse(BaseModel):
# ------- API: SPLIT -------
class SplitRequest(BaseModel):
class PostSplitRequest(BaseModel):
proofs: List[Proof]
amount: int
outputs: List[BlindedMessage]
@@ -141,11 +163,11 @@ class PostSplitResponse(BaseModel):
# ------- API: CHECK -------
class CheckRequest(BaseModel):
class GetCheckSpendableRequest(BaseModel):
proofs: List[Proof]
class CheckFeesRequest(BaseModel):
class GetCheckFeesRequest(BaseModel):
pr: str
@@ -269,21 +291,37 @@ class MintKeysets:
class TokenV1(BaseModel):
"""
A (legacy) Cashu token that includes proofs. This can only be received if the receiver knows the mint associated with the
keyset ids of the proofs.
"""
# NOTE: not used in Pydantic validation
__root__: List[Proof]
class TokenMintV2(BaseModel):
url: str
ks: List[str]
class TokenV2Mint(BaseModel):
"""
Object that describes how to reach the mints associated with the proofs in a TokenV2 object.
"""
url: str # mint URL
ids: List[str] # List of keyset id's that are from this mint
class TokenV2(BaseModel):
"""
A Cashu token that includes proofs and their respective mints. Can include proofs from multiple different mints and keysets.
"""
proofs: List[Proof]
mints: Optional[Dict[str, TokenMintV2]] = None
mints: Optional[List[TokenV2Mint]] = None
def to_dict(self):
return dict(
proofs=[p.to_dict() for p in self.proofs],
mints={k: v.dict() for k, v in self.mints.items()}, # type: ignore
)
if self.mints:
return dict(
proofs=[p.to_dict() for p in self.proofs],
mints=[m.dict() for m in self.mints],
)
else:
return dict(proofs=[p.to_dict() for p in self.proofs])

View File

@@ -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.8.2"
VERSION = "0.8.3"

View File

@@ -6,18 +6,18 @@ from secp256k1 import PublicKey
from cashu.core.base import (
BlindedMessage,
BlindedSignature,
CheckFeesRequest,
CheckFeesResponse,
CheckRequest,
GetCheckFeesRequest,
GetCheckSpendableRequest,
GetMeltResponse,
GetMintResponse,
KeysetsResponse,
KeysResponse,
MeltRequest,
PostMeltRequest,
PostMintRequest,
PostMintResponse,
PostSplitRequest,
PostSplitResponse,
SplitRequest,
)
from cashu.core.errors import CashuError
from cashu.mint.startup import ledger
@@ -25,20 +25,28 @@ from cashu.mint.startup import ledger
router: APIRouter = APIRouter()
@router.get("/keys")
@router.get(
"/keys",
name="Mint public keys",
summary="Get the public keys of the newest mint keyset",
)
async def keys() -> KeysResponse:
"""Get the public keys of the mint of the newest keyset"""
"""This endpoint returns a dictionary of all supported token values of the mint and their associated public key."""
keyset = ledger.get_keyset()
keys = KeysResponse.parse_obj(keyset)
return keys
@router.get("/keys/{idBase64Urlsafe}")
@router.get(
"/keys/{idBase64Urlsafe}",
name="Keyset public keys",
summary="Public keys of a specific keyset",
)
async def keyset_keys(idBase64Urlsafe: str) -> KeysResponse:
"""
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.
Get the public keys of the mint from a specific keyset id.
The id is encoded in idBase64Urlsafe (by a wallet) and is converted back to
normal base64 before it can be processed (by the mint).
"""
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
keyset = ledger.get_keyset(keyset_id=id)
@@ -46,14 +54,16 @@ async def keyset_keys(idBase64Urlsafe: str) -> KeysResponse:
return keys
@router.get("/keysets")
@router.get(
"/keysets", name="Active keysets", summary="Get all active keyset id of the mind"
)
async def keysets() -> KeysetsResponse:
"""Get all active keyset ids of the mint"""
"""This endpoint returns a list of keysets that the mint currently supports and will accept tokens from."""
keysets = KeysetsResponse(keysets=ledger.keysets.get_ids())
return keysets
@router.get("/mint")
@router.get("/mint", name="Request mint", summary="Request minting of new tokens")
async def request_mint(amount: int = 0) -> GetMintResponse:
"""
Request minting of new tokens. The mint responds with a Lightning invoice.
@@ -67,7 +77,11 @@ async def request_mint(amount: int = 0) -> GetMintResponse:
return resp
@router.post("/mint")
@router.post(
"/mint",
name="Mint tokens",
summary="Mint tokens in exchange for a Bitcoin paymemt that the user has made",
)
async def mint(
payload: PostMintRequest,
payment_hash: Union[str, None] = None,
@@ -85,8 +99,12 @@ async def mint(
return CashuError(code=0, error=str(exc))
@router.post("/melt")
async def melt(payload: MeltRequest) -> GetMeltResponse:
@router.post(
"/melt",
name="Melt tokens",
summary="Melt tokens for a Bitcoin payment that the mint will make for the user in exchange",
)
async def melt(payload: PostMeltRequest) -> GetMeltResponse:
"""
Requests tokens to be destroyed and sent out via Lightning.
"""
@@ -95,30 +113,41 @@ async def melt(payload: MeltRequest) -> GetMeltResponse:
return resp
@router.post("/check")
async def check_spendable(payload: CheckRequest) -> Dict[int, bool]:
@router.post(
"/check",
name="Check spendable",
summary="Check whether a proof has already been spent",
)
async def check_spendable(payload: GetCheckSpendableRequest) -> Dict[int, bool]:
"""Check whether a secret has been spent already or not."""
return await ledger.check_spendable(payload.proofs)
@router.post("/checkfees")
async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
@router.post(
"/checkfees",
name="Check fees",
summary="Check fee reserve for a Lightning payment",
)
async def check_fees(payload: GetCheckFeesRequest) -> CheckFeesResponse:
"""
Responds with the fees necessary to pay a Lightning invoice.
Used by wallets for figuring out the fees they need to supply.
Used by wallets for figuring out the fees they need to supply together with the payment amount.
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)
@router.post("/split")
@router.post("/split", name="Split", summary="Split proofs at a specified amount")
async def split(
payload: SplitRequest,
payload: PostSplitRequest,
) -> Union[CashuError, PostSplitResponse]:
"""
Requetst a set of tokens with amount "total" to be split into two
newly minted sets with amount "split" and "total-split".
This endpoint is used by Alice to split a set of proofs before making a payment to Carol.
It is then used by Carol (by setting split=total) to redeem the tokens.
"""
assert payload.outputs, Exception("no outputs provided.")
try:

View File

@@ -7,7 +7,6 @@ import os
import sys
import threading
import time
import urllib.parse
from datetime import datetime
from functools import wraps
from itertools import groupby
@@ -19,7 +18,7 @@ from typing import Dict, List
import click
from loguru import logger
from cashu.core.base import Proof
from cashu.core.base import Proof, TokenV2
from cashu.core.helpers import sum_proofs
from cashu.core.migrations import migrate_databases
from cashu.core.settings import (
@@ -51,7 +50,7 @@ from cashu.wallet.wallet import Wallet as Wallet
from .clihelpers import (
get_mint_wallet,
print_mint_balances,
proofs_to_token,
proofs_to_serialized_tokenv2,
redeem_multimint,
token_from_lnbits_link,
verify_mints,
@@ -379,7 +378,7 @@ async def receive(ctx, token: str, lock: str):
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))]
# we take the proofs parsed from the old format token and produce a new format token with it
token = await proofs_to_token(wallet, proofs, url)
token = await proofs_to_serialized_tokenv2(wallet, proofs, url)
except:
pass
@@ -392,17 +391,24 @@ async def receive(ctx, token: str, lock: str):
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
# backwards compatibility wallet to wallet < 0.8.3: V2 tokens got rid of the "MINT_NAME" key in "mints" and renamed "ks" to "ids"
if "mints" in dtoken and isinstance(dtoken["mints"], dict):
dtoken["mints"] = list(dtoken["mints"].values())
for m in dtoken["mints"]:
m["ids"] = m.pop("ks")
tokenObj = TokenV2.parse_obj(dtoken)
assert len(tokenObj.proofs), Exception("no proofs in token")
includes_mint_info: bool = tokenObj.mints is not None and len(tokenObj.mints) > 0
# 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)
await verify_mints(ctx, tokenObj)
# redeem tokens with new wallet instances
await redeem_multimint(ctx, dtoken, script, signature)
await redeem_multimint(ctx, tokenObj, script, signature)
# reload main wallet so the balance updates
await wallet.load_proofs()
else:

View File

@@ -1,40 +1,52 @@
import os
import urllib.parse
from typing import List
import click
from loguru import logger
from cashu.core.base import Proof, TokenMintV2, TokenV2, WalletKeyset
from cashu.core.base import Proof, TokenV2, TokenV2Mint, 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):
async def verify_mints(ctx, token: TokenV2):
"""
A helper function that iterates through all mints in the token and if it has
not been encountered before, asks the user to confirm.
It will instantiate a Wallet with each keyset and check whether the mint supports it.
It will then get the keys for that keyset from the mint and check whether the keyset id is correct.
"""
if token.mints is None:
return
logger.debug(f"Verifying mints")
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"]
for mint in token.mints:
for keyset in set(mint.ids):
# init a temporary wallet object
keyset_wallet = Wallet(
mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])
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)
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)
# we validate the keyset id by fetching the keys from the mint and computing the id locally
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)
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 URL: {mint.url}")
print(f"Mint keyset: {keyset}")
print("\n")
click.confirm(
@@ -43,34 +55,43 @@ async def verify_mints(ctx, dtoken):
default=True,
)
trust_token_mints = True
else:
logger.debug(f"We know keyset {mint_keysets.id} already")
assert trust_token_mints, Exception("Aborted!")
async def redeem_multimint(ctx, dtoken, script, signature):
async def redeem_multimint(ctx, token: TokenV2, script, signature):
"""
Helper function to iterate thruogh a token with multiple mints and redeem them from
these mints one keyset at a time.
"""
# 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"]
if token.mints is None:
return
for mint in token.mints:
for keyset in set(mint.ids):
logger.debug(f"Redeeming tokens from keyset {keyset}")
# init a temporary wallet object
keyset_wallet = Wallet(
mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])
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
]
redeem_proofs = [p for p in token.proofs if 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):
"""
Helper function that prints the balances for each mint URL that we have tokens from.
"""
# get balances per mint
mint_balances = await wallet.balance_per_minturl()
@@ -94,6 +115,10 @@ async def print_mint_balances(ctx, wallet, show_mints=False):
async def get_mint_wallet(ctx):
"""
Helper function that asks the user for an input to select which mint they want to load.
Useful for selecting the mint that the user wants to send tokens from.
"""
wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint()
@@ -147,17 +172,16 @@ def token_from_lnbits_link(link):
return link, ""
async def proofs_to_token(wallet, proofs, url: str):
async def proofs_to_serialized_tokenv2(wallet, proofs: List[Proof], url: str):
"""
Ingests proofs and
Ingests list of proofs and produces a serialized TokenV2
"""
# and add url and keyset id to token
token: TokenV2 = await wallet._make_token(proofs, include_mints=False)
token.mints = {}
token.mints = []
# get keysets of proofs
keysets = list(set([p.id for p in proofs]))
assert keysets is not None, "no keysets"
keysets = list(set([p.id for p in proofs if p.id is not None]))
# check whether we know the mint urls for these proofs
for k in keysets:
@@ -168,6 +192,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] = TokenMintV2(url=url, ks=keysets) # type: ignore
token.mints.append(TokenV2Mint(url=url, ids=keysets))
token_serialized = await wallet._serialize_token_base64(token)
return token_serialized

View File

@@ -15,20 +15,20 @@ import cashu.core.bolt11 as bolt11
from cashu.core.base import (
BlindedMessage,
BlindedSignature,
CheckFeesRequest,
CheckRequest,
GetCheckFeesRequest,
GetCheckSpendableRequest,
GetMintResponse,
Invoice,
KeysetsResponse,
MeltRequest,
P2SHScript,
PostMeltRequest,
PostMintRequest,
PostMintResponse,
PostMintResponseLegacy,
PostSplitRequest,
Proof,
SplitRequest,
TokenMintV2,
TokenV2,
TokenV2Mint,
WalletKeyset,
)
from cashu.core.bolt11 import Invoice as InvoiceBolt11
@@ -302,7 +302,7 @@ class LedgerAPI:
), "number of secrets does not match number of outputs"
await self._check_used_secrets(secrets)
outputs, rs = self._construct_outputs(amounts, secrets)
split_payload = SplitRequest(proofs=proofs, amount=amount, outputs=outputs)
split_payload = PostSplitRequest(proofs=proofs, amount=amount, outputs=outputs)
# construct payload
def _splitrequest_include_fields(proofs):
@@ -339,7 +339,7 @@ class LedgerAPI:
"""
Cheks whether the secrets in proofs are already spent or not and returns a list of booleans.
"""
payload = CheckRequest(proofs=proofs)
payload = GetCheckSpendableRequest(proofs=proofs)
def _check_spendable_include_fields(proofs):
"""strips away fields from the model that aren't necessary for the /split"""
@@ -359,7 +359,7 @@ class LedgerAPI:
async def check_fees(self, payment_request: str):
"""Checks whether the Lightning payment is internal."""
payload = CheckFeesRequest(pr=payment_request)
payload = GetCheckFeesRequest(pr=payment_request)
self.s = self._set_requests()
resp = self.s.post(
self.url + "/checkfees",
@@ -374,9 +374,9 @@ class LedgerAPI:
"""
Accepts proofs and a lightning invoice to pay in exchange.
"""
payload = MeltRequest(proofs=proofs, invoice=invoice)
payload = PostMeltRequest(proofs=proofs, invoice=invoice)
def _meltequest_include_fields(proofs):
def _meltrequest_include_fields(proofs):
"""strips away fields from the model that aren't necessary for the /melt"""
proofs_include = {"id", "amount", "secret", "C", "script"}
return {
@@ -388,7 +388,7 @@ class LedgerAPI:
self.s = self._set_requests()
resp = self.s.post(
self.url + "/melt",
json=payload.dict(include=_meltequest_include_fields(proofs)), # type: ignore
json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore
)
resp.raise_for_status()
return_dict = resp.json()
@@ -404,6 +404,9 @@ class Wallet(LedgerAPI):
self.db = Database("wallet", db)
self.proofs: List[Proof] = []
self.name = name
logger.debug(f"Wallet initalized with mint URL {url}")
# ---------- API ----------
async def load_mint(self, keyset_id: str = ""):
await super()._load_mint(keyset_id)
@@ -411,28 +414,6 @@ class Wallet(LedgerAPI):
async def load_proofs(self):
self.proofs = await get_proofs(db=self.db)
async def _store_proofs(self, proofs):
for proof in proofs:
await store_proof(proof, db=self.db)
@staticmethod
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]):
if id is None:
continue
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:
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())
@@ -502,6 +483,33 @@ class Wallet(LedgerAPI):
raise Exception("could not pay invoice.")
return status["paid"]
async def check_spendable(self, proofs):
return await super().check_spendable(proofs)
# ---------- TOKEN MECHANIS ----------
async def _store_proofs(self, proofs):
for proof in proofs:
await store_proof(proof, db=self.db)
@staticmethod
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]):
if id is None:
continue
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:
ret[keyset.mint_url].extend([p for p in proofs if p.id == id])
return ret
async def _make_token(self, proofs: List[Proof], include_mints=True):
"""
Takes list of proofs and produces a TokenV2 by looking up
@@ -509,34 +517,33 @@ class Wallet(LedgerAPI):
"""
# build token
token = TokenV2(proofs=proofs)
# add mint information to the token, if requested
if include_mints:
# dummy object to hold information about the mint
mints: Dict[str, TokenMintV2] = dict()
# iterate through all proofs and add their keyset to `mints`
mints: Dict[str, TokenV2Mint] = {}
# dummy object to hold all keyset id's we need to fetch from the db later
keysets: List[str] = []
# iterate through all proofs and remember their keyset ids for the next step
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 and keyset.id:
# TODO: replace this with a mint pubkey
placeholder_mint_id = keyset.mint_url
if placeholder_mint_id not in mints:
# mint information
id = TokenMintV2(
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)
keysets.append(proof.id)
# iterate through unique keyset ids
for id in set(keysets):
# load the keyset from the db
keyset = await get_keyset(id=id, db=self.db)
if keyset and keyset.mint_url and keyset.id:
# we group all mints according to URL
if keyset.mint_url not in mints:
mints[keyset.mint_url] = TokenV2Mint(
url=keyset.mint_url,
ids=[keyset.id],
)
else:
# if a mint URL has multiple keysets, append to the already existing list
mints[keyset.mint_url].ids.append(keyset.id)
if len(mints) > 0:
# add dummy object to token
token.mints = mints
# add mints grouped by url to the token
token.mints = list(mints.values())
return token
async def _serialize_token_base64(self, token: TokenV2):
@@ -589,6 +596,30 @@ class Wallet(LedgerAPI):
send_proofs.append(sorted_proofs[len(send_proofs)])
return send_proofs
async def set_reserved(self, proofs: List[Proof], reserved: bool):
"""Mark a proof as reserved to avoid reuse or delete marking."""
uuid_str = str(uuid.uuid1())
for proof in proofs:
proof.reserved = True
await update_proof_reserved(
proof, reserved=reserved, send_id=uuid_str, db=self.db
)
async def invalidate(self, proofs):
"""Invalidates all spendable tokens supplied in proofs."""
spendables = await self.check_spendable(proofs)
invalidated_proofs = []
for idx, spendable in spendables.items():
if not spendable:
invalidated_proofs.append(proofs[int(idx)])
await invalidate_proof(proofs[int(idx)], db=self.db)
invalidate_secrets = [p["secret"] for p in invalidated_proofs]
self.proofs = list(
filter(lambda p: p["secret"] not in invalidate_secrets, self.proofs)
)
# ---------- TRANSACTION HELPERS ----------
async def get_pay_amount_with_fees(self, invoice: str):
"""
Decodes the amount from a Lightning invoice and returns the
@@ -628,30 +659,7 @@ class Wallet(LedgerAPI):
await self.set_reserved(send_proofs, reserved=True)
return keep_proofs, send_proofs
async def set_reserved(self, proofs: List[Proof], reserved: bool):
"""Mark a proof as reserved to avoid reuse or delete marking."""
uuid_str = str(uuid.uuid1())
for proof in proofs:
proof.reserved = True
await update_proof_reserved(
proof, reserved=reserved, send_id=uuid_str, db=self.db
)
async def check_spendable(self, proofs):
return await super().check_spendable(proofs)
async def invalidate(self, proofs):
"""Invalidates all spendable tokens supplied in proofs."""
spendables = await self.check_spendable(proofs)
invalidated_proofs = []
for idx, spendable in spendables.items():
if not spendable:
invalidated_proofs.append(proofs[int(idx)])
await invalidate_proof(proofs[int(idx)], db=self.db)
invalidate_secrets = [p["secret"] for p in invalidated_proofs]
self.proofs = list(
filter(lambda p: p["secret"] not in invalidate_secrets, self.proofs)
)
# ---------- P2SH ----------
async def create_p2sh_lock(self):
alice_privkey = step0_carol_privkey()
@@ -668,6 +676,8 @@ class Wallet(LedgerAPI):
await store_p2sh(p2shScript, db=self.db)
return p2shScript
# ---------- BALANCE CHECKS ----------
@property
def balance(self):
return sum_proofs(self.proofs)

View File

@@ -113,7 +113,7 @@ W3siaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiRGJSS0l5YTBldGR3
#### 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.
This token format includes information about the mint as well. The field `proofs` is like a V1 token. Additionally, the field `mints` can include an array (list) of multiple mints from which the `proofs` are from. The `url` field is the URL of the mint. `ids` is a list of the keyset IDs belonging to this mint. It is important that all keyset IDs of the `proofs` must be present here to allow a wallet to map each proof to a mint.
##### Example JSON:
@@ -123,29 +123,30 @@ This token format includes information about the mint as well. The field `proofs
{
"id": "DSAl9nvvyfva",
"amount": 2,
"secret": "bdYCbHGONundLeYvv1P5dQ",
"C": "02e6117fb1b1633a8c1657ed34ab25ecf8d4974091179c4773ec59f85f4e3991cf"
"secret": "EhpennC9qB3iFlW8FZ_pZw",
"C": "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4"
},
{
"id": "DSAl9nvvyfva",
"amount": 8,
"secret": "KxyUPt5Mur_-RV8pCECJ6A",
"C": "03b9dcdb7f195e07218b95b7c2dadc8289159fc44047439830f765b8c50bfb6bda"
"secret": "TmS6Cv0YT5PU_5ATVKnukw",
"C": "02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7"
}
],
"mints": {
"MINT_NAME": {
"url": "http://server.host:3339",
"ks": ["DSAl9nvvyfva"]
"mints": [
{
"url": "https://8333.space:3338",
"ids": ["DSAl9nvvyfva"]
}
}
]
}
```
When serialized, this becomes:
```
eyJwcm9vZnMiOlt7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50IjoyLCJzZWNyZXQiOiJiZFlDYkhHT051bmRMZVl2djFQNWRRIiwiQyI6IjAyZTYxMTdmYjFiMTYzM2E4YzE2NTdlZDM0YWIyNWVjZjhkNDk3NDA5MTE3OWM0NzczZWM1OWY4NWY0ZTM5OTFjZiJ9LHsiaWQiOiJEU0FsOW52dnlmdmEiLCJhbW91bnQiOjgsInNlY3JldCI6Ikt4eVVQdDVNdXJfLVJWOHBDRUNKNkEiLCJDIjoiMDNiOWRjZGI3ZjE5NWUwNzIxOGI5NWI3YzJkYWRjODI4OTE1OWZjNDQwNDc0Mzk4MzBmNzY1YjhjNTBiZmI2YmRhIn1dLCJtaW50cyI6eyJNSU5UX05BTUUiOnsidXJsIjoiaHR0cDovL3NlcnZlci5ob3N0OjMzMzkiLCJrcyI6WyJEU0FsOW52dnlmdmEiXX19fQ==
eyJwcm9vZnMiOlt7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50IjoyLCJzZWNyZXQiOiJFaHBlbm5DOXFCM2lGbFc4RlpfcFp3IiwiQyI6IjAyYzAyMDA2N2RiNzI3ZDU4NmJjMzE4M2FlY2Y5N2ZjYjgwMGMzZjRjYzQ3NTlmNjljNjI2YzlkYjVkOGY1YjVkNCJ9LHsiaWQiOiJEU0FsOW52dnlmdmEiLCJhbW91bnQiOjgsInNlY3JldCI6IlRtUzZDdjBZVDVQVV81QVRWS251a3ciLCJDIjoiMDJhYzkxMGJlZjI4Y2JlNWQ3MzI1NDE1ZDVjMjYzMDI2ZjE1ZjliOTY3YTA3OWNhOTc3OWFiNmU1YzJkYjEzM2E3In1dLCJtaW50cyI6W3sidXJsIjoiaHR0cHM6Ly84MzMzLnNwYWNlOjMzMzgiLCJpZHMiOlsiRFNBbDludnZ5ZnZhIl19XX0=
```
[00]: 00.md

View File

@@ -2,7 +2,7 @@
Melting tokens is the opposite of minting them (see #4): the wallet `Alice` sends `Proofs` to the mint `Bob` together with a bolt11 Lightning invoice that `Alice` wants to be paid. To melt tokens, `Alice` sends a `POST /melt` request with a JSON body to the mint. The `Proofs` included in the request will be burned by the mint and the mint will pay the invoice in exchange.
`Alice`'s request **MUST** include a `MeltRequest` ([TODO: Link MeltRequest]) JSON body with `Proofs` that have at least the amount of the invoice to be paid.
`Alice`'s request **MUST** include a `PostMeltRequest` ([TODO: Link PostMeltRequest]) JSON body with `Proofs` that have at least the amount of the invoice to be paid.
## Example
@@ -12,7 +12,7 @@ Melting tokens is the opposite of minting them (see #4): the wallet `Alice` send
POST https://mint.host:3338/melt
```
With the data being of the form `MeltRequest`:
With the data being of the form `PostMeltRequest`:
```json
{

View File

@@ -22,7 +22,7 @@ Another case of how split can be useful becomes apparent if we follow up the exa
POST https://mint.host:3338/split
```
With the data being of the form `SplitRequest`:
With the data being of the form `PostSplitRequest`:
```json
{

View File

@@ -92,12 +92,12 @@ Here we describe how `Carol` can redeem new tokens from `Bob` that she previousl
Note that the following steps can also be performed by `Alice` herself if she wants to cancel the pending token transfer and claim them for herself.
- `Carol` constructs a list of `<blinded_message>`'s each with the same amount as the list list of tokens that she received. This can be done by the same procedure as during the minting of new tokens in Section 2 [*TODO: update ref*] or during sending in Section 3 [*TODO: update ref*] since the splitting into amounts is deterministic.
- `Carol` performs the same steps as `Alice` when she split the tokens before sending it to her and calls the endpoint `POIT /split` with the JSON `SplitRequests` as the body of the request.
- `Carol` performs the same steps as `Alice` when she split the tokens before sending it to her and calls the endpoint `POIT /split` with the JSON `PostSplitRequests` as the body of the request.
## 5 - Burn sent tokens
Here we describe how `Alice` checks with the mint whether the tokens she sent `Carol` have been redeemed so she can safely delete them from her database. This step is optional but highly recommended so `Alice` can properly account for the tokens and adjust her balance accordingly.
- `Alice` loads all `<send_proofs>` with `pending=True` from her database and might group them by the `send_id`.
- `Alice` constructs a JSON of the form `{"proofs" : [{"amount" : <amount>, "secret" : s, "C" : Z}, ...]}` from these (grouped) tokens. [*TODO: this object is called CheckRequest*]
- `Alice` constructs a JSON of the form `{"proofs" : [{"amount" : <amount>, "secret" : s, "C" : Z}, ...]}` from these (grouped) tokens. [*TODO: this object is called GetCheckSpendableRequest*]
- `Alice` sends them to the mint `Bob` via the endpoint `POST /check` with the JSON as the body of the request.
- `Alice` receives a JSON of the form `{"1" : <spendable : bool>, "2" : ...}` where `"1"` is the index of the proof she sent to the mint before and `<spendable>` is a boolean that is `True` if the token has not been claimed yet by `Carol` and `False` if it has already been claimed.
- If `<spendable>` is `False`, `Alice` removes the proof [*NOTE: consistent name?*] from her list of spendable proofs.
@@ -109,7 +109,7 @@ Here we describe how `Alice` can request from `Bob` to make a Lightning payment
- `Alice` asks `Bob` for the Lightning fee via `GET /checkfee` with the body `CheckFeeRequest` being the json `{pr : <invoice>}`
- `Alice` receives the `CheckFeeResponse` in the form of the json `{"fee" : <fee>}` resulting in `<total> = <invoice_amount> + <fee>`.
- `Alice` now performs the same set of instructions as in Step 3.1 and 3.2 and splits her spendable tokens into a set `<keep_proofs>` that she keeps and and a set `<send_proofs>` with a sum of at least `<total>` that she can send for making the Lightning payment.
- `Alice` constructs the JSON `MeltRequest` of the form `{"proofs" : <List[Proof]>, "invoice" : <invoice>}` [*NOTE: Maybe use notation List[Proof] everywhere. Used MeltRequest here, maybe define each payload at the beginning of each section.*]
- `Alice` constructs the JSON `PostMeltRequest` of the form `{"proofs" : <List[Proof]>, "invoice" : <invoice>}` [*NOTE: Maybe use notation List[Proof] everywhere. Used PostMeltRequest here, maybe define each payload at the beginning of each section.*]
- `Alice` requests a payment from `Bob` via the endpoint `POST /melt` with the JSON as the body of the request.
- `Alice` receives a JSON of the form `{"paid" : <status:bool>}` with `<status>` being `True` if the payment was successful and `False` otherwise.
- If `<status> == True`, `Alice` removes `<send_proofs>` from her database of spendable tokens [*NOTE: called it tokens again*]
@@ -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 `BlindedMessages` and `SplitRequests` once when they appear and reuse them.
- Define objets like `BlindedMessages` and `PostSplitRequests` once when they appear and reuse them.
- Clarify whether a `TOKEN` is a single Proof or a list of Proofs

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "cashu"
version = "0.8.2"
version = "0.8.3"
description = "Ecash wallet and mint."
authors = ["calle <callebtc@protonmail.com>"]
license = "MIT"

View File

@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]}
setuptools.setup(
name="cashu",
version="0.8.2",
version="0.8.3",
description="Ecash wallet and mint for Bitcoin Lightning",
long_description=long_description,
long_description_content_type="text/markdown",