mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-21 02:54:20 +01:00
lightning
This commit is contained in:
2
cashu
2
cashu
@@ -31,7 +31,7 @@ def coro(f):
|
|||||||
@click.option("--invalidate", default="", help="Invalidate tokens.")
|
@click.option("--invalidate", default="", help="Invalidate tokens.")
|
||||||
@coro
|
@coro
|
||||||
async def main(host, wallet, mint, send, receive, invalidate):
|
async def main(host, wallet, mint, send, receive, invalidate):
|
||||||
wallet = Wallet(host, f"data/{wallet}")
|
wallet = Wallet(host, f"data/{wallet}", wallet)
|
||||||
await m001_initial(db=wallet.db)
|
await m001_initial(db=wallet.db)
|
||||||
await wallet.load_proofs()
|
await wallet.load_proofs()
|
||||||
if mint:
|
if mint:
|
||||||
|
|||||||
3
lightning/__init__.py
Normal file
3
lightning/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from lightning.lnbits import LNbitsWallet
|
||||||
|
|
||||||
|
WALLET = LNbitsWallet()
|
||||||
88
lightning/base.py
Normal file
88
lightning/base.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import AsyncGenerator, Coroutine, NamedTuple, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class StatusResponse(NamedTuple):
|
||||||
|
error_message: Optional[str]
|
||||||
|
balance_msat: int
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceResponse(NamedTuple):
|
||||||
|
ok: bool
|
||||||
|
checking_id: Optional[str] = None # payment_hash, rpc_id
|
||||||
|
payment_request: Optional[str] = None
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentResponse(NamedTuple):
|
||||||
|
# when ok is None it means we don't know if this succeeded
|
||||||
|
ok: Optional[bool] = None
|
||||||
|
checking_id: Optional[str] = None # payment_hash, rcp_id
|
||||||
|
fee_msat: Optional[int] = None
|
||||||
|
preimage: Optional[str] = None
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentStatus(NamedTuple):
|
||||||
|
paid: Optional[bool] = None
|
||||||
|
fee_msat: Optional[int] = None
|
||||||
|
preimage: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.paid is not True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.paid == False
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.paid == True:
|
||||||
|
return "settled"
|
||||||
|
elif self.paid == False:
|
||||||
|
return "failed"
|
||||||
|
elif self.paid == None:
|
||||||
|
return "still pending"
|
||||||
|
else:
|
||||||
|
return "unknown (should never happen)"
|
||||||
|
|
||||||
|
|
||||||
|
class Wallet(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def status(self) -> Coroutine[None, None, StatusResponse]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_invoice(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
description_hash: Optional[bytes] = None,
|
||||||
|
) -> Coroutine[None, None, InvoiceResponse]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def pay_invoice(
|
||||||
|
self, bolt11: str, fee_limit_msat: int
|
||||||
|
) -> Coroutine[None, None, PaymentResponse]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_invoice_status(
|
||||||
|
self, checking_id: str
|
||||||
|
) -> Coroutine[None, None, PaymentStatus]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_payment_status(
|
||||||
|
self, checking_id: str
|
||||||
|
) -> Coroutine[None, None, PaymentStatus]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Unsupported(Exception):
|
||||||
|
pass
|
||||||
150
lightning/lnbits.py
Normal file
150
lightning/lnbits.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from os import getenv
|
||||||
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .base import (
|
||||||
|
InvoiceResponse,
|
||||||
|
PaymentResponse,
|
||||||
|
PaymentStatus,
|
||||||
|
StatusResponse,
|
||||||
|
Wallet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LNbitsWallet(Wallet):
|
||||||
|
"""https://github.com/lnbits/lnbits"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.endpoint = getenv("LNBITS_ENDPOINT")
|
||||||
|
|
||||||
|
key = getenv("LNBITS_KEY")
|
||||||
|
self.key = {"X-Api-Key": key}
|
||||||
|
self.s = requests.Session()
|
||||||
|
self.s.auth = ("user", "pass")
|
||||||
|
self.s.headers.update({"X-Api-Key": key})
|
||||||
|
|
||||||
|
async def status(self) -> StatusResponse:
|
||||||
|
try:
|
||||||
|
r = self.s.get(url=f"{self.endpoint}/api/v1/wallet", timeout=15)
|
||||||
|
except Exception as exc:
|
||||||
|
return StatusResponse(
|
||||||
|
f"Failed to connect to {self.endpoint} due to: {exc}", 0
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
except:
|
||||||
|
return StatusResponse(
|
||||||
|
f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0
|
||||||
|
)
|
||||||
|
if "detail" in data:
|
||||||
|
return StatusResponse(f"LNbits error: {data['detail']}", 0)
|
||||||
|
return StatusResponse(None, data["balance"])
|
||||||
|
|
||||||
|
async def create_invoice(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
) -> InvoiceResponse:
|
||||||
|
data: Dict = {"out": False, "amount": amount}
|
||||||
|
if description_hash:
|
||||||
|
data["description_hash"] = description_hash.hex()
|
||||||
|
if unhashed_description:
|
||||||
|
data["unhashed_description"] = unhashed_description.hex()
|
||||||
|
|
||||||
|
data["memo"] = memo or ""
|
||||||
|
try:
|
||||||
|
r = self.s.post(url=f"{self.endpoint}/api/v1/payments", json=data)
|
||||||
|
except:
|
||||||
|
return InvoiceResponse(False, None, None, r.json()["detail"])
|
||||||
|
ok, checking_id, payment_request, error_message = (
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
checking_id, payment_request = data["checking_id"], data["payment_request"]
|
||||||
|
|
||||||
|
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||||
|
|
||||||
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
|
try:
|
||||||
|
r = self.s.post(
|
||||||
|
url=f"{self.endpoint}/api/v1/payments",
|
||||||
|
json={"out": True, "bolt11": bolt11},
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
error_message = r.json()["detail"]
|
||||||
|
return PaymentResponse(None, None, None, None, error_message)
|
||||||
|
ok, checking_id, fee_msat, preimage, error_message = (
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
checking_id = data["payment_hash"]
|
||||||
|
|
||||||
|
# we do this to get the fee and preimage
|
||||||
|
payment: PaymentStatus = await self.get_payment_status(checking_id)
|
||||||
|
|
||||||
|
return PaymentResponse(ok, checking_id, payment.fee_msat, payment.preimage)
|
||||||
|
|
||||||
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
try:
|
||||||
|
|
||||||
|
r = self.s.get(
|
||||||
|
url=f"{self.endpoint}/api/v1/payments/{checking_id}",
|
||||||
|
headers=self.key,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
return PaymentStatus(None)
|
||||||
|
return PaymentStatus(r.json()["paid"])
|
||||||
|
|
||||||
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
try:
|
||||||
|
r = self.s.get(
|
||||||
|
url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
return PaymentStatus(None)
|
||||||
|
data = r.json()
|
||||||
|
if "paid" not in data and "details" not in data:
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
return PaymentStatus(data["paid"], data["details"]["fee"], data["preimage"])
|
||||||
|
|
||||||
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
url = f"{self.endpoint}/api/v1/payments/sse"
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with requests.stream("GET", url) as r:
|
||||||
|
async for line in r.aiter_lines():
|
||||||
|
if line.startswith("data:"):
|
||||||
|
try:
|
||||||
|
data = json.loads(line[5:])
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if type(data) is not dict:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield data["payment_hash"] # payment_hash
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
|
||||||
|
await asyncio.sleep(5)
|
||||||
26
mint/app.py
26
mint/app.py
@@ -7,6 +7,7 @@ import asyncio
|
|||||||
|
|
||||||
from mint.ledger import Ledger
|
from mint.ledger import Ledger
|
||||||
from mint.migrations import m001_initial
|
from mint.migrations import m001_initial
|
||||||
|
from lightning import WALLET
|
||||||
|
|
||||||
# Ledger pubkey
|
# Ledger pubkey
|
||||||
ledger = Ledger("supersecretprivatekey", "../data/mint")
|
ledger = Ledger("supersecretprivatekey", "../data/mint")
|
||||||
@@ -22,6 +23,16 @@ class MyFlaskApp(Flask):
|
|||||||
async def create_tasks_func():
|
async def create_tasks_func():
|
||||||
await asyncio.wait([m001_initial(ledger.db)])
|
await asyncio.wait([m001_initial(ledger.db)])
|
||||||
await ledger.load_used_proofs()
|
await ledger.load_used_proofs()
|
||||||
|
|
||||||
|
error_message, balance = await WALLET.status()
|
||||||
|
if error_message:
|
||||||
|
print(
|
||||||
|
f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
|
||||||
|
RuntimeWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Lightning balance: {balance} sat")
|
||||||
|
|
||||||
print("Mint started.")
|
print("Mint started.")
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
@@ -44,13 +55,18 @@ def keys():
|
|||||||
|
|
||||||
@app.route("/mint", methods=["POST"])
|
@app.route("/mint", methods=["POST"])
|
||||||
async def mint():
|
async def mint():
|
||||||
amount = int(request.args.get("amount")) or 64
|
payload = request.json
|
||||||
x = int(request.json["x"])
|
amounts = []
|
||||||
y = int(request.json["y"])
|
B_s = []
|
||||||
|
for k, v in payload.items():
|
||||||
|
amounts.append(v["amount"])
|
||||||
|
x = int(v["x"])
|
||||||
|
y = int(v["y"])
|
||||||
B_ = Point(x, y, secp256k1)
|
B_ = Point(x, y, secp256k1)
|
||||||
|
B_s.append(B_)
|
||||||
try:
|
try:
|
||||||
promise = await ledger.mint(B_, amount)
|
promises = await ledger.mint(B_s, amounts)
|
||||||
return promise
|
return promises
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"error": str(exc)}
|
return {"error": str(exc)}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
from ecc.curve import secp256k1, Point
|
from ecc.curve import secp256k1, Point
|
||||||
from ecc.key import gen_keypair
|
from ecc.key import gen_keypair
|
||||||
@@ -12,6 +13,7 @@ from core.db import Database
|
|||||||
from core.split import amount_split
|
from core.split import amount_split
|
||||||
from core.settings import MAX_ORDER
|
from core.settings import MAX_ORDER
|
||||||
from mint.crud import store_promise, invalidate_proof, get_proofs_used
|
from mint.crud import store_promise, invalidate_proof, get_proofs_used
|
||||||
|
from lightning import WALLET
|
||||||
|
|
||||||
|
|
||||||
class Ledger:
|
class Ledger:
|
||||||
@@ -109,8 +111,27 @@ class Ledger:
|
|||||||
rv.append(2**pos)
|
rv.append(2**pos)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
# Public methods
|
async def _request_lightning(self, amount):
|
||||||
|
error, balance = await WALLET.status()
|
||||||
|
if error:
|
||||||
|
raise Exception(f"Lightning wallet not responding: {error}")
|
||||||
|
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
|
||||||
|
amount, "cashu deposit"
|
||||||
|
)
|
||||||
|
print(payment_request)
|
||||||
|
|
||||||
|
timeout = time.time() + 60 # 1 minute to pay invoice
|
||||||
|
while True:
|
||||||
|
status = await WALLET.get_invoice_status(checking_id)
|
||||||
|
if status.pending and time.time() > timeout:
|
||||||
|
print("Timeout")
|
||||||
|
return False
|
||||||
|
if not status.pending:
|
||||||
|
print("paid")
|
||||||
|
return True
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Public methods
|
||||||
def get_pubkeys(self):
|
def get_pubkeys(self):
|
||||||
"""Returns public keys for possible amounts."""
|
"""Returns public keys for possible amounts."""
|
||||||
return {
|
return {
|
||||||
@@ -118,12 +139,22 @@ class Ledger:
|
|||||||
for amt in [2**i for i in range(MAX_ORDER)]
|
for amt in [2**i for i in range(MAX_ORDER)]
|
||||||
}
|
}
|
||||||
|
|
||||||
async def mint(self, B_, amount):
|
async def mint(self, B_s, amounts, lightning=True):
|
||||||
"""Mints a promise for coins for B_."""
|
"""Mints a promise for coins for B_."""
|
||||||
|
for amount in amounts:
|
||||||
if amount not in [2**i for i in range(MAX_ORDER)]:
|
if amount not in [2**i for i in range(MAX_ORDER)]:
|
||||||
raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.")
|
raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.")
|
||||||
|
|
||||||
|
if lightning:
|
||||||
|
paid = await self._request_lightning(sum(amounts))
|
||||||
|
if not paid:
|
||||||
|
raise Exception(f"Did not receive payment in time.")
|
||||||
|
|
||||||
|
promises = []
|
||||||
|
for B_, amount in zip(B_s, amounts):
|
||||||
split = amount_split(amount)
|
split = amount_split(amount)
|
||||||
return [await self._generate_promise(a, B_) for a in split]
|
promises += [await self._generate_promise(amount, B_) for a in split]
|
||||||
|
return promises
|
||||||
|
|
||||||
async def split(self, proofs, amount, output_data):
|
async def split(self, proofs, amount, output_data):
|
||||||
"""Consumes proofs and prepares new promises based on the amount split."""
|
"""Consumes proofs and prepares new promises based on the amount split."""
|
||||||
|
|||||||
10
poetry.lock
generated
10
poetry.lock
generated
@@ -33,11 +33,11 @@ python-versions = ">=3.5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2021.10.8"
|
version = "2022.6.15.1"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
@@ -438,7 +438,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "e4b8f463f9ee65e2f74b6c03444e2ca2c46788de5f1f8f7bc5b1c0b1df5771e1"
|
content-hash = "d2c4df45eea8820487f68d4f8a5509956c1a114a0b65ebd7f707187253612a1a"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
asgiref = [
|
asgiref = [
|
||||||
@@ -454,8 +454,8 @@ bech32 = [
|
|||||||
{file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"},
|
{file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"},
|
||||||
]
|
]
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
{file = "certifi-2022.6.15.1-py3-none-any.whl", hash = "sha256:43dadad18a7f168740e66944e4fa82c6611848ff9056ad910f8f7a3e46ab89e0"},
|
||||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
{file = "certifi-2022.6.15.1.tar.gz", hash = "sha256:cffdcd380919da6137f76633531a5817e3a9f268575c128249fb637e4f9e73fb"},
|
||||||
]
|
]
|
||||||
charset-normalizer = [
|
charset-normalizer = [
|
||||||
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ requests = "2.27.1"
|
|||||||
pytest-asyncio = "0.19.0"
|
pytest-asyncio = "0.19.0"
|
||||||
SQLAlchemy = "1.3.24"
|
SQLAlchemy = "1.3.24"
|
||||||
sqlalchemy-aio = "0.17.0"
|
sqlalchemy-aio = "0.17.0"
|
||||||
certifi = "2021.10.8"
|
|
||||||
charset-normalizer = "2.0.12"
|
charset-normalizer = "2.0.12"
|
||||||
click = "8.0.4"
|
click = "8.0.4"
|
||||||
Flask = "2.2.2"
|
Flask = "2.2.2"
|
||||||
|
|||||||
@@ -9,12 +9,27 @@ from wallet.migrations import m001_initial
|
|||||||
SERVER_ENDPOINT = "http://localhost:3338"
|
SERVER_ENDPOINT = "http://localhost:3338"
|
||||||
|
|
||||||
|
|
||||||
|
async def assert_err(f, msg):
|
||||||
|
"""Compute f() and expect an error message 'msg'."""
|
||||||
|
try:
|
||||||
|
await f
|
||||||
|
except Exception as exc:
|
||||||
|
assert exc.args[0] == msg, Exception(
|
||||||
|
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_amt(proofs, expected):
|
||||||
|
"""Assert amounts the proofs contain."""
|
||||||
|
assert [p["amount"] for p in proofs] == expected
|
||||||
|
|
||||||
|
|
||||||
async def run_test():
|
async def run_test():
|
||||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1")
|
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
|
||||||
await m001_initial(wallet1.db)
|
await m001_initial(wallet1.db)
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
|
|
||||||
wallet2 = Wallet1(SERVER_ENDPOINT, "data/wallet2")
|
wallet2 = Wallet1(SERVER_ENDPOINT, "data/wallet2", "wallet2")
|
||||||
await m001_initial(wallet2.db)
|
await m001_initial(wallet2.db)
|
||||||
wallet2.status()
|
wallet2.status()
|
||||||
|
|
||||||
@@ -30,17 +45,19 @@ async def run_test():
|
|||||||
proofs += await wallet1.mint(63)
|
proofs += await wallet1.mint(63)
|
||||||
assert wallet1.balance == 64 + 63
|
assert wallet1.balance == 64 + 63
|
||||||
|
|
||||||
# Error: We try to split by amount higher than available
|
|
||||||
w1_fst_proofs, w1_snd_proofs = await wallet1.split(wallet1.proofs, 65)
|
w1_fst_proofs, w1_snd_proofs = await wallet1.split(wallet1.proofs, 65)
|
||||||
# assert w1_fst_proofs == []
|
|
||||||
# assert w1_snd_proofs == []
|
|
||||||
assert wallet1.balance == 63 + 64
|
assert wallet1.balance == 63 + 64
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
|
|
||||||
# Error: We try to double-spend by providing a valid proof twice
|
# Error: We try to double-spend by providing a valid proof twice
|
||||||
w1_fst_proofs, w1_snd_proofs = await wallet1.split(wallet1.proofs + proofs, 20)
|
# try:
|
||||||
assert w1_fst_proofs == []
|
# await wallet1.split(wallet1.proofs + proofs, 20),
|
||||||
assert w1_snd_proofs == []
|
# except Exception as exc:
|
||||||
|
# print(exc.args[0])
|
||||||
|
await assert_err(
|
||||||
|
wallet1.split(wallet1.proofs + proofs, 20),
|
||||||
|
f"Error: Already spent. Secret: {proofs[0]['secret']}",
|
||||||
|
)
|
||||||
assert wallet1.balance == 63 + 64
|
assert wallet1.balance == 63 + 64
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
|
|
||||||
@@ -54,9 +71,11 @@ async def run_test():
|
|||||||
wallet1.status()
|
wallet1.status()
|
||||||
|
|
||||||
# Error: We try to double-spend and it fails
|
# Error: We try to double-spend and it fails
|
||||||
w1_fst_proofs2_fails, w1_snd_proofs2_fails = await wallet1.split([proofs[0]], 10)
|
await assert_err(
|
||||||
assert w1_fst_proofs2_fails == []
|
wallet1.split([proofs[0]], 10),
|
||||||
assert w1_snd_proofs2_fails == []
|
f"Error: Already spent. Secret: {proofs[0]['secret']}",
|
||||||
|
)
|
||||||
|
|
||||||
assert wallet1.balance == 63 + 64
|
assert wallet1.balance == 63 + 64
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
|
|
||||||
@@ -81,15 +100,22 @@ async def run_test():
|
|||||||
wallet1.status()
|
wallet1.status()
|
||||||
|
|
||||||
# Error: We try to double-spend and it fails
|
# Error: We try to double-spend and it fails
|
||||||
w1_fst_proofs2, w1_snd_proofs2 = await wallet1.split(w1_snd_proofs, 5)
|
await assert_err(
|
||||||
assert w1_fst_proofs2 == []
|
wallet1.split(w1_snd_proofs, 5),
|
||||||
assert w1_snd_proofs2 == []
|
f"Error: Already spent. Secret: {w1_snd_proofs[0]['secret']}",
|
||||||
|
)
|
||||||
|
|
||||||
assert wallet1.balance == 63 + 64 - 20
|
assert wallet1.balance == 63 + 64 - 20
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
|
|
||||||
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]
|
||||||
|
|
||||||
|
await assert_err(
|
||||||
|
wallet1.split(w1_snd_proofs, -500),
|
||||||
|
"Error: Invalid split amount: -500",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
async_unwrap(run_test())
|
async_unwrap(run_test())
|
||||||
|
|||||||
@@ -53,18 +53,25 @@ class LedgerAPI:
|
|||||||
)
|
)
|
||||||
return proofs
|
return proofs
|
||||||
|
|
||||||
def mint(self, amount):
|
def mint(self, amounts):
|
||||||
"""Mints new coins and returns a proof of promise."""
|
"""Mints new coins and returns a proof of promise."""
|
||||||
|
payload = {}
|
||||||
|
secrets = []
|
||||||
|
rs = []
|
||||||
|
for i, amount in enumerate(amounts):
|
||||||
secret = str(random.getrandbits(128))
|
secret = str(random.getrandbits(128))
|
||||||
|
secrets.append(secret)
|
||||||
B_, r = b_dhke.step1_bob(secret)
|
B_, r = b_dhke.step1_bob(secret)
|
||||||
|
rs.append(r)
|
||||||
|
payload[i] = {"amount": amount, "x": str(B_.x), "y": str(B_.y)}
|
||||||
|
|
||||||
promises = requests.post(
|
promises = requests.post(
|
||||||
self.url + "/mint",
|
self.url + "/mint",
|
||||||
params={"amount": amount},
|
json=payload,
|
||||||
json={"x": str(B_.x), "y": str(B_.y)},
|
|
||||||
).json()
|
).json()
|
||||||
if "error" in promises:
|
if "error" in promises:
|
||||||
raise Exception("Error: {}".format(promises["error"]))
|
raise Exception("Error: {}".format(promises["error"]))
|
||||||
return self._construct_proofs(promises, [(r, secret)])
|
return self._construct_proofs(promises, [(r, s) for r, s in zip(rs, secrets)])
|
||||||
|
|
||||||
def split(self, proofs, amount):
|
def split(self, proofs, amount):
|
||||||
"""Consume proofs and create new promises based on amount split."""
|
"""Consume proofs and create new promises based on amount split."""
|
||||||
@@ -109,10 +116,11 @@ class LedgerAPI:
|
|||||||
class Wallet(LedgerAPI):
|
class Wallet(LedgerAPI):
|
||||||
"""Minimal wallet wrapper."""
|
"""Minimal wallet wrapper."""
|
||||||
|
|
||||||
def __init__(self, url: str, db: str):
|
def __init__(self, url: str, db: str, name: str = "no_name"):
|
||||||
super().__init__(url)
|
super().__init__(url)
|
||||||
self.db = Database("wallet", db)
|
self.db = Database("wallet", db)
|
||||||
self.proofs: List[Proof] = []
|
self.proofs: List[Proof] = []
|
||||||
|
self.name = name
|
||||||
|
|
||||||
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)
|
||||||
@@ -123,15 +131,12 @@ class Wallet(LedgerAPI):
|
|||||||
|
|
||||||
async def mint(self, amount):
|
async def mint(self, amount):
|
||||||
split = amount_split(amount)
|
split = amount_split(amount)
|
||||||
new_proofs = []
|
proofs = super().mint(split)
|
||||||
for amount in split:
|
|
||||||
proofs = super().mint(amount)
|
|
||||||
if proofs == []:
|
if proofs == []:
|
||||||
raise Exception("received no proofs")
|
raise Exception("received no proofs")
|
||||||
new_proofs += proofs
|
|
||||||
await self._store_proofs(proofs)
|
await self._store_proofs(proofs)
|
||||||
self.proofs += new_proofs
|
self.proofs += proofs
|
||||||
return new_proofs
|
return proofs
|
||||||
|
|
||||||
async def redeem(self, proofs):
|
async def redeem(self, proofs):
|
||||||
return await self.split(proofs, sum(p["amount"] for p in proofs))
|
return await self.split(proofs, sum(p["amount"] for p in proofs))
|
||||||
@@ -153,11 +158,12 @@ class Wallet(LedgerAPI):
|
|||||||
|
|
||||||
async def invalidate(self, proofs):
|
async def invalidate(self, proofs):
|
||||||
# first we make sure that the server has invalidated these proofs
|
# first we make sure that the server has invalidated these proofs
|
||||||
fst_proofs, snd_proofs = await self.split(
|
try:
|
||||||
proofs, sum(p["amount"] for p in proofs)
|
await self.split(proofs, sum(p["amount"] for p in proofs))
|
||||||
|
except Exception as exc:
|
||||||
|
assert exc.args[0].startswith("Error: Already spent."), Exception(
|
||||||
|
"invalidating unspent tokens"
|
||||||
)
|
)
|
||||||
assert fst_proofs == []
|
|
||||||
assert snd_proofs == []
|
|
||||||
|
|
||||||
# TODO: check with server if they were redeemed already
|
# TODO: check with server if they were redeemed already
|
||||||
for proof in proofs:
|
for proof in proofs:
|
||||||
@@ -172,7 +178,7 @@ class Wallet(LedgerAPI):
|
|||||||
return sum(p["amount"] for p in self.proofs)
|
return sum(p["amount"] for p in self.proofs)
|
||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
print("Balance: {}".format(self.balance))
|
print(f"{self.name} balance: {self.balance}")
|
||||||
|
|
||||||
def proof_amounts(self):
|
def proof_amounts(self):
|
||||||
return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]
|
return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]
|
||||||
|
|||||||
Reference in New Issue
Block a user