mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-21 19:14:19 +01:00
feat: early multimint support
This commit is contained in:
@@ -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"""
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from cashu.wallet.crud import (
|
||||
get_lightning_invoices,
|
||||
get_reserved_proofs,
|
||||
get_unused_locks,
|
||||
get_keyset,
|
||||
)
|
||||
from cashu.wallet.wallet import Wallet as Wallet
|
||||
|
||||
@@ -64,14 +65,6 @@ class NaturalOrderGroup(click.Group):
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(ctx, host: str, walletname: str):
|
||||
# configure logger
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO")
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["HOST"] = host
|
||||
ctx.obj["WALLET_NAME"] = walletname
|
||||
wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, walletname))
|
||||
|
||||
if TOR and not TorProxy().check_platform():
|
||||
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. Please install Tor manually and set TOR=false and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended) or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly."
|
||||
error_str += "\n\n"
|
||||
@@ -83,6 +76,13 @@ def cli(ctx, host: str, walletname: str):
|
||||
)
|
||||
raise Exception(error_str)
|
||||
|
||||
# configure logger
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO")
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["HOST"] = host
|
||||
ctx.obj["WALLET_NAME"] = walletname
|
||||
wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, walletname))
|
||||
ctx.obj["WALLET"] = wallet
|
||||
asyncio.run(init_wallet(wallet))
|
||||
pass
|
||||
@@ -185,13 +185,22 @@ async def invoice(ctx, amount: int, hash: str):
|
||||
@coro
|
||||
async def balance(ctx, verbose):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
keyset_balances = wallet.balance_per_keyset()
|
||||
if len(keyset_balances) > 1:
|
||||
print(f"You have balances in {len(keyset_balances)} keysets:")
|
||||
# keyset_balances = wallet.balance_per_keyset()
|
||||
# if len(keyset_balances) > 1:
|
||||
# print(f"You have balances in {len(keyset_balances)} keysets:")
|
||||
# print("")
|
||||
# for k, v in keyset_balances.items():
|
||||
# print(
|
||||
# f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)"
|
||||
# )
|
||||
# print("")
|
||||
mint_balances = await wallet.balance_per_minturl()
|
||||
if len(mint_balances) > 1:
|
||||
print(f"You have balances in {len(mint_balances)} mints:")
|
||||
print("")
|
||||
for k, v in keyset_balances.items():
|
||||
for k, v in mint_balances.items():
|
||||
print(
|
||||
f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)"
|
||||
f"Mint: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)"
|
||||
)
|
||||
print("")
|
||||
if verbose:
|
||||
@@ -221,9 +230,10 @@ async def send(ctx, amount: int, lock: str):
|
||||
wallet.proofs, amount, lock, set_reserved=True
|
||||
)
|
||||
token = await wallet.serialize_proofs(
|
||||
send_proofs, hide_secrets=True if lock and not p2sh else False
|
||||
send_proofs,
|
||||
hide_secrets=True if lock and not p2sh else False,
|
||||
include_mints=True,
|
||||
)
|
||||
print(token)
|
||||
wallet.status()
|
||||
|
||||
|
||||
@@ -249,8 +259,62 @@ async def receive(ctx, token: str, lock: str):
|
||||
signature = p2shscripts[0].signature
|
||||
else:
|
||||
script, signature = None, None
|
||||
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))]
|
||||
_, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature)
|
||||
|
||||
try:
|
||||
# backwards compatibility < 0.7.0: tokens without mint information
|
||||
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))]
|
||||
_, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature)
|
||||
except:
|
||||
dtoken = json.loads(base64.urlsafe_b64decode(token))
|
||||
assert "tokens" in dtoken, Exception("no proofs in token")
|
||||
# if there is a `mints` field in the token
|
||||
# we get the mint information in the token and load the keys of each mint
|
||||
# we then redeem the tokens for each keyset individually
|
||||
if "mints" in dtoken:
|
||||
for mint_id in dtoken.get("mints"):
|
||||
for keyset in set(dtoken["mints"][mint_id]["ks"]):
|
||||
mint_url = dtoken["mints"][mint_id]["url"]
|
||||
# init a temporary wallet object
|
||||
keyset_wallet = Wallet(
|
||||
mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])
|
||||
)
|
||||
|
||||
# first we check whether we know this mint already and ask the user
|
||||
mint_keysets = await get_keyset(id=keyset, db=keyset_wallet.db)
|
||||
if mint_keysets is None:
|
||||
click.confirm(
|
||||
f"Do you want to receive tokens from mint {mint_url}?",
|
||||
abort=True,
|
||||
default=True,
|
||||
)
|
||||
|
||||
# make sure that this mint indeed supports this keyset
|
||||
mint_keysets = await keyset_wallet._get_keysets(mint_url)
|
||||
if keyset in mint_keysets["keysets"]:
|
||||
# load the keys
|
||||
await keyset_wallet.load_mint(keyset_id=keyset)
|
||||
|
||||
# redeem proofs of this keyset
|
||||
redeem_proofs = [
|
||||
Proof(**p)
|
||||
for p in dtoken["tokens"]
|
||||
if Proof(**p).id == keyset
|
||||
]
|
||||
_, _ = await keyset_wallet.redeem(
|
||||
redeem_proofs, scnd_script=script, scnd_siganture=signature
|
||||
)
|
||||
keyset_wallet.db.connect()
|
||||
|
||||
# reload proofs to update the main wallet's balance
|
||||
await wallet.load_proofs()
|
||||
|
||||
else:
|
||||
# no mint information present, assume default mint
|
||||
proofs = [Proof(**p) for p in dtoken["tokens"]]
|
||||
_, _ = await wallet.redeem(
|
||||
proofs, scnd_script=script, scnd_siganture=signature
|
||||
)
|
||||
|
||||
wallet.status()
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
):
|
||||
|
||||
@@ -107,24 +107,22 @@ class LedgerAPI:
|
||||
"""Returns base64 encoded random string."""
|
||||
return scrts.token_urlsafe(randombits // 8)
|
||||
|
||||
async def _load_mint(self):
|
||||
async def _load_mint(self, keyset_id: str = ""):
|
||||
"""
|
||||
Loads the current keys and the active keyset of the map.
|
||||
Loads the public keys of the mint. Either gets the keys for the specified
|
||||
`keyset_id` or loads the most recent one from the mint.
|
||||
Gets and the active keyset ids of the mint and stores in `self.keysets`.
|
||||
"""
|
||||
assert len(
|
||||
self.url
|
||||
), "Ledger not initialized correctly: mint URL not specified yet. "
|
||||
# get current keyset
|
||||
keyset = await self._get_keys(self.url)
|
||||
# get all active keysets
|
||||
mint_keysets = []
|
||||
try:
|
||||
keysets_resp = await self._get_keysets(self.url)
|
||||
mint_keysets = keysets_resp["keysets"]
|
||||
# store active keysets
|
||||
except:
|
||||
pass
|
||||
self.keysets = mint_keysets if len(mint_keysets) else [keyset.id]
|
||||
|
||||
if keyset_id:
|
||||
# get requested keyset
|
||||
keyset = await self._get_keyset(self.url, keyset_id)
|
||||
else:
|
||||
# get current keyset
|
||||
keyset = await self._get_keys(self.url)
|
||||
|
||||
# store current keyset
|
||||
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
|
||||
@@ -134,6 +132,16 @@ class LedgerAPI:
|
||||
if keyset_local is None:
|
||||
await store_keyset(keyset=keyset, db=self.db)
|
||||
|
||||
# get all active keysets of this mint
|
||||
mint_keysets = []
|
||||
try:
|
||||
keysets_resp = await self._get_keysets(self.url)
|
||||
mint_keysets = keysets_resp["keysets"]
|
||||
# store active keysets
|
||||
except:
|
||||
pass
|
||||
self.keysets = mint_keysets if len(mint_keysets) else [keyset.id]
|
||||
|
||||
logger.debug(f"Mint keysets: {self.keysets}")
|
||||
logger.debug(f"Current mint keyset: {keyset.id}")
|
||||
|
||||
@@ -173,7 +181,7 @@ class LedgerAPI:
|
||||
ENDPOINTS
|
||||
"""
|
||||
|
||||
async def _get_keys(self, url):
|
||||
async def _get_keys(self, url: str):
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.get(
|
||||
url + "/keys",
|
||||
@@ -188,7 +196,26 @@ class LedgerAPI:
|
||||
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
|
||||
return keyset
|
||||
|
||||
async def _get_keysets(self, url):
|
||||
async def _get_keyset(self, url: str, keyset_id: str):
|
||||
"""
|
||||
keyset_id is base64, needs to be urlsafe-encoded.
|
||||
"""
|
||||
self.s = self._set_requests()
|
||||
keyset_id_urlsafe = keyset_id.replace("-", "+").replace("_", "/")
|
||||
resp = self.s.get(
|
||||
url + f"/keys/{keyset_id_urlsafe}",
|
||||
)
|
||||
resp.raise_for_status()
|
||||
keys = resp.json()
|
||||
assert len(keys), Exception("did not receive any keys")
|
||||
keyset_keys = {
|
||||
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||
for amt, val in keys.items()
|
||||
}
|
||||
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
|
||||
return keyset
|
||||
|
||||
async def _get_keysets(self, url: str):
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.get(
|
||||
url + "/keysets",
|
||||
@@ -359,8 +386,8 @@ class Wallet(LedgerAPI):
|
||||
self.proofs: List[Proof] = []
|
||||
self.name = name
|
||||
|
||||
async def load_mint(self):
|
||||
await super()._load_mint()
|
||||
async def load_mint(self, keyset_id: str = ""):
|
||||
await super()._load_mint(keyset_id)
|
||||
|
||||
async def load_proofs(self):
|
||||
self.proofs = await get_proofs(db=self.db)
|
||||
@@ -373,6 +400,16 @@ class Wallet(LedgerAPI):
|
||||
def _get_proofs_per_keyset(proofs: List[Proof]):
|
||||
return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)}
|
||||
|
||||
async def _get_proofs_per_minturl(self, proofs: List[Proof]):
|
||||
ret = {}
|
||||
for id in set([p.id for p in proofs]):
|
||||
keyset: WalletKeyset = await get_keyset(id=id, db=self.db)
|
||||
if keyset.mint_url not in ret:
|
||||
ret[keyset.mint_url] = [p for p in proofs if p.id == id]
|
||||
else:
|
||||
ret[keyset.mint_url].extend([p for p in proofs if p.id == id])
|
||||
return ret
|
||||
|
||||
async def request_mint(self, amount):
|
||||
invoice = super().request_mint(amount)
|
||||
invoice.time_created = int(time.time())
|
||||
@@ -442,16 +479,47 @@ class Wallet(LedgerAPI):
|
||||
raise Exception("could not pay invoice.")
|
||||
return status["paid"]
|
||||
|
||||
@staticmethod
|
||||
async def serialize_proofs(proofs: List[Proof], hide_secrets=False):
|
||||
async def serialize_proofs(
|
||||
self, proofs: List[Proof], hide_secrets=False, include_mints=False
|
||||
):
|
||||
"""
|
||||
Produces sharable token with proofs and mint information.
|
||||
"""
|
||||
# serialize the list of proofs, either with secrets included or without secrets
|
||||
if hide_secrets:
|
||||
proofs_serialized = [p.to_dict_no_secret() for p in proofs]
|
||||
else:
|
||||
proofs_serialized = [p.to_dict() for p in proofs]
|
||||
token = base64.urlsafe_b64encode(
|
||||
json.dumps(proofs_serialized).encode()
|
||||
).decode()
|
||||
return token
|
||||
|
||||
token = dict(tokens=proofs_serialized)
|
||||
|
||||
# add mint information to the token, if requested
|
||||
if include_mints:
|
||||
|
||||
mints = dict()
|
||||
|
||||
# iterate through all proofs and add their keyset to `mints`
|
||||
for proof in proofs:
|
||||
if proof.id:
|
||||
keyset = await get_keyset(id=proof.id, db=self.db)
|
||||
if keyset:
|
||||
# TODO: replace this with a mint pubkey
|
||||
placeholder_mint_id = keyset.mint_url
|
||||
if placeholder_mint_id not in mints:
|
||||
id = dict(
|
||||
url=keyset.mint_url,
|
||||
ks=[keyset.id],
|
||||
)
|
||||
mints[placeholder_mint_id] = id
|
||||
else:
|
||||
if keyset.id not in mints[placeholder_mint_id]["ks"]:
|
||||
mints[placeholder_mint_id]["ks"].append(keyset.id)
|
||||
if mints:
|
||||
token["mints"] = mints
|
||||
|
||||
# encode the token as a base64 string
|
||||
token_base64 = base64.urlsafe_b64encode(json.dumps(token).encode()).decode()
|
||||
return token_base64
|
||||
|
||||
async def _get_spendable_proofs(self, proofs: List[Proof]):
|
||||
"""
|
||||
@@ -568,5 +636,15 @@ class Wallet(LedgerAPI):
|
||||
for key, proofs in self._get_proofs_per_keyset(self.proofs).items()
|
||||
}
|
||||
|
||||
async def balance_per_minturl(self):
|
||||
balances = await self._get_proofs_per_minturl(self.proofs)
|
||||
return {
|
||||
key: {
|
||||
"balance": sum_proofs(proofs),
|
||||
"available": sum_proofs([p for p in proofs if not p.reserved]),
|
||||
}
|
||||
for key, proofs in balances.items()
|
||||
}
|
||||
|
||||
def proof_amounts(self):
|
||||
return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]
|
||||
|
||||
Reference in New Issue
Block a user