This commit is contained in:
callebtc
2022-09-21 17:07:25 +03:00
parent 895a0582a1
commit 80a696d097
10 changed files with 103 additions and 32 deletions

View File

@@ -1,5 +1,7 @@
DEBUG = true DEBUG = true
CASHU_DIR=~/.cashu
# WALLET # WALLET
MINT_HOST=127.0.0.1 MINT_HOST=127.0.0.1

View File

@@ -9,6 +9,7 @@ class Proof(BaseModel):
C: str C: str
secret: str secret: str
reserved: bool = False # whether this proof is reserved for sending reserved: bool = False # whether this proof is reserved for sending
send_id: str = "" # unique ID of send attempt
@classmethod @classmethod
def from_row(cls, row: Row): def from_row(cls, row: Row):
@@ -17,6 +18,7 @@ class Proof(BaseModel):
C=row[1], C=row[1],
secret=row[2], secret=row[2],
reserved=row[3] or False, reserved=row[3] or False,
send_id=row[4] or "",
) )
@classmethod @classmethod
@@ -26,6 +28,7 @@ class Proof(BaseModel):
C=d["C"], C=d["C"],
secret=d["secret"], secret=d["secret"],
reserved=d["reserved"] or False, reserved=d["reserved"] or False,
send_id=d["send_id"] or "",
) )
def __getitem__(self, key): def __getitem__(self, key):

View File

@@ -1,9 +1,15 @@
from pathlib import Path
from environs import Env # type: ignore from environs import Env # type: ignore
env = Env() env = Env()
env.read_env() env.read_env()
DEBUG = env.bool("DEBUG", default=False) 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 = env.bool("LIGHTNING", default=True)
LIGHTNING_FEE_PERCENT = env.float("LIGHTNING_FEE_PERCENT", default=1.0) LIGHTNING_FEE_PERCENT = env.float("LIGHTNING_FEE_PERCENT", default=1.0)
assert LIGHTNING_FEE_PERCENT >= 0, "LIGHTNING_FEE_PERCENT must be at least 0" assert LIGHTNING_FEE_PERCENT >= 0, "LIGHTNING_FEE_PERCENT must be at least 0"

View File

@@ -8,8 +8,13 @@ import requests
from core.settings import LNBITS_ENDPOINT, LNBITS_KEY from core.settings import LNBITS_ENDPOINT, LNBITS_KEY
from .base import (InvoiceResponse, PaymentResponse, PaymentStatus, from .base import (
StatusResponse, Wallet) InvoiceResponse,
PaymentResponse,
PaymentStatus,
StatusResponse,
Wallet,
)
class LNbitsWallet(Wallet): class LNbitsWallet(Wallet):

View File

@@ -11,7 +11,12 @@ from secp256k1 import PublicKey
import core.settings as settings import core.settings as settings
from core.base import CheckPayload, MeltPayload, MintPayloads, SplitPayload from core.base import CheckPayload, MeltPayload, MintPayloads, SplitPayload
from core.settings import MINT_PRIVATE_KEY, MINT_SERVER_HOST, MINT_SERVER_PORT from core.settings import (
CASHU_DIR,
MINT_PRIVATE_KEY,
MINT_SERVER_HOST,
MINT_SERVER_PORT,
)
from lightning import WALLET from lightning import WALLET
from mint.ledger import Ledger from mint.ledger import Ledger
from mint.migrations import m001_initial from mint.migrations import m001_initial
@@ -33,7 +38,7 @@ def startup(app: FastAPI):
) )
logger.info(f"Lightning balance: {balance} sat") logger.info(f"Lightning balance: {balance} sat")
logger.info(f"Data dir: {CASHU_DIR}")
logger.info("Mint started.") logger.info("Mint started.")

View File

@@ -1,7 +1,7 @@
import setuptools
from os import path from os import path
import setuptools
this_directory = path.abspath(path.dirname(__file__)) this_directory = path.abspath(path.dirname(__file__))
with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
long_description = f.read() long_description = f.read()

View File

@@ -5,7 +5,8 @@ import base64
import json import json
import math import math
from functools import wraps from functools import wraps
from pathlib import Path from itertools import groupby
from operator import itemgetter
import click import click
from bech32 import bech32_decode, bech32_encode, convertbits from bech32 import bech32_decode, bech32_encode, convertbits
@@ -15,7 +16,7 @@ from core.base import Proof
from core.bolt11 import Invoice from core.bolt11 import Invoice
from core.helpers import fee_reserve from core.helpers import fee_reserve
from core.migrations import migrate_databases from core.migrations import migrate_databases
from core.settings import LIGHTNING, MINT_URL from core.settings import CASHU_DIR, LIGHTNING, MINT_URL
from wallet import migrations from wallet import migrations
from wallet.crud import get_reserved_proofs from wallet.crud import get_reserved_proofs
from wallet.wallet import Wallet as Wallet from wallet.wallet import Wallet as Wallet
@@ -46,9 +47,7 @@ def cli(
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.obj["HOST"] = host ctx.obj["HOST"] = host
ctx.obj["WALLET_NAME"] = walletname ctx.obj["WALLET_NAME"] = walletname
ctx.obj["WALLET"] = Wallet( ctx.obj["WALLET"] = Wallet(ctx.obj["HOST"], f"{CASHU_DIR}/{walletname}", walletname)
ctx.obj["HOST"], f"{str(Path.home())}/.cashu/{walletname}", walletname
)
pass pass
@@ -106,8 +105,8 @@ async def send(ctx, amount: int):
wallet.status() wallet.status()
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount) _, send_proofs = await wallet.split_to_send(wallet.proofs, amount)
await wallet.set_reserved(send_proofs, reserved=True) await wallet.set_reserved(send_proofs, reserved=True)
proofs_serialized = [p.dict() for p in send_proofs] token = await wallet.serialize_proofs(send_proofs)
print(base64.urlsafe_b64encode(json.dumps(proofs_serialized).encode()).decode()) print(token)
wallet.status() wallet.status()
@@ -146,6 +145,25 @@ async def burn(ctx, token: str, all: bool):
wallet.status() 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)
wallet.status()
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)
print(
f"Amount: {sum([p['amount'] for p in grouped_proofs])} sat. ID: {key}"
)
print(token)
@cli.command("pay", help="Pay lightning invoice.") @cli.command("pay", help="Pay lightning invoice.")
@click.argument("invoice", type=str) @click.argument("invoice", type=str)
@click.pass_context @click.pass_context

View File

@@ -1,4 +1,4 @@
import secrets import time
from typing import Optional from typing import Optional
from core.base import Proof from core.base import Proof
@@ -14,14 +14,10 @@ async def store_proof(
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO proofs INSERT INTO proofs
(amount, C, secret) (amount, C, secret, time_created)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?)
""", """,
( (proof.amount, str(proof.C), str(proof.secret), int(time.time())),
proof.amount,
str(proof.C),
str(proof.secret),
),
) )
@@ -69,24 +65,35 @@ async def invalidate_proof(
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO proofs_used INSERT INTO proofs_used
(amount, C, secret) (amount, C, secret, time_used)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?)
""", """,
( (proof.amount, str(proof.C), str(proof.secret), int(time.time())),
proof.amount,
str(proof.C),
str(proof.secret),
),
) )
async def update_proof_reserved( async def update_proof_reserved(
proof: Proof, proof: Proof,
reserved: bool, reserved: bool,
db: Database, send_id: str = None,
db: Database = None,
conn: Optional[Connection] = 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( await (conn or db).execute(
"UPDATE proofs SET reserved = ? WHERE secret = ?", f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
(reserved, str(proof.secret)), (*values, str(proof.secret)),
) )

View File

@@ -68,3 +68,14 @@ async def m002_add_proofs_reserved(db):
""" """
await db.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL") await db.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL")
async def m003_add_proofs_sendid(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")

View File

@@ -1,4 +1,7 @@
import base64
import json
import random import random
import uuid
from typing import List from typing import List
import requests import requests
@@ -186,6 +189,14 @@ class Wallet(LedgerAPI):
raise Exception("could not pay invoice.") raise Exception("could not pay invoice.")
return status["paid"] return status["paid"]
@staticmethod
async def serialize_proofs(proofs: List[Proof]):
proofs_serialized = [p.dict() for p in proofs]
token = base64.urlsafe_b64encode(
json.dumps(proofs_serialized).encode()
).decode()
return token
async def split_to_send(self, proofs: List[Proof], amount): async def split_to_send(self, proofs: List[Proof], amount):
"""Like self.split but only considers non-reserved tokens.""" """Like self.split but only considers non-reserved tokens."""
if len([p for p in proofs if not p.reserved]) <= 0: if len([p for p in proofs if not p.reserved]) <= 0:
@@ -194,9 +205,12 @@ class Wallet(LedgerAPI):
async def set_reserved(self, proofs: List[Proof], reserved: bool): async def set_reserved(self, proofs: List[Proof], reserved: bool):
"""Mark a proof as reserved to avoid reuse or delete marking.""" """Mark a proof as reserved to avoid reuse or delete marking."""
uuid_str = str(uuid.uuid1())
for proof in proofs: for proof in proofs:
proof.reserved = True proof.reserved = True
await update_proof_reserved(proof, reserved=reserved, db=self.db) await update_proof_reserved(
proof, reserved=reserved, send_id=uuid_str, db=self.db
)
async def check_spendable(self, proofs): async def check_spendable(self, proofs):
return await super().check_spendable(proofs) return await super().check_spendable(proofs)