pay to secret

This commit is contained in:
callebtc
2022-09-30 03:10:47 +02:00
parent 92052a7ed9
commit 6789a87c14
10 changed files with 182 additions and 74 deletions

View File

@@ -6,7 +6,7 @@ from pydantic import BaseModel
class Proof(BaseModel): class Proof(BaseModel):
amount: int amount: int
secret: str secret: str = ""
C: str C: str
reserved: bool = False # whether this proof is reserved for sending reserved: bool = False # whether this proof is reserved for sending
send_id: str = "" # unique ID of send attempt send_id: str = "" # unique ID of send attempt
@@ -27,12 +27,11 @@ class Proof(BaseModel):
@classmethod @classmethod
def from_dict(cls, d: dict): def from_dict(cls, d: dict):
assert "secret" in d, "no secret in proof"
assert "amount" in d, "no amount in proof" assert "amount" in d, "no amount in proof"
return cls( return cls(
amount=d.get("amount"), amount=d.get("amount"),
C=d.get("C"), C=d.get("C"),
secret=d.get("secret"), secret=d.get("secret") or "",
reserved=d.get("reserved") or False, reserved=d.get("reserved") or False,
send_id=d.get("send_id") or "", send_id=d.get("send_id") or "",
time_created=d.get("time_created") or "", time_created=d.get("time_created") or "",
@@ -42,6 +41,9 @@ class Proof(BaseModel):
def to_dict(self): def to_dict(self):
return dict(amount=self.amount, secret=self.secret, C=self.C) return dict(amount=self.amount, secret=self.secret, C=self.C)
def to_dict_no_secret(self):
return dict(amount=self.amount, C=self.C)
def __getitem__(self, key): def __getitem__(self, key):
return self.__getattribute__(key) return self.__getattribute__(key)

View File

@@ -1,3 +1,4 @@
import sys
from pathlib import Path from pathlib import Path
from environs import Env # type: ignore from environs import Env # type: ignore
@@ -6,6 +7,9 @@ env = Env()
env.read_env() env.read_env()
DEBUG = env.bool("DEBUG", default=False) DEBUG = env.bool("DEBUG", default=False)
if not DEBUG:
sys.tracebacklimit = 0
CASHU_DIR = env.str("CASHU_DIR", default="~/.cashu") CASHU_DIR = env.str("CASHU_DIR", default="~/.cashu")
CASHU_DIR = CASHU_DIR.replace("~", str(Path.home())) CASHU_DIR = CASHU_DIR.replace("~", str(Path.home()))
assert len(CASHU_DIR), "CASHU_DIR not defined" assert len(CASHU_DIR), "CASHU_DIR not defined"
@@ -32,4 +36,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
LNBITS_KEY = env.str("LNBITS_KEY", default=None) LNBITS_KEY = env.str("LNBITS_KEY", default=None)
MAX_ORDER = 64 MAX_ORDER = 64
VERSION = "0.1.10" VERSION = "0.2.0"

View File

@@ -40,7 +40,7 @@ def create_app(config_object="core.settings") -> FastAPI:
logger.log(level, record.getMessage()) logger.log(level, record.getMessage())
logger.remove() logger.remove()
log_level: str = "INFO" log_level: str = "DEBUG" if DEBUG else "INFO"
formatter = Formatter() formatter = Formatter()
logger.add(sys.stderr, level=log_level, format=formatter.format) logger.add(sys.stderr, level=log_level, format=formatter.format)

View File

@@ -227,30 +227,33 @@ class Ledger:
return {i: self._check_spendable(p) for i, p in enumerate(proofs)} return {i: self._check_spendable(p) for i, p in enumerate(proofs)}
async def split( async def split(
self, proofs: List[Proof], amount: int, output_data: List[BlindedMessage] self, proofs: List[Proof], amount: int, outputs: List[BlindedMessage]
): ):
"""Consumes proofs and prepares new promises based on the amount split.""" """Consumes proofs and prepares new promises based on the amount split."""
self._verify_split_amount(amount)
# Verify proofs are valid
if not all([self._verify_proof(p) for p in proofs]):
return False
total = sum([p.amount for p in proofs]) total = sum([p.amount for p in proofs])
if not self._verify_no_duplicates(proofs, output_data): # verify that amount is kosher
raise Exception("duplicate proofs or promises") self._verify_split_amount(amount)
# verify overspending attempt
if amount > total: if amount > total:
raise Exception("split amount is higher than the total sum") raise Exception("split amount is higher than the total sum.")
if not self._verify_outputs(total, amount, output_data): # verify that only unique proofs and outputs were used
raise Exception("split of promises is not as expected") if not self._verify_no_duplicates(proofs, outputs):
raise Exception("duplicate proofs or promises")
# verify that outputs have the correct amount
if not self._verify_outputs(total, amount, outputs):
raise Exception("split of promises is not as expected.")
# Verify proofs
if not all([self._verify_proof(p) for p in proofs]):
raise Exception("could not verify proofs.")
# Mark proofs as used and prepare new promises # Mark proofs as used and prepare new promises
await self._invalidate_proofs(proofs) await self._invalidate_proofs(proofs)
outs_fst = amount_split(total - amount) outs_fst = amount_split(total - amount)
outs_snd = amount_split(amount) outs_snd = amount_split(amount)
B_fst = [od.B_ for od in output_data[: len(outs_fst)]] B_fst = [od.B_ for od in outputs[: len(outs_fst)]]
B_snd = [od.B_ for od in output_data[len(outs_fst) :]] B_snd = [od.B_ for od in outputs[len(outs_fst) :]]
prom_fst, prom_snd = await self._generate_promises( prom_fst, prom_snd = await self._generate_promises(
outs_fst, B_fst outs_fst, B_fst
), await self._generate_promises(outs_snd, B_snd) ), await self._generate_promises(outs_snd, B_snd)

View File

@@ -82,7 +82,7 @@ async def split(payload: SplitPayload):
except Exception as exc: except Exception as exc:
return {"error": str(exc)} return {"error": str(exc)}
if not split_return: if not split_return:
"""There was a problem with the split""" # tere was a problem with the split
raise Exception("could not split tokens.") return {"error": "there was a problem with the split."}
fst_promises, snd_promises = split_return fst_promises, snd_promises = split_return
return {"fst": fst_promises, "snd": snd_promises} return {"fst": fst_promises, "snd": snd_promises}

View File

@@ -4,6 +4,8 @@ import asyncio
import base64 import base64
import json import json
import math import math
import sys
from loguru import logger
from datetime import datetime from datetime import datetime
from functools import wraps from functools import wraps
from itertools import groupby from itertools import groupby
@@ -41,10 +43,15 @@ class NaturalOrderGroup(click.Group):
@click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet to use.") @click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet to use.")
@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.ensure_object(dict)
ctx.obj["HOST"] = host ctx.obj["HOST"] = host
ctx.obj["WALLET_NAME"] = walletname ctx.obj["WALLET_NAME"] = walletname
ctx.obj["WALLET"] = Wallet(ctx.obj["HOST"], f"{CASHU_DIR}/{walletname}", walletname) wallet = Wallet(ctx.obj["HOST"], f"{CASHU_DIR}/{walletname}", walletname)
ctx.obj["WALLET"] = wallet
asyncio.run(init_wallet(wallet))
pass pass
@@ -64,7 +71,7 @@ def coro(f):
@coro @coro
async def mint(ctx, amount: int, hash: str): async def mint(ctx, amount: int, hash: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet) wallet.load_mint()
wallet.status() wallet.status()
if not LIGHTNING: if not LIGHTNING:
r = await wallet.mint(amount) r = await wallet.mint(amount)
@@ -88,35 +95,42 @@ async def mint(ctx, amount: int, hash: str):
@coro @coro
async def balance(ctx): async def balance(ctx):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet)
wallet.status() wallet.status()
@cli.command("send", help="Send tokens.") @cli.command("send", help="Send tokens.")
@click.argument("amount", type=int) @click.argument("amount", type=int)
@click.option("--secret", "-s", default="", help="Token spending condition.", type=str)
@click.pass_context @click.pass_context
@coro @coro
async def send(ctx, amount: int): async def send(ctx, amount: int, secret: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet) wallet.load_mint()
wallet.status() wallet.status()
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount) # TODO: remove this list hack
secrets = [secret] if secret else None
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount, secrets)
await wallet.set_reserved(send_proofs, reserved=True) await wallet.set_reserved(send_proofs, reserved=True)
token = await wallet.serialize_proofs(send_proofs) token = await wallet.serialize_proofs(
send_proofs, hide_secrets=True if secrets else False
)
print(token) print(token)
wallet.status() wallet.status()
@cli.command("receive", help="Receive tokens.") @cli.command("receive", help="Receive tokens.")
@click.argument("token", type=str) @click.argument("token", type=str)
@click.option("--secret", "-s", default="", help="Token spending condition.", type=str)
@click.pass_context @click.pass_context
@coro @coro
async def receive(ctx, token: str): async def receive(ctx, token: str, secret: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet) wallet.load_mint()
wallet.status() wallet.status()
# TODO: remove this list hack
secrets = [secret] if secret else None
proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))] proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))]
_, _ = await wallet.redeem(proofs) _, _ = await wallet.redeem(proofs, secrets)
wallet.status() wallet.status()
@@ -130,7 +144,7 @@ async def receive(ctx, token: str):
@coro @coro
async def burn(ctx, token: str, all: bool, force: bool): async def burn(ctx, token: str, all: bool, force: bool):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet) wallet.load_mint()
if not (all or token or force) or (token and all): if not (all or token or force) or (token and all):
print( print(
"Error: enter a token or use --all to burn all pending tokens or --force to check all tokens." "Error: enter a token or use --all to burn all pending tokens or --force to check all tokens."
@@ -157,7 +171,7 @@ async def burn(ctx, token: str, all: bool, force: bool):
@coro @coro
async def pending(ctx): async def pending(ctx):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet) wallet.load_mint()
reserved_proofs = await get_reserved_proofs(wallet.db) reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs): if len(reserved_proofs):
sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id")) sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id"))
@@ -181,7 +195,7 @@ async def pending(ctx):
@coro @coro
async def pay(ctx, invoice: str): async def pay(ctx, invoice: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet) wallet.load_mint()
wallet.status() wallet.status()
decoded_invoice: Invoice = bolt11.decode(invoice) decoded_invoice: Invoice = bolt11.decode(invoice)
amount = math.ceil( amount = math.ceil(
@@ -206,5 +220,6 @@ async def info(ctx):
print(f"Version: {VERSION}") print(f"Version: {VERSION}")
print(f"Debug: {DEBUG}") print(f"Debug: {DEBUG}")
print(f"Cashu dir: {CASHU_DIR}") print(f"Cashu dir: {CASHU_DIR}")
print(f"Wallet: {ctx.obj['WALLET_NAME']}")
print(f"Mint URL: {MINT_URL}") print(f"Mint URL: {MINT_URL}")
return return

View File

@@ -3,6 +3,7 @@ import json
import secrets as scrts import secrets as scrts
import uuid import uuid
from typing import List from typing import List
from loguru import logger
import requests import requests
@@ -31,7 +32,8 @@ from cashu.wallet.crud import (
class LedgerAPI: class LedgerAPI:
def __init__(self, url): def __init__(self, url):
self.url = url self.url = url
self.keys = self._get_keys(url) # self.keys = self._get_keys(self.url)
# self._load_mint()
@staticmethod @staticmethod
def _get_keys(url): def _get_keys(url):
@@ -51,10 +53,12 @@ class LedgerAPI:
rv.append(2**pos) rv.append(2**pos)
return rv return rv
def _construct_proofs(self, promises: List[BlindedSignature], secrets: List[str]): def _construct_proofs(
"""Returns proofs of promise from promises.""" self, promises: List[BlindedSignature], secrets: List[str], rs: List[str]
):
"""Returns proofs of promise from promises. Wants secrets and blinding factors rs."""
proofs = [] proofs = []
for promise, (r, secret) in zip(promises, secrets): for promise, secret, r in zip(promises, secrets, rs):
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
C = b_dhke.step3_alice(C_, r, self.keys[promise.amount]) C = b_dhke.step3_alice(C_, r, self.keys[promise.amount])
proof = Proof(amount=promise.amount, C=C.serialize().hex(), secret=secret) proof = Proof(amount=promise.amount, C=C.serialize().hex(), secret=secret)
@@ -65,65 +69,115 @@ class LedgerAPI:
"""Returns base64 encoded random string.""" """Returns base64 encoded random string."""
return scrts.token_urlsafe(randombits // 8) return scrts.token_urlsafe(randombits // 8)
def _load_mint(self):
assert len(
self.url
), "Ledger not initialized correctly: mint URL not specified yet. "
self.keys = self._get_keys(self.url)
assert len(self.keys) > 0, "did not receive keys from mint."
def request_mint(self, amount): def request_mint(self, amount):
"""Requests a mint from the server and returns Lightning invoice.""" """Requests a mint from the server and returns Lightning invoice."""
r = requests.get(self.url + "/mint", params={"amount": amount}) r = requests.get(self.url + "/mint", params={"amount": amount})
return r.json() return r.json()
def mint(self, amounts, payment_hash=None): @staticmethod
"""Mints new coins and returns a proof of promise.""" def _construct_outputs(amounts: List[int], secrets: List[str]):
"""Takes a list of amounts and secrets and returns outputs.
Outputs are blinded messages `payloads` and blinding factors `rs`"""
assert len(amounts) == len(
secrets
), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}"
payloads: MintPayloads = MintPayloads() payloads: MintPayloads = MintPayloads()
secrets = []
rs = [] rs = []
for amount in amounts: for secret, amount in zip(secrets, amounts):
secret = self._generate_secret()
secrets.append(secret)
B_, r = b_dhke.step1_alice(secret) B_, r = b_dhke.step1_alice(secret)
rs.append(r) rs.append(r)
payload: BlindedMessage = BlindedMessage( payload: BlindedMessage = BlindedMessage(
amount=amount, B_=B_.serialize().hex() amount=amount, B_=B_.serialize().hex()
) )
payloads.blinded_messages.append(payload) payloads.blinded_messages.append(payload)
promises_list = requests.post( return payloads, rs
def mint(self, amounts, payment_hash=None):
"""Mints new coins and returns a proof of promise."""
secrets = [self._generate_secret() for s in range(len(amounts))]
payloads, rs = self._construct_outputs(amounts, secrets)
resp = requests.post(
self.url + "/mint", self.url + "/mint",
json=payloads.dict(), json=payloads.dict(),
params={"payment_hash": payment_hash}, params={"payment_hash": payment_hash},
).json() )
try:
promises_list = resp.json()
except:
if resp.status_code >= 300:
raise Exception(f"Error: {f'mint returned {resp.status_code}'}")
else:
raise Exception("Unkown mint error.")
if "error" in promises_list: if "error" in promises_list:
raise Exception("Error: {}".format(promises_list["error"])) raise Exception("Error: {}".format(promises_list["error"]))
promises = [BlindedSignature.from_dict(p) for p in promises_list]
return self._construct_proofs(promises, [(r, s) for r, s in zip(rs, secrets)])
def split(self, proofs, amount): promises = [BlindedSignature.from_dict(p) for p in promises_list]
"""Consume proofs and create new promises based on amount split.""" return self._construct_proofs(promises, secrets, rs)
def split(self, proofs, amount, snd_secrets: List[str] = None):
"""Consume proofs and create new promises based on amount split.
If snd_secrets is None, random secrets will be generated for the tokens to keep (fst_outputs)
and the promises to send (snd_outputs).
If snd_secrets is provided, the wallet will create blinded secrets with those to attach a
predefined spending condition to the tokens they want to send."""
total = sum([p["amount"] for p in proofs]) total = sum([p["amount"] for p in proofs])
fst_amt, snd_amt = total - amount, amount fst_amt, snd_amt = total - amount, amount
fst_outputs = amount_split(fst_amt) fst_outputs = amount_split(fst_amt)
snd_outputs = amount_split(snd_amt) snd_outputs = amount_split(snd_amt)
# TODO: Refactor together with the same procedure in self.mint() amounts = fst_outputs + snd_outputs
secrets = [] if snd_secrets is None:
payloads: MintPayloads = MintPayloads() secrets = [self._generate_secret() for s in range(len(amounts))]
for output_amt in fst_outputs + snd_outputs: else:
secret = self._generate_secret() logger.debug("Creating proofs with spending condition.")
B_, r = b_dhke.step1_alice(secret) assert len(snd_secrets) == len(
secrets.append((r, secret)) snd_outputs
payload: BlindedMessage = BlindedMessage( ), "number of snd_secrets does not match number of ouptus."
amount=output_amt, B_=B_.serialize().hex() # append predefined secrets (to send) to random secrets (to keep)
) secrets = [
payloads.blinded_messages.append(payload) self._generate_secret() for s in range(len(fst_outputs))
] + snd_secrets
assert len(secrets) == len(
amounts
), "number of secrets does not match number of outputs"
payloads, rs = self._construct_outputs(amounts, secrets)
split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads) split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads)
promises_dict = requests.post( resp = requests.post(
self.url + "/split", self.url + "/split",
json=split_payload.dict(), json=split_payload.dict(),
).json() )
try:
promises_dict = resp.json()
except:
if resp.status_code >= 300:
raise Exception(f"Error: {f'mint returned {resp.status_code}'}")
else:
raise Exception("Unkown mint error.")
if "error" in promises_dict: if "error" in promises_dict:
raise Exception("Error: {}".format(promises_dict["error"])) raise Exception("Error: {}".format(promises_dict["error"]))
promises_fst = [BlindedSignature.from_dict(p) for p in promises_dict["fst"]] promises_fst = [BlindedSignature.from_dict(p) for p in promises_dict["fst"]]
promises_snd = [BlindedSignature.from_dict(p) for p in promises_dict["snd"]] promises_snd = [BlindedSignature.from_dict(p) for p in promises_dict["snd"]]
# Obtain proofs from promises # Construct proofs from promises (i.e., unblind signatures)
fst_proofs = self._construct_proofs(promises_fst, secrets[: len(promises_fst)]) fst_proofs = self._construct_proofs(
snd_proofs = self._construct_proofs(promises_snd, secrets[len(promises_fst) :]) promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)]
)
snd_proofs = self._construct_proofs(
promises_snd, secrets[len(promises_fst) :], rs[len(promises_fst) :]
)
return fst_proofs, snd_proofs return fst_proofs, snd_proofs
@@ -154,6 +208,9 @@ class Wallet(LedgerAPI):
self.proofs: List[Proof] = [] self.proofs: List[Proof] = []
self.name = name self.name = name
def load_mint(self):
super()._load_mint()
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)
@@ -173,12 +230,20 @@ class Wallet(LedgerAPI):
self.proofs += proofs self.proofs += proofs
return proofs return proofs
async def redeem(self, proofs: List[Proof]): async def redeem(self, proofs: List[Proof], snd_secrets: List[str] = None):
if snd_secrets:
logger.debug(f"Redeption secrets: {snd_secrets}")
assert len(proofs) == len(snd_secrets)
# overload proofs with custom secrets for redemption
for p, s in zip(proofs, snd_secrets):
p.secret = s
return await self.split(proofs, sum(p["amount"] for p in proofs)) return await self.split(proofs, sum(p["amount"] for p in proofs))
async def split(self, proofs: List[Proof], amount: int): async def split(
self, proofs: List[Proof], amount: int, snd_secrets: List[str] = None
):
assert len(proofs) > 0, ValueError("no proofs provided.") assert len(proofs) > 0, ValueError("no proofs provided.")
fst_proofs, snd_proofs = super().split(proofs, amount) fst_proofs, snd_proofs = super().split(proofs, amount, snd_secrets)
if len(fst_proofs) == 0 and len(snd_proofs) == 0: if len(fst_proofs) == 0 and len(snd_proofs) == 0:
raise Exception("received no splits.") raise Exception("received no splits.")
used_secrets = [p["secret"] for p in proofs] used_secrets = [p["secret"] for p in proofs]
@@ -201,18 +266,27 @@ class Wallet(LedgerAPI):
return status["paid"] return status["paid"]
@staticmethod @staticmethod
async def serialize_proofs(proofs: List[Proof]): async def serialize_proofs(proofs: List[Proof], hide_secrets=False):
proofs_serialized = [p.to_dict() for p in proofs] 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( token = base64.urlsafe_b64encode(
json.dumps(proofs_serialized).encode() json.dumps(proofs_serialized).encode()
).decode() ).decode()
return token return token
async def split_to_send(self, proofs: List[Proof], amount): async def split_to_send(
self, proofs: List[Proof], amount, snd_secrets: List[str] = None
):
"""Like self.split but only considers non-reserved tokens.""" """Like self.split but only considers non-reserved tokens."""
if snd_secrets:
logger.debug(f"Spending conditions: {snd_secrets}")
if len([p for p in proofs if not p.reserved]) <= 0: if len([p for p in proofs if not p.reserved]) <= 0:
raise Exception("balance too low.") raise Exception("balance too low.")
return await self.split([p for p in proofs if not p.reserved], amount) return await self.split(
[p for p in proofs if not p.reserved], amount, snd_secrets
)
async def set_reserved(self, proofs: List[Proof], reserved: bool): async def set_reserved(self, proofs: List[Proof], reserved: bool):
"""Mark a proof as reserved to avoid reuse or delete marking.""" """Mark a proof as reserved to avoid reuse or delete marking."""

View File

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

View File

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

View File

@@ -25,10 +25,12 @@ def assert_amt(proofs, expected):
async def run_test(): async def run_test():
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1") wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
await migrate_databases(wallet1.db, migrations) await migrate_databases(wallet1.db, migrations)
wallet1.load_mint()
wallet1.status() wallet1.status()
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2") wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
await migrate_databases(wallet2.db, migrations) await migrate_databases(wallet2.db, migrations)
wallet2.load_mint()
wallet2.status() wallet2.status()
proofs = [] proofs = []
@@ -105,6 +107,14 @@ async def run_test():
assert wallet1.proof_amounts() == [1, 2, 4, 4, 32, 64] assert wallet1.proof_amounts() == [1, 2, 4, 4, 32, 64]
assert wallet2.proof_amounts() == [4, 16] assert wallet2.proof_amounts() == [4, 16]
# manipulate the proof amount
w1_fst_proofs2[0]["amount"] = 123
await assert_err(
wallet1.split(w1_fst_proofs2, 20),
"Error: 123",
)
# try to split an invalid amount
await assert_err( await assert_err(
wallet1.split(w1_snd_proofs, -500), wallet1.split(w1_snd_proofs, -500),
"Error: invalid split amount: -500", "Error: invalid split amount: -500",