Merge pull request #3 from callebtc/lighting/peg_in

Lighting/peg in
This commit is contained in:
calle
2022-09-13 21:56:46 +03:00
committed by GitHub
18 changed files with 297 additions and 140 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
DEBUG = true
# WALLET
MINT_HOST=127.0.0.1
MINT_PORT=3338
# MINT
MINT_PRIVATE_KEY=supersecretprivatekey
MINT_SERVER_HOST=127.0.0.1
MINT_SERVER_PORT=3338
LNBITS_ENDPOINT=https://legend.lnbits.com
LNBITS_KEY=yourkeyasdasdasd

View File

@@ -2,7 +2,7 @@
**The author is NOT a cryptographer and has not tested the libraries used or the code nor has anyone reviewed the work. This means it's very likely a fatal flaw somewhere. This is meant only as educational and is not production ready.**
Ecash implementation based on David Wagner's variant of Chaumian blinding. Token logic based on [minicash](https://github.com/phyro/minicash) ([description](https://gist.github.com/phyro/935badc682057f418842c72961cf096c)) which implements a [Blind Diffie-Hellman Key Exchange](https://cypherpunks.venona.com/date/1996/03/msg01848.html) scheme written down by Ruben Somsen [here](https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406). The database mechanics and the Lightning backend is inspired by [LNbits](https://github.com/lnbits/lnbits-legend).
Ecash implementation based on David Wagner's variant of Chaumian blinding. Token logic based on [minicash](https://github.com/phyro/minicash) ([description](https://gist.github.com/phyro/935badc682057f418842c72961cf096c)) which implements a [Blind Diffie-Hellman Key Exchange](https://cypherpunks.venona.com/date/1996/03/msg01848.html) scheme written down by Ruben Somsen [here](https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406). The database mechanics and the Lightning backend uses parts from [LNbits](https://github.com/lnbits/lnbits-legend).
Big thanks to [phyro](https://github.com/phyro) for their work and further discussions and improvements.
@@ -36,6 +36,13 @@ pyenv local 3.9.13
poetry install
```
### Configuration
```bash
mv .env.example .env
# edit .env file
vim .env
```
## Run mint
```bash
poetry run uvicorn mint.app:app --port 3338
@@ -46,5 +53,14 @@ poetry run uvicorn mint.app:app --port 3338
poetry run ./cashu --wallet=wallet --mint=420
```
## Test instance
*Warning: this instance is just for demonstrations only. Currently, only Lightning deposits work but not withdrawals. The server could vanish at any moment so consider any Satoshis you deposit a donation.*
Change the appropriate `.env` file settings to
```bash
MINT_HOST=8333.space
MINT_PORT=3338
```
## Screenshot
![screenshot](https://user-images.githubusercontent.com/93376500/189533335-68a863e2-bacd-47c1-aecc-e4fb09883d11.jpg)

32
cashu
View File

@@ -1,17 +1,16 @@
#!/usr/bin/env python
from wallet.wallet import Wallet as Wallet
from wallet.migrations import m001_initial
import asyncio
import click
import json
import base64
from bech32 import bech32_encode, bech32_decode, convertbits
import asyncio
import json
from functools import wraps
import click
from bech32 import bech32_decode, bech32_encode, convertbits
from wallet.migrations import m001_initial
from wallet.wallet import Wallet as Wallet
from core.settings import MINT_URL
# https://github.com/pallets/click/issues/85#issuecomment-503464628
def coro(f):
@@ -23,20 +22,27 @@ def coro(f):
@click.command("mint")
@click.option("--host", default="http://localhost:3338", help="Mint tokens.")
@click.option("--wallet", default="wallet", help="Mint tokens.")
@click.option("--host", default=MINT_URL, help="Mint hostname.")
@click.option("--wallet", default="wallet", help="Wallet to use.")
@click.option("--mint", default=0, help="Mint tokens.")
@click.option("--hash", default="", help="Hash of the paid invoice.")
@click.option("--send", default=0, help="Send tokens.")
@click.option("--receive", default="", help="Receive tokens.")
@click.option("--invalidate", default="", help="Invalidate tokens.")
@coro
async def main(host, wallet, mint, send, receive, invalidate):
async def main(host, wallet, mint, hash, send, receive, invalidate):
print(host)
wallet = Wallet(host, f"data/{wallet}", wallet)
await m001_initial(db=wallet.db)
await wallet.load_proofs()
if mint:
if mint and not hash:
print(f"Balance: {wallet.balance}")
await wallet.mint(mint)
r = await wallet.request_mint(mint)
print(r)
if mint and hash:
print(f"Balance: {wallet.balance}")
await wallet.mint(mint, hash)
print(f"Balance: {wallet.balance}")
if send:

View File

@@ -30,9 +30,9 @@ If true, C must have originated from Alice
"""
import hashlib
from ecc.curve import secp256k1, Point
from ecc.key import gen_keypair
from ecc.curve import Point, secp256k1
from ecc.key import gen_keypair
G = secp256k1.G

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel
from typing import List
from sqlite3 import Row
from typing import List
from pydantic import BaseModel
class BasePoint(BaseModel):
@@ -27,6 +28,22 @@ class Proof(BaseModel):
)
class Invoice(BaseModel):
amount: int
pr: str
hash: str
issued: bool = False
@classmethod
def from_row(cls, row: Row):
return cls(
amount=int(row[0]),
pr=str(row[1]),
hash=str(row[2]),
issued=bool(row[3]),
)
class MintPayload(BaseModel):
amount: int
B_: BasePoint

View File

@@ -9,7 +9,6 @@ from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH"
SQLITE = "SQLITE"

View File

@@ -5,10 +5,20 @@ env.read_env()
DEBUG = env.bool("DEBUG", default=False)
MINT_PRIVATE_KEY = env.str("MINT_PRIVATE_KEY")
MINT_SERVER_HOST = env.str("MINT_SERVER_HOST", default="127.0.0.1")
MINT_SERVER_PORT = env.int("MINT_SERVER_PORT", default=3338)
MINT_HOST = env.str("MINT_HOST", default="127.0.0.1")
MINT_PORT = env.int("MINT_PORT", default=3338)
if MINT_HOST == "127.0.0.1":
MINT_URL = f"http://{MINT_HOST}:{MINT_PORT}"
else:
MINT_URL = f"https://{MINT_HOST}:{MINT_PORT}"
LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
MAX_ORDER = 64

View File

@@ -6,22 +6,19 @@ from typing import AsyncGenerator, Dict, Optional
import requests
from .base import (
InvoiceResponse,
PaymentResponse,
PaymentStatus,
StatusResponse,
Wallet,
)
from core.settings import LNBITS_ENDPOINT, LNBITS_KEY
from .base import (InvoiceResponse, PaymentResponse, PaymentStatus,
StatusResponse, Wallet)
class LNbitsWallet(Wallet):
"""https://github.com/lnbits/lnbits"""
def __init__(self):
self.endpoint = getenv("LNBITS_ENDPOINT")
self.endpoint = LNBITS_ENDPOINT
key = getenv("LNBITS_KEY")
key = LNBITS_KEY
self.key = {"X-Api-Key": key}
self.s = requests.Session()
self.s.auth = ("user", "pass")

View File

@@ -1,58 +1,20 @@
from ecc.curve import secp256k1, Point
from fastapi import FastAPI
from fastapi.routing import APIRouter
from fastapi.params import Depends, Query, Body
import sys
import asyncio
import logging
import uvicorn
import sys
from typing import Union
from ecc.curve import Point, secp256k1
from fastapi import FastAPI
from loguru import logger
import core.settings as settings
from core.base import MintPayloads, SplitPayload
from core.settings import MINT_PRIVATE_KEY
from lightning import WALLET
from mint.ledger import Ledger
from mint.migrations import m001_initial
from lightning import WALLET
import core.settings as settings
from core.base import MintPayload, MintPayloads, SplitPayload
# from .app import create_app
# Ledger pubkey
ledger = Ledger("supersecretprivatekey", "data/mint")
# class MyFlaskApp(Flask):
# """
# We overload the Flask class so we can run a startup script (migration).
# Stupid Flask.
# """
# def __init__(self, *args, **kwargs):
# async def create_tasks_func():
# await asyncio.wait([m001_initial(ledger.db)])
# 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.")
# loop = asyncio.get_event_loop()
# loop.run_until_complete(create_tasks_func())
# loop.close()
# return super().__init__(*args, **kwargs)
# def run(self, *args, **options):
# super(MyFlaskApp, self).run(*args, **options)
ledger = Ledger(MINT_PRIVATE_KEY, "data/mint")
def startup(app: FastAPI):
@@ -124,18 +86,39 @@ def create_app(config_object="core.settings") -> FastAPI:
app = create_app()
@app.get("/")
async def root():
return {"Hello": "world"}
@app.get("/keys")
def keys():
"""Get the public keys of the mint"""
return ledger.get_pubkeys()
@app.get("/mint")
async def request_mint(amount: int = 0):
"""Request minting of tokens. Server responds with a Lightning invoice."""
payment_request, payment_hash = await ledger.request_mint(amount)
print(f"Lightning invoice: {payment_request}")
return {"pr": payment_request, "hash": payment_hash}
@app.post("/mint")
async def mint(payloads: MintPayloads):
async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None):
"""
Requests the minting of tokens belonging to a paid payment request.
Parameters:
pr: payment_request of the Lightning paid invoice.
Body (JSON):
payloads: contains a list of blinded messages waiting to be signed.
NOTE:
- This needs to be replaced by the preimage otherwise someone knowing
the payment_request can request the tokens instead of the rightful
owner.
- The blinded message should ideally be provided to the server *before* payment
in the GET /mint endpoint so that the server knows to sign only these tokens
when the invoice is paid.
"""
amounts = []
B_s = []
for payload in payloads.payloads:
@@ -146,7 +129,7 @@ async def mint(payloads: MintPayloads):
B_ = Point(x, y, secp256k1)
B_s.append(B_)
try:
promises = await ledger.mint(B_s, amounts)
promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash)
return promises
except Exception as exc:
return {"error": str(exc)}

View File

@@ -1,5 +1,7 @@
import secrets
from typing import Optional
from core.base import Invoice
from core.db import Connection, Database
@@ -62,3 +64,52 @@ async def invalidate_proof(
str(proof["secret"]),
),
)
async def store_lightning_invoice(
invoice: Invoice,
db: Database,
conn: Optional[Connection] = None,
):
await (conn or db).execute(
"""
INSERT INTO invoices
(amount, pr, hash, issued)
VALUES (?, ?, ?, ?)
""",
(
invoice.amount,
invoice.pr,
invoice.hash,
invoice.issued,
),
)
async def get_lightning_invoice(
hash: str,
db: Database,
conn: Optional[Connection] = None,
):
row = await (conn or db).fetchone(
"""
SELECT * from invoices
WHERE hash = ?
""",
hash,
)
return Invoice.from_row(row)
async def update_lightning_invoice(
hash: str,
issued: bool,
db: Database,
conn: Optional[Connection] = None,
):
await (conn or db).execute(
"UPDATE invoices SET issued = ? WHERE hash = ?",
(issued, hash),
)

View File

@@ -3,17 +3,24 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c
"""
import hashlib
import time
from ecc.curve import secp256k1, Point
from ecc.curve import Point, secp256k1
from ecc.key import gen_keypair
import core.b_dhke as b_dhke
from core.base import Invoice
from core.db import Database
from core.split import amount_split
from core.settings import MAX_ORDER
from mint.crud import store_promise, invalidate_proof, get_proofs_used
from core.split import amount_split
from lightning import WALLET
from mint.crud import (
get_lightning_invoice,
get_proofs_used,
invalidate_proof,
store_lightning_invoice,
store_promise,
update_lightning_invoice,
)
class Ledger:
@@ -65,7 +72,7 @@ class Ledger:
def _verify_proof(self, proof):
"""Verifies that the proof of promise was issued by this ledger."""
if proof["secret"] in self.proofs_used:
raise Exception(f"Already spent. Secret: {proof['secret']}")
raise Exception(f"tokens already spent. Secret: {proof['secret']}")
secret_key = self.keys[proof["amount"]] # Get the correct key to check against
C = Point(proof["C"]["x"], proof["C"]["y"], secp256k1)
return b_dhke.verify(secret_key, C, proof["secret"])
@@ -94,13 +101,13 @@ class Ledger:
self._verify_amount(amount)
except:
# For better error message
raise Exception("Invalid split amount: " + str(amount))
raise Exception("invalid split amount: " + str(amount))
def _verify_amount(self, amount):
"""Any amount used should be a positive integer not larger than 2^MAX_ORDER."""
valid = isinstance(amount, int) and amount > 0 and amount < 2**MAX_ORDER
if not valid:
raise Exception("Invalid amount: " + str(amount))
raise Exception("invalid amount: " + str(amount))
return amount
def _verify_equation_balanced(self, proofs, outs):
@@ -119,42 +126,62 @@ class Ledger:
rv.append(2**pos)
return rv
async def _request_lightning(self, amount):
async def _request_lightning_invoice(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)
return payment_request, checking_id
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)
async def _check_lightning_invoice(self, payment_hash):
invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
if invoice.issued:
raise Exception("tokens already issued for this invoice")
status = await WALLET.get_invoice_status(payment_hash)
if status.paid:
await update_lightning_invoice(payment_hash, issued=True, db=self.db)
return status.paid
# async def _wait_for_lightning_invoice(self, amount):
# 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):
"""Returns public keys for possible amounts."""
return self.pub_keys
async def mint(self, B_s, amounts, lightning=False):
async def request_mint(self, amount):
"""Returns Lightning invoice and stores it in the db."""
payment_request, checking_id = await self._request_lightning_invoice(amount)
invoice = Invoice(
amount=amount, pr=payment_request, hash=checking_id, issued=False
)
if not payment_request or not checking_id:
raise Exception(f"Could not create Lightning invoice.")
await store_lightning_invoice(invoice, db=self.db)
return payment_request, checking_id
async def mint(self, B_s, amounts, payment_hash=None):
"""Mints a promise for coins for B_."""
# check if lightning invoice was paid
if payment_hash and not await self._check_lightning_invoice(payment_hash):
raise Exception("Lightning invoice not paid yet.")
for amount in amounts:
if amount not in [2**i for i in range(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)

View File

@@ -33,6 +33,20 @@ async def m001_initial(db: Database):
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS invoices (
amount INTEGER NOT NULL,
pr TEXT NOT NULL,
hash TEXT NOT NULL,
issued BOOL NOT NULL,
UNIQUE (hash)
);
"""
)
await db.execute(
"""
CREATE VIEW IF NOT EXISTS balance_issued AS

31
poetry.lock generated
View File

@@ -237,6 +237,20 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "isort"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.6.1,<4.0"
[package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "itsdangerous"
version = "2.1.1"
@@ -575,15 +589,14 @@ python-versions = ">=3.7"
[[package]]
name = "urllib3"
version = "1.26.9"
version = "1.23"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
[package.extras]
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
secure = ["pyOpenSSL (>=0.14,<18.0.0)", "pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
@@ -641,7 +654,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "ed00e50dd88910eebcd5db652d99e722f77226f4f102e3bdd1032a4b1700dd99"
content-hash = "d45268f8d5612bc589a73025d9191d26eee50ebf335d836b4ad3c10e6c72b95c"
[metadata.files]
anyio = [
@@ -737,6 +750,10 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
itsdangerous = [
{file = "itsdangerous-2.1.1-py3-none-any.whl", hash = "sha256:935642cd4b987cdbee7210080004033af76306757ff8b4c0a506a4b6e06f02cf"},
{file = "itsdangerous-2.1.1.tar.gz", hash = "sha256:7b7d3023cd35d9cb0c1fd91392f8c95c6fa02c59bf8ad64b8849be3401b95afb"},
@@ -1007,8 +1024,8 @@ typing-extensions = [
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
]
urllib3 = [
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
{file = "urllib3-1.23-py2.py3-none-any.whl", hash = "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"},
{file = "urllib3-1.23.tar.gz", hash = "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf"},
]
uvicorn = [
{file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"},

View File

@@ -20,7 +20,7 @@ idna = "3.3"
itsdangerous = "2.1.1"
Jinja2 = "3.0.3"
MarkupSafe = "2.1.1"
urllib3 = "1.26.9"
urllib3 = "1.23"
Werkzeug = "2.2.2"
ecc-pycrypto = {git = "https://github.com/lc6chang/ecc-pycrypto.git", rev = "v1.0.1"}
asgiref = "^3.5.2"
@@ -34,6 +34,7 @@ loguru = "^0.6.0"
[tool.poetry.dev-dependencies]
black = {version = "^22.8.0", allow-prereleases = true}
isort = "^5.10.1"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@@ -1,10 +1,9 @@
import asyncio
from core.helpers import async_unwrap
from wallet.migrations import m001_initial
from wallet.wallet import Wallet as Wallet1
from wallet.wallet import Wallet as Wallet2
from wallet.migrations import m001_initial
SERVER_ENDPOINT = "http://localhost:3338"
@@ -56,7 +55,7 @@ async def run_test():
# print(exc.args[0])
await assert_err(
wallet1.split(wallet1.proofs + proofs, 20),
f"Error: Already spent. Secret: {proofs[0]['secret']}",
f"Error: tokens already spent. Secret: {proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64
wallet1.status()
@@ -73,7 +72,7 @@ async def run_test():
# Error: We try to double-spend and it fails
await assert_err(
wallet1.split([proofs[0]], 10),
f"Error: Already spent. Secret: {proofs[0]['secret']}",
f"Error: tokens already spent. Secret: {proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64
@@ -102,7 +101,7 @@ async def run_test():
# Error: We try to double-spend and it fails
await assert_err(
wallet1.split(w1_snd_proofs, 5),
f"Error: Already spent. Secret: {w1_snd_proofs[0]['secret']}",
f"Error: tokens already spent. Secret: {w1_snd_proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64 - 20
@@ -113,7 +112,7 @@ async def run_test():
await assert_err(
wallet1.split(w1_snd_proofs, -500),
"Error: Invalid split amount: -500",
"Error: invalid split amount: -500",
)

View File

@@ -1,3 +1,3 @@
# from core.db import Database
import sys
# db = Database("database", "data/wallet")
sys.tracebacklimit = None

View File

@@ -1,9 +1,9 @@
import secrets
from typing import Optional
from core.db import Connection, Database
# from wallet import db
from core.base import Proof
from core.db import Connection, Database
async def store_proof(

View File

@@ -1,18 +1,14 @@
import random
import asyncio
from typing import List
import requests
from ecc.curve import secp256k1, Point
from typing import List
from core.base import Proof, BasePoint
from ecc.curve import Point, secp256k1
import core.b_dhke as b_dhke
from core.base import BasePoint, MintPayload, MintPayloads, Proof, SplitPayload
from core.db import Database
from core.split import amount_split
from wallet.crud import store_proof, invalidate_proof, get_proofs
from core.base import MintPayload, MintPayloads, SplitPayload
from wallet.crud import get_proofs, invalidate_proof, store_proof
class LedgerAPI:
@@ -48,7 +44,12 @@ class LedgerAPI:
proofs.append(proof.dict())
return proofs
def mint(self, amounts):
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."""
payloads: MintPayloads = MintPayloads()
secrets = []
@@ -61,13 +62,13 @@ class LedgerAPI:
blinded_point = BasePoint(x=str(B_.x), y=str(B_.y))
payload: MintPayload = MintPayload(amount=amount, B_=blinded_point)
payloads.payloads.append(payload)
promises = requests.post(
self.url + "/mint",
json=payloads.dict(),
params={"payment_hash": payment_hash},
).json()
if "detail" in promises:
raise Exception("Error: {}".format(promises["detail"]))
if "error" in promises:
raise Exception("Error: {}".format(promises["error"]))
return self._construct_proofs(promises, [(r, s) for r, s in zip(rs, secrets)])
def split(self, proofs, amount):
@@ -122,9 +123,12 @@ class Wallet(LedgerAPI):
for proof in proofs:
await store_proof(proof, db=self.db)
async def mint(self, amount):
async def request_mint(self, amount):
return super().request_mint(amount)
async def mint(self, amount, payment_hash=None):
split = amount_split(amount)
proofs = super().mint(split)
proofs = super().mint(split, payment_hash)
if proofs == []:
raise Exception("received no proofs")
await self._store_proofs(proofs)
@@ -154,7 +158,7 @@ class Wallet(LedgerAPI):
try:
await self.split(proofs, sum(p["amount"] for p in proofs))
except Exception as exc:
assert exc.args[0].startswith("Error: Already spent."), Exception(
assert exc.args[0].startswith("Error: tokens already spent."), Exception(
"invalidating unspent tokens"
)