mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
Merge pull request #41 from callebtc/invoices/save_in_db
save lightning invoices
This commit is contained in:
9
.github/codecov.yml
vendored
Normal file
9
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
patch: off
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
# adjust accordingly based on how flaky your tests are
|
||||||
|
# this allows a 10% drop from the previous base commit coverage
|
||||||
|
threshold: 10%
|
||||||
@@ -106,7 +106,7 @@ cashu info
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
```bash
|
```bash
|
||||||
Version: 0.4.0
|
Version: 0.4.1
|
||||||
Debug: False
|
Debug: False
|
||||||
Cashu dir: /home/user/.cashu
|
Cashu dir: /home/user/.cashu
|
||||||
Wallet: wallet
|
Wallet: wallet
|
||||||
|
|||||||
@@ -12,15 +12,6 @@ class P2SHScript(BaseModel):
|
|||||||
signature: str
|
signature: str
|
||||||
address: Union[str, None] = None
|
address: Union[str, None] = None
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row):
|
|
||||||
return cls(
|
|
||||||
address=row[0],
|
|
||||||
script=row[1],
|
|
||||||
signature=row[2],
|
|
||||||
used=row[3],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Proof(BaseModel):
|
class Proof(BaseModel):
|
||||||
id: str = ""
|
id: str = ""
|
||||||
@@ -28,36 +19,10 @@ class Proof(BaseModel):
|
|||||||
secret: str = ""
|
secret: str = ""
|
||||||
C: str = ""
|
C: str = ""
|
||||||
script: Union[P2SHScript, None] = None
|
script: Union[P2SHScript, None] = None
|
||||||
reserved: bool = False # whether this proof is reserved for sending
|
reserved: Union[None, bool] = False # whether this proof is reserved for sending
|
||||||
send_id: str = "" # unique ID of send attempt
|
send_id: Union[None, str] = "" # unique ID of send attempt
|
||||||
time_created: str = ""
|
time_created: Union[None, str] = ""
|
||||||
time_reserved: str = ""
|
time_reserved: Union[None, 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 "",
|
|
||||||
id=row[7] or "",
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, d: dict):
|
|
||||||
assert "amount" in d, "no amount in proof"
|
|
||||||
return cls(
|
|
||||||
amount=d.get("amount"),
|
|
||||||
C=d.get("C"),
|
|
||||||
secret=d.get("secret") or "",
|
|
||||||
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):
|
def to_dict(self):
|
||||||
return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C)
|
return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C)
|
||||||
@@ -81,17 +46,12 @@ class Proofs(BaseModel):
|
|||||||
class Invoice(BaseModel):
|
class Invoice(BaseModel):
|
||||||
amount: int
|
amount: int
|
||||||
pr: str
|
pr: str
|
||||||
hash: str
|
hash: Union[None, str] = None
|
||||||
issued: bool = False
|
preimage: Union[str, None] = None
|
||||||
|
issued: Union[None, bool] = False
|
||||||
@classmethod
|
paid: Union[None, bool] = False
|
||||||
def from_row(cls, row: Row):
|
time_created: Union[None, str, int, float] = ""
|
||||||
return cls(
|
time_paid: Union[None, str, int, float] = ""
|
||||||
amount=int(row[0]),
|
|
||||||
pr=str(row[1]),
|
|
||||||
hash=str(row[2]),
|
|
||||||
issued=bool(row[3]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BlindedMessage(BaseModel):
|
class BlindedMessage(BaseModel):
|
||||||
@@ -104,14 +64,6 @@ class BlindedSignature(BaseModel):
|
|||||||
amount: int
|
amount: int
|
||||||
C_: str
|
C_: str
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, d: dict):
|
|
||||||
return cls(
|
|
||||||
id=d.get("id"),
|
|
||||||
amount=d["amount"],
|
|
||||||
C_=d["C_"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MintRequest(BaseModel):
|
class MintRequest(BaseModel):
|
||||||
blinded_messages: List[BlindedMessage] = []
|
blinded_messages: List[BlindedMessage] = []
|
||||||
@@ -173,16 +125,6 @@ class KeyBase(BaseModel):
|
|||||||
amount: int
|
amount: int
|
||||||
pubkey: str
|
pubkey: str
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row):
|
|
||||||
if row is None:
|
|
||||||
return cls
|
|
||||||
return cls(
|
|
||||||
id=row[0],
|
|
||||||
amount=int(row[1]),
|
|
||||||
pubkey=row[2],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WalletKeyset:
|
class WalletKeyset:
|
||||||
id: str
|
id: str
|
||||||
@@ -213,19 +155,6 @@ class WalletKeyset:
|
|||||||
self.public_keys = pubkeys
|
self.public_keys = pubkeys
|
||||||
self.id = derive_keyset_id(self.public_keys)
|
self.id = derive_keyset_id(self.public_keys)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row):
|
|
||||||
if row is None:
|
|
||||||
return cls
|
|
||||||
return cls(
|
|
||||||
id=row[0],
|
|
||||||
mint_url=row[1],
|
|
||||||
valid_from=row[2],
|
|
||||||
valid_to=row[3],
|
|
||||||
first_seen=row[4],
|
|
||||||
active=row[5],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MintKeyset:
|
class MintKeyset:
|
||||||
id: str
|
id: str
|
||||||
@@ -266,20 +195,6 @@ class MintKeyset:
|
|||||||
self.public_keys = derive_pubkeys(self.private_keys)
|
self.public_keys = derive_pubkeys(self.private_keys)
|
||||||
self.id = derive_keyset_id(self.public_keys)
|
self.id = derive_keyset_id(self.public_keys)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row):
|
|
||||||
if row is None:
|
|
||||||
return cls
|
|
||||||
return cls(
|
|
||||||
id=row[0],
|
|
||||||
derivation_path=row[1],
|
|
||||||
valid_from=row[2],
|
|
||||||
valid_to=row[3],
|
|
||||||
first_seen=row[4],
|
|
||||||
active=row[5],
|
|
||||||
version=row[6],
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_keybase(self):
|
def get_keybase(self):
|
||||||
return {
|
return {
|
||||||
k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex())
|
k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex())
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ if os.path.isfile(ENV_FILE):
|
|||||||
env.read_env(ENV_FILE)
|
env.read_env(ENV_FILE)
|
||||||
else:
|
else:
|
||||||
ENV_FILE = ""
|
ENV_FILE = ""
|
||||||
env.read_env()
|
env.read_env(recurse=False)
|
||||||
|
|
||||||
DEBUG = env.bool("DEBUG", default=False)
|
DEBUG = env.bool("DEBUG", default=False)
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
@@ -48,4 +48,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
|
|||||||
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
|
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
|
||||||
|
|
||||||
MAX_ORDER = 64
|
MAX_ORDER = 64
|
||||||
VERSION = "0.4.0"
|
VERSION = "0.4.1"
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ async def get_lightning_invoice(
|
|||||||
""",
|
""",
|
||||||
(hash,),
|
(hash,),
|
||||||
)
|
)
|
||||||
return Invoice.from_row(row)
|
return Invoice(**row)
|
||||||
|
|
||||||
|
|
||||||
async def update_lightning_invoice(
|
async def update_lightning_invoice(
|
||||||
@@ -204,4 +204,4 @@ async def get_keyset(
|
|||||||
""",
|
""",
|
||||||
tuple(values),
|
tuple(values),
|
||||||
)
|
)
|
||||||
return [MintKeyset.from_row(row) for row in rows]
|
return [MintKeyset(**row) for row in rows]
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import math
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -17,14 +16,16 @@ from os.path import isdir, join
|
|||||||
import click
|
import click
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
import cashu.core.bolt11 as bolt11
|
|
||||||
from cashu.core.base import Proof
|
from cashu.core.base import Proof
|
||||||
from cashu.core.bolt11 import Invoice, decode
|
from cashu.core.helpers import sum_proofs
|
||||||
from cashu.core.helpers import fee_reserve, sum_proofs
|
|
||||||
from cashu.core.migrations import migrate_databases
|
from cashu.core.migrations import migrate_databases
|
||||||
from cashu.core.settings import CASHU_DIR, DEBUG, ENV_FILE, LIGHTNING, MINT_URL, VERSION
|
from cashu.core.settings import CASHU_DIR, DEBUG, ENV_FILE, LIGHTNING, MINT_URL, VERSION
|
||||||
from cashu.wallet import migrations
|
from cashu.wallet import migrations
|
||||||
from cashu.wallet.crud import get_reserved_proofs, get_unused_locks
|
from cashu.wallet.crud import (
|
||||||
|
get_lightning_invoices,
|
||||||
|
get_reserved_proofs,
|
||||||
|
get_unused_locks,
|
||||||
|
)
|
||||||
from cashu.wallet.wallet import Wallet as Wallet
|
from cashu.wallet.wallet import Wallet as Wallet
|
||||||
|
|
||||||
|
|
||||||
@@ -85,14 +86,14 @@ async def invoice(ctx, amount: int, hash: str):
|
|||||||
if not LIGHTNING:
|
if not LIGHTNING:
|
||||||
r = await wallet.mint(amount)
|
r = await wallet.mint(amount)
|
||||||
elif amount and not hash:
|
elif amount and not hash:
|
||||||
r = await wallet.request_mint(amount)
|
invoice = await wallet.request_mint(amount)
|
||||||
if "pr" in r:
|
if invoice.pr:
|
||||||
print(f"Pay invoice to mint {amount} sat:")
|
print(f"Pay invoice to mint {amount} sat:")
|
||||||
print("")
|
print("")
|
||||||
print(f"Invoice: {r['pr']}")
|
print(f"Invoice: {invoice.pr}")
|
||||||
print("")
|
print("")
|
||||||
print(
|
print(
|
||||||
f"Execute this command if you abort the check:\ncashu invoice {amount} --hash {r['hash']}"
|
f"Execute this command if you abort the check:\ncashu invoice {amount} --hash {invoice.hash}"
|
||||||
)
|
)
|
||||||
check_until = time.time() + 5 * 60 # check for five minutes
|
check_until = time.time() + 5 * 60 # check for five minutes
|
||||||
print("")
|
print("")
|
||||||
@@ -105,7 +106,7 @@ async def invoice(ctx, amount: int, hash: str):
|
|||||||
while time.time() < check_until and not paid:
|
while time.time() < check_until and not paid:
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
try:
|
try:
|
||||||
await wallet.mint(amount, r["hash"])
|
await wallet.mint(amount, invoice.hash)
|
||||||
paid = True
|
paid = True
|
||||||
print(" Invoice paid.")
|
print(" Invoice paid.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -130,17 +131,10 @@ async def pay(ctx, invoice: str, yes: bool):
|
|||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
await wallet.load_mint()
|
await wallet.load_mint()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
decoded_invoice: Invoice = bolt11.decode(invoice)
|
amount, fees = await wallet.get_pay_amount_with_fees(invoice)
|
||||||
|
|
||||||
# check if it's an internal payment
|
|
||||||
fees = (await wallet.check_fees(invoice))["fee"]
|
|
||||||
amount = math.ceil(
|
|
||||||
(decoded_invoice.amount_msat + fees * 1000) / 1000
|
|
||||||
) # 1% fee for Lightning
|
|
||||||
|
|
||||||
if not yes:
|
if not yes:
|
||||||
click.confirm(
|
click.confirm(
|
||||||
f"Pay {decoded_invoice.amount_msat//1000} sat ({amount} sat incl. fees)?",
|
f"Pay {amount - fees} sat ({amount} sat incl. fees)?",
|
||||||
abort=True,
|
abort=True,
|
||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
@@ -221,7 +215,7 @@ async def receive(ctx, coin: str, lock: str):
|
|||||||
signature = p2shscripts[0].signature
|
signature = p2shscripts[0].signature
|
||||||
else:
|
else:
|
||||||
script, signature = None, None
|
script, signature = None, None
|
||||||
proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(coin))]
|
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(coin))]
|
||||||
_, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature)
|
_, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature)
|
||||||
wallet.status()
|
wallet.status()
|
||||||
|
|
||||||
@@ -250,9 +244,7 @@ async def burn(ctx, coin: str, all: bool, force: bool):
|
|||||||
proofs = wallet.proofs
|
proofs = wallet.proofs
|
||||||
else:
|
else:
|
||||||
# check only the specified ones
|
# check only the specified ones
|
||||||
proofs = [
|
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(coin))]
|
||||||
Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(coin))
|
|
||||||
]
|
|
||||||
wallet.status()
|
wallet.status()
|
||||||
await wallet.invalidate(proofs)
|
await wallet.invalidate(proofs)
|
||||||
wallet.status()
|
wallet.status()
|
||||||
@@ -330,6 +322,41 @@ async def locks(ctx):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("invoices", help="List of all pending invoices.")
|
||||||
|
@click.pass_context
|
||||||
|
@coro
|
||||||
|
async def invoices(ctx):
|
||||||
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
|
invoices = await get_lightning_invoices(db=wallet.db)
|
||||||
|
if len(invoices):
|
||||||
|
print("")
|
||||||
|
print(f"--------------------------\n")
|
||||||
|
for invoice in invoices:
|
||||||
|
print(f"Paid: {invoice.paid}")
|
||||||
|
print(f"Incoming: {invoice.amount > 0}")
|
||||||
|
print(f"Amount: {abs(invoice.amount)}")
|
||||||
|
if invoice.hash:
|
||||||
|
print(f"Hash: {invoice.hash}")
|
||||||
|
if invoice.preimage:
|
||||||
|
print(f"Preimage: {invoice.preimage}")
|
||||||
|
if invoice.time_created:
|
||||||
|
d = datetime.utcfromtimestamp(
|
||||||
|
int(float(invoice.time_created))
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
print(f"Created: {d}")
|
||||||
|
if invoice.time_paid:
|
||||||
|
d = datetime.utcfromtimestamp(int(float(invoice.time_paid))).strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
print(f"Paid: {d}")
|
||||||
|
print("")
|
||||||
|
print(f"Payment request: {invoice.pr}")
|
||||||
|
print("")
|
||||||
|
print(f"--------------------------\n")
|
||||||
|
else:
|
||||||
|
print("No invoices found.")
|
||||||
|
|
||||||
|
|
||||||
@cli.command("wallets", help="List of all available wallets.")
|
@cli.command("wallets", help="List of all available wallets.")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@coro
|
@coro
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from cashu.core.base import KeyBase, P2SHScript, Proof, WalletKeyset
|
from cashu.core.base import Invoice, KeyBase, P2SHScript, Proof, WalletKeyset
|
||||||
from cashu.core.db import Connection, Database
|
from cashu.core.db import Connection, Database
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ async def get_proofs(
|
|||||||
SELECT * from proofs
|
SELECT * from proofs
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
return [Proof.from_row(r) for r in rows]
|
return [Proof(**dict(r)) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_reserved_proofs(
|
async def get_reserved_proofs(
|
||||||
@@ -45,7 +45,7 @@ async def get_reserved_proofs(
|
|||||||
WHERE reserved
|
WHERE reserved
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
return [Proof.from_row(r) for r in rows]
|
return [Proof(**r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
async def invalidate_proof(
|
async def invalidate_proof(
|
||||||
@@ -162,7 +162,7 @@ async def get_unused_locks(
|
|||||||
""",
|
""",
|
||||||
tuple(args),
|
tuple(args),
|
||||||
)
|
)
|
||||||
return [P2SHScript.from_row(r) for r in rows]
|
return [P2SHScript(**r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
async def update_p2sh_used(
|
async def update_p2sh_used(
|
||||||
@@ -233,4 +233,104 @@ async def get_keyset(
|
|||||||
""",
|
""",
|
||||||
tuple(values),
|
tuple(values),
|
||||||
)
|
)
|
||||||
return WalletKeyset.from_row(row) if row is not None else None
|
return WalletKeyset(**row) if row is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
async def store_lightning_invoice(
|
||||||
|
db: Database,
|
||||||
|
invoice: Invoice,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
await (conn or db).execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO invoices
|
||||||
|
(amount, pr, hash, preimage, paid, time_created, time_paid)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
invoice.amount,
|
||||||
|
invoice.pr,
|
||||||
|
invoice.hash,
|
||||||
|
invoice.preimage,
|
||||||
|
invoice.paid,
|
||||||
|
invoice.time_created,
|
||||||
|
invoice.time_paid,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lightning_invoice(
|
||||||
|
db: Database,
|
||||||
|
hash: str = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
clauses = []
|
||||||
|
values: List[Any] = []
|
||||||
|
if hash:
|
||||||
|
clauses.append("hash = ?")
|
||||||
|
values.append(hash)
|
||||||
|
|
||||||
|
where = ""
|
||||||
|
if clauses:
|
||||||
|
where = f"WHERE {' AND '.join(clauses)}"
|
||||||
|
|
||||||
|
row = await (conn or db).fetchone(
|
||||||
|
f"""
|
||||||
|
SELECT * from invoices
|
||||||
|
{where}
|
||||||
|
""",
|
||||||
|
tuple(values),
|
||||||
|
)
|
||||||
|
return Invoice(**row)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lightning_invoices(
|
||||||
|
db: Database,
|
||||||
|
paid: bool = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
clauses: List[Any] = []
|
||||||
|
values: List[Any] = []
|
||||||
|
|
||||||
|
if paid is not None:
|
||||||
|
clauses.append("paid = ?")
|
||||||
|
values.append(paid)
|
||||||
|
|
||||||
|
where = ""
|
||||||
|
if clauses:
|
||||||
|
where = f"WHERE {' AND '.join(clauses)}"
|
||||||
|
|
||||||
|
rows = await (conn or db).fetchall(
|
||||||
|
f"""
|
||||||
|
SELECT * from invoices
|
||||||
|
{where}
|
||||||
|
""",
|
||||||
|
tuple(values),
|
||||||
|
)
|
||||||
|
return [Invoice(**r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def update_lightning_invoice(
|
||||||
|
db: Database,
|
||||||
|
hash: str,
|
||||||
|
paid: bool,
|
||||||
|
time_paid: int = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
clauses = []
|
||||||
|
values: List[Any] = []
|
||||||
|
clauses.append("paid = ?")
|
||||||
|
values.append(paid)
|
||||||
|
|
||||||
|
if time_paid:
|
||||||
|
clauses.append("time_paid = ?")
|
||||||
|
values.append(time_paid)
|
||||||
|
|
||||||
|
await (conn or db).execute(
|
||||||
|
f"UPDATE invoices SET {', '.join(clauses)} WHERE hash = ?",
|
||||||
|
(
|
||||||
|
*values,
|
||||||
|
hash,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -119,18 +119,28 @@ async def m005_wallet_keysets(db: Database):
|
|||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
# await db.execute(
|
|
||||||
# f"""
|
|
||||||
# CREATE TABLE IF NOT EXISTS mint_pubkeys (
|
|
||||||
# id TEXT NOT NULL,
|
|
||||||
# amount INTEGER NOT NULL,
|
|
||||||
# pubkey TEXT NOT NULL,
|
|
||||||
|
|
||||||
# UNIQUE (id, pubkey)
|
|
||||||
|
|
||||||
# );
|
|
||||||
# """
|
|
||||||
# )
|
|
||||||
|
|
||||||
await db.execute("ALTER TABLE proofs ADD COLUMN id TEXT")
|
await db.execute("ALTER TABLE proofs ADD COLUMN id TEXT")
|
||||||
await db.execute("ALTER TABLE proofs_used ADD COLUMN id TEXT")
|
await db.execute("ALTER TABLE proofs_used ADD COLUMN id TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m006_invoices(db: Database):
|
||||||
|
"""
|
||||||
|
Stores Lightning invoices.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS invoices (
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
pr TEXT NOT NULL,
|
||||||
|
hash TEXT,
|
||||||
|
preimage TEXT,
|
||||||
|
paid BOOL DEFAULT FALSE,
|
||||||
|
time_created TIMESTAMP DEFAULT {db.timestamp_now},
|
||||||
|
time_paid TIMESTAMP DEFAULT {db.timestamp_now},
|
||||||
|
|
||||||
|
UNIQUE (hash)
|
||||||
|
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import secrets as scrts
|
import secrets as scrts
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
@@ -9,11 +11,13 @@ import requests
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
import cashu.core.b_dhke as b_dhke
|
import cashu.core.b_dhke as b_dhke
|
||||||
|
import cashu.core.bolt11 as bolt11
|
||||||
from cashu.core.base import (
|
from cashu.core.base import (
|
||||||
BlindedMessage,
|
BlindedMessage,
|
||||||
BlindedSignature,
|
BlindedSignature,
|
||||||
CheckFeesRequest,
|
CheckFeesRequest,
|
||||||
CheckRequest,
|
CheckRequest,
|
||||||
|
Invoice,
|
||||||
MeltRequest,
|
MeltRequest,
|
||||||
MintRequest,
|
MintRequest,
|
||||||
P2SHScript,
|
P2SHScript,
|
||||||
@@ -21,6 +25,7 @@ from cashu.core.base import (
|
|||||||
SplitRequest,
|
SplitRequest,
|
||||||
WalletKeyset,
|
WalletKeyset,
|
||||||
)
|
)
|
||||||
|
from cashu.core.bolt11 import Invoice as InvoiceBolt11
|
||||||
from cashu.core.db import Database
|
from cashu.core.db import Database
|
||||||
from cashu.core.helpers import sum_proofs
|
from cashu.core.helpers import sum_proofs
|
||||||
from cashu.core.script import (
|
from cashu.core.script import (
|
||||||
@@ -38,8 +43,10 @@ from cashu.wallet.crud import (
|
|||||||
invalidate_proof,
|
invalidate_proof,
|
||||||
secret_used,
|
secret_used,
|
||||||
store_keyset,
|
store_keyset,
|
||||||
|
store_lightning_invoice,
|
||||||
store_p2sh,
|
store_p2sh,
|
||||||
store_proof,
|
store_proof,
|
||||||
|
update_lightning_invoice,
|
||||||
update_proof_reserved,
|
update_proof_reserved,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,7 +182,7 @@ class LedgerAPI:
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return_dict = resp.json()
|
return_dict = resp.json()
|
||||||
self.raise_on_error(return_dict)
|
self.raise_on_error(return_dict)
|
||||||
return return_dict
|
return Invoice(amount=amount, pr=return_dict["pr"], hash=return_dict["hash"])
|
||||||
|
|
||||||
async def mint(self, amounts, payment_hash=None):
|
async def mint(self, amounts, payment_hash=None):
|
||||||
"""Mints new coins and returns a proof of promise."""
|
"""Mints new coins and returns a proof of promise."""
|
||||||
@@ -192,7 +199,7 @@ class LedgerAPI:
|
|||||||
promises_list = resp.json()
|
promises_list = resp.json()
|
||||||
self.raise_on_error(promises_list)
|
self.raise_on_error(promises_list)
|
||||||
|
|
||||||
promises = [BlindedSignature.from_dict(p) for p in promises_list]
|
promises = [BlindedSignature(**p) for p in promises_list]
|
||||||
return self._construct_proofs(promises, secrets, rs)
|
return self._construct_proofs(promises, secrets, rs)
|
||||||
|
|
||||||
async def split(self, proofs, amount, scnd_secret: str = None):
|
async def split(self, proofs, amount, scnd_secret: str = None):
|
||||||
@@ -246,8 +253,8 @@ class LedgerAPI:
|
|||||||
promises_dict = resp.json()
|
promises_dict = resp.json()
|
||||||
self.raise_on_error(promises_dict)
|
self.raise_on_error(promises_dict)
|
||||||
|
|
||||||
promises_fst = [BlindedSignature.from_dict(p) for p in promises_dict["fst"]]
|
promises_fst = [BlindedSignature(**p) for p in promises_dict["fst"]]
|
||||||
promises_snd = [BlindedSignature.from_dict(p) for p in promises_dict["snd"]]
|
promises_snd = [BlindedSignature(**p) for p in promises_dict["snd"]]
|
||||||
# Construct proofs from promises (i.e., unblind signatures)
|
# Construct proofs from promises (i.e., unblind signatures)
|
||||||
frst_proofs = self._construct_proofs(
|
frst_proofs = self._construct_proofs(
|
||||||
promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)]
|
promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)]
|
||||||
@@ -340,7 +347,10 @@ class Wallet(LedgerAPI):
|
|||||||
return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)}
|
return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)}
|
||||||
|
|
||||||
async def request_mint(self, amount):
|
async def request_mint(self, amount):
|
||||||
return super().request_mint(amount)
|
invoice = super().request_mint(amount)
|
||||||
|
invoice.time_created = int(time.time())
|
||||||
|
await store_lightning_invoice(db=self.db, invoice=invoice)
|
||||||
|
return invoice
|
||||||
|
|
||||||
async def mint(self, amount: int, payment_hash: str = None):
|
async def mint(self, amount: int, payment_hash: str = None):
|
||||||
split = amount_split(amount)
|
split = amount_split(amount)
|
||||||
@@ -348,6 +358,10 @@ class Wallet(LedgerAPI):
|
|||||||
if proofs == []:
|
if proofs == []:
|
||||||
raise Exception("received no proofs.")
|
raise Exception("received no proofs.")
|
||||||
await self._store_proofs(proofs)
|
await self._store_proofs(proofs)
|
||||||
|
if payment_hash:
|
||||||
|
await update_lightning_invoice(
|
||||||
|
db=self.db, hash=payment_hash, paid=True, time_paid=int(time.time())
|
||||||
|
)
|
||||||
self.proofs += proofs
|
self.proofs += proofs
|
||||||
return proofs
|
return proofs
|
||||||
|
|
||||||
@@ -389,6 +403,14 @@ class Wallet(LedgerAPI):
|
|||||||
status = await super().pay_lightning(proofs, invoice)
|
status = await super().pay_lightning(proofs, invoice)
|
||||||
if status["paid"] == True:
|
if status["paid"] == True:
|
||||||
await self.invalidate(proofs)
|
await self.invalidate(proofs)
|
||||||
|
invoice_obj = Invoice(
|
||||||
|
amount=-sum_proofs(proofs),
|
||||||
|
pr=invoice,
|
||||||
|
preimage=status.get("preimage"),
|
||||||
|
paid=True,
|
||||||
|
time_paid=time.time(),
|
||||||
|
)
|
||||||
|
await store_lightning_invoice(db=self.db, invoice=invoice_obj)
|
||||||
else:
|
else:
|
||||||
raise Exception("could not pay invoice.")
|
raise Exception("could not pay invoice.")
|
||||||
return status["paid"]
|
return status["paid"]
|
||||||
@@ -417,6 +439,25 @@ class Wallet(LedgerAPI):
|
|||||||
proofs = [p for p in proofs if not p.reserved]
|
proofs = [p for p in proofs if not p.reserved]
|
||||||
return proofs
|
return proofs
|
||||||
|
|
||||||
|
async def get_pay_amount_with_fees(self, invoice: str):
|
||||||
|
"""
|
||||||
|
Decodes the amount from a Lightning invoice and returns the
|
||||||
|
total amount (amount+fees) to be paid.
|
||||||
|
"""
|
||||||
|
decoded_invoice: InvoiceBolt11 = bolt11.decode(invoice)
|
||||||
|
# check if it's an internal payment
|
||||||
|
fees = int((await self.check_fees(invoice))["fee"])
|
||||||
|
amount = math.ceil((decoded_invoice.amount_msat + fees * 1000) / 1000) # 1% fee
|
||||||
|
return amount, fees
|
||||||
|
|
||||||
|
async def split_to_pay(self, invoice: str):
|
||||||
|
"""
|
||||||
|
Splits proofs such that a Lightning invoice can be paid.
|
||||||
|
"""
|
||||||
|
amount, _ = await self.get_pay_amount_with_fees(invoice)
|
||||||
|
_, send_proofs = await self.split_to_send(self.proofs, amount)
|
||||||
|
return send_proofs
|
||||||
|
|
||||||
async def split_to_send(
|
async def split_to_send(
|
||||||
self,
|
self,
|
||||||
proofs: List[Proof],
|
proofs: List[Proof],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "cashu"
|
name = "cashu"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
description = "Ecash wallet and mint."
|
description = "Ecash wallet and mint."
|
||||||
authors = ["calle <callebtc@protonmail.com>"]
|
authors = ["calle <callebtc@protonmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]}
|
|||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="cashu",
|
name="cashu",
|
||||||
version="0.4.0",
|
version="0.4.1",
|
||||||
description="Ecash wallet and mint with Bitcoin Lightning support",
|
description="Ecash wallet and mint with Bitcoin Lightning support",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|||||||
Reference in New Issue
Block a user