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):
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user