mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
pending
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user