mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
pay to secret
This commit is contained in:
@@ -6,7 +6,7 @@ from pydantic import BaseModel
|
||||
|
||||
class Proof(BaseModel):
|
||||
amount: int
|
||||
secret: str
|
||||
secret: str = ""
|
||||
C: str
|
||||
reserved: bool = False # whether this proof is reserved for sending
|
||||
send_id: str = "" # unique ID of send attempt
|
||||
@@ -27,12 +27,11 @@ class Proof(BaseModel):
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict):
|
||||
assert "secret" in d, "no secret in proof"
|
||||
assert "amount" in d, "no amount in proof"
|
||||
return cls(
|
||||
amount=d.get("amount"),
|
||||
C=d.get("C"),
|
||||
secret=d.get("secret"),
|
||||
secret=d.get("secret") or "",
|
||||
reserved=d.get("reserved") or False,
|
||||
send_id=d.get("send_id") or "",
|
||||
time_created=d.get("time_created") or "",
|
||||
@@ -42,6 +41,9 @@ class Proof(BaseModel):
|
||||
def to_dict(self):
|
||||
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):
|
||||
return self.__getattribute__(key)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from environs import Env # type: ignore
|
||||
@@ -6,6 +7,9 @@ env = Env()
|
||||
env.read_env()
|
||||
|
||||
DEBUG = env.bool("DEBUG", default=False)
|
||||
if not DEBUG:
|
||||
sys.tracebacklimit = 0
|
||||
|
||||
CASHU_DIR = env.str("CASHU_DIR", default="~/.cashu")
|
||||
CASHU_DIR = CASHU_DIR.replace("~", str(Path.home()))
|
||||
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)
|
||||
|
||||
MAX_ORDER = 64
|
||||
VERSION = "0.1.10"
|
||||
VERSION = "0.2.0"
|
||||
|
||||
@@ -40,7 +40,7 @@ def create_app(config_object="core.settings") -> FastAPI:
|
||||
logger.log(level, record.getMessage())
|
||||
|
||||
logger.remove()
|
||||
log_level: str = "INFO"
|
||||
log_level: str = "DEBUG" if DEBUG else "INFO"
|
||||
formatter = Formatter()
|
||||
logger.add(sys.stderr, level=log_level, format=formatter.format)
|
||||
|
||||
|
||||
@@ -227,30 +227,33 @@ class Ledger:
|
||||
return {i: self._check_spendable(p) for i, p in enumerate(proofs)}
|
||||
|
||||
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."""
|
||||
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])
|
||||
|
||||
if not self._verify_no_duplicates(proofs, output_data):
|
||||
raise Exception("duplicate proofs or promises")
|
||||
# verify that amount is kosher
|
||||
self._verify_split_amount(amount)
|
||||
# verify overspending attempt
|
||||
if amount > total:
|
||||
raise Exception("split amount is higher than the total sum")
|
||||
if not self._verify_outputs(total, amount, output_data):
|
||||
raise Exception("split of promises is not as expected")
|
||||
raise Exception("split amount is higher than the total sum.")
|
||||
# verify that only unique proofs and outputs were used
|
||||
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
|
||||
await self._invalidate_proofs(proofs)
|
||||
|
||||
outs_fst = amount_split(total - amount)
|
||||
outs_snd = amount_split(amount)
|
||||
B_fst = [od.B_ for od in output_data[: len(outs_fst)]]
|
||||
B_snd = [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 outputs[len(outs_fst) :]]
|
||||
prom_fst, prom_snd = await self._generate_promises(
|
||||
outs_fst, B_fst
|
||||
), await self._generate_promises(outs_snd, B_snd)
|
||||
|
||||
@@ -82,7 +82,7 @@ async def split(payload: SplitPayload):
|
||||
except Exception as exc:
|
||||
return {"error": str(exc)}
|
||||
if not split_return:
|
||||
"""There was a problem with the split"""
|
||||
raise Exception("could not split tokens.")
|
||||
# tere was a problem with the split
|
||||
return {"error": "there was a problem with the split."}
|
||||
fst_promises, snd_promises = split_return
|
||||
return {"fst": fst_promises, "snd": snd_promises}
|
||||
|
||||
@@ -4,6 +4,8 @@ import asyncio
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from loguru import logger
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from itertools import groupby
|
||||
@@ -41,10 +43,15 @@ class NaturalOrderGroup(click.Group):
|
||||
@click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet to use.")
|
||||
@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
|
||||
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
|
||||
|
||||
|
||||
@@ -64,7 +71,7 @@ def coro(f):
|
||||
@coro
|
||||
async def mint(ctx, amount: int, hash: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await init_wallet(wallet)
|
||||
wallet.load_mint()
|
||||
wallet.status()
|
||||
if not LIGHTNING:
|
||||
r = await wallet.mint(amount)
|
||||
@@ -88,35 +95,42 @@ async def mint(ctx, amount: int, hash: str):
|
||||
@coro
|
||||
async def balance(ctx):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await init_wallet(wallet)
|
||||
wallet.status()
|
||||
|
||||
|
||||
@cli.command("send", help="Send tokens.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.option("--secret", "-s", default="", help="Token spending condition.", type=str)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def send(ctx, amount: int):
|
||||
async def send(ctx, amount: int, secret: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await init_wallet(wallet)
|
||||
wallet.load_mint()
|
||||
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)
|
||||
token = await wallet.serialize_proofs(send_proofs)
|
||||
token = await wallet.serialize_proofs(
|
||||
send_proofs, hide_secrets=True if secrets else False
|
||||
)
|
||||
print(token)
|
||||
wallet.status()
|
||||
|
||||
|
||||
@cli.command("receive", help="Receive tokens.")
|
||||
@click.argument("token", type=str)
|
||||
@click.option("--secret", "-s", default="", help="Token spending condition.", type=str)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def receive(ctx, token: str):
|
||||
async def receive(ctx, token: str, secret: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await init_wallet(wallet)
|
||||
wallet.load_mint()
|
||||
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))]
|
||||
_, _ = await wallet.redeem(proofs)
|
||||
_, _ = await wallet.redeem(proofs, secrets)
|
||||
wallet.status()
|
||||
|
||||
|
||||
@@ -130,7 +144,7 @@ async def receive(ctx, token: str):
|
||||
@coro
|
||||
async def burn(ctx, token: str, all: bool, force: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await init_wallet(wallet)
|
||||
wallet.load_mint()
|
||||
if not (all or token or force) or (token and all):
|
||||
print(
|
||||
"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
|
||||
async def pending(ctx):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await init_wallet(wallet)
|
||||
wallet.load_mint()
|
||||
reserved_proofs = await get_reserved_proofs(wallet.db)
|
||||
if len(reserved_proofs):
|
||||
sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id"))
|
||||
@@ -181,7 +195,7 @@ async def pending(ctx):
|
||||
@coro
|
||||
async def pay(ctx, invoice: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await init_wallet(wallet)
|
||||
wallet.load_mint()
|
||||
wallet.status()
|
||||
decoded_invoice: Invoice = bolt11.decode(invoice)
|
||||
amount = math.ceil(
|
||||
@@ -206,5 +220,6 @@ async def info(ctx):
|
||||
print(f"Version: {VERSION}")
|
||||
print(f"Debug: {DEBUG}")
|
||||
print(f"Cashu dir: {CASHU_DIR}")
|
||||
print(f"Wallet: {ctx.obj['WALLET_NAME']}")
|
||||
print(f"Mint URL: {MINT_URL}")
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
import secrets as scrts
|
||||
import uuid
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
import requests
|
||||
|
||||
@@ -31,7 +32,8 @@ from cashu.wallet.crud import (
|
||||
class LedgerAPI:
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
self.keys = self._get_keys(url)
|
||||
# self.keys = self._get_keys(self.url)
|
||||
# self._load_mint()
|
||||
|
||||
@staticmethod
|
||||
def _get_keys(url):
|
||||
@@ -51,10 +53,12 @@ class LedgerAPI:
|
||||
rv.append(2**pos)
|
||||
return rv
|
||||
|
||||
def _construct_proofs(self, promises: List[BlindedSignature], secrets: List[str]):
|
||||
"""Returns proofs of promise from promises."""
|
||||
def _construct_proofs(
|
||||
self, promises: List[BlindedSignature], secrets: List[str], rs: List[str]
|
||||
):
|
||||
"""Returns proofs of promise from promises. Wants secrets and blinding factors rs."""
|
||||
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 = b_dhke.step3_alice(C_, r, self.keys[promise.amount])
|
||||
proof = Proof(amount=promise.amount, C=C.serialize().hex(), secret=secret)
|
||||
@@ -65,65 +69,115 @@ class LedgerAPI:
|
||||
"""Returns base64 encoded random string."""
|
||||
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):
|
||||
"""Requests a mint from the server and returns Lightning invoice."""
|
||||
r = requests.get(self.url + "/mint", params={"amount": amount})
|
||||
return r.json()
|
||||
|
||||
def mint(self, amounts, payment_hash=None):
|
||||
"""Mints new coins and returns a proof of promise."""
|
||||
@staticmethod
|
||||
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()
|
||||
secrets = []
|
||||
rs = []
|
||||
for amount in amounts:
|
||||
secret = self._generate_secret()
|
||||
secrets.append(secret)
|
||||
for secret, amount in zip(secrets, amounts):
|
||||
B_, r = b_dhke.step1_alice(secret)
|
||||
rs.append(r)
|
||||
payload: BlindedMessage = BlindedMessage(
|
||||
amount=amount, B_=B_.serialize().hex()
|
||||
)
|
||||
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",
|
||||
json=payloads.dict(),
|
||||
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:
|
||||
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):
|
||||
"""Consume proofs and create new promises based on amount split."""
|
||||
promises = [BlindedSignature.from_dict(p) for p in promises_list]
|
||||
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])
|
||||
fst_amt, snd_amt = total - amount, amount
|
||||
fst_outputs = amount_split(fst_amt)
|
||||
snd_outputs = amount_split(snd_amt)
|
||||
|
||||
# TODO: Refactor together with the same procedure in self.mint()
|
||||
secrets = []
|
||||
payloads: MintPayloads = MintPayloads()
|
||||
for output_amt in fst_outputs + snd_outputs:
|
||||
secret = self._generate_secret()
|
||||
B_, r = b_dhke.step1_alice(secret)
|
||||
secrets.append((r, secret))
|
||||
payload: BlindedMessage = BlindedMessage(
|
||||
amount=output_amt, B_=B_.serialize().hex()
|
||||
)
|
||||
payloads.blinded_messages.append(payload)
|
||||
amounts = fst_outputs + snd_outputs
|
||||
if snd_secrets is None:
|
||||
secrets = [self._generate_secret() for s in range(len(amounts))]
|
||||
else:
|
||||
logger.debug("Creating proofs with spending condition.")
|
||||
assert len(snd_secrets) == len(
|
||||
snd_outputs
|
||||
), "number of snd_secrets does not match number of ouptus."
|
||||
# append predefined secrets (to send) to random secrets (to keep)
|
||||
secrets = [
|
||||
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)
|
||||
promises_dict = requests.post(
|
||||
resp = requests.post(
|
||||
self.url + "/split",
|
||||
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:
|
||||
raise Exception("Error: {}".format(promises_dict["error"]))
|
||||
promises_fst = [BlindedSignature.from_dict(p) for p in promises_dict["fst"]]
|
||||
promises_snd = [BlindedSignature.from_dict(p) for p in promises_dict["snd"]]
|
||||
# Obtain proofs from promises
|
||||
fst_proofs = self._construct_proofs(promises_fst, secrets[: len(promises_fst)])
|
||||
snd_proofs = self._construct_proofs(promises_snd, secrets[len(promises_fst) :])
|
||||
# Construct proofs from promises (i.e., unblind signatures)
|
||||
fst_proofs = self._construct_proofs(
|
||||
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
|
||||
|
||||
@@ -154,6 +208,9 @@ class Wallet(LedgerAPI):
|
||||
self.proofs: List[Proof] = []
|
||||
self.name = name
|
||||
|
||||
def load_mint(self):
|
||||
super()._load_mint()
|
||||
|
||||
async def load_proofs(self):
|
||||
self.proofs = await get_proofs(db=self.db)
|
||||
|
||||
@@ -173,12 +230,20 @@ class Wallet(LedgerAPI):
|
||||
self.proofs += 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))
|
||||
|
||||
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.")
|
||||
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:
|
||||
raise Exception("received no splits.")
|
||||
used_secrets = [p["secret"] for p in proofs]
|
||||
@@ -201,18 +266,27 @@ class Wallet(LedgerAPI):
|
||||
return status["paid"]
|
||||
|
||||
@staticmethod
|
||||
async def serialize_proofs(proofs: List[Proof]):
|
||||
async def serialize_proofs(proofs: List[Proof], hide_secrets=False):
|
||||
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
|
||||
|
||||
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."""
|
||||
if snd_secrets:
|
||||
logger.debug(f"Spending conditions: {snd_secrets}")
|
||||
if len([p for p in proofs if not p.reserved]) <= 0:
|
||||
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):
|
||||
"""Mark a proof as reserved to avoid reuse or delete marking."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cashu"
|
||||
version = "0.1.10"
|
||||
version = "0.2.0"
|
||||
description = "Ecash wallet and mint."
|
||||
authors = ["calle <callebtc@protonmail.com>"]
|
||||
license = "MIT"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cahu.wallet.cli:cli"]}
|
||||
|
||||
setuptools.setup(
|
||||
name="cashu",
|
||||
version="0.1.10",
|
||||
version="0.2.0",
|
||||
description="Ecash wallet and mint with Bitcoin Lightning support",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@@ -25,10 +25,12 @@ def assert_amt(proofs, expected):
|
||||
async def run_test():
|
||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
wallet1.load_mint()
|
||||
wallet1.status()
|
||||
|
||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.load_mint()
|
||||
wallet2.status()
|
||||
|
||||
proofs = []
|
||||
@@ -105,6 +107,14 @@ async def run_test():
|
||||
assert wallet1.proof_amounts() == [1, 2, 4, 4, 32, 64]
|
||||
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(
|
||||
wallet1.split(w1_snd_proofs, -500),
|
||||
"Error: invalid split amount: -500",
|
||||
|
||||
Reference in New Issue
Block a user