feat: early multimint support

This commit is contained in:
callebtc
2022-12-24 01:53:58 +01:00
parent ca63e85e9e
commit bea17fbd1a
5 changed files with 198 additions and 44 deletions

View File

@@ -23,11 +23,23 @@ router: APIRouter = APIRouter()
@router.get("/keys")
async def keys() -> dict[int, str]:
"""Get the public keys of the mint"""
"""Get the public keys of the mint of the newest keyset"""
keyset = ledger.get_keyset()
return keyset
@router.get("/keys/{idBase64Urlsafe}")
async def keyset_keys(idBase64Urlsafe: str) -> dict[int, str]:
"""
Get the public keys of the mint of a specificy keyset id.
The id is encoded in base64_urlsafe and needs to be converted back to
normal base64 before it can be processed.
"""
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
keyset = ledger.get_keyset(keyset_id=id)
return keyset
@router.get("/keysets")
async def keysets() -> dict[str, list[str]]:
"""Get all active keysets of the mint"""

View File

@@ -16,7 +16,7 @@ ledger = Ledger(
db=Database("mint", "data/mint"),
seed=MINT_PRIVATE_KEY,
# seed="asd",
derivation_path="0/0/0/0",
derivation_path="0/0/0/1",
lightning=LNbitsWallet() if LIGHTNING else None,
)

View File

@@ -36,6 +36,7 @@ from cashu.wallet.crud import (
get_lightning_invoices,
get_reserved_proofs,
get_unused_locks,
get_keyset,
)
from cashu.wallet.wallet import Wallet as Wallet
@@ -64,14 +65,6 @@ class NaturalOrderGroup(click.Group):
)
@click.pass_context
def cli(ctx, host: str, walletname: str):
# configure logger
logger.remove()
logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO")
ctx.ensure_object(dict)
ctx.obj["HOST"] = host
ctx.obj["WALLET_NAME"] = walletname
wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, walletname))
if TOR and not TorProxy().check_platform():
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. Please install Tor manually and set TOR=false and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended) or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly."
error_str += "\n\n"
@@ -83,6 +76,13 @@ def cli(ctx, host: str, walletname: str):
)
raise Exception(error_str)
# configure logger
logger.remove()
logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO")
ctx.ensure_object(dict)
ctx.obj["HOST"] = host
ctx.obj["WALLET_NAME"] = walletname
wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, walletname))
ctx.obj["WALLET"] = wallet
asyncio.run(init_wallet(wallet))
pass
@@ -185,13 +185,22 @@ async def invoice(ctx, amount: int, hash: str):
@coro
async def balance(ctx, verbose):
wallet: Wallet = ctx.obj["WALLET"]
keyset_balances = wallet.balance_per_keyset()
if len(keyset_balances) > 1:
print(f"You have balances in {len(keyset_balances)} keysets:")
# keyset_balances = wallet.balance_per_keyset()
# if len(keyset_balances) > 1:
# print(f"You have balances in {len(keyset_balances)} keysets:")
# print("")
# for k, v in keyset_balances.items():
# print(
# f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)"
# )
# print("")
mint_balances = await wallet.balance_per_minturl()
if len(mint_balances) > 1:
print(f"You have balances in {len(mint_balances)} mints:")
print("")
for k, v in keyset_balances.items():
for k, v in mint_balances.items():
print(
f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)"
f"Mint: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)"
)
print("")
if verbose:
@@ -221,9 +230,10 @@ async def send(ctx, amount: int, lock: str):
wallet.proofs, amount, lock, set_reserved=True
)
token = await wallet.serialize_proofs(
send_proofs, hide_secrets=True if lock and not p2sh else False
send_proofs,
hide_secrets=True if lock and not p2sh else False,
include_mints=True,
)
print(token)
wallet.status()
@@ -249,8 +259,62 @@ async def receive(ctx, token: str, lock: str):
signature = p2shscripts[0].signature
else:
script, signature = None, None
try:
# backwards compatibility < 0.7.0: tokens without mint information
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))]
_, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature)
except:
dtoken = json.loads(base64.urlsafe_b64decode(token))
assert "tokens" in dtoken, Exception("no proofs in token")
# if there is a `mints` field in the token
# we get the mint information in the token and load the keys of each mint
# we then redeem the tokens for each keyset individually
if "mints" in dtoken:
for mint_id in dtoken.get("mints"):
for keyset in set(dtoken["mints"][mint_id]["ks"]):
mint_url = dtoken["mints"][mint_id]["url"]
# init a temporary wallet object
keyset_wallet = Wallet(
mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])
)
# first we check whether we know this mint already and ask the user
mint_keysets = await get_keyset(id=keyset, db=keyset_wallet.db)
if mint_keysets is None:
click.confirm(
f"Do you want to receive tokens from mint {mint_url}?",
abort=True,
default=True,
)
# make sure that this mint indeed supports this keyset
mint_keysets = await keyset_wallet._get_keysets(mint_url)
if keyset in mint_keysets["keysets"]:
# load the keys
await keyset_wallet.load_mint(keyset_id=keyset)
# redeem proofs of this keyset
redeem_proofs = [
Proof(**p)
for p in dtoken["tokens"]
if Proof(**p).id == keyset
]
_, _ = await keyset_wallet.redeem(
redeem_proofs, scnd_script=script, scnd_siganture=signature
)
keyset_wallet.db.connect()
# reload proofs to update the main wallet's balance
await wallet.load_proofs()
else:
# no mint information present, assume default mint
proofs = [Proof(**p) for p in dtoken["tokens"]]
_, _ = await wallet.redeem(
proofs, scnd_script=script, scnd_siganture=signature
)
wallet.status()

View File

@@ -207,8 +207,8 @@ async def store_keyset(
async def get_keyset(
id: str = None,
mint_url: str = None,
id: str = "",
mint_url: str = "",
db: Database = None,
conn: Optional[Connection] = None,
):

View File

@@ -107,24 +107,22 @@ class LedgerAPI:
"""Returns base64 encoded random string."""
return scrts.token_urlsafe(randombits // 8)
async def _load_mint(self):
async def _load_mint(self, keyset_id: str = ""):
"""
Loads the current keys and the active keyset of the map.
Loads the public keys of the mint. Either gets the keys for the specified
`keyset_id` or loads the most recent one from the mint.
Gets and the active keyset ids of the mint and stores in `self.keysets`.
"""
assert len(
self.url
), "Ledger not initialized correctly: mint URL not specified yet. "
if keyset_id:
# get requested keyset
keyset = await self._get_keyset(self.url, keyset_id)
else:
# get current keyset
keyset = await self._get_keys(self.url)
# get all active keysets
mint_keysets = []
try:
keysets_resp = await self._get_keysets(self.url)
mint_keysets = keysets_resp["keysets"]
# store active keysets
except:
pass
self.keysets = mint_keysets if len(mint_keysets) else [keyset.id]
# store current keyset
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
@@ -134,6 +132,16 @@ class LedgerAPI:
if keyset_local is None:
await store_keyset(keyset=keyset, db=self.db)
# get all active keysets of this mint
mint_keysets = []
try:
keysets_resp = await self._get_keysets(self.url)
mint_keysets = keysets_resp["keysets"]
# store active keysets
except:
pass
self.keysets = mint_keysets if len(mint_keysets) else [keyset.id]
logger.debug(f"Mint keysets: {self.keysets}")
logger.debug(f"Current mint keyset: {keyset.id}")
@@ -173,7 +181,7 @@ class LedgerAPI:
ENDPOINTS
"""
async def _get_keys(self, url):
async def _get_keys(self, url: str):
self.s = self._set_requests()
resp = self.s.get(
url + "/keys",
@@ -188,7 +196,26 @@ class LedgerAPI:
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
return keyset
async def _get_keysets(self, url):
async def _get_keyset(self, url: str, keyset_id: str):
"""
keyset_id is base64, needs to be urlsafe-encoded.
"""
self.s = self._set_requests()
keyset_id_urlsafe = keyset_id.replace("-", "+").replace("_", "/")
resp = self.s.get(
url + f"/keys/{keyset_id_urlsafe}",
)
resp.raise_for_status()
keys = resp.json()
assert len(keys), Exception("did not receive any keys")
keyset_keys = {
int(amt): PublicKey(bytes.fromhex(val), raw=True)
for amt, val in keys.items()
}
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
return keyset
async def _get_keysets(self, url: str):
self.s = self._set_requests()
resp = self.s.get(
url + "/keysets",
@@ -359,8 +386,8 @@ class Wallet(LedgerAPI):
self.proofs: List[Proof] = []
self.name = name
async def load_mint(self):
await super()._load_mint()
async def load_mint(self, keyset_id: str = ""):
await super()._load_mint(keyset_id)
async def load_proofs(self):
self.proofs = await get_proofs(db=self.db)
@@ -373,6 +400,16 @@ class Wallet(LedgerAPI):
def _get_proofs_per_keyset(proofs: List[Proof]):
return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)}
async def _get_proofs_per_minturl(self, proofs: List[Proof]):
ret = {}
for id in set([p.id for p in proofs]):
keyset: WalletKeyset = await get_keyset(id=id, db=self.db)
if keyset.mint_url not in ret:
ret[keyset.mint_url] = [p for p in proofs if p.id == id]
else:
ret[keyset.mint_url].extend([p for p in proofs if p.id == id])
return ret
async def request_mint(self, amount):
invoice = super().request_mint(amount)
invoice.time_created = int(time.time())
@@ -442,16 +479,47 @@ class Wallet(LedgerAPI):
raise Exception("could not pay invoice.")
return status["paid"]
@staticmethod
async def serialize_proofs(proofs: List[Proof], hide_secrets=False):
async def serialize_proofs(
self, proofs: List[Proof], hide_secrets=False, include_mints=False
):
"""
Produces sharable token with proofs and mint information.
"""
# serialize the list of proofs, either with secrets included or without secrets
if hide_secrets:
proofs_serialized = [p.to_dict_no_secret() for p in proofs]
else:
proofs_serialized = [p.to_dict() for p in proofs]
token = base64.urlsafe_b64encode(
json.dumps(proofs_serialized).encode()
).decode()
return token
token = dict(tokens=proofs_serialized)
# add mint information to the token, if requested
if include_mints:
mints = dict()
# iterate through all proofs and add their keyset to `mints`
for proof in proofs:
if proof.id:
keyset = await get_keyset(id=proof.id, db=self.db)
if keyset:
# TODO: replace this with a mint pubkey
placeholder_mint_id = keyset.mint_url
if placeholder_mint_id not in mints:
id = dict(
url=keyset.mint_url,
ks=[keyset.id],
)
mints[placeholder_mint_id] = id
else:
if keyset.id not in mints[placeholder_mint_id]["ks"]:
mints[placeholder_mint_id]["ks"].append(keyset.id)
if mints:
token["mints"] = mints
# encode the token as a base64 string
token_base64 = base64.urlsafe_b64encode(json.dumps(token).encode()).decode()
return token_base64
async def _get_spendable_proofs(self, proofs: List[Proof]):
"""
@@ -568,5 +636,15 @@ class Wallet(LedgerAPI):
for key, proofs in self._get_proofs_per_keyset(self.proofs).items()
}
async def balance_per_minturl(self):
balances = await self._get_proofs_per_minturl(self.proofs)
return {
key: {
"balance": sum_proofs(proofs),
"available": sum_proofs([p for p in proofs if not p.reserved]),
}
for key, proofs in balances.items()
}
def proof_amounts(self):
return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]