From 35a3e5e57a5a677a9f5f8c5bc8c921cf54d3217b Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Wed, 28 Sep 2022 17:44:43 +0200
Subject: [PATCH] clean
---
README.md | 17 +-
core/__init__.py | 0
core/b_dhke.py | 93 -----------
core/base.py | 108 ------------
core/bolt11.py | 370 ------------------------------------------
core/db.py | 182 ---------------------
core/helpers.py | 35 ----
core/migrations.py | 51 ------
core/secp.py | 52 ------
core/settings.py | 34 ----
core/split.py | 8 -
lightning/__init__.py | 3 -
lightning/base.py | 88 ----------
lightning/lnbits.py | 149 -----------------
mint/__init__.py | 6 -
mint/__main__.py | 6 -
mint/api.py | 0
mint/app.py | 86 ----------
mint/crud.py | 110 -------------
mint/ledger.py | 258 -----------------------------
mint/main.py | 52 ------
mint/migrations.py | 87 ----------
mint/router.py | 86 ----------
poetry.lock | 14 +-
pyproject.toml | 4 +-
setup.py | 2 +-
wallet/__init__.py | 3 -
wallet/cashu.py | 212 ------------------------
wallet/crud.py | 99 -----------
wallet/migrations.py | 81 ---------
wallet/wallet.py | 252 ----------------------------
31 files changed, 24 insertions(+), 2524 deletions(-)
delete mode 100644 core/__init__.py
delete mode 100644 core/b_dhke.py
delete mode 100644 core/base.py
delete mode 100644 core/bolt11.py
delete mode 100644 core/db.py
delete mode 100644 core/helpers.py
delete mode 100644 core/migrations.py
delete mode 100644 core/secp.py
delete mode 100644 core/settings.py
delete mode 100644 core/split.py
delete mode 100644 lightning/__init__.py
delete mode 100644 lightning/base.py
delete mode 100644 lightning/lnbits.py
delete mode 100644 mint/__init__.py
delete mode 100644 mint/__main__.py
delete mode 100644 mint/api.py
delete mode 100644 mint/app.py
delete mode 100644 mint/crud.py
delete mode 100644 mint/ledger.py
delete mode 100644 mint/main.py
delete mode 100644 mint/migrations.py
delete mode 100644 mint/router.py
delete mode 100644 wallet/__init__.py
delete mode 100755 wallet/cashu.py
delete mode 100644 wallet/crud.py
delete mode 100644 wallet/migrations.py
delete mode 100644 wallet/wallet.py
diff --git a/README.md b/README.md
index 2d49e35..bf9d857 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,17 @@
Cashu is an 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).
+
+Quick links:
+Cashu client protocol ·
+Quick Install ·
+Manual install ·
+Configuration ·
+Using Cashu ·
+Run a mint
+
+
+
## Cashu client protocol
There are ongoing efforts to implement alternative Cashu clients that use the same protocol such as a [Cashu Javascript wallet](https://github.com/motorina0/cashu-js-wallet). If you are interested in helping with Cashu development, please see the [docs](docs/) for the notation and conventions used.
@@ -20,8 +31,8 @@ To update Cashu, use `pip install cashu -U`. If you have problems running the co
You can skip the entire next section about Poetry and jump right to [Using Cashu](#using-cashu).
-### Hard install: Poetry
-These steps help you install Python via pyenv and Poetry. If you already have Poetry running on your computer, you can skip this step and jump right to [Install Cashu](#install-cashu).
+## Hard install: Poetry
+These steps help you install Python via pyenv and Poetry. If you already have Poetry running on your computer, you can skip this step and jump right to [Install Cashu](#poetry-install-cashu).
#### Poetry: Prerequisites
@@ -168,7 +179,7 @@ Balance: 351 sat (Available: 351 sat in 7 tokens)
Balance: 339 sat (Available: 339 sat in 8 tokens)
```
-## Run a mint yourself
+# Running a mint
This command runs the mint on your local computer. Skip this step if you want to use the [public test mint](#test-instance) instead.
```bash
mint
diff --git a/core/__init__.py b/core/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/core/b_dhke.py b/core/b_dhke.py
deleted file mode 100644
index be9a141..0000000
--- a/core/b_dhke.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Don't trust me with cryptography.
-
-"""
-Implementation of https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406
-Alice:
-A = a*G
-return A
-Bob:
-Y = hash_to_point(secret_message)
-r = random blinding factor
-B'= Y + r*G
-return B'
-Alice:
-C' = a*B'
- (= a*Y + a*r*G)
-return C'
-Bob:
-C = C' - r*A
- (= C' - a*r*G)
- (= a*Y)
-return C, secret_message
-Alice:
-Y = hash_to_point(secret_message)
-C == a*Y
-If true, C must have originated from Alice
-"""
-
-import hashlib
-
-from secp256k1 import PrivateKey, PublicKey
-
-
-def hash_to_point(secret_msg):
- """Generates x coordinate from the message hash and checks if the point lies on the curve.
- If it does not, it tries computing again a new x coordinate from the hash of the coordinate."""
- point = None
- msg = secret_msg
- while point is None:
- _hash = hashlib.sha256(msg).hexdigest().encode("utf-8")
- try:
- # We construct compressed pub which has x coordinate encoded with even y
- _hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes
- _hash[0] = 0x02 # set first byte to represent even y coord
- _hash = bytes(_hash)
- point = PublicKey(_hash, raw=True)
- except:
- msg = _hash
-
- return point
-
-
-def step1_alice(secret_msg):
- secret_msg = secret_msg.encode("utf-8")
- Y = hash_to_point(secret_msg)
- r = PrivateKey()
- B_ = Y + r.pubkey
- return B_, r
-
-
-def step2_bob(B_, a):
- C_ = B_.mult(a)
- return C_
-
-
-def step3_alice(C_, r, A):
- C = C_ - A.mult(r)
- return C
-
-
-def verify(a, C, secret_msg):
- Y = hash_to_point(secret_msg.encode("utf-8"))
- return C == Y.mult(a)
-
-
-### Below is a test of a simple positive and negative case
-
-# # Alice's keys
-# a = PrivateKey()
-# A = a.pubkey
-# secret_msg = "test"
-# B_, r = step1_alice(secret_msg)
-# C_ = step2_bob(B_, a)
-# C = step3_alice(C_, r, A)
-# print("C:{}, secret_msg:{}".format(C, secret_msg))
-# assert verify(a, C, secret_msg)
-# assert verify(a, C + C, secret_msg) == False # adding C twice shouldn't pass
-# assert verify(a, A, secret_msg) == False # A shouldn't pass
-
-# # Test operations
-# b = PrivateKey()
-# B = b.pubkey
-# assert -A -A + A == -A # neg
-# assert B.mult(a) == A.mult(b) # a*B = A*b
diff --git a/core/base.py b/core/base.py
deleted file mode 100644
index a78f7ee..0000000
--- a/core/base.py
+++ /dev/null
@@ -1,108 +0,0 @@
-from sqlite3 import Row
-from typing import List
-
-from pydantic import BaseModel
-
-
-class Proof(BaseModel):
- amount: int
- secret: str
- C: str
- reserved: bool = False # whether this proof is reserved for sending
- send_id: str = "" # unique ID of send attempt
- time_created: str = ""
- time_reserved: str = ""
-
- @classmethod
- def from_row(cls, row: Row):
- return cls(
- amount=row[0],
- C=row[1],
- secret=row[2],
- reserved=row[3] or False,
- send_id=row[4] or "",
- time_created=row[5] or "",
- time_reserved=row[6] or "",
- )
-
- @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"),
- reserved=d.get("reserved") or False,
- send_id=d.get("send_id") or "",
- time_created=d.get("time_created") or "",
- time_reserved=d.get("time_reserved") or "",
- )
-
- def to_dict(self):
- return dict(amount=self.amount, secret=self.secret, C=self.C)
-
- def __getitem__(self, key):
- return self.__getattribute__(key)
-
- def __setitem__(self, key, val):
- self.__setattr__(key, val)
-
-
-class Proofs(BaseModel):
- """TODO: Use this model"""
-
- proofs: List[Proof]
-
-
-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 BlindedMessage(BaseModel):
- amount: int
- B_: str
-
-
-class BlindedSignature(BaseModel):
- amount: int
- C_: str
-
- @classmethod
- def from_dict(cls, d: dict):
- return cls(
- amount=d["amount"],
- C_=d["C_"],
- )
-
-
-class MintPayloads(BaseModel):
- blinded_messages: List[BlindedMessage] = []
-
-
-class SplitPayload(BaseModel):
- proofs: List[Proof]
- amount: int
- output_data: MintPayloads
-
-
-class CheckPayload(BaseModel):
- proofs: List[Proof]
-
-
-class MeltPayload(BaseModel):
- proofs: List[Proof]
- amount: int
- invoice: str
diff --git a/core/bolt11.py b/core/bolt11.py
deleted file mode 100644
index 962581d..0000000
--- a/core/bolt11.py
+++ /dev/null
@@ -1,370 +0,0 @@
-import hashlib
-import re
-import time
-from binascii import unhexlify
-from decimal import Decimal
-from typing import List, NamedTuple, Optional
-
-import bitstring # type: ignore
-import secp256k1
-from bech32 import CHARSET, bech32_decode, bech32_encode
-from ecdsa import SECP256k1, VerifyingKey # type: ignore
-from ecdsa.util import sigdecode_string # type: ignore
-
-
-class Route(NamedTuple):
- pubkey: str
- short_channel_id: str
- base_fee_msat: int
- ppm_fee: int
- cltv: int
-
-
-class Invoice(object):
- payment_hash: str
- amount_msat: int = 0
- description: Optional[str] = None
- description_hash: Optional[str] = None
- payee: Optional[str] = None
- date: int
- expiry: int = 3600
- secret: Optional[str] = None
- route_hints: List[Route] = []
- min_final_cltv_expiry: int = 18
-
-
-def decode(pr: str) -> Invoice:
- """bolt11 decoder,
- based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py
- """
-
- hrp, decoded_data = bech32_decode(pr)
- if hrp is None or decoded_data is None:
- raise ValueError("Bad bech32 checksum")
- if not hrp.startswith("ln"):
- raise ValueError("Does not start with ln")
-
- bitarray = _u5_to_bitarray(decoded_data)
-
- # final signature 65 bytes, split it off.
- if len(bitarray) < 65 * 8:
- raise ValueError("Too short to contain signature")
-
- # extract the signature
- signature = bitarray[-65 * 8 :].tobytes()
-
- # the tagged fields as a bitstream
- data = bitstring.ConstBitStream(bitarray[: -65 * 8])
-
- # build the invoice object
- invoice = Invoice()
-
- # decode the amount from the hrp
- m = re.search(r"[^\d]+", hrp[2:])
- if m:
- amountstr = hrp[2 + m.end() :]
- if amountstr != "":
- invoice.amount_msat = _unshorten_amount(amountstr)
-
- # pull out date
- invoice.date = data.read(35).uint
-
- while data.pos != data.len:
- tag, tagdata, data = _pull_tagged(data)
- data_length = len(tagdata) / 5
-
- if tag == "d":
- invoice.description = _trim_to_bytes(tagdata).decode("utf-8")
- elif tag == "h" and data_length == 52:
- invoice.description_hash = _trim_to_bytes(tagdata).hex()
- elif tag == "p" and data_length == 52:
- invoice.payment_hash = _trim_to_bytes(tagdata).hex()
- elif tag == "x":
- invoice.expiry = tagdata.uint
- elif tag == "n":
- invoice.payee = _trim_to_bytes(tagdata).hex()
- # this won't work in most cases, we must extract the payee
- # from the signature
- elif tag == "s":
- invoice.secret = _trim_to_bytes(tagdata).hex()
- elif tag == "r":
- s = bitstring.ConstBitStream(tagdata)
- while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
- route = Route(
- pubkey=s.read(264).tobytes().hex(),
- short_channel_id=_readable_scid(s.read(64).intbe),
- base_fee_msat=s.read(32).intbe,
- ppm_fee=s.read(32).intbe,
- cltv=s.read(16).intbe,
- )
- invoice.route_hints.append(route)
-
- # BOLT #11:
- # A reader MUST check that the `signature` is valid (see the `n` tagged
- # field specified below).
- # A reader MUST use the `n` field to validate the signature instead of
- # performing signature recovery if a valid `n` field is provided.
- message = bytearray([ord(c) for c in hrp]) + data.tobytes()
- sig = signature[0:64]
- if invoice.payee:
- key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1)
- key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
- else:
- keys = VerifyingKey.from_public_key_recovery(
- sig, message, SECP256k1, hashlib.sha256
- )
- signaling_byte = signature[64]
- key = keys[int(signaling_byte)]
- invoice.payee = key.to_string("compressed").hex()
-
- return invoice
-
-
-def encode(options):
- """Convert options into LnAddr and pass it to the encoder"""
- addr = LnAddr()
- addr.currency = options["currency"]
- addr.fallback = options["fallback"] if options["fallback"] else None
- if options["amount"]:
- addr.amount = options["amount"]
- if options["timestamp"]:
- addr.date = int(options["timestamp"])
-
- addr.paymenthash = unhexlify(options["paymenthash"])
-
- if options["description"]:
- addr.tags.append(("d", options["description"]))
- if options["description_hash"]:
- addr.tags.append(("h", options["description_hash"]))
- if options["expires"]:
- addr.tags.append(("x", options["expires"]))
-
- if options["fallback"]:
- addr.tags.append(("f", options["fallback"]))
- if options["route"]:
- for r in options["route"]:
- splits = r.split("/")
- route = []
- while len(splits) >= 5:
- route.append(
- (
- unhexlify(splits[0]),
- unhexlify(splits[1]),
- int(splits[2]),
- int(splits[3]),
- int(splits[4]),
- )
- )
- splits = splits[5:]
- assert len(splits) == 0
- addr.tags.append(("r", route))
- return lnencode(addr, options["privkey"])
-
-
-def lnencode(addr, privkey):
- if addr.amount:
- amount = Decimal(str(addr.amount))
- # We can only send down to millisatoshi.
- if amount * 10**12 % 10:
- raise ValueError(
- "Cannot encode {}: too many decimal places".format(addr.amount)
- )
-
- amount = addr.currency + shorten_amount(amount)
- else:
- amount = addr.currency if addr.currency else ""
-
- hrp = "ln" + amount + "0n"
-
- # Start with the timestamp
- data = bitstring.pack("uint:35", addr.date)
-
- # Payment hash
- data += tagged_bytes("p", addr.paymenthash)
- tags_set = set()
-
- for k, v in addr.tags:
-
- # BOLT #11:
- #
- # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
- if k in ("d", "h", "n", "x"):
- if k in tags_set:
- raise ValueError("Duplicate '{}' tag".format(k))
-
- if k == "r":
- route = bitstring.BitArray()
- for step in v:
- pubkey, channel, feebase, feerate, cltv = step
- route.append(
- bitstring.BitArray(pubkey)
- + bitstring.BitArray(channel)
- + bitstring.pack("intbe:32", feebase)
- + bitstring.pack("intbe:32", feerate)
- + bitstring.pack("intbe:16", cltv)
- )
- data += tagged("r", route)
- elif k == "f":
- data += encode_fallback(v, addr.currency)
- elif k == "d":
- data += tagged_bytes("d", v.encode())
- elif k == "x":
- # Get minimal length by trimming leading 5 bits at a time.
- expirybits = bitstring.pack("intbe:64", v)[4:64]
- while expirybits.startswith("0b00000"):
- expirybits = expirybits[5:]
- data += tagged("x", expirybits)
- elif k == "h":
- data += tagged_bytes("h", v)
- elif k == "n":
- data += tagged_bytes("n", v)
- else:
- # FIXME: Support unknown tags?
- raise ValueError("Unknown tag {}".format(k))
-
- tags_set.add(k)
-
- # BOLT #11:
- #
- # A writer MUST include either a `d` or `h` field, and MUST NOT include
- # both.
- if "d" in tags_set and "h" in tags_set:
- raise ValueError("Cannot include both 'd' and 'h'")
- if not "d" in tags_set and not "h" in tags_set:
- raise ValueError("Must include either 'd' or 'h'")
-
- # We actually sign the hrp, then data (padded to 8 bits with zeroes).
- privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
- sig = privkey.ecdsa_sign_recoverable(
- bytearray([ord(c) for c in hrp]) + data.tobytes()
- )
- # This doesn't actually serialize, but returns a pair of values :(
- sig, recid = privkey.ecdsa_recoverable_serialize(sig)
- data += bytes(sig) + bytes([recid])
-
- return bech32_encode(hrp, bitarray_to_u5(data))
-
-
-class LnAddr(object):
- def __init__(
- self, paymenthash=None, amount=None, currency="bc", tags=None, date=None
- ):
- self.date = int(time.time()) if not date else int(date)
- self.tags = [] if not tags else tags
- self.unknown_tags = []
- self.paymenthash = paymenthash
- self.signature = None
- self.pubkey = None
- self.currency = currency
- self.amount = amount
-
- def __str__(self):
- return "LnAddr[{}, amount={}{} tags=[{}]]".format(
- hexlify(self.pubkey.serialize()).decode("utf-8"),
- self.amount,
- self.currency,
- ", ".join([k + "=" + str(v) for k, v in self.tags]),
- )
-
-
-def shorten_amount(amount):
- """Given an amount in bitcoin, shorten it"""
- # Convert to pico initially
- amount = int(amount * 10**12)
- units = ["p", "n", "u", "m", ""]
- for unit in units:
- if amount % 1000 == 0:
- amount //= 1000
- else:
- break
- return str(amount) + unit
-
-
-def _unshorten_amount(amount: str) -> int:
- """Given a shortened amount, return millisatoshis"""
- # BOLT #11:
- # The following `multiplier` letters are defined:
- #
- # * `m` (milli): multiply by 0.001
- # * `u` (micro): multiply by 0.000001
- # * `n` (nano): multiply by 0.000000001
- # * `p` (pico): multiply by 0.000000000001
- units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3}
- unit = str(amount)[-1]
-
- # BOLT #11:
- # A reader SHOULD fail if `amount` contains a non-digit, or is followed by
- # anything except a `multiplier` in the table above.
- if not re.fullmatch(r"\d+[pnum]?", str(amount)):
- raise ValueError("Invalid amount '{}'".format(amount))
-
- if unit in units:
- return int(int(amount[:-1]) * 100_000_000_000 / units[unit])
- else:
- return int(amount) * 100_000_000_000
-
-
-def _pull_tagged(stream):
- tag = stream.read(5).uint
- length = stream.read(5).uint * 32 + stream.read(5).uint
- return (CHARSET[tag], stream.read(length * 5), stream)
-
-
-def is_p2pkh(currency, prefix):
- return prefix == base58_prefix_map[currency][0]
-
-
-def is_p2sh(currency, prefix):
- return prefix == base58_prefix_map[currency][1]
-
-
-# Tagged field containing BitArray
-def tagged(char, l):
- # Tagged fields need to be zero-padded to 5 bits.
- while l.len % 5 != 0:
- l.append("0b0")
- return (
- bitstring.pack(
- "uint:5, uint:5, uint:5",
- CHARSET.find(char),
- (l.len / 5) / 32,
- (l.len / 5) % 32,
- )
- + l
- )
-
-
-def tagged_bytes(char, l):
- return tagged(char, bitstring.BitArray(l))
-
-
-def _trim_to_bytes(barr):
- # Adds a byte if necessary.
- b = barr.tobytes()
- if barr.len % 8 != 0:
- return b[:-1]
- return b
-
-
-def _readable_scid(short_channel_id: int) -> str:
- return "{blockheight}x{transactionindex}x{outputindex}".format(
- blockheight=((short_channel_id >> 40) & 0xFFFFFF),
- transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
- outputindex=(short_channel_id & 0xFFFF),
- )
-
-
-def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
- ret = bitstring.BitArray()
- for a in arr:
- ret += bitstring.pack("uint:5", a)
- return ret
-
-
-def bitarray_to_u5(barr):
- assert barr.len % 5 == 0
- ret = []
- s = bitstring.ConstBitStream(barr)
- while s.pos != s.len:
- ret.append(s.read(5).uint)
- return ret
diff --git a/core/db.py b/core/db.py
deleted file mode 100644
index e393aa1..0000000
--- a/core/db.py
+++ /dev/null
@@ -1,182 +0,0 @@
-import asyncio
-import datetime
-import os
-import time
-from contextlib import asynccontextmanager
-from typing import Optional
-
-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"
-
-
-class Compat:
- type: Optional[str] = ""
- schema: Optional[str] = ""
-
- def interval_seconds(self, seconds: int) -> str:
- if self.type in {POSTGRES, COCKROACH}:
- return f"interval '{seconds} seconds'"
- elif self.type == SQLITE:
- return f"{seconds}"
- return ""
-
- @property
- def timestamp_now(self) -> str:
- if self.type in {POSTGRES, COCKROACH}:
- return "now()"
- elif self.type == SQLITE:
- return "(strftime('%s', 'now'))"
- return ""
-
- @property
- def serial_primary_key(self) -> str:
- if self.type in {POSTGRES, COCKROACH}:
- return "SERIAL PRIMARY KEY"
- elif self.type == SQLITE:
- return "INTEGER PRIMARY KEY AUTOINCREMENT"
- return ""
-
- @property
- def references_schema(self) -> str:
- if self.type in {POSTGRES, COCKROACH}:
- return f"{self.schema}."
- elif self.type == SQLITE:
- return ""
- return ""
-
-
-class Connection(Compat):
- def __init__(self, conn: AsyncConnection, txn, typ, name, schema):
- self.conn = conn
- self.txn = txn
- self.type = typ
- self.name = name
- self.schema = schema
-
- def rewrite_query(self, query) -> str:
- if self.type in {POSTGRES, COCKROACH}:
- query = query.replace("%", "%%")
- query = query.replace("?", "%s")
- return query
-
- async def fetchall(self, query: str, values: tuple = ()) -> list:
- result = await self.conn.execute(self.rewrite_query(query), values)
- return await result.fetchall()
-
- async def fetchone(self, query: str, values: tuple = ()):
- result = await self.conn.execute(self.rewrite_query(query), values)
- row = await result.fetchone()
- await result.close()
- return row
-
- async def execute(self, query: str, values: tuple = ()):
- return await self.conn.execute(self.rewrite_query(query), values)
-
-
-class Database(Compat):
- def __init__(self, db_name: str, db_location: str):
- self.name = db_name
- self.db_location = db_location
- self.db_location_is_url = "://" in self.db_location
-
- if self.db_location_is_url:
- database_uri = self.db_location
-
- if database_uri.startswith("cockroachdb://"):
- self.type = COCKROACH
- else:
- self.type = POSTGRES
-
- import psycopg2 # type: ignore
-
- def _parse_timestamp(value, _):
- f = "%Y-%m-%d %H:%M:%S.%f"
- if not "." in value:
- f = "%Y-%m-%d %H:%M:%S"
- return time.mktime(datetime.datetime.strptime(value, f).timetuple())
-
- psycopg2.extensions.register_type(
- psycopg2.extensions.new_type(
- psycopg2.extensions.DECIMAL.values,
- "DEC2FLOAT",
- lambda value, curs: float(value) if value is not None else None,
- )
- )
- psycopg2.extensions.register_type(
- psycopg2.extensions.new_type(
- (1082, 1083, 1266),
- "DATE2INT",
- lambda value, curs: time.mktime(value.timetuple())
- if value is not None
- else None,
- )
- )
-
- psycopg2.extensions.register_type(
- psycopg2.extensions.new_type(
- (1184, 1114), "TIMESTAMP2INT", _parse_timestamp
- )
- )
- else:
- if not os.path.exists(self.db_location):
- print(f"Creating database directory: {self.db_location}")
- os.makedirs(self.db_location)
- self.path = os.path.join(self.db_location, f"{self.name}.sqlite3")
- database_uri = f"sqlite:///{self.path}"
- self.type = SQLITE
-
- self.schema = self.name
- if self.name.startswith("ext_"):
- self.schema = self.name[4:]
- else:
- self.schema = None
-
- self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
- self.lock = asyncio.Lock()
-
- @asynccontextmanager
- async def connect(self):
- await self.lock.acquire()
- try:
- async with self.engine.connect() as conn:
- async with conn.begin() as txn:
- wconn = Connection(conn, txn, self.type, self.name, self.schema)
-
- if self.schema:
- if self.type in {POSTGRES, COCKROACH}:
- await wconn.execute(
- f"CREATE SCHEMA IF NOT EXISTS {self.schema}"
- )
- elif self.type == SQLITE:
- await wconn.execute(
- f"ATTACH '{self.path}' AS {self.schema}"
- )
-
- yield wconn
- finally:
- self.lock.release()
-
- async def fetchall(self, query: str, values: tuple = ()) -> list:
- async with self.connect() as conn:
- result = await conn.execute(query, values)
- return await result.fetchall()
-
- async def fetchone(self, query: str, values: tuple = ()):
- async with self.connect() as conn:
- result = await conn.execute(query, values)
- row = await result.fetchone()
- await result.close()
- return row
-
- async def execute(self, query: str, values: tuple = ()):
- async with self.connect() as conn:
- return await conn.execute(query, values)
-
- @asynccontextmanager
- async def reuse_conn(self, conn: Connection):
- yield conn
diff --git a/core/helpers.py b/core/helpers.py
deleted file mode 100644
index 75dab61..0000000
--- a/core/helpers.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import asyncio
-from functools import partial, wraps
-
-from core.settings import LIGHTNING_FEE_PERCENT, LIGHTNING_RESERVE_FEE_MIN
-
-
-def async_wrap(func):
- @wraps(func)
- async def run(*args, loop=None, executor=None, **kwargs):
- if loop is None:
- loop = asyncio.get_event_loop()
- partial_func = partial(func, *args, **kwargs)
- return await loop.run_in_executor(executor, partial_func)
-
- return run
-
-
-def async_unwrap(to_await):
- async_response = []
-
- async def run_and_capture_result():
- r = await to_await
- async_response.append(r)
-
- loop = asyncio.get_event_loop()
- coroutine = run_and_capture_result()
- loop.run_until_complete(coroutine)
- return async_response[0]
-
-
-def fee_reserve(amount_msat: int) -> int:
- """Function for calculating the Lightning fee reserve"""
- return max(
- int(LIGHTNING_RESERVE_FEE_MIN), int(amount_msat * LIGHTNING_FEE_PERCENT / 100.0)
- )
diff --git a/core/migrations.py b/core/migrations.py
deleted file mode 100644
index 1133de6..0000000
--- a/core/migrations.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import re
-
-from loguru import logger
-
-from core.db import COCKROACH, POSTGRES, SQLITE, Database
-
-
-async def migrate_databases(db: Database, migrations_module):
- """Creates the necessary databases if they don't exist already; or migrates them."""
-
- async def set_migration_version(conn, db_name, version):
- await conn.execute(
- """
- INSERT INTO dbversions (db, version) VALUES (?, ?)
- ON CONFLICT (db) DO UPDATE SET version = ?
- """,
- (db_name, version, version),
- )
-
- async def run_migration(db, migrations_module):
- db_name = migrations_module.__name__.split(".")[-2]
- for key, migrate in migrations_module.__dict__.items():
- match = match = matcher.match(key)
- if match:
- version = int(match.group(1))
- if version > current_versions.get(db_name, 0):
- await migrate(db)
-
- if db.schema == None:
- await set_migration_version(db, db_name, version)
- else:
- async with db.connect() as conn:
- await set_migration_version(conn, db_name, version)
-
- async with db.connect() as conn:
- if conn.type == SQLITE:
- exists = await conn.fetchone(
- "SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
- )
- elif conn.type in {POSTGRES, COCKROACH}:
- exists = await conn.fetchone(
- "SELECT * FROM information_schema.tables WHERE table_name = 'dbversions'"
- )
-
- if not exists:
- await migrations_module.m000_create_migrations_table(conn)
-
- rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
- current_versions = {row["db"]: row["version"] for row in rows}
- matcher = re.compile(r"^m(\d\d\d)_")
- await run_migration(conn, migrations_module)
diff --git a/core/secp.py b/core/secp.py
deleted file mode 100644
index 3341643..0000000
--- a/core/secp.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from secp256k1 import PrivateKey, PublicKey
-
-
-# We extend the public key to define some operations on points
-# Picked from https://github.com/WTRMQDev/secp256k1-zkp-py/blob/master/secp256k1_zkp/__init__.py
-class PublicKeyExt(PublicKey):
- def __add__(self, pubkey2):
- if isinstance(pubkey2, PublicKey):
- new_pub = PublicKey()
- new_pub.combine([self.public_key, pubkey2.public_key])
- return new_pub
- else:
- raise TypeError("Cant add pubkey and %s" % pubkey2.__class__)
-
- def __neg__(self):
- serialized = self.serialize()
- first_byte, remainder = serialized[:1], serialized[1:]
- # flip odd/even byte
- first_byte = {b"\x03": b"\x02", b"\x02": b"\x03"}[first_byte]
- return PublicKey(first_byte + remainder, raw=True)
-
- def __sub__(self, pubkey2):
- if isinstance(pubkey2, PublicKey):
- return self + (-pubkey2)
- else:
- raise TypeError("Can't add pubkey and %s" % pubkey2.__class__)
-
- def mult(self, privkey):
- if isinstance(privkey, PrivateKey):
- return self.tweak_mul(privkey.private_key)
- else:
- raise TypeError("Can't multiply with non privatekey")
-
- def __eq__(self, pubkey2):
- if isinstance(pubkey2, PublicKey):
- seq1 = self.to_data()
- seq2 = pubkey2.to_data()
- return seq1 == seq2
- else:
- raise TypeError("Can't compare pubkey and %s" % pubkey2.__class__)
-
- def to_data(self):
- return [self.public_key.data[i] for i in range(64)]
-
-
-# Horrible monkeypatching
-PublicKey.__add__ = PublicKeyExt.__add__
-PublicKey.__neg__ = PublicKeyExt.__neg__
-PublicKey.__sub__ = PublicKeyExt.__sub__
-PublicKey.mult = PublicKeyExt.mult
-PublicKey.__eq__ = PublicKeyExt.__eq__
-PublicKey.to_data = PublicKeyExt.to_data
diff --git a/core/settings.py b/core/settings.py
deleted file mode 100644
index d24c0c4..0000000
--- a/core/settings.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from pathlib import Path
-
-from environs import Env # type: ignore
-
-env = Env()
-env.read_env()
-
-DEBUG = env.bool("DEBUG", default=False)
-CASHU_DIR = env.str("CASHU_DIR", default="~/.cashu")
-CASHU_DIR = CASHU_DIR.replace("~", str(Path.home()))
-assert len(CASHU_DIR), "CASHU_DIR not defined"
-
-LIGHTNING = env.bool("LIGHTNING", default=True)
-LIGHTNING_FEE_PERCENT = env.float("LIGHTNING_FEE_PERCENT", default=1.0)
-assert LIGHTNING_FEE_PERCENT >= 0, "LIGHTNING_FEE_PERCENT must be at least 0"
-LIGHTNING_RESERVE_FEE_MIN = env.float("LIGHTNING_RESERVE_FEE_MIN", default=4000)
-
-MINT_PRIVATE_KEY = env.str("MINT_PRIVATE_KEY", default=None)
-
-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="8333.space")
-MINT_PORT = env.int("MINT_PORT", default=3338)
-
-if MINT_HOST in ["localhost", "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
diff --git a/core/split.py b/core/split.py
deleted file mode 100644
index 44b9cf5..0000000
--- a/core/split.py
+++ /dev/null
@@ -1,8 +0,0 @@
-def amount_split(amount):
- """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
- bits_amt = bin(amount)[::-1][:-2]
- rv = []
- for (pos, bit) in enumerate(bits_amt):
- if bit == "1":
- rv.append(2**pos)
- return rv
diff --git a/lightning/__init__.py b/lightning/__init__.py
deleted file mode 100644
index baa53c7..0000000
--- a/lightning/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from lightning.lnbits import LNbitsWallet
-
-WALLET = LNbitsWallet()
diff --git a/lightning/base.py b/lightning/base.py
deleted file mode 100644
index e38b6d8..0000000
--- a/lightning/base.py
+++ /dev/null
@@ -1,88 +0,0 @@
-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
diff --git a/lightning/lnbits.py b/lightning/lnbits.py
deleted file mode 100644
index 94a49eb..0000000
--- a/lightning/lnbits.py
+++ /dev/null
@@ -1,149 +0,0 @@
-import asyncio
-import hashlib
-import json
-from os import getenv
-from typing import AsyncGenerator, Dict, Optional
-
-import requests
-
-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 = LNBITS_ENDPOINT
-
- key = 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)
- if "detail" in r.json():
- return PaymentResponse(None, None, None, None, r.json()["detail"])
- 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)
diff --git a/mint/__init__.py b/mint/__init__.py
deleted file mode 100644
index 9fa043b..0000000
--- a/mint/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from core.settings import MINT_PRIVATE_KEY
-from mint.ledger import Ledger
-
-print("init")
-
-ledger = Ledger(MINT_PRIVATE_KEY, "data/mint")
diff --git a/mint/__main__.py b/mint/__main__.py
deleted file mode 100644
index 90e97df..0000000
--- a/mint/__main__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import main
-
-print("main")
-
-
-main()
diff --git a/mint/api.py b/mint/api.py
deleted file mode 100644
index e69de29..0000000
diff --git a/mint/app.py b/mint/app.py
deleted file mode 100644
index e3a9cbd..0000000
--- a/mint/app.py
+++ /dev/null
@@ -1,86 +0,0 @@
-import asyncio
-import logging
-import sys
-
-
-from fastapi import FastAPI
-from loguru import logger
-
-from core.settings import CASHU_DIR, DEBUG
-
-from lightning import WALLET
-from mint.migrations import m001_initial
-
-from . import ledger
-
-
-def startup(app: FastAPI):
- @app.on_event("startup")
- async def load_ledger():
- await asyncio.wait([m001_initial(ledger.db)])
- await ledger.load_used_proofs()
-
- error_message, balance = await WALLET.status()
- if error_message:
- logger.warning(
- f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
- RuntimeWarning,
- )
-
- logger.info(f"Lightning balance: {balance} sat")
- logger.info(f"Data dir: {CASHU_DIR}")
- logger.info("Mint started.")
-
-
-def create_app(config_object="core.settings") -> FastAPI:
- def configure_logger() -> None:
- class Formatter:
- def __init__(self):
- self.padding = 0
- self.minimal_fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level} | {message}\n"
- if DEBUG:
- self.fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level: <4} | {name}:{function}:{line} | {message}\n"
- else:
- self.fmt: str = self.minimal_fmt
-
- def format(self, record):
- function = "{function}".format(**record)
- if function == "emit": # uvicorn logs
- return self.minimal_fmt
- return self.fmt
-
- class InterceptHandler(logging.Handler):
- def emit(self, record):
- try:
- level = logger.level(record.levelname).name
- except ValueError:
- level = record.levelno
- logger.log(level, record.getMessage())
-
- logger.remove()
- log_level: str = "INFO"
- formatter = Formatter()
- logger.add(sys.stderr, level=log_level, format=formatter.format)
-
- logging.getLogger("uvicorn").handlers = [InterceptHandler()]
- logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
-
- configure_logger()
-
- app = FastAPI(
- title="Cashu Mint",
- description="Ecash wallet and mint.",
- license_info={
- "name": "MIT License",
- "url": "https://raw.githubusercontent.com/callebtc/cashu/main/LICENSE",
- },
- )
-
- startup(app)
- return app
-
-
-# if __name__ == "__main__":
-# main()
-
-app = create_app()
diff --git a/mint/crud.py b/mint/crud.py
deleted file mode 100644
index 5b92271..0000000
--- a/mint/crud.py
+++ /dev/null
@@ -1,110 +0,0 @@
-import secrets
-from typing import Optional
-
-from core.base import Invoice, Proof
-from core.db import Connection, Database
-
-
-async def store_promise(
- amount: int,
- B_: str,
- C_: str,
- db: Database,
- conn: Optional[Connection] = None,
-):
-
- await (conn or db).execute(
- """
- INSERT INTO promises
- (amount, B_b, C_b)
- VALUES (?, ?, ?)
- """,
- (
- amount,
- str(B_),
- str(C_),
- ),
- )
-
-
-async def get_proofs_used(
- db: Database,
- conn: Optional[Connection] = None,
-):
-
- rows = await (conn or db).fetchall(
- """
- SELECT secret from proofs_used
- """
- )
- return [row[0] for row in rows]
-
-
-async def invalidate_proof(
- proof: Proof,
- db: Database,
- conn: Optional[Connection] = None,
-):
-
- # we add the proof and secret to the used list
- await (conn or db).execute(
- """
- INSERT INTO proofs_used
- (amount, C, secret)
- VALUES (?, ?, ?)
- """,
- (
- proof.amount,
- str(proof.C),
- 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),
- )
diff --git a/mint/ledger.py b/mint/ledger.py
deleted file mode 100644
index f5ce49f..0000000
--- a/mint/ledger.py
+++ /dev/null
@@ -1,258 +0,0 @@
-"""
-Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c
-"""
-
-import hashlib
-from typing import List, Set
-
-import core.b_dhke as b_dhke
-from core.base import BlindedMessage, BlindedSignature, Invoice, Proof
-from core.db import Database
-from core.helpers import fee_reserve
-from core.secp import PrivateKey, PublicKey
-from core.settings import LIGHTNING, MAX_ORDER
-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:
- def __init__(self, secret_key: str, db: str):
- self.proofs_used: Set[str] = set()
-
- self.master_key: str = secret_key
- self.keys: List[PrivateKey] = self._derive_keys(self.master_key)
- self.pub_keys: List[PublicKey] = self._derive_pubkeys(self.keys)
- self.db: Database = Database("mint", db)
-
- async def load_used_proofs(self):
- self.proofs_used = set(await get_proofs_used(db=self.db))
-
- @staticmethod
- def _derive_keys(master_key: str):
- """Deterministic derivation of keys for 2^n values."""
- return {
- 2
- ** i: PrivateKey(
- hashlib.sha256((str(master_key) + str(i)).encode("utf-8"))
- .hexdigest()
- .encode("utf-8")[:32],
- raw=True,
- )
- for i in range(MAX_ORDER)
- }
-
- @staticmethod
- def _derive_pubkeys(keys: List[PrivateKey]):
- return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]}
-
- async def _generate_promises(self, amounts: List[int], B_s: List[str]):
- """Generates promises that sum to the given amount."""
- return [
- await self._generate_promise(amount, PublicKey(bytes.fromhex(B_), raw=True))
- for (amount, B_) in zip(amounts, B_s)
- ]
-
- async def _generate_promise(self, amount: int, B_: PublicKey):
- """Generates a promise for given amount and returns a pair (amount, C')."""
- secret_key = self.keys[amount] # Get the correct key
- C_ = b_dhke.step2_bob(B_, secret_key)
- await store_promise(
- amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db
- )
- return BlindedSignature(amount=amount, C_=C_.serialize().hex())
-
- def _check_spendable(self, proof: Proof):
- """Checks whether the proof was already spent."""
- return not proof.secret in self.proofs_used
-
- def _verify_proof(self, proof: Proof):
- """Verifies that the proof of promise was issued by this ledger."""
- if not self._check_spendable(proof):
- raise Exception(f"tokens already spent. Secret: {proof.secret}")
- secret_key = self.keys[proof.amount] # Get the correct key to check against
- C = PublicKey(bytes.fromhex(proof.C), raw=True)
- return b_dhke.verify(secret_key, C, proof.secret)
-
- def _verify_outputs(
- self, total: int, amount: int, output_data: List[BlindedMessage]
- ):
- """Verifies the expected split was correctly computed"""
- fst_amt, snd_amt = total - amount, amount # we have two amounts to split to
- fst_outputs = amount_split(fst_amt)
- snd_outputs = amount_split(snd_amt)
- expected = fst_outputs + snd_outputs
- given = [o.amount for o in output_data]
- return given == expected
-
- def _verify_no_duplicates(
- self, proofs: List[Proof], output_data: List[BlindedMessage]
- ):
- secrets = [p.secret for p in proofs]
- if len(secrets) != len(list(set(secrets))):
- return False
- B_s = [od.B_ for od in output_data]
- if len(B_s) != len(list(set(B_s))):
- return False
- return True
-
- def _verify_split_amount(self, amount: int):
- """Split amount like output amount can't be negative or too big."""
- try:
- self._verify_amount(amount)
- except:
- # For better error message
- raise Exception("invalid split amount: " + str(amount))
-
- def _verify_amount(self, amount: int):
- """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))
- return amount
-
- def _verify_equation_balanced(
- self, proofs: List[Proof], outs: List[BlindedMessage]
- ):
- """Verify that Σoutputs - Σinputs = 0."""
- sum_inputs = sum(self._verify_amount(p.amount) for p in proofs)
- sum_outputs = sum(self._verify_amount(p.amount) for p in outs)
- assert sum_outputs - sum_inputs == 0
-
- def _get_output_split(self, amount: int):
- """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
- self._verify_amount(amount)
- bits_amt = bin(amount)[::-1][:-2]
- rv = []
- for (pos, bit) in enumerate(bits_amt):
- if bit == "1":
- rv.append(2**pos)
- return rv
-
- async def _request_lightning_invoice(self, amount: int):
- """Returns an invoice from the Lightning backend."""
- 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"
- )
- return payment_request, checking_id
-
- async def _check_lightning_invoice(self, payment_hash: str):
- """Checks with the Lightning backend whether an invoice with this payment_hash was paid."""
- 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 _pay_lightning_invoice(self, invoice: str, amount: int):
- """Returns an invoice from the Lightning backend."""
- error, balance = await WALLET.status()
- if error:
- raise Exception(f"Lightning wallet not responding: {error}")
- ok, checking_id, fee_msat, preimage, error_message = await WALLET.pay_invoice(
- invoice, fee_limit_msat=fee_reserve(amount * 1000)
- )
- return ok, preimage
-
- async def _invalidate_proofs(self, proofs: List[Proof]):
- """Adds secrets of proofs to the list of knwon secrets and stores them in the db."""
- # Mark proofs as used and prepare new promises
- proof_msgs = set([p.secret for p in proofs])
- self.proofs_used |= proof_msgs
- # store in db
- for p in proofs:
- await invalidate_proof(p, db=self.db)
-
- # Public methods
- def get_pubkeys(self):
- """Returns public keys for possible amounts."""
- return {a: p.serialize().hex() for a, p in self.pub_keys.items()}
-
- 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: List[PublicKey], amounts: List[int], payment_hash=None):
- """Mints a promise for coins for B_."""
- # check if lightning invoice was paid
- if LIGHTNING and (
- 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}.")
-
- promises = [
- await self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts)
- ]
- return promises
-
- async def melt(self, proofs: List[Proof], amount: int, invoice: str):
- """Invalidates proofs and pays a Lightning invoice."""
- # if not LIGHTNING:
- total = sum([p["amount"] for p in proofs])
- # check that lightning fees are included
- assert total + fee_reserve(amount * 1000) >= amount, Exception(
- "provided proofs not enough for Lightning payment."
- )
-
- status, preimage = await self._pay_lightning_invoice(invoice, amount)
- if status == True:
- await self._invalidate_proofs(proofs)
- return status, preimage
-
- async def check_spendable(self, proofs: List[Proof]):
- """Checks if all provided proofs are valid and still spendable (i.e. have not been spent)."""
- 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]
- ):
- """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")
- 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")
-
- # 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) :]]
- prom_fst, prom_snd = await self._generate_promises(
- outs_fst, B_fst
- ), await self._generate_promises(outs_snd, B_snd)
- self._verify_equation_balanced(proofs, prom_fst + prom_snd)
- return prom_fst, prom_snd
diff --git a/mint/main.py b/mint/main.py
deleted file mode 100644
index 7f70c5b..0000000
--- a/mint/main.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import click
-import uvicorn
-
-from core.settings import (
- MINT_SERVER_HOST,
- MINT_SERVER_PORT,
-)
-
-
-@click.command(
- context_settings=dict(
- ignore_unknown_options=True,
- allow_extra_args=True,
- )
-)
-@click.option("--port", default=MINT_SERVER_PORT, help="Port to listen on")
-@click.option("--host", default=MINT_SERVER_HOST, help="Host to run mint on")
-@click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile")
-@click.option("--ssl-certfile", default=None, help="Path to SSL certificate")
-@click.pass_context
-def main(
- ctx,
- port: int = MINT_SERVER_PORT,
- host: str = MINT_SERVER_HOST,
- ssl_keyfile: str = None,
- ssl_certfile: str = None,
-):
- """Launched with `poetry run mint` at root level"""
- # this beautiful beast parses all command line arguments and passes them to the uvicorn server
- d = dict()
- for a in ctx.args:
- item = a.split("=")
- if len(item) > 1: # argument like --key=value
- print(a, item)
- d[item[0].strip("--").replace("-", "_")] = (
- int(item[1]) # need to convert to int if it's a number
- if item[1].isdigit()
- else item[1]
- )
- else:
- d[a.strip("--")] = True # argument like --key
-
- config = uvicorn.Config(
- "mint.app:app",
- port=port,
- host=host,
- ssl_keyfile=ssl_keyfile,
- ssl_certfile=ssl_certfile,
- **d,
- )
- server = uvicorn.Server(config)
- server.run()
diff --git a/mint/migrations.py b/mint/migrations.py
deleted file mode 100644
index 231013c..0000000
--- a/mint/migrations.py
+++ /dev/null
@@ -1,87 +0,0 @@
-from core.db import Database
-
-
-async def m000_create_migrations_table(db):
- await db.execute(
- """
- CREATE TABLE IF NOT EXISTS dbversions (
- db TEXT PRIMARY KEY,
- version INT NOT NULL
- )
- """
- )
-
-
-async def m001_initial(db: Database):
- await db.execute(
- """
- CREATE TABLE IF NOT EXISTS promises (
- amount INTEGER NOT NULL,
- B_b TEXT NOT NULL,
- C_b TEXT NOT NULL,
-
- UNIQUE (B_b)
-
- );
- """
- )
-
- await db.execute(
- """
- CREATE TABLE IF NOT EXISTS proofs_used (
- amount INTEGER NOT NULL,
- C TEXT NOT NULL,
- secret TEXT NOT NULL,
-
- UNIQUE (secret)
-
- );
- """
- )
-
- 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
- SELECT COALESCE(SUM(s), 0) AS balance FROM (
- SELECT SUM(amount) AS s
- FROM promises
- WHERE amount > 0
- );
- """
- )
-
- await db.execute(
- """
- CREATE VIEW IF NOT EXISTS balance_used AS
- SELECT COALESCE(SUM(s), 0) AS balance FROM (
- SELECT SUM(amount) AS s
- FROM proofs_used
- WHERE amount > 0
- );
- """
- )
-
- await db.execute(
- """
- CREATE VIEW IF NOT EXISTS balance AS
- SELECT s_issued - s_used AS balance FROM (
- SELECT bi.balance AS s_issued, bu.balance AS s_used
- FROM balance_issued bi
- CROSS JOIN balance_used bu
- );
- """
- )
diff --git a/mint/router.py b/mint/router.py
deleted file mode 100644
index 7f5ac35..0000000
--- a/mint/router.py
+++ /dev/null
@@ -1,86 +0,0 @@
-from fastapi import APIRouter
-from mint import ledger
-from core.base import CheckPayload, MeltPayload, MintPayloads, SplitPayload
-from typing import Union
-from secp256k1 import PublicKey
-
-router: APIRouter = APIRouter()
-
-
-@router.get("/keys")
-def keys():
- """Get the public keys of the mint"""
- return ledger.get_pubkeys()
-
-
-@router.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}
-
-
-@router.post("/mint")
-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.blinded_messages:
- amounts.append(payload.amount)
- B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True))
- try:
- promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash)
- return promises
- except Exception as exc:
- return {"error": str(exc)}
-
-
-@router.post("/melt")
-async def melt(payload: MeltPayload):
- """
- Requests tokens to be destroyed and sent out via Lightning.
- """
- ok, preimage = await ledger.melt(payload.proofs, payload.amount, payload.invoice)
- return {"paid": ok, "preimage": preimage}
-
-
-@router.post("/check")
-async def check_spendable(payload: CheckPayload):
- return await ledger.check_spendable(payload.proofs)
-
-
-@router.post("/split")
-async def split(payload: SplitPayload):
- """
- Requetst a set of tokens with amount "total" to be split into two
- newly minted sets with amount "split" and "total-split".
- """
- proofs = payload.proofs
- amount = payload.amount
- output_data = payload.output_data.blinded_messages
- try:
- split_return = await ledger.split(proofs, amount, output_data)
- 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.")
- fst_promises, snd_promises = split_return
- return {"fst": fst_promises, "snd": snd_promises}
diff --git a/poetry.lock b/poetry.lock
index 4f84b80..c0dac60 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -85,7 +85,7 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
-version = "2022.9.14"
+version = "2022.9.24"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
@@ -205,11 +205,11 @@ dotenv = ["python-dotenv"]
[[package]]
name = "h11"
-version = "0.13.0"
+version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.dependencies]
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
@@ -759,8 +759,8 @@ black = [
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
]
certifi = [
- {file = "certifi-2022.9.14-py3-none-any.whl", hash = "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"},
- {file = "certifi-2022.9.14.tar.gz", hash = "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5"},
+ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
+ {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
]
cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@@ -857,8 +857,8 @@ Flask = [
{file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"},
]
h11 = [
- {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"},
- {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"},
+ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
diff --git a/pyproject.toml b/pyproject.toml
index 4bf03ad..8588b47 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,6 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
-mint = "mint.app:main"
-cashu = "wallet.cashu:cli"
+mint = "cashu.mint.app:main"
+cashu = "cashu.wallet.cli:cli"
wallet-test = "tests.test_wallet:test"
\ No newline at end of file
diff --git a/setup.py b/setup.py
index f80f9ca..01bb877 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@ with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
with open("requirements.txt") as f:
requirements = f.read().splitlines()
-entry_points = {"console_scripts": ["cashu = wallet.cashu:cli"]}
+entry_points = {"console_scripts": ["cashu = cahu.wallet.cashu:cli"]}
setuptools.setup(
name="cashu",
diff --git a/wallet/__init__.py b/wallet/__init__.py
deleted file mode 100644
index bf11b51..0000000
--- a/wallet/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-import sys
-
-sys.tracebacklimit = None
diff --git a/wallet/cashu.py b/wallet/cashu.py
deleted file mode 100755
index c252b46..0000000
--- a/wallet/cashu.py
+++ /dev/null
@@ -1,212 +0,0 @@
-#!/usr/bin/env python
-
-import asyncio
-import base64
-import json
-import math
-from datetime import datetime
-from functools import wraps
-from itertools import groupby
-from operator import itemgetter
-
-import click
-from bech32 import bech32_decode, bech32_encode, convertbits
-
-import core.bolt11 as bolt11
-from core.base import Proof
-from core.bolt11 import Invoice
-from core.helpers import fee_reserve
-from core.migrations import migrate_databases
-from core.settings import CASHU_DIR, DEBUG, LIGHTNING, MINT_URL
-from wallet import migrations
-from wallet.crud import get_reserved_proofs
-from wallet.wallet import Wallet as Wallet
-
-
-async def init_wallet(wallet: Wallet):
- """Performs migrations and loads proofs from db."""
- await migrate_databases(wallet.db, migrations)
- await wallet.load_proofs()
-
-
-class NaturalOrderGroup(click.Group):
- """For listing commands in help in order of definition"""
-
- def list_commands(self, ctx):
- return self.commands.keys()
-
-
-@click.group(cls=NaturalOrderGroup)
-@click.option("--host", "-h", default=MINT_URL, help="Mint address.")
-@click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet to use.")
-@click.pass_context
-def cli(ctx, host: str, walletname: str):
- 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)
- pass
-
-
-# https://github.com/pallets/click/issues/85#issuecomment-503464628
-def coro(f):
- @wraps(f)
- def wrapper(*args, **kwargs):
- return asyncio.run(f(*args, **kwargs))
-
- return wrapper
-
-
-@cli.command("mint", help="Mint tokens.")
-@click.argument("amount", type=int)
-@click.option("--hash", default="", help="Hash of the paid invoice.", type=str)
-@click.pass_context
-@coro
-async def mint(ctx, amount: int, hash: str):
- wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
- wallet.status()
- if not LIGHTNING:
- r = await wallet.mint(amount)
- elif amount and not hash:
- r = await wallet.request_mint(amount)
- if "pr" in r:
- print(f"Pay this invoice to mint {amount} sat:")
- print(f"Invoice: {r['pr']}")
- print("")
- print(
- f"After paying the invoice, run this command:\ncashu mint {amount} --hash {r['hash']}"
- )
- elif amount and hash:
- await wallet.mint(amount, hash)
- wallet.status()
- return
-
-
-@cli.command("balance", help="See balance.")
-@click.pass_context
-@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.pass_context
-@coro
-async def send(ctx, amount: int):
- wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
- wallet.status()
- _, send_proofs = await wallet.split_to_send(wallet.proofs, amount)
- await wallet.set_reserved(send_proofs, reserved=True)
- token = await wallet.serialize_proofs(send_proofs)
- print(token)
- wallet.status()
-
-
-@cli.command("receive", help="Receive tokens.")
-@click.argument("token", type=str)
-@click.pass_context
-@coro
-async def receive(ctx, token: str):
- wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
- wallet.status()
- proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))]
- _, _ = await wallet.redeem(proofs)
- wallet.status()
-
-
-@cli.command("burn", help="Burn spent tokens.")
-@click.argument("token", required=False, type=str)
-@click.option("--all", "-a", default=False, is_flag=True, help="Burn all spent tokens.")
-@click.option(
- "--force", "-f", default=False, is_flag=True, help="Force check on all tokens."
-)
-@click.pass_context
-@coro
-async def burn(ctx, token: str, all: bool, force: bool):
- wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
- 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."
- )
- return
- if all:
- # check only those who are flagged as reserved
- proofs = await get_reserved_proofs(wallet.db)
- elif force:
- # check all proofs in db
- proofs = wallet.proofs
- else:
- # check only the specified ones
- proofs = [
- Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))
- ]
- wallet.status()
- await wallet.invalidate(proofs)
- wallet.status()
-
-
-@cli.command("pending", help="Show pending tokens.")
-@click.pass_context
-@coro
-async def pending(ctx):
- wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
- reserved_proofs = await get_reserved_proofs(wallet.db)
- if len(reserved_proofs):
- sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id"))
- for key, value in groupby(sorted_proofs, key=itemgetter("send_id")):
- grouped_proofs = list(value)
- token = await wallet.serialize_proofs(grouped_proofs)
- reserved_date = datetime.utcfromtimestamp(
- int(grouped_proofs[0].time_reserved)
- ).strftime("%Y-%m-%d %H:%M:%S")
- print(
- f"Amount: {sum([p['amount'] for p in grouped_proofs])} sat Sent: {reserved_date} ID: {key}\n"
- )
- print(token)
- print("")
- wallet.status()
-
-
-@cli.command("pay", help="Pay lightning invoice.")
-@click.argument("invoice", type=str)
-@click.pass_context
-@coro
-async def pay(ctx, invoice: str):
- wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
- wallet.status()
- decoded_invoice: Invoice = bolt11.decode(invoice)
- amount = math.ceil(
- (decoded_invoice.amount_msat + fee_reserve(decoded_invoice.amount_msat)) / 1000
- ) # 1% fee for Lightning
- print(
- f"Paying Lightning invoice of {decoded_invoice.amount_msat // 1000} sat ({amount} sat incl. fees)"
- )
- assert amount > 0, "amount is not positive"
- if wallet.available_balance < amount:
- print("Error: Balance too low.")
- return
- _, send_proofs = await wallet.split_to_send(wallet.proofs, amount)
- await wallet.pay_lightning(send_proofs, amount, invoice)
- wallet.status()
-
-
-@cli.command("info", help="Information about Cashu wallet.")
-@click.pass_context
-@coro
-async def info(ctx):
- wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
- wallet.status()
- print(f"Debug: {DEBUG}")
- print(f"Cashu dir: {CASHU_DIR}")
- print(f"Mint URL: {MINT_URL}")
- return
diff --git a/wallet/crud.py b/wallet/crud.py
deleted file mode 100644
index 55bb293..0000000
--- a/wallet/crud.py
+++ /dev/null
@@ -1,99 +0,0 @@
-import time
-from typing import Optional
-
-from core.base import Proof
-from core.db import Connection, Database
-
-
-async def store_proof(
- proof: Proof,
- db: Database,
- conn: Optional[Connection] = None,
-):
-
- await (conn or db).execute(
- """
- INSERT INTO proofs
- (amount, C, secret, time_created)
- VALUES (?, ?, ?, ?)
- """,
- (proof.amount, str(proof.C), str(proof.secret), int(time.time())),
- )
-
-
-async def get_proofs(
- db: Database,
- conn: Optional[Connection] = None,
-):
-
- rows = await (conn or db).fetchall(
- """
- SELECT * from proofs
- """
- )
- return [Proof.from_row(r) for r in rows]
-
-
-async def get_reserved_proofs(
- db: Database,
- conn: Optional[Connection] = None,
-):
-
- rows = await (conn or db).fetchall(
- """
- SELECT * from proofs
- WHERE reserved
- """
- )
- return [Proof.from_row(r) for r in rows]
-
-
-async def invalidate_proof(
- proof: Proof,
- db: Database,
- conn: Optional[Connection] = None,
-):
-
- await (conn or db).execute(
- f"""
- DELETE FROM proofs
- WHERE secret = ?
- """,
- str(proof["secret"]),
- )
-
- await (conn or db).execute(
- """
- INSERT INTO proofs_used
- (amount, C, secret, time_used)
- VALUES (?, ?, ?, ?)
- """,
- (proof.amount, str(proof.C), str(proof.secret), int(time.time())),
- )
-
-
-async def update_proof_reserved(
- proof: Proof,
- reserved: bool,
- send_id: str = None,
- db: Database = None,
- conn: Optional[Connection] = None,
-):
- clauses = []
- values = []
- clauses.append("reserved = ?")
- values.append(reserved)
-
- if send_id:
- clauses.append("send_id = ?")
- values.append(send_id)
-
- if reserved:
- # set the time of reserving
- clauses.append("time_reserved = ?")
- values.append(int(time.time()))
-
- await (conn or db).execute(
- f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
- (*values, str(proof.secret)),
- )
diff --git a/wallet/migrations.py b/wallet/migrations.py
deleted file mode 100644
index 8745577..0000000
--- a/wallet/migrations.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from core.db import Database
-
-
-async def m000_create_migrations_table(db):
- await db.execute(
- """
- CREATE TABLE IF NOT EXISTS dbversions (
- db TEXT PRIMARY KEY,
- version INT NOT NULL
- )
- """
- )
-
-
-async def m001_initial(db: Database):
- await db.execute(
- """
- CREATE TABLE IF NOT EXISTS proofs (
- amount INTEGER NOT NULL,
- C TEXT NOT NULL,
- secret TEXT NOT NULL,
-
- UNIQUE (secret)
-
- );
- """
- )
-
- await db.execute(
- """
- CREATE TABLE IF NOT EXISTS proofs_used (
- amount INTEGER NOT NULL,
- C TEXT NOT NULL,
- secret TEXT NOT NULL,
-
- UNIQUE (secret)
-
- );
- """
- )
-
- await db.execute(
- """
- CREATE VIEW IF NOT EXISTS balance AS
- SELECT COALESCE(SUM(s), 0) AS balance FROM (
- SELECT SUM(amount) AS s
- FROM proofs
- WHERE amount > 0
- );
- """
- )
-
- await db.execute(
- """
- CREATE VIEW IF NOT EXISTS balance_used AS
- SELECT COALESCE(SUM(s), 0) AS used FROM (
- SELECT SUM(amount) AS s
- FROM proofs_used
- WHERE amount > 0
- );
- """
- )
-
-
-async def m002_add_proofs_reserved(db):
- """
- Column for marking proofs as reserved when they are being sent.
- """
-
- await db.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL")
-
-
-async def m003_add_proofs_sendid_and_timestamps(db):
- """
- Column with unique ID for each initiated send attempt
- so proofs can be later grouped together for each send attempt.
- """
- await db.execute("ALTER TABLE proofs ADD COLUMN send_id TEXT")
- await db.execute("ALTER TABLE proofs ADD COLUMN time_created TIMESTAMP")
- await db.execute("ALTER TABLE proofs ADD COLUMN time_reserved TIMESTAMP")
- await db.execute("ALTER TABLE proofs_used ADD COLUMN time_used TIMESTAMP")
diff --git a/wallet/wallet.py b/wallet/wallet.py
deleted file mode 100644
index 17172d5..0000000
--- a/wallet/wallet.py
+++ /dev/null
@@ -1,252 +0,0 @@
-import base64
-import json
-import random
-import secrets as scrts
-import uuid
-from typing import List
-
-import requests
-
-import core.b_dhke as b_dhke
-from core.base import (
- BlindedMessage,
- BlindedSignature,
- CheckPayload,
- MeltPayload,
- MintPayloads,
- Proof,
- SplitPayload,
-)
-from core.db import Database
-from core.secp import PublicKey
-from core.settings import DEBUG
-from core.split import amount_split
-from wallet.crud import get_proofs, invalidate_proof, store_proof, update_proof_reserved
-
-
-class LedgerAPI:
- def __init__(self, url):
- self.url = url
- self.keys = self._get_keys(url)
-
- @staticmethod
- def _get_keys(url):
- resp = requests.get(url + "/keys").json()
- return {
- int(amt): PublicKey(bytes.fromhex(val), raw=True)
- for amt, val in resp.items()
- }
-
- @staticmethod
- def _get_output_split(amount):
- """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
- bits_amt = bin(amount)[::-1][:-2]
- rv = []
- for (pos, bit) in enumerate(bits_amt):
- if bit == "1":
- rv.append(2**pos)
- return rv
-
- def _construct_proofs(self, promises: List[BlindedSignature], secrets: List[str]):
- """Returns proofs of promise from promises."""
- proofs = []
- for promise, (r, secret) in zip(promises, secrets):
- 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)
- proofs.append(proof)
- return proofs
-
- def _generate_secret(self, randombits=128):
- """Returns base64 encoded random string."""
- return scrts.token_urlsafe(randombits // 8)
-
- 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 = []
- rs = []
- for amount in amounts:
- secret = self._generate_secret()
- secrets.append(secret)
- 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(
- self.url + "/mint",
- json=payloads.dict(),
- params={"payment_hash": payment_hash},
- ).json()
- 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."""
- 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)
- split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads)
- promises_dict = requests.post(
- self.url + "/split",
- json=split_payload.dict(),
- ).json()
- 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) :])
-
- return fst_proofs, snd_proofs
-
- async def check_spendable(self, proofs: List[Proof]):
- payload = CheckPayload(proofs=proofs)
- return_dict = requests.post(
- self.url + "/check",
- json=payload.dict(),
- ).json()
-
- return return_dict
-
- async def pay_lightning(self, proofs: List[Proof], amount: int, invoice: str):
- payload = MeltPayload(proofs=proofs, amount=amount, invoice=invoice)
- return_dict = requests.post(
- self.url + "/melt",
- json=payload.dict(),
- ).json()
- return return_dict
-
-
-class Wallet(LedgerAPI):
- """Minimal wallet wrapper."""
-
- def __init__(self, url: str, db: str, name: str = "no_name"):
- super().__init__(url)
- self.db = Database("wallet", db)
- self.proofs: List[Proof] = []
- self.name = name
-
- async def load_proofs(self):
- self.proofs = await get_proofs(db=self.db)
-
- async def _store_proofs(self, proofs):
- for proof in proofs:
- await store_proof(proof, db=self.db)
-
- async def request_mint(self, amount):
- return super().request_mint(amount)
-
- async def mint(self, amount: int, payment_hash: str = None):
- split = amount_split(amount)
- proofs = super().mint(split, payment_hash)
- if proofs == []:
- raise Exception("received no proofs.")
- await self._store_proofs(proofs)
- self.proofs += proofs
- return proofs
-
- async def redeem(self, proofs: List[Proof]):
- return await self.split(proofs, sum(p["amount"] for p in proofs))
-
- async def split(self, proofs: List[Proof], amount: int):
- assert len(proofs) > 0, ValueError("no proofs provided.")
- fst_proofs, snd_proofs = super().split(proofs, amount)
- if len(fst_proofs) == 0 and len(snd_proofs) == 0:
- raise Exception("received no splits.")
- used_secrets = [p["secret"] for p in proofs]
- self.proofs = list(
- filter(lambda p: p["secret"] not in used_secrets, self.proofs)
- )
- self.proofs += fst_proofs + snd_proofs
- await self._store_proofs(fst_proofs + snd_proofs)
- for proof in proofs:
- await invalidate_proof(proof, db=self.db)
- return fst_proofs, snd_proofs
-
- async def pay_lightning(self, proofs: List[Proof], amount: int, invoice: str):
- """Pays a lightning invoice"""
- status = await super().pay_lightning(proofs, amount, invoice)
- if status["paid"] == True:
- await self.invalidate(proofs)
- else:
- raise Exception("could not pay invoice.")
- return status["paid"]
-
- @staticmethod
- async def serialize_proofs(proofs: List[Proof]):
- 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):
- """Like self.split but only considers non-reserved tokens."""
- 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)
-
- async def set_reserved(self, proofs: List[Proof], reserved: bool):
- """Mark a proof as reserved to avoid reuse or delete marking."""
- uuid_str = str(uuid.uuid1())
- for proof in proofs:
- proof.reserved = True
- await update_proof_reserved(
- proof, reserved=reserved, send_id=uuid_str, db=self.db
- )
-
- async def check_spendable(self, proofs):
- return await super().check_spendable(proofs)
-
- async def invalidate(self, proofs):
- """Invalidates all spendable tokens supplied in proofs."""
- spendables = await self.check_spendable(proofs)
- invalidated_proofs = []
- for idx, spendable in spendables.items():
- if not spendable:
- invalidated_proofs.append(proofs[int(idx)])
- await invalidate_proof(proofs[int(idx)], db=self.db)
- invalidate_secrets = [p["secret"] for p in invalidated_proofs]
- self.proofs = list(
- filter(lambda p: p["secret"] not in invalidate_secrets, self.proofs)
- )
-
- @property
- def balance(self):
- return sum(p["amount"] for p in self.proofs)
-
- @property
- def available_balance(self):
- return sum(p["amount"] for p in self.proofs if not p.reserved)
-
- def status(self):
- print(
- f"Balance: {self.balance} sat (Available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)"
- )
-
- def proof_amounts(self):
- return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]