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")
|
@router.get("/keys")
|
||||||
async def keys() -> dict[int, str]:
|
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()
|
keyset = ledger.get_keyset()
|
||||||
return 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")
|
@router.get("/keysets")
|
||||||
async def keysets() -> dict[str, list[str]]:
|
async def keysets() -> dict[str, list[str]]:
|
||||||
"""Get all active keysets of the mint"""
|
"""Get all active keysets of the mint"""
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ ledger = Ledger(
|
|||||||
db=Database("mint", "data/mint"),
|
db=Database("mint", "data/mint"),
|
||||||
seed=MINT_PRIVATE_KEY,
|
seed=MINT_PRIVATE_KEY,
|
||||||
# seed="asd",
|
# seed="asd",
|
||||||
derivation_path="0/0/0/0",
|
derivation_path="0/0/0/1",
|
||||||
lightning=LNbitsWallet() if LIGHTNING else None,
|
lightning=LNbitsWallet() if LIGHTNING else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from cashu.wallet.crud import (
|
|||||||
get_lightning_invoices,
|
get_lightning_invoices,
|
||||||
get_reserved_proofs,
|
get_reserved_proofs,
|
||||||
get_unused_locks,
|
get_unused_locks,
|
||||||
|
get_keyset,
|
||||||
)
|
)
|
||||||
from cashu.wallet.wallet import Wallet as Wallet
|
from cashu.wallet.wallet import Wallet as Wallet
|
||||||
|
|
||||||
@@ -64,14 +65,6 @@ class NaturalOrderGroup(click.Group):
|
|||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, host: str, walletname: str):
|
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():
|
if TOR and not TorProxy().check_platform():
|
||||||
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. Please install Tor manually and set TOR=false and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended) or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly."
|
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. 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"
|
error_str += "\n\n"
|
||||||
@@ -83,6 +76,13 @@ def cli(ctx, host: str, walletname: str):
|
|||||||
)
|
)
|
||||||
raise Exception(error_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
|
ctx.obj["WALLET"] = wallet
|
||||||
asyncio.run(init_wallet(wallet))
|
asyncio.run(init_wallet(wallet))
|
||||||
pass
|
pass
|
||||||
@@ -185,13 +185,22 @@ async def invoice(ctx, amount: int, hash: str):
|
|||||||
@coro
|
@coro
|
||||||
async def balance(ctx, verbose):
|
async def balance(ctx, verbose):
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
keyset_balances = wallet.balance_per_keyset()
|
# keyset_balances = wallet.balance_per_keyset()
|
||||||
if len(keyset_balances) > 1:
|
# if len(keyset_balances) > 1:
|
||||||
print(f"You have balances in {len(keyset_balances)} keysets:")
|
# 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("")
|
print("")
|
||||||
for k, v in keyset_balances.items():
|
for k, v in mint_balances.items():
|
||||||
print(
|
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("")
|
print("")
|
||||||
if verbose:
|
if verbose:
|
||||||
@@ -221,9 +230,10 @@ async def send(ctx, amount: int, lock: str):
|
|||||||
wallet.proofs, amount, lock, set_reserved=True
|
wallet.proofs, amount, lock, set_reserved=True
|
||||||
)
|
)
|
||||||
token = await wallet.serialize_proofs(
|
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()
|
wallet.status()
|
||||||
|
|
||||||
|
|
||||||
@@ -249,8 +259,62 @@ async def receive(ctx, token: str, lock: str):
|
|||||||
signature = p2shscripts[0].signature
|
signature = p2shscripts[0].signature
|
||||||
else:
|
else:
|
||||||
script, signature = None, None
|
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()
|
wallet.status()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -207,8 +207,8 @@ async def store_keyset(
|
|||||||
|
|
||||||
|
|
||||||
async def get_keyset(
|
async def get_keyset(
|
||||||
id: str = None,
|
id: str = "",
|
||||||
mint_url: str = None,
|
mint_url: str = "",
|
||||||
db: Database = None,
|
db: Database = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -107,24 +107,22 @@ class LedgerAPI:
|
|||||||
"""Returns base64 encoded random string."""
|
"""Returns base64 encoded random string."""
|
||||||
return scrts.token_urlsafe(randombits // 8)
|
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(
|
assert len(
|
||||||
self.url
|
self.url
|
||||||
), "Ledger not initialized correctly: mint URL not specified yet. "
|
), "Ledger not initialized correctly: mint URL not specified yet. "
|
||||||
# get current keyset
|
|
||||||
keyset = await self._get_keys(self.url)
|
if keyset_id:
|
||||||
# get all active keysets
|
# get requested keyset
|
||||||
mint_keysets = []
|
keyset = await self._get_keyset(self.url, keyset_id)
|
||||||
try:
|
else:
|
||||||
keysets_resp = await self._get_keysets(self.url)
|
# get current keyset
|
||||||
mint_keysets = keysets_resp["keysets"]
|
keyset = await self._get_keys(self.url)
|
||||||
# store active keysets
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.keysets = mint_keysets if len(mint_keysets) else [keyset.id]
|
|
||||||
|
|
||||||
# store current keyset
|
# store current keyset
|
||||||
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
|
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
|
||||||
@@ -134,6 +132,16 @@ class LedgerAPI:
|
|||||||
if keyset_local is None:
|
if keyset_local is None:
|
||||||
await store_keyset(keyset=keyset, db=self.db)
|
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"Mint keysets: {self.keysets}")
|
||||||
logger.debug(f"Current mint keyset: {keyset.id}")
|
logger.debug(f"Current mint keyset: {keyset.id}")
|
||||||
|
|
||||||
@@ -173,7 +181,7 @@ class LedgerAPI:
|
|||||||
ENDPOINTS
|
ENDPOINTS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def _get_keys(self, url):
|
async def _get_keys(self, url: str):
|
||||||
self.s = self._set_requests()
|
self.s = self._set_requests()
|
||||||
resp = self.s.get(
|
resp = self.s.get(
|
||||||
url + "/keys",
|
url + "/keys",
|
||||||
@@ -188,7 +196,26 @@ class LedgerAPI:
|
|||||||
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
|
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
|
||||||
return keyset
|
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()
|
self.s = self._set_requests()
|
||||||
resp = self.s.get(
|
resp = self.s.get(
|
||||||
url + "/keysets",
|
url + "/keysets",
|
||||||
@@ -359,8 +386,8 @@ class Wallet(LedgerAPI):
|
|||||||
self.proofs: List[Proof] = []
|
self.proofs: List[Proof] = []
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
async def load_mint(self):
|
async def load_mint(self, keyset_id: str = ""):
|
||||||
await super()._load_mint()
|
await super()._load_mint(keyset_id)
|
||||||
|
|
||||||
async def load_proofs(self):
|
async def load_proofs(self):
|
||||||
self.proofs = await get_proofs(db=self.db)
|
self.proofs = await get_proofs(db=self.db)
|
||||||
@@ -373,6 +400,16 @@ class Wallet(LedgerAPI):
|
|||||||
def _get_proofs_per_keyset(proofs: List[Proof]):
|
def _get_proofs_per_keyset(proofs: List[Proof]):
|
||||||
return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)}
|
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):
|
async def request_mint(self, amount):
|
||||||
invoice = super().request_mint(amount)
|
invoice = super().request_mint(amount)
|
||||||
invoice.time_created = int(time.time())
|
invoice.time_created = int(time.time())
|
||||||
@@ -442,16 +479,47 @@ class Wallet(LedgerAPI):
|
|||||||
raise Exception("could not pay invoice.")
|
raise Exception("could not pay invoice.")
|
||||||
return status["paid"]
|
return status["paid"]
|
||||||
|
|
||||||
@staticmethod
|
async def serialize_proofs(
|
||||||
async def serialize_proofs(proofs: List[Proof], hide_secrets=False):
|
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:
|
if hide_secrets:
|
||||||
proofs_serialized = [p.to_dict_no_secret() for p in proofs]
|
proofs_serialized = [p.to_dict_no_secret() for p in proofs]
|
||||||
else:
|
else:
|
||||||
proofs_serialized = [p.to_dict() for p in proofs]
|
proofs_serialized = [p.to_dict() for p in proofs]
|
||||||
token = base64.urlsafe_b64encode(
|
|
||||||
json.dumps(proofs_serialized).encode()
|
token = dict(tokens=proofs_serialized)
|
||||||
).decode()
|
|
||||||
return token
|
# 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]):
|
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()
|
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):
|
def proof_amounts(self):
|
||||||
return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]
|
return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]
|
||||||
|
|||||||
Reference in New Issue
Block a user