mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
TokenV3 and new Mint startup in tests (#149)
* tokenv3 send and receive * receive v2 and v1 tokens with tests
This commit is contained in:
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@@ -27,14 +27,6 @@ jobs:
|
||||
run: |
|
||||
poetry install --with dev
|
||||
shell: bash
|
||||
- name: Run mint
|
||||
env:
|
||||
LIGHTNING: False
|
||||
MINT_PRIVATE_KEY: "testingkey"
|
||||
MINT_LISTEN_HOST: 0.0.0.0
|
||||
MINT_LISTEN_PORT: 3337
|
||||
run: |
|
||||
nohup poetry run mint &
|
||||
- name: Run tests
|
||||
env:
|
||||
LIGHTNING: False
|
||||
|
||||
@@ -331,3 +331,29 @@ class TokenV2(BaseModel):
|
||||
)
|
||||
else:
|
||||
return dict(proofs=[p.to_dict() for p in self.proofs])
|
||||
|
||||
|
||||
class TokenV3Token(BaseModel):
|
||||
mint: Optional[str] = None
|
||||
proofs: List[Proof]
|
||||
|
||||
def to_dict(self):
|
||||
return_dict = dict(proofs=[p.to_dict() for p in self.proofs])
|
||||
if self.mint:
|
||||
return_dict.update(dict(mint=self.mint)) # type: ignore
|
||||
return return_dict
|
||||
|
||||
|
||||
class TokenV3(BaseModel):
|
||||
"""
|
||||
A Cashu token that includes proofs and their respective mints. Can include proofs from multiple different mints and keysets.
|
||||
"""
|
||||
|
||||
token: List[TokenV3Token] = []
|
||||
memo: Optional[str] = None
|
||||
|
||||
def to_dict(self):
|
||||
return_dict = dict(token=[t.to_dict() for t in self.token])
|
||||
if self.memo:
|
||||
return_dict.update(dict(memo=self.memo)) # type: ignore
|
||||
return return_dict
|
||||
|
||||
@@ -23,7 +23,7 @@ lightning_backend = getattr(wallets_module, settings.mint_lightning_backend)()
|
||||
ledger = Ledger(
|
||||
db=Database("mint", settings.mint_database),
|
||||
seed=settings.mint_private_key,
|
||||
derivation_path="0/0/0/0",
|
||||
derivation_path="0/0/0/1",
|
||||
lightning=lightning_backend,
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import click
|
||||
from click import Context
|
||||
from loguru import logger
|
||||
|
||||
from cashu.core.base import Proof, TokenV2
|
||||
from cashu.core.base import Proof, TokenV1, TokenV2
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import settings
|
||||
@@ -26,6 +26,7 @@ from cashu.nostr.nostr.client.client import NostrClient
|
||||
from cashu.tor.tor import TorProxy
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.crud import (
|
||||
get_keyset,
|
||||
get_lightning_invoices,
|
||||
get_reserved_proofs,
|
||||
get_unused_locks,
|
||||
@@ -35,10 +36,9 @@ from cashu.wallet.wallet import Wallet as Wallet
|
||||
from .cli_helpers import (
|
||||
get_mint_wallet,
|
||||
print_mint_balances,
|
||||
proofs_to_serialized_tokenv2,
|
||||
redeem_multimint,
|
||||
token_from_lnbits_link,
|
||||
verify_mints,
|
||||
redeem_TokenV3_multimint,
|
||||
serialize_TokenV1_to_TokenV3,
|
||||
serialize_TokenV2_to_TokenV3,
|
||||
)
|
||||
from .nostr import receive_nostr, send_nostr
|
||||
|
||||
@@ -243,9 +243,7 @@ async def send(ctx: Context, amount: int, lock: str, legacy: bool):
|
||||
|
||||
if legacy:
|
||||
print("")
|
||||
print(
|
||||
"Legacy token without mint information for older clients. This token can only be be received by wallets who use the mint the token is issued from:"
|
||||
)
|
||||
print("Old token format:")
|
||||
print("")
|
||||
token = await wallet.serialize_proofs(
|
||||
send_proofs,
|
||||
@@ -306,7 +304,7 @@ async def send_command(
|
||||
|
||||
async def receive(ctx: Context, token: str, lock: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
# await wallet.load_mint()
|
||||
|
||||
# check for P2SH locks
|
||||
if lock:
|
||||
@@ -325,60 +323,63 @@ async def receive(ctx: Context, token: str, lock: str):
|
||||
|
||||
# ----- backwards compatibility -----
|
||||
|
||||
# we support old tokens (< 0.7) without mint information and (W3siaWQ...)
|
||||
# new tokens (>= 0.7) with multiple mint support (eyJ0b2...)
|
||||
# V2Tokens (0.7-0.11.0) (eyJwcm9...)
|
||||
if token.startswith("eyJwcm9"):
|
||||
try:
|
||||
# backwards compatibility: tokens without mint information
|
||||
# supports tokens of the form W3siaWQiOiJH
|
||||
|
||||
# if it's an lnbits https:// link with a token as an argument, speacial treatment
|
||||
token, url = token_from_lnbits_link(token)
|
||||
|
||||
# assume W3siaWQiOiJH.. token
|
||||
# next line trows an error if the desirialization with the old format doesn't
|
||||
# work and we can assume it's the new format
|
||||
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_serialized_tokenv2(wallet, proofs, url)
|
||||
tokenv2 = TokenV2.parse_obj(json.loads(base64.urlsafe_b64decode(token)))
|
||||
token = await serialize_TokenV2_to_TokenV3(wallet, tokenv2)
|
||||
except:
|
||||
pass
|
||||
|
||||
# V1Tokens (<0.7) (W3siaWQ...)
|
||||
if token.startswith("W3siaWQ"):
|
||||
try:
|
||||
tokenv1 = TokenV1.parse_obj(json.loads(base64.urlsafe_b64decode(token)))
|
||||
token = await serialize_TokenV1_to_TokenV3(wallet, tokenv1)
|
||||
print(token)
|
||||
except:
|
||||
pass
|
||||
# ----- receive token -----
|
||||
|
||||
# deserialize token
|
||||
dtoken = json.loads(base64.urlsafe_b64decode(token))
|
||||
# dtoken = json.loads(base64.urlsafe_b64decode(token))
|
||||
tokenObj = wallet._deserialize_token_V3(token)
|
||||
|
||||
# backwards compatibility wallet to wallet < 0.8.0: V2 tokens renamed "tokens" field to "proofs"
|
||||
if "tokens" in dtoken:
|
||||
dtoken["proofs"] = dtoken.pop("tokens")
|
||||
|
||||
# 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
|
||||
# tokenObj = TokenV2.parse_obj(dtoken)
|
||||
assert len(tokenObj.token), Exception("no proofs in token")
|
||||
assert len(tokenObj.token[0].proofs), Exception("no proofs in token")
|
||||
includes_mint_info: bool = any([t.mint for t in tokenObj.token])
|
||||
|
||||
# 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, tokenObj)
|
||||
# await verify_mints(ctx, tokenObj)
|
||||
# redeem tokens with new wallet instances
|
||||
await redeem_multimint(ctx, tokenObj, script, signature)
|
||||
# reload main wallet so the balance updates
|
||||
await wallet.load_proofs()
|
||||
await redeem_TokenV3_multimint(ctx, tokenObj, script, signature)
|
||||
else:
|
||||
# no mint information present, we extract the proofs and use wallet's default mint
|
||||
proofs = [Proof(**p) for p in dtoken["proofs"]]
|
||||
_, _ = await wallet.redeem(proofs, script, signature)
|
||||
|
||||
proofs = [p for t in tokenObj.token for p in t.proofs]
|
||||
# first we load the mint URL from the DB
|
||||
keyset_in_token = proofs[0].id
|
||||
assert keyset_in_token
|
||||
# we get the keyset from the db
|
||||
mint_keysets = await get_keyset(id=keyset_in_token, db=wallet.db)
|
||||
assert mint_keysets, Exception("we don't know this keyset")
|
||||
assert mint_keysets.mint_url, Exception("we don't know this mint's URL")
|
||||
# now we have the URL
|
||||
mint_wallet = Wallet(
|
||||
mint_keysets.mint_url,
|
||||
os.path.join(settings.cashu_dir, ctx.obj["WALLET_NAME"]),
|
||||
)
|
||||
await mint_wallet.load_mint(keyset_in_token)
|
||||
_, _ = await mint_wallet.redeem(proofs, script, signature)
|
||||
print(f"Received {sum_proofs(proofs)} sats")
|
||||
|
||||
# reload main wallet so the balance updates
|
||||
await wallet.load_proofs()
|
||||
wallet.status()
|
||||
|
||||
|
||||
@@ -413,7 +414,7 @@ async def receive_cli(
|
||||
elif all:
|
||||
reserved_proofs = await get_reserved_proofs(wallet.db)
|
||||
if len(reserved_proofs):
|
||||
for (key, value) in groupby(reserved_proofs, key=itemgetter("send_id")):
|
||||
for (key, value) in groupby(reserved_proofs, key=itemgetter("send_id")): # type: ignore
|
||||
proofs = list(value)
|
||||
token = await wallet.serialize_proofs(proofs)
|
||||
await receive(ctx, token, lock)
|
||||
|
||||
@@ -6,14 +6,49 @@ import click
|
||||
from click import Context
|
||||
from loguru import logger
|
||||
|
||||
from cashu.core.base import Proof, TokenV2, TokenV2Mint, WalletKeyset
|
||||
from cashu.core.base import (
|
||||
Proof,
|
||||
TokenV1,
|
||||
TokenV2,
|
||||
TokenV2Mint,
|
||||
TokenV3,
|
||||
TokenV3Token,
|
||||
WalletKeyset,
|
||||
)
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.crud import get_keyset
|
||||
from cashu.wallet.wallet import Wallet as Wallet
|
||||
|
||||
|
||||
async def verify_mints(ctx: Context, token: TokenV2):
|
||||
async def verify_mint(mint_wallet: Wallet, url: str):
|
||||
"""A helper function that asks the user if they trust the mint if the user
|
||||
has not encountered the mint before (there is no entry in the database).
|
||||
|
||||
Throws an Exception if the user chooses to not trust the mint.
|
||||
"""
|
||||
logger.debug(f"Verifying mint {url}")
|
||||
# dummy Wallet to check the database later
|
||||
# mint_wallet = Wallet(url, os.path.join(settings.cashu_dir, ctx.obj["WALLET_NAME"]))
|
||||
# we check the db whether we know this mint already and ask the user if not
|
||||
mint_keysets = await get_keyset(mint_url=url, db=mint_wallet.db)
|
||||
if mint_keysets is None:
|
||||
# we encountered a new mint and ask for a user confirmation
|
||||
print("")
|
||||
print("Warning: Tokens are from a mint you don't know yet.")
|
||||
print("\n")
|
||||
print(f"Mint URL: {url}")
|
||||
print("\n")
|
||||
click.confirm(
|
||||
f"Do you trust this mint and want to receive the tokens?",
|
||||
abort=True,
|
||||
default=True,
|
||||
)
|
||||
else:
|
||||
logger.debug(f"We know keyset {mint_keysets.id} already")
|
||||
|
||||
|
||||
async def verify_mints_tokenv2(ctx: Context, 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.
|
||||
@@ -64,7 +99,7 @@ async def verify_mints(ctx: Context, token: TokenV2):
|
||||
assert trust_token_mints, Exception("Aborted!")
|
||||
|
||||
|
||||
async def redeem_multimint(ctx: Context, token: TokenV2, script, signature):
|
||||
async def redeem_TokenV2_multimint(ctx: Context, 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.
|
||||
@@ -95,6 +130,30 @@ async def redeem_multimint(ctx: Context, token: TokenV2, script, signature):
|
||||
print(f"Received {sum_proofs(redeem_proofs)} sats")
|
||||
|
||||
|
||||
async def redeem_TokenV3_multimint(ctx: Context, token: TokenV3, script, signature):
|
||||
"""
|
||||
Helper function to iterate thruogh a token with multiple mints and redeem them from
|
||||
these mints one keyset at a time.
|
||||
"""
|
||||
for t in token.token:
|
||||
assert t.mint, Exception("Multimint redeem without URL")
|
||||
mint_wallet = Wallet(
|
||||
t.mint, os.path.join(settings.cashu_dir, ctx.obj["WALLET_NAME"])
|
||||
)
|
||||
await verify_mint(mint_wallet, t.mint)
|
||||
keysets = mint_wallet._get_proofs_keysets(t.proofs)
|
||||
# logger.debug(f"Keysets in tokens: {keysets}")
|
||||
# loop over all keysets
|
||||
for keyset in set(keysets):
|
||||
await mint_wallet.load_mint(keyset_id=keyset)
|
||||
# redeem proofs of this keyset
|
||||
redeem_proofs = [p for p in t.proofs if p.id == keyset]
|
||||
_, _ = await mint_wallet.redeem(
|
||||
redeem_proofs, scnd_script=script, scnd_siganture=signature
|
||||
)
|
||||
print(f"Received {sum_proofs(redeem_proofs)} sats")
|
||||
|
||||
|
||||
async def print_mint_balances(ctx: Context, wallet, show_mints=False):
|
||||
"""
|
||||
Helper function that prints the balances for each mint URL that we have tokens from.
|
||||
@@ -165,49 +224,29 @@ async def get_mint_wallet(ctx: Context):
|
||||
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 serialize_TokenV2_to_TokenV3(wallet: Wallet, tokenv2: TokenV2):
|
||||
"""Helper function for the CLI to receive legacy TokenV2 tokens.
|
||||
Takes a list of proofs and constructs a *serialized* TokenV3 to be received through
|
||||
the ordinary path.
|
||||
|
||||
|
||||
async def proofs_to_serialized_tokenv2(wallet, proofs: List[Proof], url: str):
|
||||
Returns:
|
||||
TokenV3: TokenV3
|
||||
"""
|
||||
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 = []
|
||||
|
||||
# get keysets of proofs
|
||||
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:
|
||||
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 {settings.mint_url}): ")
|
||||
or settings.mint_url
|
||||
)
|
||||
|
||||
token.mints.append(TokenV2Mint(url=url, ids=keysets))
|
||||
token_serialized = await wallet._serialize_token_base64(token)
|
||||
tokenv3 = TokenV3(token=[TokenV3Token(proofs=tokenv2.proofs)])
|
||||
if tokenv2.mints:
|
||||
tokenv3.token[0].mint = tokenv2.mints[0].url
|
||||
token_serialized = await wallet._serialize_token_V3(tokenv3)
|
||||
return token_serialized
|
||||
|
||||
|
||||
async def serialize_TokenV1_to_TokenV3(wallet: Wallet, tokenv1: TokenV1):
|
||||
"""Helper function for the CLI to receive legacy TokenV1 tokens.
|
||||
Takes a list of proofs and constructs a *serialized* TokenV3 to be received through
|
||||
the ordinary path.
|
||||
|
||||
Returns:
|
||||
TokenV3: TokenV3
|
||||
"""
|
||||
tokenv3 = TokenV3(token=[TokenV3Token(proofs=tokenv1.__root__)])
|
||||
token_serialized = await wallet._serialize_token_V3(tokenv3)
|
||||
return token_serialized
|
||||
|
||||
@@ -31,6 +31,8 @@ from cashu.core.base import (
|
||||
Proof,
|
||||
TokenV2,
|
||||
TokenV2Mint,
|
||||
TokenV3,
|
||||
TokenV3Token,
|
||||
WalletKeyset,
|
||||
)
|
||||
from cashu.core.bolt11 import Invoice as InvoiceBolt11
|
||||
@@ -630,7 +632,105 @@ class Wallet(LedgerAPI):
|
||||
ret[keyset.mint_url].extend([p for p in proofs if p.id == id])
|
||||
return ret
|
||||
|
||||
def _get_proofs_keysets(self, proofs: List[Proof]):
|
||||
"""Extracts all keyset ids from a list of proofs.
|
||||
|
||||
Args:
|
||||
proofs (List[Proof]): List of proofs to get the keyset id's of
|
||||
"""
|
||||
keysets: List[str] = [proof.id for proof in proofs if proof.id]
|
||||
return keysets
|
||||
|
||||
async def _get_keyset_urls(self, keysets: List[str]):
|
||||
"""Retrieves the mint URLs for a list of keyset id's from the wallet's database.
|
||||
Returns a dictionary from URL to keyset ID
|
||||
|
||||
Args:
|
||||
keysets (List[str]): List of keysets.
|
||||
"""
|
||||
mint_urls: Dict[str, List[str]] = {}
|
||||
for ks in set(keysets):
|
||||
keyset_db = await get_keyset(id=ks, db=self.db)
|
||||
if keyset_db and keyset_db.mint_url:
|
||||
mint_urls[keyset_db.mint_url] = (
|
||||
mint_urls[keyset_db.mint_url] + [ks]
|
||||
if mint_urls.get(keyset_db.mint_url)
|
||||
else [ks]
|
||||
)
|
||||
return mint_urls
|
||||
|
||||
async def _make_token(self, proofs: List[Proof], include_mints=True):
|
||||
"""
|
||||
Takes list of proofs and produces a TokenV3 by looking up
|
||||
the mint URLs by the keyset id from the database.
|
||||
"""
|
||||
token = TokenV3()
|
||||
|
||||
if include_mints:
|
||||
# we create a map from mint url to keyset id and then group
|
||||
# all proofs with their mint url to build a tokenv3
|
||||
|
||||
# extract all keysets from proofs
|
||||
keysets = self._get_proofs_keysets(proofs)
|
||||
# get all mint URLs for all unique keysets from db
|
||||
mint_urls = await self._get_keyset_urls(keysets)
|
||||
|
||||
# append all url-grouped proofs to token
|
||||
for url, ids in mint_urls.items():
|
||||
mint_proofs = [p for p in proofs if p.id in ids]
|
||||
token.token.append(TokenV3Token(mint=url, proofs=mint_proofs))
|
||||
else:
|
||||
token_proofs = TokenV3Token(proofs=proofs)
|
||||
token.token.append(token_proofs)
|
||||
return token
|
||||
|
||||
async def _serialize_token_V3(self, token: TokenV3):
|
||||
"""
|
||||
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
||||
"""
|
||||
prefix = "cashuA"
|
||||
tokenv3_serialized = prefix
|
||||
# encode the token as a base64 string
|
||||
tokenv3_serialized += base64.urlsafe_b64encode(
|
||||
json.dumps(token.to_dict()).encode()
|
||||
).decode()
|
||||
return tokenv3_serialized
|
||||
|
||||
def _deserialize_token_V3(self, tokenv3_serialized: str) -> TokenV3:
|
||||
"""
|
||||
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
||||
"""
|
||||
prefix = "cashuA"
|
||||
assert tokenv3_serialized.startswith(prefix), Exception(
|
||||
f"Token prefix not valid. Expected {prefix}."
|
||||
)
|
||||
token_base64 = tokenv3_serialized[len(prefix) :]
|
||||
token = json.loads(base64.urlsafe_b64decode(token_base64))
|
||||
return TokenV3.parse_obj(token)
|
||||
|
||||
async def serialize_proofs(
|
||||
self, proofs: List[Proof], include_mints=True, legacy=False
|
||||
):
|
||||
"""
|
||||
Produces sharable token with proofs and mint information.
|
||||
"""
|
||||
|
||||
if legacy:
|
||||
# V2 tokens
|
||||
token = await self._make_token_v2(proofs, include_mints)
|
||||
return await self._serialize_token_base64_tokenv2(token)
|
||||
|
||||
# # deprecated code for V1 tokens
|
||||
# proofs_serialized = [p.to_dict() for p in proofs]
|
||||
# return base64.urlsafe_b64encode(
|
||||
# json.dumps(proofs_serialized).encode()
|
||||
# ).decode()
|
||||
|
||||
# V3 tokens
|
||||
token = await self._make_token(proofs, include_mints)
|
||||
return await self._serialize_token_V3(token)
|
||||
|
||||
async def _make_token_v2(self, proofs: List[Proof], include_mints=True):
|
||||
"""
|
||||
Takes list of proofs and produces a TokenV2 by looking up
|
||||
the keyset id and mint URLs from the database.
|
||||
@@ -642,31 +742,27 @@ class Wallet(LedgerAPI):
|
||||
# dummy object to hold information about the mint
|
||||
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:
|
||||
keysets.append(proof.id)
|
||||
keysets: List[str] = [proof.id for proof in proofs if 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:
|
||||
keyset_db = await get_keyset(id=id, db=self.db)
|
||||
if keyset_db and keyset_db.mint_url and keyset_db.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],
|
||||
if keyset_db.mint_url not in mints:
|
||||
mints[keyset_db.mint_url] = TokenV2Mint(
|
||||
url=keyset_db.mint_url,
|
||||
ids=[keyset_db.id],
|
||||
)
|
||||
else:
|
||||
# if a mint URL has multiple keysets, append to the already existing list
|
||||
mints[keyset.mint_url].ids.append(keyset.id)
|
||||
mints[keyset_db.mint_url].ids.append(keyset_db.id)
|
||||
if len(mints) > 0:
|
||||
# add mints grouped by url to the token
|
||||
token.mints = list(mints.values())
|
||||
return token
|
||||
|
||||
async def _serialize_token_base64(self, token: TokenV2):
|
||||
async def _serialize_token_base64_tokenv2(self, token: TokenV2):
|
||||
"""
|
||||
Takes a TokenV2 and serializes it in urlsafe_base64.
|
||||
"""
|
||||
@@ -676,22 +772,6 @@ class Wallet(LedgerAPI):
|
||||
).decode()
|
||||
return token_base64
|
||||
|
||||
async def serialize_proofs(
|
||||
self, proofs: List[Proof], include_mints=True, legacy=False
|
||||
):
|
||||
"""
|
||||
Produces sharable token with proofs and mint information.
|
||||
"""
|
||||
|
||||
if legacy:
|
||||
proofs_serialized = [p.to_dict() for p in proofs]
|
||||
return base64.urlsafe_b64encode(
|
||||
json.dumps(proofs_serialized).encode()
|
||||
).decode()
|
||||
|
||||
token = await self._make_token(proofs, include_mints)
|
||||
return await self._serialize_token_base64(token)
|
||||
|
||||
async def _select_proofs_to_send(self, proofs: List[Proof], amount_to_send: int):
|
||||
"""
|
||||
Selects proofs that can be used with the current mint.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import multiprocessing
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
@@ -7,6 +9,7 @@ import uvicorn
|
||||
from uvicorn import Config, Server
|
||||
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
|
||||
@@ -23,15 +26,32 @@ class UvicornServer(multiprocessing.Process):
|
||||
self.terminate()
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
settings.lightning = False
|
||||
settings.mint_lightning_backend = "FakeWallet"
|
||||
settings.mint_listen_port = 3337
|
||||
settings.mint_database = "data/test_mint"
|
||||
settings.mint_private_key = "privatekeyofthemint"
|
||||
|
||||
dirpath = Path(settings.mint_database)
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
dirpath = Path("data/test_wallet")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
self.server.run()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def mint():
|
||||
|
||||
settings.mint_listen_port = 3337
|
||||
settings.port = 3337
|
||||
settings.mint_url = "http://localhost:3337"
|
||||
settings.port = settings.mint_listen_port
|
||||
config = uvicorn.Config(
|
||||
"cashu.mint.app:app",
|
||||
port=3337,
|
||||
port=settings.mint_listen_port,
|
||||
host="127.0.0.1",
|
||||
)
|
||||
|
||||
|
||||
@@ -10,23 +10,35 @@ from cashu.wallet.cli.cli import cli
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT, mint
|
||||
|
||||
cli_prefix = ["--wallet", "test_wallet", "--host", SERVER_ENDPOINT]
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def cli_prefix():
|
||||
yield ["--wallet", "test_wallet", "--host", settings.mint_url]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def wallet():
|
||||
wallet = Wallet(settings.mint_host, "data/test_wallet", "wallet")
|
||||
asyncio.run(migrate_databases(wallet.db, migrations))
|
||||
asyncio.run(wallet.load_proofs())
|
||||
yield wallet
|
||||
|
||||
|
||||
async def init_wallet():
|
||||
wallet = Wallet(SERVER_ENDPOINT, "data/test_wallet", "wallet")
|
||||
wallet = Wallet(settings.mint_host, "data/test_wallet", "wallet")
|
||||
await migrate_databases(wallet.db, migrations)
|
||||
await wallet.load_proofs()
|
||||
return wallet
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_info():
|
||||
def test_info(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "info"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INFO")
|
||||
print(result.output)
|
||||
result.output.startswith(f"Version: {settings.version}")
|
||||
@@ -34,26 +46,43 @@ def test_info():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_balance():
|
||||
def test_balance(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "balance"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("------ BALANCE ------")
|
||||
print(result.output)
|
||||
wallet = asyncio.run(init_wallet())
|
||||
assert f"Balance: {wallet.available_balance} sat" in result.output
|
||||
w = asyncio.run(init_wallet())
|
||||
assert f"Balance: {w.available_balance} sat" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_wallets():
|
||||
def test_invoice(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "1000"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INVOICE")
|
||||
print(result.output)
|
||||
# wallet = asyncio.run(init_wallet())
|
||||
# assert f"Balance: {wallet.available_balance} sat" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_wallets(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "wallets"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("WALLETS")
|
||||
# on github this is empty
|
||||
if len(result.output):
|
||||
@@ -62,62 +91,109 @@ def test_wallets():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_invoice():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "1000"],
|
||||
)
|
||||
print("INVOICE")
|
||||
print(result.output)
|
||||
wallet = asyncio.run(init_wallet())
|
||||
assert f"Balance: {wallet.available_balance} sat" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_send(mint):
|
||||
def test_send(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "send", "10"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("SEND")
|
||||
print(result.output)
|
||||
|
||||
token = [l for l in result.output.split("\n") if l.startswith("ey")][0]
|
||||
print("TOKEN")
|
||||
print(token)
|
||||
assert "cashuA" in result.output, "output does not have a token"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_receive_tokenv2(mint):
|
||||
def test_receive_tokenv3(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
token = "eyJwcm9vZnMiOiBbeyJpZCI6ICJEU0FsOW52dnlmdmEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJ3MEs4dE9OcFJOdVFvUzQ1Y2g1NkJ3IiwgIkMiOiAiMDI3NzcxODY4NWQ0MDgxNmQ0MTdmZGE1NWUzN2YxOTFkN2E5ODA0N2QyYWE2YzFlNDRhMWZjNTM1ZmViZDdjZDQ5In0sIHsiaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiX2J4cDVHeG1JQUVaRFB5Sm5qaFUxdyIsICJDIjogIjAzZTY2M2UzOWYyNTZlZTAzOTBiNGFiMThkZDA2OTc0NjRjZjIzYTM4OTc1MDlmZDFlYzQ1MzMxMTRlMTcwMDQ2NCJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIkRTQWw5bnZ2eWZ2YSJdfV19"
|
||||
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogImF5TVViZTk4NVVzeiIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogImdQNDlRdl9EZkhxck5zMjVxY1E4a0EiLCAiQyI6ICIwMzZiNjY1MzIxYzBlNGRkYTIwMTI1YTYwOWU4Y2FlMmEzMzRkODRhZDhjZWU4NjY2NTQxYjYyZjk1YjA0Y2FhNmUifSwgeyJpZCI6ICJheU1VYmU5ODVVc3oiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJzTWJ4WGtVTlZKVTh0MTd5cVFJMnFBIiwgIkMiOiAiMDM5ZmIzMTQxN2IyNmY2YWUwMjE1NmYxNzgyZWExYTQ4NTAwMzU2OTVlMTUxODZkNmMwM2MxMzI3ZWU3YWQwZjhlIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "receive", token],
|
||||
[
|
||||
*cli_prefix,
|
||||
"receive",
|
||||
token,
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exception is None
|
||||
print("RECEIVE")
|
||||
print(result.output)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_receive_tokenv1(mint):
|
||||
def test_receive_tokenv3_no_mint(mint, cli_prefix):
|
||||
# this test works only if the previous test succeeds because we simulate the case where the mint URL is not in the token
|
||||
# therefore, we need to know the mint keyset already and have the mint URL in the db
|
||||
runner = CliRunner()
|
||||
token = "3siaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiX3VOV1ZNeDRhQndieWszRDZoLWREZyIsICJDIjogIjAyMmEzMzRmZTIzYTA1OTJhZmM3OTk3OWQyZDJmMmUwOTgxMGNkZTRlNDY5ZGYwYzZhMGE4ZDg0ZmY1MmIxOTZhNyJ9LCB7ImlkIjogIkRTQWw5bnZ2eWZ2YSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk9VUUxnRE90WXhHOXJUMzZKdHFwbWciLCAiQyI6ICIwMzVmMGM2NTNhNTEzMGY4ZmQwNjY5NDg5YzEwMDY3N2Q5NGU0MGFlZjhkYWE0OWZiZDIyZTgzZjhjNThkZjczMTUifV0"
|
||||
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogImF5TVViZTk4NVVzeiIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIkw4XzlBc3d0Rzh1UENmZ29xWnRVRFEiLCAiQyI6ICIwMmE1ZWMzYmY0Nzk2ZTg1NjJhNGRjYjM2YWRkOWYwNDhmZTU3ZGU0ZjEyMjgxMzA3N2FlZjBlM2Y2ZGIwY2U3ZGQifSwgeyJpZCI6ICJheU1VYmU5ODVVc3oiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJ2WWJKZXNhS3BMTnNwaXl3cXd3ejFRIiwgIkMiOiAiMDJjNWVkNDc4YjZjOWU0MTExYjhlOGU1MjBlNThhMTVhYzQzMjUwMGM1MTU2ZmFjNDkyN2Q0ODVhNzM3ZTdlYzA4In1dfV19"
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
*cli_prefix,
|
||||
"receive",
|
||||
token,
|
||||
],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("RECEIVE")
|
||||
print(result.output)
|
||||
|
||||
|
||||
# @pytest.mark.asyncio
|
||||
# def test_receive_tokenv3(mint):
|
||||
# wallet = asyncio.run(init_wallet())
|
||||
# runner = CliRunner()
|
||||
# token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjVXRWJoUzJiOXZrTyIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInpINEM3OXpwZWJYaDIxTDBEWk1qb1EiLCAiQyI6ICIwMmI4ZDZjYzA3NjliMWNiZmQyNzkwN2U2YTQ5YmY2MGMyYzUwYmUwNzhmOGNjMWU1YWE1NTY2NjE1Y2QwOGZmM2YifSwgeyJpZCI6ICI1V0ViaFMyYjl2a08iLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJSYW1aZEJ4a01ybWtmdXh6SjFIOU9RIiwgIkMiOiAiMDI2ZGU2ZDNjZDlmNDY4MDYzMTJkYTczZDE2YzQ2ZDc3NGNkODlhZTk2NzUwMWI3MzA1MmQwNTVmODZkNmJmMmMwIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
|
||||
# result = runner.invoke(
|
||||
# cli,
|
||||
# [*cli_prefix, "receive", token],
|
||||
# )
|
||||
# assert result.exception is None
|
||||
# print("RECEIVE")
|
||||
# print(result.output)
|
||||
|
||||
|
||||
# @pytest.mark.asyncio
|
||||
# def test_receive_tokenv3_no_mint(mint):
|
||||
# runner = CliRunner()
|
||||
# token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjVXRWJoUzJiOXZrTyIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInpINEM3OXpwZWJYaDIxTDBEWk1qb1EiLCAiQyI6ICIwMmI4ZDZjYzA3NjliMWNiZmQyNzkwN2U2YTQ5YmY2MGMyYzUwYmUwNzhmOGNjMWU1YWE1NTY2NjE1Y2QwOGZmM2YifSwgeyJpZCI6ICI1V0ViaFMyYjl2a08iLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJSYW1aZEJ4a01ybWtmdXh6SjFIOU9RIiwgIkMiOiAiMDI2ZGU2ZDNjZDlmNDY4MDYzMTJkYTczZDE2YzQ2ZDc3NGNkODlhZTk2NzUwMWI3MzA1MmQwNTVmODZkNmJmMmMwIn1dfV19"
|
||||
# result = runner.invoke(
|
||||
# cli,
|
||||
# [*cli_prefix, "receive", token],
|
||||
# )
|
||||
# assert result.exception is None
|
||||
# print("RECEIVE")
|
||||
# print(result.output)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_receive_tokenv2(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
token = "eyJwcm9vZnMiOiBbeyJpZCI6ICJheU1VYmU5ODVVc3oiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJ5WWxWR2lmSmJQbGRJZmp5YUxYSnNBIiwgIkMiOiAiMDJlNDE5ZjExNGFlNTFiMzI1MGVkYjE5YTI4NzQ0MjgwMjAwMGE3NTFhZmEwZGZmZDM2N2QxYTI0NTI3NjY2NmIwIn0sIHsiaWQiOiAiYXlNVWJlOTg1VXN6IiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiVlZraDZGTW5sUVZ2WlZOR2Z6emUwQSIsICJDIjogIjAyZGMxZDhjZmFiNDA2NGI4MWFhZThiZWEzNTBjNjIzNWM1NDIzOGNiN2E5ZmYxNTJjNjMxMTAwN2FlNDEzZmFlNyJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbImF5TVViZTk4NVVzeiJdfV19"
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "receive", token],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("RECEIVE")
|
||||
print(result.output)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_receive_tokenv1(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
token = "W3siaWQiOiAiYXlNVWJlOTg1VXN6IiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAicTR6WFdzYl84cGlBRHRQSzB1MFAwdyIsICJDIjogIjAyNDVlYjFmY2E1ODhlYWM0Y2M3OGJkZTJiYmMzOGQwMmY4YTIyZTEyMjcyMjQ2M2RiNDk5ZjA0ZWQ2ZDMzNjZkZCJ9LCB7ImlkIjogImF5TVViZTk4NVVzeiIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInBsaTNKX0QwNkxQZ3RmaW5EZkFWckEiLCAiQyI6ICIwMmU0MDFlMTBhYjI3ODJlYzQzYjMxZmZmMGMxZjc4N2FlYjgyODViNjkxMTAyMzlmYTJiN2VkNzA2MzdhMTliNzUifV0="
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "receive", token],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("RECEIVE")
|
||||
print(result.output)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
def test_nostr_send(mint):
|
||||
def test_nostr_send(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
@@ -130,6 +206,6 @@ def test_nostr_send(mint):
|
||||
"-y",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exception is None
|
||||
print("NOSTR_SEND")
|
||||
print(result.output)
|
||||
|
||||
Reference in New Issue
Block a user