mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
Wallet: Lightning interface (#318)
* mint does not start yet * fix import * revert mint db migrations * handle zero fee case * cli: adjust fee message * wallet: replace requests with httpx * clean up * rename http client decorator * fix pending check in main, todo: TEST PROXIES WITH HTTPX * fix up * use httpx for nostr as well * update packages to same versions as https://github.com/lnbits/lnbits/pull/1609/files * fix proof deserialization * check for string * tests passing * adjust wallet api tests * lockfile * add correct responses to Lightning interface and delete melt_id for proofs for which the payent has failed * fix create_invoice checking_id response * migrations atomic * proofs are stored automatically when created * make format * use bolt11 lib * stricter type checking * add fee response to payments * assert fees in test_melt * test that mint_id and melt_id is stored correctly in proofs and proofs_used * remove traces * refactor: Lightning interface into own file and LedgerCrud with typing * fix tests * fix payment response * rename variable
This commit is contained in:
2
Makefile
2
Makefile
@@ -11,7 +11,7 @@ black-check:
|
||||
poetry run black . --check
|
||||
|
||||
mypy:
|
||||
poetry run mypy cashu --ignore-missing
|
||||
poetry run mypy cashu --ignore-missing --check-untyped-defs
|
||||
|
||||
format: black ruff
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ This command will return a Lightning invoice that you need to pay to mint new ec
|
||||
cashu invoice 420
|
||||
```
|
||||
|
||||
The client will check every few seconds if the invoice has been paid. If you abort this step but still pay the invoice, you can use the command `cashu invoice <amount> --hash <hash>`.
|
||||
The client will check every few seconds if the invoice has been paid. If you abort this step but still pay the invoice, you can use the command `cashu invoice <amount> --id <id>`.
|
||||
|
||||
#### Pay a Lightning invoice
|
||||
```bash
|
||||
|
||||
@@ -88,10 +88,16 @@ class Proof(BaseModel):
|
||||
time_created: Union[None, str] = ""
|
||||
time_reserved: Union[None, str] = ""
|
||||
derivation_path: Union[None, str] = "" # derivation path of the proof
|
||||
mint_id: Union[None, str] = (
|
||||
None # holds the id of the mint operation that created this proof
|
||||
)
|
||||
melt_id: Union[None, str] = (
|
||||
None # holds the id of the melt operation that destroyed this proof
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, proof_dict: dict):
|
||||
if proof_dict.get("dleq"):
|
||||
if proof_dict.get("dleq") and isinstance(proof_dict["dleq"], str):
|
||||
proof_dict["dleq"] = DLEQWallet(**json.loads(proof_dict["dleq"]))
|
||||
c = cls(**proof_dict)
|
||||
return c
|
||||
@@ -181,8 +187,9 @@ class BlindedMessages(BaseModel):
|
||||
|
||||
class Invoice(BaseModel):
|
||||
amount: int
|
||||
pr: str
|
||||
hash: str
|
||||
bolt11: str
|
||||
id: str
|
||||
out: Union[None, bool] = None
|
||||
payment_hash: Union[None, str] = None
|
||||
preimage: Union[str, None] = None
|
||||
issued: Union[None, bool] = False
|
||||
|
||||
@@ -1,369 +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
|
||||
@@ -3,7 +3,7 @@ import datetime
|
||||
import os
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy_aio.base import AsyncConnection
|
||||
@@ -118,7 +118,7 @@ class Database(Compat):
|
||||
(1082, 1083, 1266),
|
||||
"DATE2INT",
|
||||
lambda value, curs: (
|
||||
time.mktime(value.timetuple()) if value is not None else None
|
||||
time.mktime(value.timetuple()) if value is not None else None # type: ignore
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -189,7 +189,7 @@ class Database(Compat):
|
||||
|
||||
|
||||
# public functions for LNbits to use (we don't want to change the Database or Compat classes above)
|
||||
def table_with_schema(db: Database, table: str):
|
||||
def table_with_schema(db: Union[Database, Connection], table: str):
|
||||
return f"{db.references_schema if db.schema else ''}{table}"
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import math
|
||||
from functools import partial, wraps
|
||||
from typing import List
|
||||
|
||||
from ..core.base import Proof
|
||||
from ..core.base import BlindedSignature, Proof
|
||||
from ..core.settings import settings
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ def sum_proofs(proofs: List[Proof]):
|
||||
return sum([p.amount for p in proofs])
|
||||
|
||||
|
||||
def sum_promises(promises: List[BlindedSignature]):
|
||||
return sum([p.amount for p in promises])
|
||||
|
||||
|
||||
def async_wrap(func):
|
||||
@wraps(func)
|
||||
async def run(*args, loop=None, executor=None, **kwargs):
|
||||
|
||||
@@ -18,9 +18,9 @@ def hash_to_point_pre_0_3_3(secret_msg):
|
||||
_hash = hashlib.sha256(msg).hexdigest().encode("utf-8") # type: ignore
|
||||
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)
|
||||
_hash_list = list(_hash[:33]) # take the 33 bytes and get a list of bytes
|
||||
_hash_list[0] = 0x02 # set first byte to represent even y coord
|
||||
_hash = bytes(_hash_list)
|
||||
point = PublicKey(_hash, raw=True)
|
||||
except Exception:
|
||||
msg = _hash
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
def amount_split(amount: int):
|
||||
from typing import List
|
||||
|
||||
|
||||
def amount_split(amount: int) -> List[int]:
|
||||
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
|
||||
bits_amt = bin(amount)[::-1][:-2]
|
||||
rv = []
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Coroutine, NamedTuple, Optional
|
||||
from typing import Coroutine, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class StatusResponse(NamedTuple):
|
||||
class StatusResponse(BaseModel):
|
||||
error_message: Optional[str]
|
||||
balance_msat: int
|
||||
|
||||
|
||||
class InvoiceResponse(NamedTuple):
|
||||
ok: bool
|
||||
checking_id: Optional[str] = None # payment_hash, rpc_id
|
||||
class InvoiceResponse(BaseModel):
|
||||
ok: bool # True: invoice created, False: failed
|
||||
checking_id: Optional[str] = None
|
||||
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
|
||||
class PaymentResponse(BaseModel):
|
||||
ok: Optional[bool] = None # True: paid, False: failed, None: pending or unknown
|
||||
checking_id: Optional[str] = None
|
||||
fee_msat: Optional[int] = None
|
||||
preimage: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class PaymentStatus(NamedTuple):
|
||||
class PaymentStatus(BaseModel):
|
||||
paid: Optional[bool] = None
|
||||
fee_msat: Optional[int] = None
|
||||
preimage: Optional[str] = None
|
||||
|
||||
@@ -2,9 +2,18 @@ import asyncio
|
||||
import hashlib
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import AsyncGenerator, Dict, Optional, Set
|
||||
from os import urandom
|
||||
from typing import AsyncGenerator, Optional, Set
|
||||
|
||||
from bolt11 import (
|
||||
Bolt11,
|
||||
MilliSatoshi,
|
||||
TagChar,
|
||||
Tags,
|
||||
decode,
|
||||
encode,
|
||||
)
|
||||
|
||||
from ..core.bolt11 import Invoice, decode, encode
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentResponse,
|
||||
@@ -14,6 +23,8 @@ from .base import (
|
||||
)
|
||||
|
||||
BRR = True
|
||||
DELAY_PAYMENT = False
|
||||
STOCHASTIC_INVOICE = False
|
||||
|
||||
|
||||
class FakeWallet(Wallet):
|
||||
@@ -31,7 +42,7 @@ class FakeWallet(Wallet):
|
||||
).hex()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
return StatusResponse(None, 1337)
|
||||
return StatusResponse(error_message=None, balance_msat=1337)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
@@ -39,65 +50,80 @@ class FakeWallet(Wallet):
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
expiry: Optional[int] = None,
|
||||
payment_secret: Optional[bytes] = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
data: Dict = {
|
||||
"out": False,
|
||||
"amount": amount * 1000,
|
||||
"currency": "bc",
|
||||
"privkey": self.privkey,
|
||||
"memo": memo,
|
||||
"description_hash": b"",
|
||||
"description": "",
|
||||
"fallback": None,
|
||||
"expires": kwargs.get("expiry"),
|
||||
"timestamp": datetime.now().timestamp(),
|
||||
"route": None,
|
||||
"tags_set": [],
|
||||
}
|
||||
tags = Tags()
|
||||
|
||||
if description_hash:
|
||||
data["tags_set"] = ["h"]
|
||||
data["description_hash"] = description_hash
|
||||
tags.add(TagChar.description_hash, description_hash.hex())
|
||||
elif unhashed_description:
|
||||
data["tags_set"] = ["d"]
|
||||
data["description_hash"] = hashlib.sha256(unhashed_description).digest()
|
||||
tags.add(
|
||||
TagChar.description_hash,
|
||||
hashlib.sha256(unhashed_description).hexdigest(),
|
||||
)
|
||||
else:
|
||||
data["tags_set"] = ["d"]
|
||||
data["memo"] = memo
|
||||
data["description"] = memo
|
||||
randomHash = (
|
||||
tags.add(TagChar.description, memo or "")
|
||||
|
||||
if expiry:
|
||||
tags.add(TagChar.expire_time, expiry)
|
||||
|
||||
# random hash
|
||||
checking_id = (
|
||||
self.privkey[:6]
|
||||
+ hashlib.sha256(str(random.getrandbits(256)).encode()).hexdigest()[6:]
|
||||
)
|
||||
data["paymenthash"] = randomHash
|
||||
payment_request = encode(data)
|
||||
checking_id = randomHash
|
||||
|
||||
return InvoiceResponse(True, checking_id, payment_request)
|
||||
tags.add(TagChar.payment_hash, checking_id)
|
||||
|
||||
if payment_secret:
|
||||
secret = payment_secret.hex()
|
||||
else:
|
||||
secret = urandom(32).hex()
|
||||
tags.add(TagChar.payment_secret, secret)
|
||||
|
||||
bolt11 = Bolt11(
|
||||
currency="bc",
|
||||
amount_msat=MilliSatoshi(amount * 1000),
|
||||
date=int(datetime.now().timestamp()),
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
payment_request = encode(bolt11, self.privkey)
|
||||
|
||||
return InvoiceResponse(
|
||||
ok=True, checking_id=checking_id, payment_request=payment_request
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
invoice = decode(bolt11)
|
||||
# await asyncio.sleep(5)
|
||||
|
||||
if DELAY_PAYMENT:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
if invoice.payment_hash[:6] == self.privkey[:6] or BRR:
|
||||
await self.queue.put(invoice)
|
||||
self.paid_invoices.add(invoice.payment_hash)
|
||||
return PaymentResponse(True, invoice.payment_hash, 0)
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=invoice.payment_hash, fee_msat=0
|
||||
)
|
||||
else:
|
||||
return PaymentResponse(
|
||||
ok=False, error_message="Only internal invoices can be used!"
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
# paid = random.random() > 0.7
|
||||
# return PaymentStatus(paid)
|
||||
if STOCHASTIC_INVOICE:
|
||||
paid = random.random() > 0.7
|
||||
return PaymentStatus(paid=paid)
|
||||
paid = checking_id in self.paid_invoices or BRR
|
||||
return PaymentStatus(paid or None)
|
||||
return PaymentStatus(paid=paid or None)
|
||||
|
||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
||||
return PaymentStatus(None)
|
||||
return PaymentStatus(paid=None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
value: Invoice = await self.queue.get()
|
||||
value: Bolt11 = await self.queue.get()
|
||||
yield value.payment_hash
|
||||
|
||||
@@ -8,44 +8,155 @@ class LedgerCrud:
|
||||
"""
|
||||
Database interface for Cashu mint.
|
||||
|
||||
This class needs to be overloaded by any app that imports the Cashu mint.
|
||||
This class needs to be overloaded by any app that imports the Cashu mint and wants
|
||||
to use their own database.
|
||||
"""
|
||||
|
||||
async def get_keyset(*args, **kwags):
|
||||
return await get_keyset(*args, **kwags) # type: ignore
|
||||
async def get_keyset(
|
||||
self,
|
||||
db: Database,
|
||||
id: str = "",
|
||||
derivation_path: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await get_keyset(
|
||||
db=db,
|
||||
id=id,
|
||||
derivation_path=derivation_path,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def get_lightning_invoice(*args, **kwags):
|
||||
return await get_lightning_invoice(*args, **kwags) # type: ignore
|
||||
async def get_lightning_invoice(
|
||||
self,
|
||||
db: Database,
|
||||
id: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await get_lightning_invoice(
|
||||
db=db,
|
||||
id=id,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def get_secrets_used(*args, **kwags):
|
||||
return await get_secrets_used(*args, **kwags) # type: ignore
|
||||
async def get_secrets_used(
|
||||
self,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await get_secrets_used(db=db, conn=conn)
|
||||
|
||||
async def invalidate_proof(*args, **kwags):
|
||||
return await invalidate_proof(*args, **kwags) # type: ignore
|
||||
async def invalidate_proof(
|
||||
self,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await invalidate_proof(
|
||||
db=db,
|
||||
proof=proof,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def get_proofs_pending(*args, **kwags):
|
||||
return await get_proofs_pending(*args, **kwags) # type: ignore
|
||||
async def get_proofs_pending(
|
||||
self,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await get_proofs_pending(db=db, conn=conn)
|
||||
|
||||
async def set_proof_pending(*args, **kwags):
|
||||
return await set_proof_pending(*args, **kwags) # type: ignore
|
||||
async def set_proof_pending(
|
||||
self,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await set_proof_pending(
|
||||
db=db,
|
||||
proof=proof,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def unset_proof_pending(*args, **kwags):
|
||||
return await unset_proof_pending(*args, **kwags) # type: ignore
|
||||
async def unset_proof_pending(
|
||||
self, proof: Proof, db: Database, conn: Optional[Connection] = None
|
||||
):
|
||||
return await unset_proof_pending(
|
||||
proof=proof,
|
||||
db=db,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def store_keyset(*args, **kwags):
|
||||
return await store_keyset(*args, **kwags) # type: ignore
|
||||
async def store_keyset(
|
||||
self,
|
||||
db: Database,
|
||||
keyset: MintKeyset,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await store_keyset(
|
||||
db=db,
|
||||
keyset=keyset,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def store_lightning_invoice(*args, **kwags):
|
||||
return await store_lightning_invoice(*args, **kwags) # type: ignore
|
||||
async def store_lightning_invoice(
|
||||
self,
|
||||
db: Database,
|
||||
invoice: Invoice,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await store_lightning_invoice(
|
||||
db=db,
|
||||
invoice=invoice,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def store_promise(*args, **kwags):
|
||||
return await store_promise(*args, **kwags) # type: ignore
|
||||
async def store_promise(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
amount: int,
|
||||
B_: str,
|
||||
C_: str,
|
||||
id: str,
|
||||
e: str = "",
|
||||
s: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await store_promise(
|
||||
db=db,
|
||||
amount=amount,
|
||||
B_=B_,
|
||||
C_=C_,
|
||||
id=id,
|
||||
e=e,
|
||||
s=s,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def get_promise(*args, **kwags):
|
||||
return await get_promise(*args, **kwags) # type: ignore
|
||||
async def get_promise(
|
||||
self,
|
||||
db: Database,
|
||||
B_: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await get_promise(
|
||||
db=db,
|
||||
B_=B_,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def update_lightning_invoice(*args, **kwags):
|
||||
return await update_lightning_invoice(*args, **kwags) # type: ignore
|
||||
async def update_lightning_invoice(
|
||||
self,
|
||||
db: Database,
|
||||
id: str,
|
||||
issued: bool,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await update_lightning_invoice(
|
||||
db=db,
|
||||
id=id,
|
||||
issued=issued,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
|
||||
async def store_promise(
|
||||
@@ -174,46 +285,47 @@ async def store_lightning_invoice(
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'invoices')}
|
||||
(amount, pr, hash, issued, payment_hash)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
(amount, bolt11, id, issued, payment_hash, out)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
invoice.amount,
|
||||
invoice.pr,
|
||||
invoice.hash,
|
||||
invoice.bolt11,
|
||||
invoice.id,
|
||||
invoice.issued,
|
||||
invoice.payment_hash,
|
||||
invoice.out,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_lightning_invoice(
|
||||
db: Database,
|
||||
hash: str,
|
||||
id: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'invoices')}
|
||||
WHERE hash = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(hash,),
|
||||
(id,),
|
||||
)
|
||||
|
||||
return Invoice(**row) if row else None
|
||||
row_dict = dict(row)
|
||||
return Invoice(**row_dict) if row_dict else None
|
||||
|
||||
|
||||
async def update_lightning_invoice(
|
||||
db: Database,
|
||||
hash: str,
|
||||
id: str,
|
||||
issued: bool,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
await (conn or db).execute(
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET issued = ? WHERE hash = ?",
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET issued = ? WHERE id = ?",
|
||||
(
|
||||
issued,
|
||||
hash,
|
||||
id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import asyncio
|
||||
import math
|
||||
from typing import Dict, List, Literal, Optional, Set, Tuple, Union
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
import bolt11
|
||||
from loguru import logger
|
||||
|
||||
from ..core import bolt11
|
||||
from ..core.base import (
|
||||
DLEQ,
|
||||
BlindedMessage,
|
||||
@@ -19,7 +19,6 @@ from ..core.crypto.keys import derive_pubkey, random_hash
|
||||
from ..core.crypto.secp import PublicKey
|
||||
from ..core.db import Connection, Database
|
||||
from ..core.errors import (
|
||||
InvoiceNotPaidError,
|
||||
KeysetError,
|
||||
KeysetNotFoundError,
|
||||
LightningError,
|
||||
@@ -29,13 +28,14 @@ from ..core.errors import (
|
||||
from ..core.helpers import fee_reserve, sum_proofs
|
||||
from ..core.settings import settings
|
||||
from ..core.split import amount_split
|
||||
from ..lightning.base import Wallet
|
||||
from ..lightning.base import PaymentResponse, Wallet
|
||||
from ..mint.crud import LedgerCrud
|
||||
from .conditions import LedgerSpendingConditions
|
||||
from .lightning import LedgerLightning
|
||||
from .verification import LedgerVerification
|
||||
|
||||
|
||||
class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerLightning):
|
||||
locks: Dict[str, asyncio.Lock] = {} # holds multiprocessing locks
|
||||
proofs_pending_lock: asyncio.Lock = (
|
||||
asyncio.Lock()
|
||||
@@ -46,8 +46,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
db: Database,
|
||||
seed: str,
|
||||
lightning: Wallet,
|
||||
crud: LedgerCrud,
|
||||
derivation_path="",
|
||||
crud=LedgerCrud,
|
||||
):
|
||||
self.secrets_used: Set[str] = set()
|
||||
self.master_key = seed
|
||||
@@ -146,113 +146,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
assert keyset.public_keys, KeysetError("no public keys for this keyset")
|
||||
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}
|
||||
|
||||
# ------- LIGHTNING -------
|
||||
|
||||
async def _request_lightning_invoice(self, amount: int) -> Tuple[str, str]:
|
||||
"""Generate a Lightning invoice using the funding source backend.
|
||||
|
||||
Args:
|
||||
amount (int): Amount of invoice (in Satoshis)
|
||||
|
||||
Raises:
|
||||
Exception: Error with funding source.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: Bolt11 invoice and payment hash (for lookup)
|
||||
"""
|
||||
logger.trace(
|
||||
"_request_lightning_invoice: Requesting Lightning invoice for"
|
||||
f" {amount} satoshis."
|
||||
)
|
||||
error, balance = await self.lightning.status()
|
||||
logger.trace(f"_request_lightning_invoice: Lightning wallet balance: {balance}")
|
||||
if error:
|
||||
raise LightningError(f"Lightning wallet not responding: {error}")
|
||||
(
|
||||
ok,
|
||||
checking_id,
|
||||
payment_request,
|
||||
error_message,
|
||||
) = await self.lightning.create_invoice(amount, "Cashu deposit")
|
||||
logger.trace(
|
||||
f"_request_lightning_invoice: Lightning invoice: {payment_request}"
|
||||
)
|
||||
|
||||
if not ok:
|
||||
raise LightningError(f"Lightning wallet error: {error_message}")
|
||||
assert payment_request and checking_id, LightningError(
|
||||
"could not fetch invoice from Lightning backend"
|
||||
)
|
||||
return payment_request, checking_id
|
||||
|
||||
async def _check_lightning_invoice(
|
||||
self, amount: int, hash: str, conn: Optional[Connection] = None
|
||||
) -> Literal[True]:
|
||||
"""Checks with the Lightning backend whether an invoice stored with `hash` was paid.
|
||||
|
||||
Args:
|
||||
amount (int): Amount of the outputs the wallet wants in return (in Satoshis).
|
||||
hash (str): Hash to look up Lightning invoice by.
|
||||
|
||||
Raises:
|
||||
Exception: Invoice not found.
|
||||
Exception: Tokens for invoice already issued.
|
||||
Exception: Amount larger than invoice amount.
|
||||
Exception: Invoice not paid yet
|
||||
e: Update database and pass through error.
|
||||
|
||||
Returns:
|
||||
bool: True if invoice has been paid, else False
|
||||
"""
|
||||
invoice: Union[Invoice, None] = await self.crud.get_lightning_invoice(
|
||||
hash=hash, db=self.db, conn=conn
|
||||
)
|
||||
if invoice is None:
|
||||
raise LightningError("invoice not found.")
|
||||
if invoice.issued:
|
||||
raise LightningError("tokens already issued for this invoice.")
|
||||
if amount > invoice.amount:
|
||||
raise LightningError(
|
||||
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
|
||||
)
|
||||
assert invoice.payment_hash, "invoice has no payment hash."
|
||||
status = await self.lightning.get_invoice_status(invoice.payment_hash)
|
||||
logger.trace(
|
||||
f"_check_lightning_invoice: invoice {invoice.payment_hash} status: {status}"
|
||||
)
|
||||
if not status.paid:
|
||||
raise InvoiceNotPaidError()
|
||||
|
||||
return status.paid
|
||||
|
||||
async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int):
|
||||
"""Pays a Lightning invoice via the funding source backend.
|
||||
|
||||
Args:
|
||||
invoice (str): Bolt11 Lightning invoice
|
||||
fee_limit_msat (int): Maximum fee reserve for payment (in Millisatoshi)
|
||||
|
||||
Raises:
|
||||
Exception: Funding source error.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, string, int]: Returns payment status, preimage of invoice, paid fees (in Millisatoshi)
|
||||
"""
|
||||
error, balance = await self.lightning.status()
|
||||
if error:
|
||||
raise LightningError(f"Lightning wallet not responding: {error}")
|
||||
(
|
||||
ok,
|
||||
checking_id,
|
||||
fee_msat,
|
||||
preimage,
|
||||
error_message,
|
||||
) = await self.lightning.pay_invoice(invoice, fee_limit_msat=fee_limit_msat)
|
||||
logger.trace(f"_pay_lightning_invoice: Lightning payment status: {ok}")
|
||||
# make sure that fee is positive
|
||||
fee_msat = abs(fee_msat) if fee_msat else fee_msat
|
||||
return ok, preimage, fee_msat
|
||||
|
||||
# ------- ECASH -------
|
||||
|
||||
async def _invalidate_proofs(self, proofs: List[Proof]) -> None:
|
||||
@@ -343,7 +236,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
Exception: Invoice creation failed.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: Bolt11 invoice and a hash (for looking it up later)
|
||||
Tuple[str, str]: Bolt11 invoice and a id (for looking it up later)
|
||||
"""
|
||||
logger.trace("called request_mint")
|
||||
if settings.mint_max_peg_in and amount > settings.mint_max_peg_in:
|
||||
@@ -354,40 +247,43 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
raise NotAllowedError("Mint does not allow minting new tokens.")
|
||||
|
||||
logger.trace(f"requesting invoice for {amount} satoshis")
|
||||
payment_request, payment_hash = await self._request_lightning_invoice(amount)
|
||||
logger.trace(f"got invoice {payment_request} with hash {payment_hash}")
|
||||
assert payment_request and payment_hash, LightningError(
|
||||
"could not fetch invoice from Lightning backend"
|
||||
invoice_response = await self._request_lightning_invoice(amount)
|
||||
logger.trace(
|
||||
f"got invoice {invoice_response.payment_request} with check id"
|
||||
f" {invoice_response.checking_id}"
|
||||
)
|
||||
assert (
|
||||
invoice_response.payment_request and invoice_response.checking_id
|
||||
), LightningError("could not fetch invoice from Lightning backend")
|
||||
|
||||
invoice = Invoice(
|
||||
amount=amount,
|
||||
hash=random_hash(),
|
||||
pr=payment_request,
|
||||
payment_hash=payment_hash, # what we got from the backend
|
||||
id=random_hash(),
|
||||
bolt11=invoice_response.payment_request,
|
||||
payment_hash=invoice_response.checking_id, # what we got from the backend
|
||||
issued=False,
|
||||
)
|
||||
logger.trace(f"crud: storing invoice {invoice.hash} in db")
|
||||
logger.trace(f"crud: storing invoice {invoice.id} in db")
|
||||
await self.crud.store_lightning_invoice(invoice=invoice, db=self.db)
|
||||
logger.trace(f"crud: stored invoice {invoice.hash} in db")
|
||||
return payment_request, invoice.hash
|
||||
logger.trace(f"crud: stored invoice {invoice.id} in db")
|
||||
return invoice_response.payment_request, invoice.id
|
||||
|
||||
async def mint(
|
||||
self,
|
||||
B_s: List[BlindedMessage],
|
||||
hash: Optional[str] = None,
|
||||
id: Optional[str] = None,
|
||||
keyset: Optional[MintKeyset] = None,
|
||||
) -> List[BlindedSignature]:
|
||||
"""Mints a promise for coins for B_.
|
||||
|
||||
Args:
|
||||
B_s (List[BlindedMessage]): Outputs (blinded messages) to sign.
|
||||
hash (Optional[str], optional): Hash of (paid) Lightning invoice. Defaults to None.
|
||||
id (Optional[str], optional): Id of (paid) Lightning invoice. Defaults to None.
|
||||
keyset (Optional[MintKeyset], optional): Keyset to use. If not provided, uses active keyset. Defaults to None.
|
||||
|
||||
Raises:
|
||||
Exception: Lightning invvoice is not paid.
|
||||
Exception: Lightning is turned on but no payment hash is provided.
|
||||
Exception: Lightning invoice is not paid.
|
||||
Exception: Lightning is turned on but no id is provided.
|
||||
Exception: Something went wrong with the invoice check.
|
||||
Exception: Amount too large.
|
||||
|
||||
@@ -398,21 +294,19 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
amount_outputs = sum([b.amount for b in B_s])
|
||||
|
||||
if settings.lightning:
|
||||
if not hash:
|
||||
raise NotAllowedError("no hash provided.")
|
||||
self.locks[hash] = (
|
||||
self.locks.get(hash) or asyncio.Lock()
|
||||
if not id:
|
||||
raise NotAllowedError("no id provided.")
|
||||
self.locks[id] = (
|
||||
self.locks.get(id) or asyncio.Lock()
|
||||
) # create a new lock if it doesn't exist
|
||||
async with self.locks[hash]:
|
||||
async with self.locks[id]:
|
||||
# will raise an exception if the invoice is not paid or tokens are
|
||||
# already issued or the requested amount is too high
|
||||
await self._check_lightning_invoice(amount_outputs, hash)
|
||||
await self._check_lightning_invoice(amount=amount_outputs, id=id)
|
||||
|
||||
logger.trace(f"crud: setting invoice {hash} as issued")
|
||||
await self.crud.update_lightning_invoice(
|
||||
hash=hash, issued=True, db=self.db
|
||||
)
|
||||
del self.locks[hash]
|
||||
logger.trace(f"crud: setting invoice {id} as issued")
|
||||
await self.crud.update_lightning_invoice(id=id, issued=True, db=self.db)
|
||||
del self.locks[id]
|
||||
|
||||
self._verify_outputs(B_s)
|
||||
|
||||
@@ -439,6 +333,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
|
||||
logger.trace("melt called")
|
||||
|
||||
# set proofs to pending to avoid race conditions
|
||||
await self._set_proofs_pending(proofs)
|
||||
|
||||
try:
|
||||
@@ -465,20 +360,19 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
|
||||
if settings.lightning:
|
||||
logger.trace(f"paying lightning invoice {invoice}")
|
||||
status, preimage, paid_fee_msat = await self._pay_lightning_invoice(
|
||||
payment = await self._pay_lightning_invoice(
|
||||
invoice, reserve_fees_sat * 1000
|
||||
)
|
||||
preimage = preimage or ""
|
||||
logger.trace("paid lightning invoice")
|
||||
else:
|
||||
status, preimage, paid_fee_msat = True, "preimage", 0
|
||||
payment = PaymentResponse(ok=True, preimage="preimage", fee_msat=0)
|
||||
|
||||
logger.debug(
|
||||
f"Melt status: {status}: preimage: {preimage}, fee_msat:"
|
||||
f" {paid_fee_msat}"
|
||||
f"Melt status: {payment.ok}: preimage: {payment.preimage}, fee_msat:"
|
||||
f" {payment.fee_msat}"
|
||||
)
|
||||
|
||||
if not status:
|
||||
if not payment.ok:
|
||||
raise LightningError("Lightning payment unsuccessful.")
|
||||
|
||||
# melt successful, invalidate proofs
|
||||
@@ -486,11 +380,11 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
|
||||
# prepare change to compensate wallet for overpaid fees
|
||||
return_promises: List[BlindedSignature] = []
|
||||
if outputs and paid_fee_msat is not None:
|
||||
if outputs and payment.fee_msat is not None:
|
||||
return_promises = await self._generate_change_promises(
|
||||
total_provided=total_provided,
|
||||
invoice_amount=invoice_amount,
|
||||
ln_fee_msat=paid_fee_msat,
|
||||
ln_fee_msat=payment.fee_msat,
|
||||
outputs=outputs,
|
||||
)
|
||||
|
||||
@@ -501,7 +395,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
# delete proofs from pending list
|
||||
await self._unset_proofs_pending(proofs)
|
||||
|
||||
return status, preimage, return_promises
|
||||
return payment.ok, payment.preimage or "", return_promises
|
||||
|
||||
async def get_melt_fees(self, pr: str) -> int:
|
||||
"""Returns the fee reserve (in sat) that a wallet must add to its proofs
|
||||
@@ -515,19 +409,24 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
"""
|
||||
# hack: check if it's internal, if it exists, it will return paid = False,
|
||||
# if id does not exist (not internal), it returns paid = None
|
||||
amount_msat = 0
|
||||
if settings.lightning:
|
||||
decoded_invoice = bolt11.decode(pr)
|
||||
amount_msat = decoded_invoice.amount_msat
|
||||
assert decoded_invoice.amount_msat, "invoice has no amount."
|
||||
amount_msat = int(decoded_invoice.amount_msat)
|
||||
logger.trace(
|
||||
"get_melt_fees: checking lightning invoice:"
|
||||
f" {decoded_invoice.payment_hash}"
|
||||
)
|
||||
paid = await self.lightning.get_invoice_status(decoded_invoice.payment_hash)
|
||||
logger.trace(f"get_melt_fees: paid: {paid}")
|
||||
internal = paid.paid is False
|
||||
payment = await self.lightning.get_invoice_status(
|
||||
decoded_invoice.payment_hash
|
||||
)
|
||||
logger.trace(f"get_melt_fees: paid: {payment.paid}")
|
||||
internal = payment.paid is False
|
||||
else:
|
||||
amount_msat = 0
|
||||
internal = True
|
||||
|
||||
fees_msat = fee_reserve(amount_msat, internal)
|
||||
fee_sat = math.ceil(fees_msat / 1000)
|
||||
return fee_sat
|
||||
@@ -732,7 +631,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
Raises:
|
||||
Exception: At least one proof already in pending table.
|
||||
"""
|
||||
# first we check whether these proofs are pending aready
|
||||
# first we check whether these proofs are pending already
|
||||
async with self.proofs_pending_lock:
|
||||
await self._validate_proofs_pending(proofs, conn)
|
||||
for p in proofs:
|
||||
|
||||
137
cashu/mint/lightning.py
Normal file
137
cashu/mint/lightning.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import (
|
||||
Invoice,
|
||||
)
|
||||
from ..core.db import Connection, Database
|
||||
from ..core.errors import (
|
||||
InvoiceNotPaidError,
|
||||
LightningError,
|
||||
)
|
||||
from ..lightning.base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
from ..mint.crud import LedgerCrud
|
||||
from .protocols import SupportLightning, SupportsDb
|
||||
|
||||
|
||||
class LedgerLightning(SupportLightning, SupportsDb):
|
||||
"""Lightning functions for the ledger."""
|
||||
|
||||
lightning: Wallet
|
||||
crud: LedgerCrud
|
||||
db: Database
|
||||
|
||||
async def _request_lightning_invoice(self, amount: int) -> InvoiceResponse:
|
||||
"""Generate a Lightning invoice using the funding source backend.
|
||||
|
||||
Args:
|
||||
amount (int): Amount of invoice (in Satoshis)
|
||||
|
||||
Raises:
|
||||
Exception: Error with funding source.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: Bolt11 invoice and payment id (for lookup)
|
||||
"""
|
||||
logger.trace(
|
||||
"_request_lightning_invoice: Requesting Lightning invoice for"
|
||||
f" {amount} satoshis."
|
||||
)
|
||||
status = await self.lightning.status()
|
||||
logger.trace(
|
||||
"_request_lightning_invoice: Lightning wallet balance:"
|
||||
f" {status.balance_msat}"
|
||||
)
|
||||
if status.error_message:
|
||||
raise LightningError(
|
||||
f"Lightning wallet not responding: {status.error_message}"
|
||||
)
|
||||
payment = await self.lightning.create_invoice(amount, "Cashu deposit")
|
||||
logger.trace(
|
||||
f"_request_lightning_invoice: Lightning invoice: {payment.payment_request}"
|
||||
)
|
||||
|
||||
if not payment.ok:
|
||||
raise LightningError(f"Lightning wallet error: {payment.error_message}")
|
||||
assert payment.payment_request and payment.checking_id, LightningError(
|
||||
"could not fetch invoice from Lightning backend"
|
||||
)
|
||||
return payment
|
||||
|
||||
async def _check_lightning_invoice(
|
||||
self, *, amount: int, id: str, conn: Optional[Connection] = None
|
||||
) -> PaymentStatus:
|
||||
"""Checks with the Lightning backend whether an invoice with `id` was paid.
|
||||
|
||||
Args:
|
||||
amount (int): Amount of the outputs the wallet wants in return (in Satoshis).
|
||||
id (str): Id to look up Lightning invoice by.
|
||||
|
||||
Raises:
|
||||
Exception: Invoice not found.
|
||||
Exception: Tokens for invoice already issued.
|
||||
Exception: Amount larger than invoice amount.
|
||||
Exception: Invoice not paid yet
|
||||
e: Update database and pass through error.
|
||||
|
||||
Returns:
|
||||
bool: True if invoice has been paid, else False
|
||||
"""
|
||||
invoice: Union[Invoice, None] = await self.crud.get_lightning_invoice(
|
||||
id=id, db=self.db, conn=conn
|
||||
)
|
||||
if invoice is None:
|
||||
raise LightningError("invoice not found.")
|
||||
if invoice.issued:
|
||||
raise LightningError("tokens already issued for this invoice.")
|
||||
if amount > invoice.amount:
|
||||
raise LightningError(
|
||||
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
|
||||
)
|
||||
assert invoice.payment_hash, "invoice has no payment hash."
|
||||
# set this invoice as issued
|
||||
await self.crud.update_lightning_invoice(
|
||||
id=id, issued=True, db=self.db, conn=conn
|
||||
)
|
||||
|
||||
try:
|
||||
status = await self.lightning.get_invoice_status(invoice.payment_hash)
|
||||
if status.paid:
|
||||
return status
|
||||
else:
|
||||
raise InvoiceNotPaidError()
|
||||
except Exception as e:
|
||||
# unset issued
|
||||
await self.crud.update_lightning_invoice(
|
||||
id=id, issued=False, db=self.db, conn=conn
|
||||
)
|
||||
raise e
|
||||
|
||||
async def _pay_lightning_invoice(
|
||||
self, invoice: str, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
"""Pays a Lightning invoice via the funding source backend.
|
||||
|
||||
Args:
|
||||
invoice (str): Bolt11 Lightning invoice
|
||||
fee_limit_msat (int): Maximum fee reserve for payment (in Millisatoshi)
|
||||
|
||||
Raises:
|
||||
Exception: Funding source error.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, string, int]: Returns payment status, preimage of invoice, paid fees (in Millisatoshi)
|
||||
"""
|
||||
status = await self.lightning.status()
|
||||
if status.error_message:
|
||||
raise LightningError(
|
||||
f"Lightning wallet not responding: {status.error_message}"
|
||||
)
|
||||
payment = await self.lightning.pay_invoice(
|
||||
invoice, fee_limit_msat=fee_limit_msat
|
||||
)
|
||||
logger.trace(f"_pay_lightning_invoice: Lightning payment status: {payment.ok}")
|
||||
# make sure that fee is positive and not None
|
||||
payment.fee_msat = abs(payment.fee_msat) if payment.fee_msat else 0
|
||||
return payment
|
||||
@@ -1,9 +1,9 @@
|
||||
from ..core.db import Database, table_with_schema
|
||||
from ..core.db import Connection, Database, table_with_schema
|
||||
|
||||
|
||||
async def m000_create_migrations_table(db: Database):
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'dbversions')} (
|
||||
async def m000_create_migrations_table(conn: Connection):
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(conn, 'dbversions')} (
|
||||
db TEXT PRIMARY KEY,
|
||||
version INT NOT NULL
|
||||
)
|
||||
@@ -11,120 +11,127 @@ async def m000_create_migrations_table(db: Database):
|
||||
|
||||
|
||||
async def m001_initial(db: Database):
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
|
||||
amount {db.big_int} NOT NULL,
|
||||
B_b TEXT NOT NULL,
|
||||
C_b TEXT NOT NULL,
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
|
||||
amount {db.big_int} NOT NULL,
|
||||
B_b TEXT NOT NULL,
|
||||
C_b TEXT NOT NULL,
|
||||
|
||||
UNIQUE (B_b)
|
||||
UNIQUE (B_b)
|
||||
|
||||
);
|
||||
);
|
||||
""")
|
||||
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
||||
amount {db.big_int} NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
|
||||
UNIQUE (secret)
|
||||
|
||||
);
|
||||
""")
|
||||
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} (
|
||||
amount {db.big_int} NOT NULL,
|
||||
pr TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
issued BOOL NOT NULL,
|
||||
|
||||
UNIQUE (hash)
|
||||
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
async def m002_add_balance_views(db: Database):
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(f"""
|
||||
CREATE VIEW {table_with_schema(db, 'balance_issued')} AS
|
||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT SUM(amount)
|
||||
FROM {table_with_schema(db, 'promises')}
|
||||
WHERE amount > 0
|
||||
) AS s;
|
||||
""")
|
||||
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
||||
amount {db.big_int} NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
|
||||
UNIQUE (secret)
|
||||
|
||||
);
|
||||
await conn.execute(f"""
|
||||
CREATE VIEW {table_with_schema(db, 'balance_redeemed')} AS
|
||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT SUM(amount)
|
||||
FROM {table_with_schema(db, 'proofs_used')}
|
||||
WHERE amount > 0
|
||||
) AS s;
|
||||
""")
|
||||
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} (
|
||||
amount {db.big_int} NOT NULL,
|
||||
pr TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
issued BOOL NOT NULL,
|
||||
|
||||
UNIQUE (hash)
|
||||
|
||||
);
|
||||
await conn.execute(f"""
|
||||
CREATE VIEW {table_with_schema(db, 'balance')} AS
|
||||
SELECT s_issued - s_used FROM (
|
||||
SELECT bi.balance AS s_issued, bu.balance AS s_used
|
||||
FROM {table_with_schema(db, 'balance_issued')} bi
|
||||
CROSS JOIN {table_with_schema(db, 'balance_redeemed')} bu
|
||||
) AS balance;
|
||||
""")
|
||||
|
||||
await db.execute(f"""
|
||||
CREATE VIEW {table_with_schema(db, 'balance_issued')} AS
|
||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT SUM(amount)
|
||||
FROM {table_with_schema(db, 'promises')}
|
||||
WHERE amount > 0
|
||||
) AS s;
|
||||
""")
|
||||
|
||||
await db.execute(f"""
|
||||
CREATE VIEW {table_with_schema(db, 'balance_redeemed')} AS
|
||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT SUM(amount)
|
||||
FROM {table_with_schema(db, 'proofs_used')}
|
||||
WHERE amount > 0
|
||||
) AS s;
|
||||
""")
|
||||
|
||||
await db.execute(f"""
|
||||
CREATE VIEW {table_with_schema(db, 'balance')} AS
|
||||
SELECT s_issued - s_used FROM (
|
||||
SELECT bi.balance AS s_issued, bu.balance AS s_used
|
||||
FROM {table_with_schema(db, 'balance_issued')} bi
|
||||
CROSS JOIN {table_with_schema(db, 'balance_redeemed')} bu
|
||||
) AS balance;
|
||||
""")
|
||||
|
||||
|
||||
async def m003_mint_keysets(db: Database):
|
||||
"""
|
||||
Stores mint keysets from different mints and epochs.
|
||||
"""
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} (
|
||||
id TEXT NOT NULL,
|
||||
derivation_path TEXT,
|
||||
valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
active BOOL DEFAULT TRUE,
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} (
|
||||
id TEXT NOT NULL,
|
||||
derivation_path TEXT,
|
||||
valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
active BOOL DEFAULT TRUE,
|
||||
|
||||
UNIQUE (derivation_path)
|
||||
UNIQUE (derivation_path)
|
||||
|
||||
);
|
||||
""")
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_pubkeys')} (
|
||||
id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
);
|
||||
""")
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_pubkeys')} (
|
||||
id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
|
||||
UNIQUE (id, pubkey)
|
||||
UNIQUE (id, pubkey)
|
||||
|
||||
);
|
||||
""")
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
async def m004_keysets_add_version(db: Database):
|
||||
"""
|
||||
Column that remembers with which version
|
||||
"""
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT"
|
||||
)
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT"
|
||||
)
|
||||
|
||||
|
||||
async def m005_pending_proofs_table(db: Database) -> None:
|
||||
"""
|
||||
Store pending proofs.
|
||||
"""
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
||||
amount INTEGER NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
||||
amount INTEGER NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
|
||||
UNIQUE (secret)
|
||||
UNIQUE (secret)
|
||||
|
||||
);
|
||||
""")
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
async def m006_invoices_add_payment_hash(db: Database):
|
||||
@@ -133,38 +140,67 @@ async def m006_invoices_add_payment_hash(db: Database):
|
||||
the column hash as a random identifier now
|
||||
(see https://github.com/cashubtc/nuts/pull/14).
|
||||
"""
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN payment_hash TEXT"
|
||||
)
|
||||
await db.execute(
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash"
|
||||
)
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN payment_hash"
|
||||
" TEXT"
|
||||
)
|
||||
await conn.execute(
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash"
|
||||
)
|
||||
|
||||
|
||||
async def m007_proofs_and_promises_store_id(db: Database):
|
||||
"""
|
||||
Column that remembers the payment_hash as we're using
|
||||
the column hash as a random identifier now
|
||||
(see https://github.com/cashubtc/nuts/pull/14).
|
||||
Column that stores the id of the proof or promise.
|
||||
"""
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT"
|
||||
)
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT"
|
||||
)
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT"
|
||||
)
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT"
|
||||
)
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT"
|
||||
)
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT"
|
||||
)
|
||||
|
||||
|
||||
async def m008_promises_dleq(db: Database):
|
||||
"""
|
||||
Add columns for DLEQ proof to promises table.
|
||||
"""
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN e TEXT"
|
||||
)
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN s TEXT"
|
||||
)
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN e TEXT"
|
||||
)
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN s TEXT"
|
||||
)
|
||||
|
||||
|
||||
async def m009_add_out_to_invoices(db: Database):
|
||||
# column in invoices for marking whether the invoice is incoming (out=False) or outgoing (out=True)
|
||||
async with db.connect() as conn:
|
||||
# we have to drop the balance views first and recreate them later
|
||||
await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance_issued')}")
|
||||
await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance_redeemed')}")
|
||||
await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance')}")
|
||||
|
||||
# rename column pr to bolt11
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'invoices')} RENAME COLUMN pr TO"
|
||||
" bolt11"
|
||||
)
|
||||
# rename column hash to payment_hash
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'invoices')} RENAME COLUMN hash TO id"
|
||||
)
|
||||
|
||||
# recreate balance views
|
||||
await m002_add_balance_views(db)
|
||||
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN out BOOL"
|
||||
)
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
from typing import Protocol
|
||||
|
||||
from ..core.base import MintKeyset, MintKeysets
|
||||
from ..core.db import Database
|
||||
from ..lightning.base import Wallet
|
||||
from ..mint.crud import LedgerCrud
|
||||
|
||||
|
||||
class SupportsKeysets(Protocol):
|
||||
keyset: MintKeyset
|
||||
keysets: MintKeysets
|
||||
|
||||
|
||||
class SupportLightning(Protocol):
|
||||
lightning: Wallet
|
||||
|
||||
|
||||
class SupportsDb(Protocol):
|
||||
db: Database
|
||||
crud: LedgerCrud
|
||||
|
||||
@@ -162,10 +162,10 @@ async def mint(
|
||||
|
||||
# BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash
|
||||
# We use the payment_hash to lookup the hash from the database and pass that one along.
|
||||
hash = payment_hash or hash
|
||||
id = payment_hash or hash
|
||||
# END: backwards compatibility < 0.12
|
||||
|
||||
promises = await ledger.mint(payload.outputs, hash=hash)
|
||||
promises = await ledger.mint(payload.outputs, id=id)
|
||||
blinded_signatures = PostMintResponse(promises=promises)
|
||||
logger.trace(f"< POST /mint: {blinded_signatures}")
|
||||
return blinded_signatures
|
||||
|
||||
@@ -10,6 +10,7 @@ from ..core.db import Database
|
||||
from ..core.migrations import migrate_databases
|
||||
from ..core.settings import settings
|
||||
from ..mint import migrations
|
||||
from ..mint.crud import LedgerCrud
|
||||
from ..mint.ledger import Ledger
|
||||
|
||||
logger.debug("Enviroment Settings:")
|
||||
@@ -26,6 +27,7 @@ ledger = Ledger(
|
||||
seed=settings.mint_private_key,
|
||||
derivation_path=settings.mint_derivation_path,
|
||||
lightning=lightning_backend,
|
||||
crud=LedgerCrud(),
|
||||
)
|
||||
|
||||
|
||||
@@ -50,14 +52,14 @@ async def start_mint_init():
|
||||
|
||||
if settings.lightning:
|
||||
logger.info(f"Using backend: {settings.mint_lightning_backend}")
|
||||
error_message, balance = await ledger.lightning.status()
|
||||
if error_message:
|
||||
status = await ledger.lightning.status()
|
||||
if status.error_message:
|
||||
logger.warning(
|
||||
f"The backend for {ledger.lightning.__class__.__name__} isn't working"
|
||||
f" properly: '{error_message}'",
|
||||
f"The backend for {ledger.lightning.__class__.__name__} isn't"
|
||||
f" working properly: '{status.error_message}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
logger.info(f"Lightning balance: {balance} msat")
|
||||
logger.info(f"Lightning balance: {status.balance_msat} msat")
|
||||
|
||||
logger.info(f"Data dir: {settings.cashu_dir}")
|
||||
logger.info("Mint started.")
|
||||
|
||||
@@ -6,15 +6,13 @@ from ...core.base import Invoice
|
||||
|
||||
|
||||
class PayResponse(BaseModel):
|
||||
amount: int
|
||||
fee: int
|
||||
amount_with_fee: int
|
||||
ok: Optional[bool] = None
|
||||
|
||||
|
||||
class InvoiceResponse(BaseModel):
|
||||
amount: Optional[int] = None
|
||||
invoice: Optional[Invoice] = None
|
||||
hash: Optional[str] = None
|
||||
id: Optional[str] = None
|
||||
|
||||
|
||||
class SwapResponse(BaseModel):
|
||||
|
||||
@@ -11,6 +11,12 @@ from fastapi import APIRouter, Query
|
||||
from ...core.base import TokenV3
|
||||
from ...core.helpers import sum_proofs
|
||||
from ...core.settings import settings
|
||||
from ...lightning.base import (
|
||||
InvoiceResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
from ...nostr.client.client import NostrClient
|
||||
from ...tor.tor import TorProxy
|
||||
from ...wallet.crud import get_lightning_invoices, get_reserved_proofs
|
||||
@@ -23,16 +29,15 @@ from ...wallet.helpers import (
|
||||
)
|
||||
from ...wallet.nostr import receive_nostr, send_nostr
|
||||
from ...wallet.wallet import Wallet as Wallet
|
||||
from ..lightning.lightning import LightningWallet
|
||||
from .api_helpers import verify_mints
|
||||
from .responses import (
|
||||
BalanceResponse,
|
||||
BurnResponse,
|
||||
InfoResponse,
|
||||
InvoiceResponse,
|
||||
InvoicesResponse,
|
||||
LockResponse,
|
||||
LocksResponse,
|
||||
PayResponse,
|
||||
PendingResponse,
|
||||
ReceiveResponse,
|
||||
RestoreResponse,
|
||||
@@ -44,17 +49,19 @@ from .responses import (
|
||||
router: APIRouter = APIRouter()
|
||||
|
||||
|
||||
async def mint_wallet(mint_url: Optional[str] = None):
|
||||
wallet: Wallet = await Wallet.with_db(
|
||||
async def mint_wallet(
|
||||
mint_url: Optional[str] = None, raise_connection_error: bool = True
|
||||
) -> LightningWallet:
|
||||
lightning_wallet = await LightningWallet.with_db(
|
||||
mint_url or settings.mint_url,
|
||||
db=os.path.join(settings.cashu_dir, settings.wallet_name),
|
||||
name=settings.wallet_name,
|
||||
)
|
||||
await wallet.load_mint()
|
||||
return wallet
|
||||
await lightning_wallet.async_init(raise_connection_error=raise_connection_error)
|
||||
return lightning_wallet
|
||||
|
||||
|
||||
wallet: Wallet = Wallet(
|
||||
wallet = LightningWallet(
|
||||
settings.mint_url,
|
||||
db=os.path.join(settings.cashu_dir, settings.wallet_name),
|
||||
name=settings.wallet_name,
|
||||
@@ -64,87 +71,101 @@ wallet: Wallet = Wallet(
|
||||
@router.on_event("startup")
|
||||
async def start_wallet():
|
||||
global wallet
|
||||
wallet = await Wallet.with_db(
|
||||
settings.mint_url,
|
||||
db=os.path.join(settings.cashu_dir, settings.wallet_name),
|
||||
name=settings.wallet_name,
|
||||
)
|
||||
|
||||
wallet = await mint_wallet(settings.mint_url, raise_connection_error=False)
|
||||
if settings.tor and not TorProxy().check_platform():
|
||||
raise Exception("tor not working.")
|
||||
await wallet.load_mint()
|
||||
|
||||
|
||||
@router.post("/pay", name="Pay lightning invoice", response_model=PayResponse)
|
||||
@router.post(
|
||||
"/lightning/pay_invoice",
|
||||
name="Pay lightning invoice",
|
||||
response_model=PaymentResponse,
|
||||
)
|
||||
async def pay(
|
||||
invoice: str = Query(default=..., description="Lightning invoice to pay"),
|
||||
bolt11: str = Query(default=..., description="Lightning invoice to pay"),
|
||||
mint: str = Query(
|
||||
default=None,
|
||||
description="Mint URL to pay from (None for default mint)",
|
||||
),
|
||||
):
|
||||
if not settings.lightning:
|
||||
raise Exception("lightning not enabled.")
|
||||
|
||||
) -> PaymentResponse:
|
||||
global wallet
|
||||
wallet = await mint_wallet(mint)
|
||||
await wallet.load_proofs(reload=True)
|
||||
|
||||
total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
|
||||
assert total_amount > 0, "amount has to be larger than zero."
|
||||
assert wallet.available_balance >= total_amount, "balance is too low."
|
||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||
await wallet.pay_lightning(send_proofs, invoice, fee_reserve_sat)
|
||||
return PayResponse(
|
||||
amount=total_amount - fee_reserve_sat,
|
||||
fee=fee_reserve_sat,
|
||||
amount_with_fee=total_amount,
|
||||
)
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
payment_response = await wallet.pay_invoice(bolt11)
|
||||
return payment_response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/invoice", name="Request lightning invoice", response_model=InvoiceResponse
|
||||
@router.get(
|
||||
"/lightning/payment_state",
|
||||
name="Request lightning invoice",
|
||||
response_model=PaymentStatus,
|
||||
)
|
||||
async def invoice(
|
||||
amount: int = Query(default=..., description="Amount to request in invoice"),
|
||||
hash: str = Query(default=None, description="Hash of paid invoice"),
|
||||
async def payment_state(
|
||||
payment_hash: str = Query(default=None, description="Id of paid invoice"),
|
||||
mint: str = Query(
|
||||
default=None,
|
||||
description="Mint URL to create an invoice at (None for default mint)",
|
||||
),
|
||||
split: int = Query(
|
||||
default=None, description="Split minted tokens with a specific amount."
|
||||
),
|
||||
):
|
||||
# in case the user wants a specific split, we create a list of amounts
|
||||
optional_split = None
|
||||
if split:
|
||||
assert amount % split == 0, "split must be divisor or amount"
|
||||
assert amount >= split, "split must smaller or equal amount"
|
||||
n_splits = amount // split
|
||||
optional_split = [split] * n_splits
|
||||
print(f"Requesting split with {n_splits}*{split} sat tokens.")
|
||||
|
||||
) -> PaymentStatus:
|
||||
global wallet
|
||||
wallet = await mint_wallet(mint)
|
||||
if not settings.lightning:
|
||||
await wallet.mint(amount, split=optional_split)
|
||||
return InvoiceResponse(
|
||||
amount=amount,
|
||||
)
|
||||
elif amount and not hash:
|
||||
invoice = await wallet.request_mint(amount)
|
||||
return InvoiceResponse(
|
||||
amount=amount,
|
||||
invoice=invoice,
|
||||
)
|
||||
elif amount and hash:
|
||||
await wallet.mint(amount, split=optional_split, hash=hash)
|
||||
return InvoiceResponse(
|
||||
amount=amount,
|
||||
hash=hash,
|
||||
)
|
||||
return
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
state = await wallet.get_payment_status(payment_hash)
|
||||
return state
|
||||
|
||||
|
||||
@router.post(
|
||||
"/lightning/create_invoice",
|
||||
name="Request lightning invoice",
|
||||
response_model=InvoiceResponse,
|
||||
)
|
||||
async def create_invoice(
|
||||
amount: int = Query(default=..., description="Amount to request in invoice"),
|
||||
mint: str = Query(
|
||||
default=None,
|
||||
description="Mint URL to create an invoice at (None for default mint)",
|
||||
),
|
||||
) -> InvoiceResponse:
|
||||
global wallet
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
invoice = await wallet.create_invoice(amount)
|
||||
return invoice
|
||||
|
||||
|
||||
@router.get(
|
||||
"/lightning/invoice_state",
|
||||
name="Request lightning invoice",
|
||||
response_model=PaymentStatus,
|
||||
)
|
||||
async def invoice_state(
|
||||
payment_hash: str = Query(default=None, description="Payment hash of paid invoice"),
|
||||
mint: str = Query(
|
||||
default=None,
|
||||
description="Mint URL to create an invoice at (None for default mint)",
|
||||
),
|
||||
) -> PaymentStatus:
|
||||
global wallet
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
state = await wallet.get_invoice_status(payment_hash)
|
||||
return state
|
||||
|
||||
|
||||
@router.get(
|
||||
"/lightning/balance",
|
||||
name="Balance",
|
||||
summary="Display balance.",
|
||||
response_model=StatusResponse,
|
||||
)
|
||||
async def lightning_balance() -> StatusResponse:
|
||||
try:
|
||||
await wallet.load_proofs(reload=True)
|
||||
except Exception as exc:
|
||||
return StatusResponse(error_message=str(exc), balance_msat=0)
|
||||
return StatusResponse(
|
||||
error_message=None, balance_msat=wallet.available_balance * 1000
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -171,7 +192,7 @@ async def swap(
|
||||
# pay invoice from outgoing mint
|
||||
await outgoing_wallet.load_proofs(reload=True)
|
||||
total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees(
|
||||
invoice.pr
|
||||
invoice.bolt11
|
||||
)
|
||||
assert total_amount > 0, "amount must be positive"
|
||||
if outgoing_wallet.available_balance < total_amount:
|
||||
@@ -180,10 +201,10 @@ async def swap(
|
||||
_, send_proofs = await outgoing_wallet.split_to_send(
|
||||
outgoing_wallet.proofs, total_amount, set_reserved=True
|
||||
)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.pr, fee_reserve_sat)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.bolt11, fee_reserve_sat)
|
||||
|
||||
# mint token in incoming mint
|
||||
await incoming_wallet.mint(amount, hash=invoice.hash)
|
||||
await incoming_wallet.mint(amount, id=invoice.id)
|
||||
await incoming_wallet.load_proofs(reload=True)
|
||||
mint_balances = await incoming_wallet.balance_per_minturl()
|
||||
return SwapResponse(
|
||||
@@ -223,6 +244,8 @@ async def send_command(
|
||||
),
|
||||
):
|
||||
global wallet
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
if not nostr:
|
||||
balance, token = await send(
|
||||
wallet, amount=amount, lock=lock, legacy=False, split=not nosplit
|
||||
@@ -284,7 +307,7 @@ async def burn(
|
||||
if not (all or token or force or delete) or (token and all):
|
||||
raise Exception(
|
||||
"enter a token or use --all to burn all pending tokens, --force to"
|
||||
" check all tokensor --delete with send ID to force-delete pending"
|
||||
" check all tokens or --delete with send ID to force-delete pending"
|
||||
" token from list if mint is unavailable.",
|
||||
)
|
||||
if all:
|
||||
|
||||
@@ -168,9 +168,12 @@ async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
wallet.status()
|
||||
total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
|
||||
if not yes:
|
||||
potential = (
|
||||
f" ({total_amount} sat with potential fees)" if fee_reserve_sat else ""
|
||||
)
|
||||
message = f"Pay {total_amount - fee_reserve_sat} sat{potential}?"
|
||||
click.confirm(
|
||||
f"Pay {total_amount - fee_reserve_sat} sat ({total_amount} sat with"
|
||||
" potential fees)?",
|
||||
message,
|
||||
abort=True,
|
||||
default=True,
|
||||
)
|
||||
@@ -187,7 +190,7 @@ async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
|
||||
@cli.command("invoice", help="Create Lighting invoice.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.option("--hash", default="", help="Hash of the paid invoice.", type=str)
|
||||
@click.option("--id", default="", help="Id of the paid invoice.", type=str)
|
||||
@click.option(
|
||||
"--split",
|
||||
"-s",
|
||||
@@ -197,7 +200,7 @@ async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def invoice(ctx: Context, amount: int, hash: str, split: int):
|
||||
async def invoice(ctx: Context, amount: int, id: str, split: int):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
@@ -213,16 +216,16 @@ async def invoice(ctx: Context, amount: int, hash: str, split: int):
|
||||
if not settings.lightning:
|
||||
await wallet.mint(amount, split=optional_split)
|
||||
# user requests an invoice
|
||||
elif amount and not hash:
|
||||
elif amount and not id:
|
||||
invoice = await wallet.request_mint(amount)
|
||||
if invoice.pr:
|
||||
if invoice.bolt11:
|
||||
print(f"Pay invoice to mint {amount} sat:")
|
||||
print("")
|
||||
print(f"Invoice: {invoice.pr}")
|
||||
print(f"Invoice: {invoice.bolt11}")
|
||||
print("")
|
||||
print(
|
||||
"If you abort this you can use this command to recheck the"
|
||||
f" invoice:\ncashu invoice {amount} --hash {invoice.hash}"
|
||||
f" invoice:\ncashu invoice {amount} --id {invoice.id}"
|
||||
)
|
||||
check_until = time.time() + 5 * 60 # check for five minutes
|
||||
print("")
|
||||
@@ -235,7 +238,7 @@ async def invoice(ctx: Context, amount: int, hash: str, split: int):
|
||||
while time.time() < check_until and not paid:
|
||||
time.sleep(3)
|
||||
try:
|
||||
await wallet.mint(amount, split=optional_split, hash=invoice.hash)
|
||||
await wallet.mint(amount, split=optional_split, id=invoice.id)
|
||||
paid = True
|
||||
print(" Invoice paid.")
|
||||
except Exception as e:
|
||||
@@ -253,8 +256,8 @@ async def invoice(ctx: Context, amount: int, hash: str, split: int):
|
||||
)
|
||||
|
||||
# user paid invoice and want to check it
|
||||
elif amount and hash:
|
||||
await wallet.mint(amount, split=optional_split, hash=hash)
|
||||
elif amount and id:
|
||||
await wallet.mint(amount, split=optional_split, id=id)
|
||||
wallet.status()
|
||||
return
|
||||
|
||||
@@ -285,17 +288,17 @@ async def swap(ctx: Context):
|
||||
|
||||
# pay invoice from outgoing mint
|
||||
total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees(
|
||||
invoice.pr
|
||||
invoice.bolt11
|
||||
)
|
||||
if outgoing_wallet.available_balance < total_amount:
|
||||
raise Exception("balance too low")
|
||||
_, send_proofs = await outgoing_wallet.split_to_send(
|
||||
outgoing_wallet.proofs, total_amount, set_reserved=True
|
||||
)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.pr, fee_reserve_sat)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.bolt11, fee_reserve_sat)
|
||||
|
||||
# mint token in incoming mint
|
||||
await incoming_wallet.mint(amount, hash=invoice.hash)
|
||||
await incoming_wallet.mint(amount, id=invoice.id)
|
||||
|
||||
await incoming_wallet.load_proofs(reload=True)
|
||||
await print_mint_balances(incoming_wallet, show_mints=True)
|
||||
@@ -629,8 +632,8 @@ async def invoices(ctx):
|
||||
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.id:
|
||||
print(f"ID: {invoice.id}")
|
||||
if invoice.preimage:
|
||||
print(f"Preimage: {invoice.preimage}")
|
||||
if invoice.time_created:
|
||||
@@ -644,7 +647,7 @@ async def invoices(ctx):
|
||||
)
|
||||
print(f"Paid: {d}")
|
||||
print("")
|
||||
print(f"Payment request: {invoice.pr}")
|
||||
print(f"Payment request: {invoice.bolt11}")
|
||||
print("")
|
||||
print("--------------------------\n")
|
||||
else:
|
||||
|
||||
@@ -14,8 +14,8 @@ async def store_proof(
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO proofs
|
||||
(id, amount, C, secret, time_created, derivation_path, dleq)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
(id, amount, C, secret, time_created, derivation_path, dleq, mint_id, melt_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.id,
|
||||
@@ -25,18 +25,42 @@ async def store_proof(
|
||||
int(time.time()),
|
||||
proof.derivation_path,
|
||||
json.dumps(proof.dleq.dict()) if proof.dleq else "",
|
||||
proof.mint_id,
|
||||
proof.melt_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_proofs(
|
||||
*,
|
||||
db: Database,
|
||||
melt_id: str = "",
|
||||
mint_id: str = "",
|
||||
table: str = "proofs",
|
||||
conn: Optional[Connection] = None,
|
||||
) -> List[Proof]:
|
||||
rows = await (conn or db).fetchall("""
|
||||
SELECT * from proofs
|
||||
""")
|
||||
return [Proof.from_dict(dict(r)) for r in rows]
|
||||
):
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
|
||||
if melt_id:
|
||||
clauses.append("melt_id = ?")
|
||||
values.append(melt_id)
|
||||
if mint_id:
|
||||
clauses.append("mint_id = ?")
|
||||
values.append(mint_id)
|
||||
where = ""
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
rows = (
|
||||
await (conn or db).fetchall(
|
||||
f"""
|
||||
SELECT * from {table}
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
),
|
||||
)
|
||||
return [Proof.from_dict(dict(r)) for r in rows[0]] if rows else []
|
||||
|
||||
|
||||
async def get_reserved_proofs(
|
||||
@@ -66,8 +90,8 @@ async def invalidate_proof(
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO proofs_used
|
||||
(amount, C, secret, time_used, id, derivation_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(amount, C, secret, time_used, id, derivation_path, mint_id, melt_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.amount,
|
||||
@@ -76,14 +100,19 @@ async def invalidate_proof(
|
||||
int(time.time()),
|
||||
proof.id,
|
||||
proof.derivation_path,
|
||||
proof.mint_id,
|
||||
proof.melt_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def update_proof_reserved(
|
||||
async def update_proof(
|
||||
proof: Proof,
|
||||
reserved: bool,
|
||||
send_id: str = "",
|
||||
*,
|
||||
reserved: Optional[bool] = None,
|
||||
send_id: Optional[str] = None,
|
||||
mint_id: Optional[str] = None,
|
||||
melt_id: Optional[str] = None,
|
||||
db: Optional[Database] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
@@ -92,15 +121,22 @@ async def update_proof_reserved(
|
||||
clauses.append("reserved = ?")
|
||||
values.append(reserved)
|
||||
|
||||
if send_id:
|
||||
if send_id is not None:
|
||||
clauses.append("send_id = ?")
|
||||
values.append(send_id)
|
||||
|
||||
if reserved:
|
||||
# set the time of reserving
|
||||
if reserved is not None:
|
||||
clauses.append("time_reserved = ?")
|
||||
values.append(int(time.time()))
|
||||
|
||||
if mint_id is not None:
|
||||
clauses.append("mint_id = ?")
|
||||
values.append(mint_id)
|
||||
|
||||
if melt_id is not None:
|
||||
clauses.append("melt_id = ?")
|
||||
values.append(melt_id)
|
||||
|
||||
await (conn or db).execute( # type: ignore
|
||||
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
|
||||
(*values, str(proof.secret)),
|
||||
@@ -184,44 +220,55 @@ async def store_lightning_invoice(
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO invoices
|
||||
(amount, pr, hash, preimage, paid, time_created, time_paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
(amount, bolt11, id, payment_hash, preimage, paid, time_created, time_paid, out)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
invoice.amount,
|
||||
invoice.pr,
|
||||
invoice.hash,
|
||||
invoice.bolt11,
|
||||
invoice.id,
|
||||
invoice.payment_hash,
|
||||
invoice.preimage,
|
||||
invoice.paid,
|
||||
invoice.time_created,
|
||||
invoice.time_paid,
|
||||
invoice.out,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_lightning_invoice(
|
||||
*,
|
||||
db: Database,
|
||||
hash: str = "",
|
||||
id: str = "",
|
||||
payment_hash: str = "",
|
||||
out: Optional[bool] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Invoice:
|
||||
) -> Optional[Invoice]:
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
if hash:
|
||||
clauses.append("hash = ?")
|
||||
values.append(hash)
|
||||
if id:
|
||||
clauses.append("id = ?")
|
||||
values.append(id)
|
||||
if payment_hash:
|
||||
clauses.append("payment_hash = ?")
|
||||
values.append(payment_hash)
|
||||
if out is not None:
|
||||
clauses.append("out = ?")
|
||||
values.append(out)
|
||||
|
||||
where = ""
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
query = f"""
|
||||
SELECT * from invoices
|
||||
{where}
|
||||
""",
|
||||
"""
|
||||
row = await (conn or db).fetchone(
|
||||
query,
|
||||
tuple(values),
|
||||
)
|
||||
return Invoice(**row)
|
||||
return Invoice(**row) if row else None
|
||||
|
||||
|
||||
async def get_lightning_invoices(
|
||||
@@ -252,9 +299,10 @@ async def get_lightning_invoices(
|
||||
|
||||
async def update_lightning_invoice(
|
||||
db: Database,
|
||||
hash: str,
|
||||
id: str,
|
||||
paid: bool,
|
||||
time_paid: Optional[int] = None,
|
||||
preimage: Optional[str] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
clauses = []
|
||||
@@ -265,12 +313,15 @@ async def update_lightning_invoice(
|
||||
if time_paid:
|
||||
clauses.append("time_paid = ?")
|
||||
values.append(time_paid)
|
||||
if preimage:
|
||||
clauses.append("preimage = ?")
|
||||
values.append(preimage)
|
||||
|
||||
await (conn or db).execute(
|
||||
f"UPDATE invoices SET {', '.join(clauses)} WHERE hash = ?",
|
||||
f"UPDATE invoices SET {', '.join(clauses)} WHERE id = ?",
|
||||
(
|
||||
*values,
|
||||
hash,
|
||||
id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
|
||||
from ..core import bolt11 as bolt11
|
||||
from ..core.base import HTLCWitness, Proof
|
||||
from ..core.db import Database
|
||||
from ..core.htlc import (
|
||||
|
||||
1
cashu/wallet/lightning/__init__.py
Normal file
1
cashu/wallet/lightning/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .lightning import LightningWallet # noqa
|
||||
BIN
cashu/wallet/lightning/data/lightning.db/wallet.sqlite3
Normal file
BIN
cashu/wallet/lightning/data/lightning.db/wallet.sqlite3
Normal file
Binary file not shown.
153
cashu/wallet/lightning/lightning.py
Normal file
153
cashu/wallet/lightning/lightning.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import bolt11
|
||||
|
||||
from ...core.helpers import sum_promises
|
||||
from ...core.settings import settings
|
||||
from ...lightning.base import (
|
||||
InvoiceResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
from ...wallet.crud import get_lightning_invoice, get_proofs
|
||||
from ..wallet import Wallet
|
||||
|
||||
|
||||
class LightningWallet(Wallet):
|
||||
"""
|
||||
Lightning wallet interface for Cashu
|
||||
"""
|
||||
|
||||
async def async_init(self, raise_connection_error: bool = True):
|
||||
"""Async init for lightning wallet"""
|
||||
settings.tor = False
|
||||
await self.load_proofs()
|
||||
try:
|
||||
await self.load_mint()
|
||||
except Exception as e:
|
||||
if raise_connection_error:
|
||||
raise e
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not args and not kwargs:
|
||||
pass
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def create_invoice(self, amount: int) -> InvoiceResponse:
|
||||
"""Create lightning invoice
|
||||
|
||||
Args:
|
||||
amount (int): amount in satoshis
|
||||
Returns:
|
||||
str: invoice
|
||||
"""
|
||||
invoice = await self.request_mint(amount)
|
||||
return InvoiceResponse(
|
||||
ok=True, payment_request=invoice.bolt11, checking_id=invoice.payment_hash
|
||||
)
|
||||
|
||||
async def pay_invoice(self, pr: str) -> PaymentResponse:
|
||||
"""Pay lightning invoice
|
||||
|
||||
Args:
|
||||
pr (str): bolt11 payment request
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
total_amount, fee_reserve_sat = await self.get_pay_amount_with_fees(pr)
|
||||
assert total_amount > 0, "amount is not positive"
|
||||
if self.available_balance < total_amount:
|
||||
print("Error: Balance too low.")
|
||||
return PaymentResponse(ok=False)
|
||||
_, send_proofs = await self.split_to_send(self.proofs, total_amount)
|
||||
try:
|
||||
resp = await self.pay_lightning(send_proofs, pr, fee_reserve_sat)
|
||||
if resp.change:
|
||||
fees_paid_sat = fee_reserve_sat - sum_promises(resp.change)
|
||||
else:
|
||||
fees_paid_sat = fee_reserve_sat
|
||||
|
||||
invoice_obj = bolt11.decode(pr)
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=invoice_obj.payment_hash,
|
||||
preimage=resp.preimage,
|
||||
fee_msat=fees_paid_sat * 1000,
|
||||
)
|
||||
except Exception as e:
|
||||
print("Exception:", e)
|
||||
return PaymentResponse(ok=False, error_message=str(e))
|
||||
|
||||
async def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||
"""Get lightning invoice status (incoming)
|
||||
|
||||
Args:
|
||||
invoice (str): lightning invoice
|
||||
|
||||
Returns:
|
||||
str: status
|
||||
"""
|
||||
invoice = await get_lightning_invoice(
|
||||
db=self.db, payment_hash=payment_hash, out=False
|
||||
)
|
||||
if not invoice:
|
||||
return PaymentStatus(paid=None)
|
||||
if invoice.paid:
|
||||
return PaymentStatus(paid=True)
|
||||
try:
|
||||
# to check the invoice state, we try minting tokens
|
||||
await self.mint(invoice.amount, id=invoice.id)
|
||||
return PaymentStatus(paid=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return PaymentStatus(paid=False)
|
||||
|
||||
async def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||
"""Get lightning payment status (outgoing)
|
||||
|
||||
Args:
|
||||
payment_hash (str): lightning invoice payment_hash
|
||||
|
||||
Returns:
|
||||
str: status
|
||||
"""
|
||||
|
||||
# NOTE: consider adding this in wallet.py and update invoice state to paid in DB
|
||||
|
||||
invoice = await get_lightning_invoice(
|
||||
db=self.db, payment_hash=payment_hash, out=True
|
||||
)
|
||||
|
||||
if not invoice:
|
||||
return PaymentStatus(paid=False) # "invoice not found (in db)"
|
||||
if invoice.paid:
|
||||
return PaymentStatus(paid=True, preimage=invoice.preimage) # "paid (in db)"
|
||||
proofs = await get_proofs(db=self.db, melt_id=invoice.id)
|
||||
if not proofs:
|
||||
return PaymentStatus(paid=False) # "proofs not fount (in db)"
|
||||
proofs_states = await self.check_proof_state(proofs)
|
||||
if (
|
||||
not proofs_states
|
||||
or not proofs_states.spendable
|
||||
or not proofs_states.pending
|
||||
):
|
||||
return PaymentStatus(paid=False) # "states not fount"
|
||||
|
||||
if all(proofs_states.spendable) and all(proofs_states.pending):
|
||||
return PaymentStatus(paid=None) # "pending (with check)"
|
||||
if not any(proofs_states.spendable) and not any(proofs_states.pending):
|
||||
# NOTE: consider adding this check in wallet.py and mark the invoice as paid if all proofs are spent
|
||||
return PaymentStatus(paid=True) # "paid (with check)"
|
||||
if all(proofs_states.spendable) and not any(proofs_states.pending):
|
||||
return PaymentStatus(paid=False) # "failed (with check)"
|
||||
return PaymentStatus(paid=None) # "undefined state"
|
||||
|
||||
async def get_balance(self) -> StatusResponse:
|
||||
"""Get lightning balance
|
||||
|
||||
Returns:
|
||||
int: balance in satoshis
|
||||
"""
|
||||
return StatusResponse(
|
||||
error_message=None, balance_msat=self.available_balance * 1000
|
||||
)
|
||||
@@ -1,8 +1,8 @@
|
||||
from ..core.db import Database
|
||||
from ..core.db import Connection, Database
|
||||
|
||||
|
||||
async def m000_create_migrations_table(db: Database):
|
||||
await db.execute("""
|
||||
async def m000_create_migrations_table(conn: Connection):
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS dbversions (
|
||||
db TEXT PRIMARY KEY,
|
||||
version INT NOT NULL
|
||||
@@ -11,53 +11,54 @@ async def m000_create_migrations_table(db: Database):
|
||||
|
||||
|
||||
async def m001_initial(db: Database):
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS proofs (
|
||||
amount {db.big_int} NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS proofs (
|
||||
amount {db.big_int} NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
|
||||
UNIQUE (secret)
|
||||
UNIQUE (secret)
|
||||
|
||||
);
|
||||
""")
|
||||
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS proofs_used (
|
||||
amount {db.big_int} NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
|
||||
UNIQUE (secret)
|
||||
|
||||
);
|
||||
""")
|
||||
|
||||
await conn.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(f"""
|
||||
CREATE TABLE IF NOT EXISTS proofs_used (
|
||||
amount {db.big_int} NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
|
||||
UNIQUE (secret)
|
||||
|
||||
await conn.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
|
||||
);
|
||||
""")
|
||||
|
||||
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: Database):
|
||||
"""
|
||||
Column for marking proofs as reserved when they are being sent.
|
||||
"""
|
||||
|
||||
await db.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL")
|
||||
async with db.connect() as conn:
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL")
|
||||
|
||||
|
||||
async def m003_add_proofs_sendid_and_timestamps(db: Database):
|
||||
@@ -65,17 +66,19 @@ async def m003_add_proofs_sendid_and_timestamps(db: Database):
|
||||
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")
|
||||
async with db.connect() as conn:
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN send_id TEXT")
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN time_created TIMESTAMP")
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN time_reserved TIMESTAMP")
|
||||
await conn.execute("ALTER TABLE proofs_used ADD COLUMN time_used TIMESTAMP")
|
||||
|
||||
|
||||
async def m004_p2sh_locks(db: Database):
|
||||
"""
|
||||
DEPRECATED: Stores P2SH addresses and unlock scripts.
|
||||
"""
|
||||
# await db.execute("""
|
||||
# async with db.connect() as conn:
|
||||
# await conn.execute("""
|
||||
# CREATE TABLE IF NOT EXISTS p2sh (
|
||||
# address TEXT NOT NULL,
|
||||
# script TEXT NOT NULL,
|
||||
@@ -92,91 +95,117 @@ async def m005_wallet_keysets(db: Database):
|
||||
"""
|
||||
Stores mint keysets from different mints and epochs.
|
||||
"""
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS keysets (
|
||||
id TEXT,
|
||||
mint_url TEXT,
|
||||
valid_from TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
valid_to TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
first_seen TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
active BOOL DEFAULT TRUE,
|
||||
async with db.connect() as conn:
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS keysets (
|
||||
id TEXT,
|
||||
mint_url TEXT,
|
||||
valid_from TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
valid_to TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
first_seen TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
active BOOL DEFAULT TRUE,
|
||||
|
||||
UNIQUE (id, mint_url)
|
||||
UNIQUE (id, mint_url)
|
||||
|
||||
);
|
||||
""")
|
||||
);
|
||||
""")
|
||||
|
||||
await db.execute("ALTER TABLE proofs ADD COLUMN id TEXT")
|
||||
await db.execute("ALTER TABLE proofs_used ADD COLUMN id TEXT")
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN id TEXT")
|
||||
await conn.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},
|
||||
async with db.connect() as conn:
|
||||
await conn.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)
|
||||
UNIQUE (hash)
|
||||
|
||||
);
|
||||
""")
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
async def m007_nostr(db: Database):
|
||||
"""
|
||||
Stores timestamps of nostr operations.
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS nostr (
|
||||
type TEXT NOT NULL,
|
||||
last TIMESTAMP DEFAULT NULL
|
||||
)
|
||||
""")
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO nostr
|
||||
(type, last)
|
||||
VALUES (?, ?)
|
||||
""",
|
||||
(
|
||||
"dm",
|
||||
None,
|
||||
),
|
||||
)
|
||||
# async with db.connect() as conn:
|
||||
# await conn.execute("""
|
||||
# CREATE TABLE IF NOT EXISTS nostr (
|
||||
# type TEXT NOT NULL,
|
||||
# last TIMESTAMP DEFAULT NULL
|
||||
# )
|
||||
# """)
|
||||
# await conn.execute(
|
||||
# """
|
||||
# INSERT INTO nostr
|
||||
# (type, last)
|
||||
# VALUES (?, ?)
|
||||
# """,
|
||||
# (
|
||||
# "dm",
|
||||
# None,
|
||||
# ),
|
||||
# )
|
||||
|
||||
|
||||
async def m008_keysets_add_public_keys(db: Database):
|
||||
"""
|
||||
Stores public keys of mint in a new column of table keysets.
|
||||
"""
|
||||
await db.execute("ALTER TABLE keysets ADD COLUMN public_keys TEXT")
|
||||
async with db.connect() as conn:
|
||||
await conn.execute("ALTER TABLE keysets ADD COLUMN public_keys TEXT")
|
||||
|
||||
|
||||
async def m009_privatekey_and_determinstic_key_derivation(db: Database):
|
||||
await db.execute("ALTER TABLE keysets ADD COLUMN counter INTEGER DEFAULT 0")
|
||||
await db.execute("ALTER TABLE proofs ADD COLUMN derivation_path TEXT")
|
||||
await db.execute("ALTER TABLE proofs_used ADD COLUMN derivation_path TEXT")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS seed (
|
||||
seed TEXT NOT NULL,
|
||||
mnemonic TEXT NOT NULL,
|
||||
async with db.connect() as conn:
|
||||
await conn.execute("ALTER TABLE keysets ADD COLUMN counter INTEGER DEFAULT 0")
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN derivation_path TEXT")
|
||||
await conn.execute("ALTER TABLE proofs_used ADD COLUMN derivation_path TEXT")
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS seed (
|
||||
seed TEXT NOT NULL,
|
||||
mnemonic TEXT NOT NULL,
|
||||
|
||||
UNIQUE (seed, mnemonic)
|
||||
);
|
||||
""")
|
||||
# await db.execute("INSERT INTO secret_derivation (counter) VALUES (0)")
|
||||
UNIQUE (seed, mnemonic)
|
||||
);
|
||||
""")
|
||||
# await conn.execute("INSERT INTO secret_derivation (counter) VALUES (0)")
|
||||
|
||||
|
||||
async def m010_add_proofs_dleq(db: Database):
|
||||
"""
|
||||
Columns to store DLEQ proofs for proofs.
|
||||
"""
|
||||
await db.execute("ALTER TABLE proofs ADD COLUMN dleq TEXT")
|
||||
async with db.connect() as conn:
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN dleq TEXT")
|
||||
|
||||
|
||||
async def m010_add_ids_to_proofs_and_out_to_invoices(db: Database):
|
||||
"""
|
||||
Columns that store mint and melt id for proofs and invoices.
|
||||
"""
|
||||
async with db.connect() as conn:
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN mint_id TEXT")
|
||||
await conn.execute("ALTER TABLE proofs_used ADD COLUMN mint_id TEXT")
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN melt_id TEXT")
|
||||
await conn.execute("ALTER TABLE proofs_used ADD COLUMN melt_id TEXT")
|
||||
|
||||
# column in invoices for marking whether the invoice is incoming (out=False) or outgoing (out=True)
|
||||
await conn.execute("ALTER TABLE invoices ADD COLUMN out BOOL")
|
||||
# rename column pr to bolt11
|
||||
await conn.execute("ALTER TABLE invoices RENAME COLUMN pr TO bolt11")
|
||||
# rename column hash to payment_hash
|
||||
await conn.execute("ALTER TABLE invoices RENAME COLUMN hash TO id")
|
||||
# add column payment_hash
|
||||
await conn.execute("ALTER TABLE invoices ADD COLUMN payment_hash TEXT")
|
||||
|
||||
@@ -2,8 +2,8 @@ import asyncio
|
||||
import threading
|
||||
|
||||
import click
|
||||
from httpx import ConnectError
|
||||
from loguru import logger
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from ..core.settings import settings
|
||||
from ..nostr.client.client import NostrClient
|
||||
@@ -27,11 +27,11 @@ async def nip5_to_pubkey(wallet: Wallet, address: str):
|
||||
user, host = address.split("@")
|
||||
resp_dict = {}
|
||||
try:
|
||||
resp = wallet.s.get(
|
||||
resp = await wallet.httpx.get(
|
||||
f"https://{host}/.well-known/nostr.json?name={user}",
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except ConnectionError:
|
||||
except ConnectError:
|
||||
raise Exception(f"Could not connect to {host}")
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core import bolt11 as bolt11
|
||||
from ..core.base import (
|
||||
BlindedMessage,
|
||||
P2PKWitness,
|
||||
|
||||
@@ -6,7 +6,6 @@ from bip32 import BIP32
|
||||
from loguru import logger
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
from ..core import bolt11 as bolt11
|
||||
from ..core.crypto.secp import PrivateKey
|
||||
from ..core.db import Database
|
||||
from ..core.settings import settings
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import secrets as scrts
|
||||
import time
|
||||
import uuid
|
||||
from itertools import groupby
|
||||
from posixpath import join
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import requests
|
||||
import bolt11
|
||||
import httpx
|
||||
from bip32 import BIP32
|
||||
from httpx import Response
|
||||
from loguru import logger
|
||||
from requests import Response
|
||||
|
||||
from ..core import bolt11 as bolt11
|
||||
from ..core.base import (
|
||||
BlindedMessage,
|
||||
BlindedSignature,
|
||||
@@ -38,7 +37,6 @@ from ..core.base import (
|
||||
TokenV3Token,
|
||||
WalletKeyset,
|
||||
)
|
||||
from ..core.bolt11 import Invoice as InvoiceBolt11
|
||||
from ..core.crypto import b_dhke
|
||||
from ..core.crypto.secp import PrivateKey, PublicKey
|
||||
from ..core.db import Database
|
||||
@@ -59,7 +57,7 @@ from ..wallet.crud import (
|
||||
store_lightning_invoice,
|
||||
store_proof,
|
||||
update_lightning_invoice,
|
||||
update_proof_reserved,
|
||||
update_proof,
|
||||
)
|
||||
from . import migrations
|
||||
from .htlc import WalletHTLC
|
||||
@@ -67,19 +65,16 @@ from .p2pk import WalletP2PK
|
||||
from .secrets import WalletSecrets
|
||||
|
||||
|
||||
def async_set_requests(func):
|
||||
def async_set_httpx_client(func):
|
||||
"""
|
||||
Decorator that wraps around any async class method of LedgerAPI that makes
|
||||
API calls. Sets some HTTP headers and starts a Tor instance if none is
|
||||
already running and and sets local proxy to use it.
|
||||
already running and and sets local proxy to use it.
|
||||
"""
|
||||
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
self.s.headers.update({"Client-version": settings.version})
|
||||
if settings.debug:
|
||||
self.s.verify = False
|
||||
|
||||
# set proxy
|
||||
proxies_dict = {}
|
||||
proxy_url: Union[str, None] = None
|
||||
if settings.tor and TorProxy().check_platform():
|
||||
self.tor = TorProxy(timeout=True)
|
||||
@@ -90,10 +85,17 @@ def async_set_requests(func):
|
||||
elif settings.http_proxy:
|
||||
proxy_url = settings.http_proxy
|
||||
if proxy_url:
|
||||
self.s.proxies.update({"http": proxy_url})
|
||||
self.s.proxies.update({"https": proxy_url})
|
||||
proxies_dict.update({"http": proxy_url})
|
||||
proxies_dict.update({"https": proxy_url})
|
||||
|
||||
self.s.headers.update({"User-Agent": scrts.token_urlsafe(8)})
|
||||
headers_dict = {"Client-version": settings.version}
|
||||
|
||||
self.httpx = httpx.AsyncClient(
|
||||
verify=not settings.debug,
|
||||
proxies=proxies_dict, # type: ignore
|
||||
headers=headers_dict,
|
||||
base_url=self.url,
|
||||
)
|
||||
return await func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -106,16 +108,15 @@ class LedgerAPI(object):
|
||||
|
||||
mint_info: GetInfoResponse # holds info about mint
|
||||
tor: TorProxy
|
||||
s: requests.Session
|
||||
db: Database
|
||||
httpx: httpx.AsyncClient
|
||||
|
||||
def __init__(self, url: str, db: Database):
|
||||
self.url = url
|
||||
self.s = requests.Session()
|
||||
self.db = db
|
||||
self.keysets = {}
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def _init_s(self):
|
||||
"""Dummy function that can be called from outside to use LedgerAPI.s"""
|
||||
return
|
||||
@@ -262,7 +263,7 @@ class LedgerAPI(object):
|
||||
ENDPOINTS
|
||||
"""
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def _get_keys(self, url: str) -> WalletKeyset:
|
||||
"""API that gets the current keys of the mint
|
||||
|
||||
@@ -275,7 +276,7 @@ class LedgerAPI(object):
|
||||
Raises:
|
||||
Exception: If no keys are received from the mint
|
||||
"""
|
||||
resp = self.s.get(
|
||||
resp = await self.httpx.get(
|
||||
join(url, "keys"),
|
||||
)
|
||||
self.raise_on_error(resp)
|
||||
@@ -288,7 +289,7 @@ class LedgerAPI(object):
|
||||
keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url)
|
||||
return keyset
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def _get_keys_of_keyset(self, url: str, keyset_id: str) -> WalletKeyset:
|
||||
"""API that gets the keys of a specific keyset from the mint.
|
||||
|
||||
@@ -304,7 +305,7 @@ class LedgerAPI(object):
|
||||
Exception: If no keys are received from the mint
|
||||
"""
|
||||
keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_")
|
||||
resp = self.s.get(
|
||||
resp = await self.httpx.get(
|
||||
join(url, f"keys/{keyset_id_urlsafe}"),
|
||||
)
|
||||
self.raise_on_error(resp)
|
||||
@@ -317,7 +318,7 @@ class LedgerAPI(object):
|
||||
keyset = WalletKeyset(id=keyset_id, public_keys=keyset_keys, mint_url=url)
|
||||
return keyset
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def _get_keyset_ids(self, url: str) -> List[str]:
|
||||
"""API that gets a list of all active keysets of the mint.
|
||||
|
||||
@@ -330,7 +331,7 @@ class LedgerAPI(object):
|
||||
Raises:
|
||||
Exception: If no keysets are received from the mint
|
||||
"""
|
||||
resp = self.s.get(
|
||||
resp = await self.httpx.get(
|
||||
join(url, "keysets"),
|
||||
)
|
||||
self.raise_on_error(resp)
|
||||
@@ -339,7 +340,7 @@ class LedgerAPI(object):
|
||||
assert len(keysets.keysets), Exception("did not receive any keysets")
|
||||
return keysets.keysets
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def _get_info(self, url: str) -> GetInfoResponse:
|
||||
"""API that gets the mint info.
|
||||
|
||||
@@ -352,7 +353,7 @@ class LedgerAPI(object):
|
||||
Raises:
|
||||
Exception: If the mint info request fails
|
||||
"""
|
||||
resp = self.s.get(
|
||||
resp = await self.httpx.get(
|
||||
join(url, "info"),
|
||||
)
|
||||
self.raise_on_error(resp)
|
||||
@@ -360,7 +361,7 @@ class LedgerAPI(object):
|
||||
mint_info: GetInfoResponse = GetInfoResponse.parse_obj(data)
|
||||
return mint_info
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def request_mint(self, amount) -> Invoice:
|
||||
"""Requests a mint from the server and returns Lightning invoice.
|
||||
|
||||
@@ -374,21 +375,29 @@ class LedgerAPI(object):
|
||||
Exception: If the mint request fails
|
||||
"""
|
||||
logger.trace("Requesting mint: GET /mint")
|
||||
resp = self.s.get(join(self.url, "mint"), params={"amount": amount})
|
||||
resp = await self.httpx.get(join(self.url, "mint"), params={"amount": amount})
|
||||
self.raise_on_error(resp)
|
||||
return_dict = resp.json()
|
||||
mint_response = GetMintResponse.parse_obj(return_dict)
|
||||
return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash)
|
||||
decoded_invoice = bolt11.decode(mint_response.pr)
|
||||
return Invoice(
|
||||
amount=amount,
|
||||
bolt11=mint_response.pr,
|
||||
payment_hash=decoded_invoice.payment_hash,
|
||||
id=mint_response.hash,
|
||||
out=False,
|
||||
time_created=int(time.time()),
|
||||
)
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def mint(
|
||||
self, outputs: List[BlindedMessage], hash: Optional[str] = None
|
||||
self, outputs: List[BlindedMessage], id: Optional[str] = None
|
||||
) -> List[BlindedSignature]:
|
||||
"""Mints new coins and returns a proof of promise.
|
||||
|
||||
Args:
|
||||
outputs (List[BlindedMessage]): Outputs to mint new tokens with
|
||||
hash (str, optional): Hash of the paid invoice. Defaults to None.
|
||||
id (str, optional): Id of the paid invoice. Defaults to None.
|
||||
|
||||
Returns:
|
||||
list[Proof]: List of proofs.
|
||||
@@ -398,12 +407,12 @@ class LedgerAPI(object):
|
||||
"""
|
||||
outputs_payload = PostMintRequest(outputs=outputs)
|
||||
logger.trace("Checking Lightning invoice. POST /mint")
|
||||
resp = self.s.post(
|
||||
resp = await self.httpx.post(
|
||||
join(self.url, "mint"),
|
||||
json=outputs_payload.dict(),
|
||||
params={
|
||||
"hash": hash,
|
||||
"payment_hash": hash, # backwards compatibility pre 0.12.0
|
||||
"hash": id,
|
||||
"payment_hash": id, # backwards compatibility pre 0.12.0
|
||||
},
|
||||
)
|
||||
self.raise_on_error(resp)
|
||||
@@ -412,7 +421,7 @@ class LedgerAPI(object):
|
||||
promises = PostMintResponse.parse_obj(response_dict).promises
|
||||
return promises
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def split(
|
||||
self,
|
||||
proofs: List[Proof],
|
||||
@@ -437,7 +446,7 @@ class LedgerAPI(object):
|
||||
"proofs": {i: proofs_include for i in range(len(proofs))},
|
||||
}
|
||||
|
||||
resp = self.s.post(
|
||||
resp = await self.httpx.post(
|
||||
join(self.url, "split"),
|
||||
json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore
|
||||
)
|
||||
@@ -451,7 +460,7 @@ class LedgerAPI(object):
|
||||
|
||||
return promises
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def check_proof_state(self, proofs: List[Proof]):
|
||||
"""
|
||||
Checks whether the secrets in proofs are already spent or not and returns a list of booleans.
|
||||
@@ -464,7 +473,7 @@ class LedgerAPI(object):
|
||||
"proofs": {i: {"secret"} for i in range(len(proofs))},
|
||||
}
|
||||
|
||||
resp = self.s.post(
|
||||
resp = await self.httpx.post(
|
||||
join(self.url, "check"),
|
||||
json=payload.dict(include=_check_proof_state_include_fields(proofs)), # type: ignore
|
||||
)
|
||||
@@ -474,11 +483,11 @@ class LedgerAPI(object):
|
||||
states = CheckSpendableResponse.parse_obj(return_dict)
|
||||
return states
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def check_fees(self, payment_request: str):
|
||||
"""Checks whether the Lightning payment is internal."""
|
||||
payload = CheckFeesRequest(pr=payment_request)
|
||||
resp = self.s.post(
|
||||
resp = await self.httpx.post(
|
||||
join(self.url, "checkfees"),
|
||||
json=payload.dict(),
|
||||
)
|
||||
@@ -487,15 +496,16 @@ class LedgerAPI(object):
|
||||
return_dict = resp.json()
|
||||
return return_dict
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def pay_lightning(
|
||||
self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]]
|
||||
):
|
||||
) -> GetMeltResponse:
|
||||
"""
|
||||
Accepts proofs and a lightning invoice to pay in exchange.
|
||||
"""
|
||||
|
||||
payload = PostMeltRequest(proofs=proofs, pr=invoice, outputs=outputs)
|
||||
logger.debug("Calling melt. POST /melt")
|
||||
|
||||
def _meltrequest_include_fields(proofs: List[Proof]):
|
||||
"""strips away fields from the model that aren't necessary for the /melt"""
|
||||
@@ -506,16 +516,17 @@ class LedgerAPI(object):
|
||||
"outputs": ...,
|
||||
}
|
||||
|
||||
resp = self.s.post(
|
||||
resp = await self.httpx.post(
|
||||
join(self.url, "melt"),
|
||||
json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore
|
||||
timeout=None,
|
||||
)
|
||||
self.raise_on_error(resp)
|
||||
return_dict = resp.json()
|
||||
|
||||
return GetMeltResponse.parse_obj(return_dict)
|
||||
|
||||
@async_set_requests
|
||||
@async_set_httpx_client
|
||||
async def restore_promises(
|
||||
self, outputs: List[BlindedMessage]
|
||||
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
|
||||
@@ -523,7 +534,7 @@ class LedgerAPI(object):
|
||||
Asks the mint to restore promises corresponding to outputs.
|
||||
"""
|
||||
payload = PostMintRequest(outputs=outputs)
|
||||
resp = self.s.post(join(self.url, "restore"), json=payload.dict())
|
||||
resp = await self.httpx.post(join(self.url, "restore"), json=payload.dict())
|
||||
self.raise_on_error(resp)
|
||||
response_dict = resp.json()
|
||||
returnObj = PostRestoreResponse.parse_obj(response_dict)
|
||||
@@ -620,7 +631,6 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
Invoice: Lightning invoice
|
||||
"""
|
||||
invoice = await super().request_mint(amount)
|
||||
invoice.time_created = int(time.time())
|
||||
await store_lightning_invoice(db=self.db, invoice=invoice)
|
||||
return invoice
|
||||
|
||||
@@ -628,14 +638,14 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
self,
|
||||
amount: int,
|
||||
split: Optional[List[int]] = None,
|
||||
hash: Optional[str] = None,
|
||||
id: Optional[str] = None,
|
||||
) -> List[Proof]:
|
||||
"""Mint tokens of a specific amount after an invoice has been paid.
|
||||
|
||||
Args:
|
||||
amount (int): Total amount of tokens to be minted
|
||||
split (Optional[List[str]], optional): List of desired amount splits to be minted. Total must sum to `amount`.
|
||||
hash (Optional[str], optional): Hash for looking up the paid Lightning invoice. Defaults to None (for testing with LIGHTNING=False).
|
||||
id (Optional[str], optional): Id for looking up the paid Lightning invoice. Defaults to None (for testing with LIGHTNING=False).
|
||||
|
||||
Raises:
|
||||
Exception: Raises exception if `amounts` does not sum to `amount` or has unsupported value.
|
||||
@@ -668,7 +678,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
outputs, rs = self._construct_outputs(amounts, secrets, rs)
|
||||
|
||||
# will raise exception if mint is unsuccessful
|
||||
promises = await super().mint(outputs, hash)
|
||||
promises = await super().mint(outputs, id)
|
||||
|
||||
# success, bump secret counter in database
|
||||
await bump_secret_derivation(
|
||||
@@ -676,10 +686,15 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
)
|
||||
proofs = await self._construct_proofs(promises, secrets, rs, derivation_paths)
|
||||
|
||||
if hash:
|
||||
if id:
|
||||
await update_lightning_invoice(
|
||||
db=self.db, hash=hash, paid=True, time_paid=int(time.time())
|
||||
db=self.db, id=id, paid=True, time_paid=int(time.time())
|
||||
)
|
||||
# store the mint_id in proofs
|
||||
async with self.db.connect() as conn:
|
||||
for p in proofs:
|
||||
p.mint_id = id
|
||||
await update_proof(p, mint_id=id, conn=conn)
|
||||
return proofs
|
||||
|
||||
async def redeem(
|
||||
@@ -782,7 +797,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
|
||||
async def pay_lightning(
|
||||
self, proofs: List[Proof], invoice: str, fee_reserve_sat: int
|
||||
) -> bool:
|
||||
) -> GetMeltResponse:
|
||||
"""Pays a lightning invoice and returns the status of the payment.
|
||||
|
||||
Args:
|
||||
@@ -795,41 +810,72 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
# Generate a number of blank outputs for any overpaid fees. As described in
|
||||
# NUT-08, the mint will imprint these outputs with a value depending on the
|
||||
# amount of fees we overpaid.
|
||||
n_return_outputs = calculate_number_of_blank_outputs(fee_reserve_sat)
|
||||
secrets, rs, derivation_paths = await self.generate_n_secrets(n_return_outputs)
|
||||
outputs, rs = self._construct_outputs(n_return_outputs * [0], secrets, rs)
|
||||
n_change_outputs = calculate_number_of_blank_outputs(fee_reserve_sat)
|
||||
change_secrets, change_rs, change_derivation_paths = (
|
||||
await self.generate_n_secrets(n_change_outputs)
|
||||
)
|
||||
change_outputs, change_rs = self._construct_outputs(
|
||||
n_change_outputs * [1], change_secrets, change_rs
|
||||
)
|
||||
|
||||
status = await super().pay_lightning(proofs, invoice, outputs)
|
||||
# we store the invoice object in the database to later be able to check the invoice state
|
||||
# generate a random ID for this transaction
|
||||
melt_id = await self._generate_secret()
|
||||
|
||||
if status.paid:
|
||||
# the payment was successful
|
||||
invoice_obj = Invoice(
|
||||
amount=-sum_proofs(proofs),
|
||||
pr=invoice,
|
||||
preimage=status.preimage,
|
||||
paid=True,
|
||||
time_paid=time.time(),
|
||||
hash="",
|
||||
)
|
||||
# we have a unique constraint on the hash, so we generate a random one if it doesn't exist
|
||||
invoice_obj.hash = invoice_obj.hash or await self._generate_secret()
|
||||
await store_lightning_invoice(db=self.db, invoice=invoice_obj)
|
||||
# store the melt_id in proofs
|
||||
async with self.db.connect() as conn:
|
||||
for p in proofs:
|
||||
p.melt_id = melt_id
|
||||
await update_proof(p, melt_id=melt_id, conn=conn)
|
||||
|
||||
# handle change and produce proofs
|
||||
if status.change:
|
||||
change_proofs = await self._construct_proofs(
|
||||
status.change,
|
||||
secrets[: len(status.change)],
|
||||
rs[: len(status.change)],
|
||||
derivation_paths[: len(status.change)],
|
||||
)
|
||||
logger.debug(f"Received change: {sum_proofs(change_proofs)} sat")
|
||||
decoded_invoice = bolt11.decode(invoice)
|
||||
invoice_obj = Invoice(
|
||||
amount=-sum_proofs(proofs),
|
||||
bolt11=invoice,
|
||||
payment_hash=decoded_invoice.payment_hash,
|
||||
# preimage=status.preimage,
|
||||
paid=False,
|
||||
time_paid=int(time.time()),
|
||||
id=melt_id, # store the same ID in the invoice
|
||||
out=True, # outgoing invoice
|
||||
)
|
||||
# store invoice in db as not paid yet
|
||||
await store_lightning_invoice(db=self.db, invoice=invoice_obj)
|
||||
|
||||
await self.invalidate(proofs)
|
||||
status = await super().pay_lightning(proofs, invoice, change_outputs)
|
||||
|
||||
else:
|
||||
# if payment fails
|
||||
if not status.paid:
|
||||
# remove the melt_id in proofs
|
||||
for p in proofs:
|
||||
p.melt_id = None
|
||||
await update_proof(p, melt_id=None, db=self.db)
|
||||
raise Exception("could not pay invoice.")
|
||||
return status.paid
|
||||
|
||||
# invoice was paid successfully
|
||||
# we don't have to recheck the spendable sate of these tokens when invalidating
|
||||
await self.invalidate(proofs, check_spendable=False)
|
||||
|
||||
# update paid status in db
|
||||
logger.trace(f"Settings invoice {melt_id} to paid.")
|
||||
await update_lightning_invoice(
|
||||
db=self.db,
|
||||
id=melt_id,
|
||||
paid=True,
|
||||
time_paid=int(time.time()),
|
||||
preimage=status.preimage,
|
||||
)
|
||||
|
||||
# handle change and produce proofs
|
||||
if status.change:
|
||||
change_proofs = await self._construct_proofs(
|
||||
status.change,
|
||||
change_secrets[: len(status.change)],
|
||||
change_rs[: len(status.change)],
|
||||
change_derivation_paths[: len(status.change)],
|
||||
)
|
||||
logger.debug(f"Received change: {sum_proofs(change_proofs)} sat")
|
||||
return status
|
||||
|
||||
async def check_proof_state(self, proofs):
|
||||
return await super().check_proof_state(proofs)
|
||||
@@ -965,7 +1011,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
|
||||
async def _store_proofs(self, proofs):
|
||||
try:
|
||||
async with self.db.connect() as conn: # type: ignore
|
||||
async with self.db.connect() as conn:
|
||||
for proof in proofs:
|
||||
await store_proof(proof, db=self.db, conn=conn)
|
||||
except Exception as e:
|
||||
@@ -1171,7 +1217,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
proof_to_add = sorted_proofs_of_current_keyset.pop()
|
||||
send_proofs.append(proof_to_add)
|
||||
|
||||
logger.debug(f"selected proof amounts: {[p.amount for p in send_proofs]}")
|
||||
logger.trace(f"selected proof amounts: {[p.amount for p in send_proofs]}")
|
||||
return send_proofs
|
||||
|
||||
async def set_reserved(self, proofs: List[Proof], reserved: bool) -> None:
|
||||
@@ -1184,9 +1230,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
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
|
||||
)
|
||||
await update_proof(proof, reserved=reserved, send_id=uuid_str, db=self.db)
|
||||
|
||||
async def invalidate(
|
||||
self, proofs: List[Proof], check_spendable=True
|
||||
@@ -1232,7 +1276,8 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
Decodes the amount from a Lightning invoice and returns the
|
||||
total amount (amount+fees) to be paid.
|
||||
"""
|
||||
decoded_invoice: InvoiceBolt11 = bolt11.decode(invoice)
|
||||
decoded_invoice = bolt11.decode(invoice)
|
||||
assert decoded_invoice.amount_msat, "invoices has no amount."
|
||||
# check if it's an internal payment
|
||||
fees = int((await self.check_fees(invoice))["fee"])
|
||||
logger.debug(f"Mint wants {fees} sat as fee reserve.")
|
||||
|
||||
1
mypy.ini
1
mypy.ini
@@ -1,6 +1,7 @@
|
||||
[mypy]
|
||||
python_version = 3.9
|
||||
# disallow_untyped_defs = True
|
||||
; check_untyped_defs = True
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-cashu.nostr.*]
|
||||
|
||||
1020
poetry.lock
generated
1020
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,9 @@ SQLAlchemy = "^1.3.24"
|
||||
click = "^8.1.7"
|
||||
pydantic = "^1.10.2"
|
||||
bech32 = "^1.2.0"
|
||||
fastapi = "^0.101.1"
|
||||
fastapi = "0.103.0"
|
||||
environs = "^9.5.0"
|
||||
uvicorn = "^0.18.3"
|
||||
uvicorn = "0.23.2"
|
||||
loguru = "^0.7.0"
|
||||
ecdsa = "^0.18.0"
|
||||
bitstring = "^3.1.9"
|
||||
@@ -29,9 +29,10 @@ setuptools = "^68.1.2"
|
||||
wheel = "^0.41.1"
|
||||
importlib-metadata = "^6.8.0"
|
||||
psycopg2-binary = { version = "^2.9.7", optional = true }
|
||||
httpx = "^0.24.1"
|
||||
httpx = "0.25.0"
|
||||
bip32 = "^3.4"
|
||||
mnemonic = "^0.20"
|
||||
bolt11 = "^2.0.5"
|
||||
|
||||
[tool.poetry.extras]
|
||||
pgsql = ["psycopg2-binary"]
|
||||
|
||||
@@ -14,6 +14,7 @@ from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import settings
|
||||
from cashu.lightning.fake import FakeWallet
|
||||
from cashu.mint import migrations as migrations_mint
|
||||
from cashu.mint.crud import LedgerCrud
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
SERVER_PORT = 3337
|
||||
@@ -64,6 +65,7 @@ async def ledger():
|
||||
seed=settings.mint_private_key,
|
||||
derivation_path=settings.mint_derivation_path,
|
||||
lightning=FakeWallet(),
|
||||
crud=LedgerCrud(),
|
||||
)
|
||||
await start_mint_init(ledger)
|
||||
yield ledger
|
||||
|
||||
@@ -66,14 +66,14 @@ async def test_get_keyset(ledger: Ledger):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger):
|
||||
invoice, payment_hash = await ledger.request_mint(8)
|
||||
invoice, id = await ledger.request_mint(8)
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
)
|
||||
]
|
||||
promises = await ledger.mint(blinded_messages_mock, hash=payment_hash)
|
||||
promises = await ledger.mint(blinded_messages_mock, id=id)
|
||||
assert len(promises)
|
||||
assert promises[0].amount == 8
|
||||
assert (
|
||||
@@ -84,7 +84,7 @@ async def test_mint(ledger: Ledger):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
invoice, payment_hash = await ledger.request_mint(8)
|
||||
invoice, id = await ledger.request_mint(8)
|
||||
blinded_messages_mock_invalid_key = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
@@ -92,7 +92,7 @@ async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
)
|
||||
]
|
||||
await assert_err(
|
||||
ledger.mint(blinded_messages_mock_invalid_key, hash=payment_hash),
|
||||
ledger.mint(blinded_messages_mock_invalid_key, id=id),
|
||||
"invalid public key",
|
||||
)
|
||||
|
||||
|
||||
@@ -23,23 +23,25 @@ async def wallet1(mint):
|
||||
async def test_melt(wallet1: Wallet, ledger: Ledger):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
assert wallet1.balance == 128
|
||||
total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(invoice.pr)
|
||||
mint_fees = await ledger.get_melt_fees(invoice.pr)
|
||||
total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(
|
||||
invoice.bolt11
|
||||
)
|
||||
mint_fees = await ledger.get_melt_fees(invoice.bolt11)
|
||||
assert mint_fees == fee_reserve_sat
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
||||
|
||||
await ledger.melt(send_proofs, invoice.pr, outputs=None)
|
||||
await ledger.melt(send_proofs, invoice.bolt11, outputs=None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split(wallet1: Wallet, ledger: Ledger):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10)
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(len(send_proofs))
|
||||
@@ -55,7 +57,7 @@ async def test_split(wallet1: Wallet, ledger: Ledger):
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from cashu.core.base import Proof
|
||||
from cashu.core.errors import CashuError, KeysetNotFoundError
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.crud import get_lightning_invoice, get_proofs
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
@@ -137,16 +138,28 @@ async def test_get_keyset_ids(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# verify that proofs in proofs_used db have the same mint_id as the invoice in the db
|
||||
assert invoice.payment_hash
|
||||
invoice_db = await get_lightning_invoice(
|
||||
db=wallet1.db, payment_hash=invoice.payment_hash, out=False
|
||||
)
|
||||
assert invoice_db
|
||||
proofs_minted = await get_proofs(
|
||||
db=wallet1.db, mint_id=invoice_db.id, table="proofs"
|
||||
)
|
||||
assert len(proofs_minted) == 1
|
||||
assert all([p.mint_id == invoice.id for p in proofs_minted])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_amounts(wallet1: Wallet):
|
||||
"""Mint predefined amounts"""
|
||||
invoice = await wallet1.request_mint(64)
|
||||
amts = [1, 1, 1, 2, 2, 4, 16]
|
||||
await wallet1.mint(amount=sum(amts), split=amts, hash=invoice.hash)
|
||||
await wallet1.mint(amount=sum(amts), split=amts, id=invoice.id)
|
||||
assert wallet1.balance == 27
|
||||
assert wallet1.proof_amounts == amts
|
||||
|
||||
@@ -174,7 +187,7 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_split(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
assert wallet1.balance == 64
|
||||
p1, p2 = await wallet1.split(wallet1.proofs, 20)
|
||||
assert wallet1.balance == 64
|
||||
@@ -189,7 +202,7 @@ async def test_split(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_to_send(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
keep_proofs, spendable_proofs = await wallet1.split_to_send(
|
||||
wallet1.proofs, 32, set_reserved=True
|
||||
)
|
||||
@@ -204,7 +217,7 @@ async def test_split_to_send(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_more_than_balance(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs, 128),
|
||||
# "Mint Error: inputs do not have same amount as outputs",
|
||||
@@ -217,24 +230,45 @@ async def test_split_more_than_balance(wallet1: Wallet):
|
||||
async def test_melt(wallet1: Wallet):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
assert wallet1.balance == 128
|
||||
total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(invoice.pr)
|
||||
|
||||
total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(
|
||||
invoice.bolt11
|
||||
)
|
||||
assert total_amount == 66
|
||||
|
||||
assert fee_reserve_sat == 2
|
||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
||||
|
||||
await wallet1.pay_lightning(
|
||||
send_proofs, invoice=invoice.pr, fee_reserve_sat=fee_reserve_sat
|
||||
send_proofs, invoice=invoice.bolt11, fee_reserve_sat=fee_reserve_sat
|
||||
)
|
||||
|
||||
# verify that proofs in proofs_used db have the same melt_id as the invoice in the db
|
||||
assert invoice.payment_hash
|
||||
invoice_db = await get_lightning_invoice(
|
||||
db=wallet1.db, payment_hash=invoice.payment_hash, out=True
|
||||
)
|
||||
assert invoice_db
|
||||
proofs_used = await get_proofs(
|
||||
db=wallet1.db, melt_id=invoice_db.id, table="proofs_used"
|
||||
)
|
||||
|
||||
assert len(proofs_used) == len(send_proofs)
|
||||
assert all([p.melt_id == invoice_db.id for p in proofs_used])
|
||||
|
||||
# the payment was without fees so we need to remove it from the total amount
|
||||
assert wallet1.balance == 128 - (total_amount - fee_reserve_sat)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_to_send_more_than_balance(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await assert_err(
|
||||
wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True),
|
||||
"balance too low.",
|
||||
@@ -246,7 +280,7 @@ async def test_split_to_send_more_than_balance(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_double_spend(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
doublespend = await wallet1.mint(64, hash=invoice.hash)
|
||||
doublespend = await wallet1.mint(64, id=invoice.id)
|
||||
await wallet1.split(wallet1.proofs, 20)
|
||||
await assert_err(
|
||||
wallet1.split(doublespend, 20),
|
||||
@@ -259,7 +293,7 @@ async def test_double_spend(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_proofs_double_spent(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
doublespend = await wallet1.mint(64, hash=invoice.hash)
|
||||
doublespend = await wallet1.mint(64, id=invoice.id)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs + doublespend, 20),
|
||||
"Mint Error: proofs already pending.",
|
||||
@@ -271,7 +305,7 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
_, spendable_proofs = await wallet1.split_to_send(
|
||||
wallet1.proofs, 32, set_reserved=True
|
||||
)
|
||||
@@ -289,7 +323,7 @@ async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
|
||||
async def test_invalidate_unspent_proofs(wallet1: Wallet):
|
||||
"""Try to invalidate proofs that have not been spent yet. Should not work!"""
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await wallet1.invalidate(wallet1.proofs)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
@@ -298,7 +332,7 @@ async def test_invalidate_unspent_proofs(wallet1: Wallet):
|
||||
async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet):
|
||||
"""Try to invalidate proofs that have not been spent yet but force no check."""
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await wallet1.invalidate(wallet1.proofs, check_spendable=False)
|
||||
assert wallet1.balance == 0
|
||||
|
||||
@@ -306,7 +340,7 @@ async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_invalid_amount(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs, -1),
|
||||
"amount must be positive.",
|
||||
@@ -316,7 +350,7 @@ async def test_split_invalid_amount(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_state(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
assert wallet1.balance == 64
|
||||
resp = await wallet1.check_proof_state(wallet1.proofs)
|
||||
assert resp.dict()["spendable"]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from cashu.core.settings import settings
|
||||
from cashu.lightning.base import InvoiceResponse, PaymentStatus
|
||||
from cashu.wallet.api.app import app
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
@@ -23,25 +25,19 @@ async def wallet(mint):
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoice(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/invoice?amount=100")
|
||||
response = client.post("/lightning/create_invoice?amount=100")
|
||||
assert response.status_code == 200
|
||||
if settings.lightning:
|
||||
assert response.json()["invoice"]
|
||||
else:
|
||||
assert response.json()["amount"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoice_with_split(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/invoice?amount=10&split=1")
|
||||
assert response.status_code == 200
|
||||
if settings.lightning:
|
||||
assert response.json()["invoice"]
|
||||
else:
|
||||
assert response.json()["amount"]
|
||||
# await wallet.load_proofs(reload=True)
|
||||
# assert wallet.proof_amounts.count(1) >= 10
|
||||
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||
state = PaymentStatus(paid=False)
|
||||
while not state.paid:
|
||||
print("checking invoice state")
|
||||
response2 = client.get(
|
||||
f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}"
|
||||
)
|
||||
state = PaymentStatus.parse_obj(response2.json())
|
||||
await asyncio.sleep(0.1)
|
||||
print("state:", state)
|
||||
print("paid")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -49,7 +45,7 @@ async def test_balance():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/balance")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["balance"]
|
||||
assert "balance" in response.json()
|
||||
assert response.json()["keysets"]
|
||||
assert response.json()["mints"]
|
||||
|
||||
@@ -89,7 +85,7 @@ async def test_receive_all(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/receive?all=true")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["initial_balance"] == 0
|
||||
assert response.json()["initial_balance"]
|
||||
assert response.json()["balance"]
|
||||
|
||||
|
||||
@@ -100,24 +96,21 @@ async def test_burn_all(wallet: Wallet):
|
||||
assert response.status_code == 200
|
||||
response = client.post("/burn?all=true")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["balance"] == 0
|
||||
assert response.json()["balance"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pay():
|
||||
with TestClient(app) as client:
|
||||
invoice = (
|
||||
"lnbc100n1pjzp22cpp58xvjxvagzywky9xz3vurue822aaax"
|
||||
"735hzc5pj5fg307y58v5znqdq4vdshx6r4ypjx2ur0wd5hgl"
|
||||
"h6ahauv24wdmac4zk478pmwfzd7sdvm8tje3dmfue3lc2g4l"
|
||||
"9g40a073h39748uez9p8mxws5vqwjmkqr4wl5l7n4dlhj6z6"
|
||||
"va963cqvufrs4"
|
||||
"lnbc100n1pjjcqzfdq4gdshx6r4ypjx2ur0wd5hgpp58xvj8yn00d5"
|
||||
"7uhshwzcwgy9uj3vwf5y2lr5fjf78s4w9l4vhr6xssp5stezsyty9r"
|
||||
"hv3lat69g4mhqxqun56jyehhkq3y8zufh83xyfkmmq4usaqwrt5q4f"
|
||||
"adm44g6crckp0hzvuyv9sja7t65hxj0ucf9y46qstkay7gfnwhuxgr"
|
||||
"krf7djs38rml39l8wpn5ug9shp3n55quxhdecqfwxg23"
|
||||
)
|
||||
response = client.post(f"/pay?invoice={invoice}")
|
||||
if not settings.lightning:
|
||||
assert response.status_code == 400
|
||||
else:
|
||||
assert response.status_code == 200
|
||||
response = client.post(f"/lightning/pay_invoice?bolt11={invoice}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -159,21 +152,31 @@ async def test_info():
|
||||
@pytest.mark.asyncio
|
||||
async def test_flow(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
if not settings.lightning:
|
||||
response = client.get("/balance")
|
||||
initial_balance = response.json()["balance"]
|
||||
response = client.post("/invoice?amount=100")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + 100
|
||||
response = client.post("/send?amount=50")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + 50
|
||||
response = client.post("/send?amount=50")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance
|
||||
response = client.get("/pending")
|
||||
token = response.json()["pending_token"]["0"]["token"]
|
||||
amount = response.json()["pending_token"]["0"]["amount"]
|
||||
response = client.post(f"/receive?token={token}")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + amount
|
||||
response = client.get("/balance")
|
||||
initial_balance = response.json()["balance"]
|
||||
response = client.post("/lightning/create_invoice?amount=100")
|
||||
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||
state = PaymentStatus(paid=False)
|
||||
while not state.paid:
|
||||
print("checking invoice state")
|
||||
response2 = client.get(
|
||||
f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}"
|
||||
)
|
||||
state = PaymentStatus.parse_obj(response2.json())
|
||||
await asyncio.sleep(0.1)
|
||||
print("state:", state)
|
||||
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + 100
|
||||
response = client.post("/send?amount=50")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + 50
|
||||
response = client.post("/send?amount=50")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance
|
||||
response = client.get("/pending")
|
||||
token = response.json()["pending_token"]["0"]["token"]
|
||||
amount = response.json()["pending_token"]["0"]["amount"]
|
||||
response = client.post(f"/receive?token={token}")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + amount
|
||||
|
||||
@@ -59,7 +59,7 @@ async def wallet2(mint):
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_htlc_secret(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
@@ -69,7 +69,7 @@ async def test_create_htlc_secret(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
@@ -82,7 +82,7 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
@@ -96,7 +96,7 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
@@ -114,7 +114,7 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet)
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -134,7 +134,7 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -159,7 +159,7 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -181,7 +181,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
@@ -215,7 +215,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
|
||||
@@ -60,7 +60,7 @@ async def wallet2(mint):
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_p2pk_pubkey(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
PublicKey(bytes.fromhex(pubkey), raw=True)
|
||||
|
||||
@@ -68,7 +68,7 @@ async def test_create_p2pk_pubkey(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side
|
||||
@@ -81,7 +81,7 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
@@ -96,7 +96,7 @@ async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side
|
||||
@@ -116,7 +116,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
@@ -141,7 +141,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key(
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
@@ -169,7 +169,7 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
@@ -204,7 +204,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
@@ -235,7 +235,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
@@ -256,7 +256,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
@@ -279,7 +279,7 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
@@ -299,7 +299,7 @@ async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wal
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
@@ -323,7 +323,7 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
@@ -340,7 +340,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
await wallet1.mint(64, hash=invoice.hash)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
wrong_pubklic_key = PrivateKey().pubkey
|
||||
|
||||
@@ -147,7 +147,7 @@ async def test_generate_secrets_from_to(wallet3: Wallet):
|
||||
async def test_restore_wallet_after_mint(wallet3: Wallet):
|
||||
await reset_wallet_db(wallet3)
|
||||
invoice = await wallet3.request_mint(64)
|
||||
await wallet3.mint(64, hash=invoice.hash)
|
||||
await wallet3.mint(64, id=invoice.id)
|
||||
assert wallet3.balance == 64
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs()
|
||||
@@ -177,7 +177,7 @@ async def test_restore_wallet_after_split_to_send(wallet3: Wallet):
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
invoice = await wallet3.request_mint(64)
|
||||
await wallet3.mint(64, hash=invoice.hash)
|
||||
await wallet3.mint(64, id=invoice.id)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
||||
@@ -199,7 +199,7 @@ async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: W
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
invoice = await wallet3.request_mint(64)
|
||||
await wallet3.mint(64, hash=invoice.hash)
|
||||
await wallet3.mint(64, id=invoice.id)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
||||
@@ -239,7 +239,7 @@ async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet):
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
invoice = await wallet3.request_mint(64)
|
||||
await wallet3.mint(64, hash=invoice.hash)
|
||||
await wallet3.mint(64, id=invoice.id)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
||||
@@ -265,7 +265,7 @@ async def test_restore_wallet_after_send_twice(
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
invoice = await wallet3.request_mint(2)
|
||||
await wallet3.mint(2, hash=invoice.hash)
|
||||
await wallet3.mint(2, id=invoice.id)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
@@ -319,7 +319,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
invoice = await wallet3.request_mint(64)
|
||||
await wallet3.mint(64, hash=invoice.hash)
|
||||
await wallet3.mint(64, id=invoice.id)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
|
||||
Reference in New Issue
Block a user