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:
callebtc
2023-10-21 14:38:16 +02:00
committed by GitHub
parent 8a4813aee6
commit 0490f20932
41 changed files with 1899 additions and 1664 deletions

View File

@@ -11,7 +11,7 @@ black-check:
poetry run black . --check poetry run black . --check
mypy: mypy:
poetry run mypy cashu --ignore-missing poetry run mypy cashu --ignore-missing --check-untyped-defs
format: black ruff format: black ruff

View File

@@ -140,7 +140,7 @@ This command will return a Lightning invoice that you need to pay to mint new ec
cashu invoice 420 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 #### Pay a Lightning invoice
```bash ```bash

View File

@@ -88,10 +88,16 @@ class Proof(BaseModel):
time_created: Union[None, str] = "" time_created: Union[None, str] = ""
time_reserved: Union[None, str] = "" time_reserved: Union[None, str] = ""
derivation_path: Union[None, str] = "" # derivation path of the proof 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 @classmethod
def from_dict(cls, proof_dict: dict): 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"])) proof_dict["dleq"] = DLEQWallet(**json.loads(proof_dict["dleq"]))
c = cls(**proof_dict) c = cls(**proof_dict)
return c return c
@@ -181,8 +187,9 @@ class BlindedMessages(BaseModel):
class Invoice(BaseModel): class Invoice(BaseModel):
amount: int amount: int
pr: str bolt11: str
hash: str id: str
out: Union[None, bool] = None
payment_hash: Union[None, str] = None payment_hash: Union[None, str] = None
preimage: Union[str, None] = None preimage: Union[str, None] = None
issued: Union[None, bool] = False issued: Union[None, bool] = False

View File

@@ -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

View File

@@ -3,7 +3,7 @@ import datetime
import os import os
import time import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Optional from typing import Optional, Union
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection from sqlalchemy_aio.base import AsyncConnection
@@ -118,7 +118,7 @@ class Database(Compat):
(1082, 1083, 1266), (1082, 1083, 1266),
"DATE2INT", "DATE2INT",
lambda value, curs: ( 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) # 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}" return f"{db.references_schema if db.schema else ''}{table}"

View File

@@ -3,7 +3,7 @@ import math
from functools import partial, wraps from functools import partial, wraps
from typing import List from typing import List
from ..core.base import Proof from ..core.base import BlindedSignature, Proof
from ..core.settings import settings from ..core.settings import settings
@@ -11,6 +11,10 @@ def sum_proofs(proofs: List[Proof]):
return sum([p.amount for p in proofs]) 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): def async_wrap(func):
@wraps(func) @wraps(func)
async def run(*args, loop=None, executor=None, **kwargs): async def run(*args, loop=None, executor=None, **kwargs):

View File

@@ -18,9 +18,9 @@ def hash_to_point_pre_0_3_3(secret_msg):
_hash = hashlib.sha256(msg).hexdigest().encode("utf-8") # type: ignore _hash = hashlib.sha256(msg).hexdigest().encode("utf-8") # type: ignore
try: try:
# We construct compressed pub which has x coordinate encoded with even y # 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_list = 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_list[0] = 0x02 # set first byte to represent even y coord
_hash = bytes(_hash) _hash = bytes(_hash_list)
point = PublicKey(_hash, raw=True) point = PublicKey(_hash, raw=True)
except Exception: except Exception:
msg = _hash msg = _hash

View File

@@ -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].""" """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
bits_amt = bin(amount)[::-1][:-2] bits_amt = bin(amount)[::-1][:-2]
rv = [] rv = []

View File

@@ -1,29 +1,30 @@
from abc import ABC, abstractmethod 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] error_message: Optional[str]
balance_msat: int balance_msat: int
class InvoiceResponse(NamedTuple): class InvoiceResponse(BaseModel):
ok: bool ok: bool # True: invoice created, False: failed
checking_id: Optional[str] = None # payment_hash, rpc_id checking_id: Optional[str] = None
payment_request: Optional[str] = None payment_request: Optional[str] = None
error_message: Optional[str] = None error_message: Optional[str] = None
class PaymentResponse(NamedTuple): class PaymentResponse(BaseModel):
# when ok is None it means we don't know if this succeeded ok: Optional[bool] = None # True: paid, False: failed, None: pending or unknown
ok: Optional[bool] = None checking_id: Optional[str] = None
checking_id: Optional[str] = None # payment_hash, rcp_id
fee_msat: Optional[int] = None fee_msat: Optional[int] = None
preimage: Optional[str] = None preimage: Optional[str] = None
error_message: Optional[str] = None error_message: Optional[str] = None
class PaymentStatus(NamedTuple): class PaymentStatus(BaseModel):
paid: Optional[bool] = None paid: Optional[bool] = None
fee_msat: Optional[int] = None fee_msat: Optional[int] = None
preimage: Optional[str] = None preimage: Optional[str] = None

View File

@@ -2,9 +2,18 @@ import asyncio
import hashlib import hashlib
import random import random
from datetime import datetime 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 ( from .base import (
InvoiceResponse, InvoiceResponse,
PaymentResponse, PaymentResponse,
@@ -14,6 +23,8 @@ from .base import (
) )
BRR = True BRR = True
DELAY_PAYMENT = False
STOCHASTIC_INVOICE = False
class FakeWallet(Wallet): class FakeWallet(Wallet):
@@ -31,7 +42,7 @@ class FakeWallet(Wallet):
).hex() ).hex()
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
return StatusResponse(None, 1337) return StatusResponse(error_message=None, balance_msat=1337)
async def create_invoice( async def create_invoice(
self, self,
@@ -39,65 +50,80 @@ class FakeWallet(Wallet):
memo: Optional[str] = None, memo: Optional[str] = None,
description_hash: Optional[bytes] = None, description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None, unhashed_description: Optional[bytes] = None,
**kwargs, expiry: Optional[int] = None,
payment_secret: Optional[bytes] = None,
**_,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: Dict = { tags = Tags()
"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": [],
}
if description_hash: if description_hash:
data["tags_set"] = ["h"] tags.add(TagChar.description_hash, description_hash.hex())
data["description_hash"] = description_hash
elif unhashed_description: elif unhashed_description:
data["tags_set"] = ["d"] tags.add(
data["description_hash"] = hashlib.sha256(unhashed_description).digest() TagChar.description_hash,
hashlib.sha256(unhashed_description).hexdigest(),
)
else: else:
data["tags_set"] = ["d"] tags.add(TagChar.description, memo or "")
data["memo"] = memo
data["description"] = memo if expiry:
randomHash = ( tags.add(TagChar.expire_time, expiry)
# random hash
checking_id = (
self.privkey[:6] self.privkey[:6]
+ hashlib.sha256(str(random.getrandbits(256)).encode()).hexdigest()[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: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = decode(bolt11) invoice = decode(bolt11)
# await asyncio.sleep(5)
if DELAY_PAYMENT:
await asyncio.sleep(5)
if invoice.payment_hash[:6] == self.privkey[:6] or BRR: if invoice.payment_hash[:6] == self.privkey[:6] or BRR:
await self.queue.put(invoice) await self.queue.put(invoice)
self.paid_invoices.add(invoice.payment_hash) 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: else:
return PaymentResponse( return PaymentResponse(
ok=False, error_message="Only internal invoices can be used!" ok=False, error_message="Only internal invoices can be used!"
) )
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
# paid = random.random() > 0.7 if STOCHASTIC_INVOICE:
# return PaymentStatus(paid) paid = random.random() > 0.7
return PaymentStatus(paid=paid)
paid = checking_id in self.paid_invoices or BRR 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: async def get_payment_status(self, _: str) -> PaymentStatus:
return PaymentStatus(None) return PaymentStatus(paid=None)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while True: while True:
value: Invoice = await self.queue.get() value: Bolt11 = await self.queue.get()
yield value.payment_hash yield value.payment_hash

View File

@@ -8,44 +8,155 @@ class LedgerCrud:
""" """
Database interface for Cashu mint. 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): async def get_keyset(
return await get_keyset(*args, **kwags) # type: ignore 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): async def get_lightning_invoice(
return await get_lightning_invoice(*args, **kwags) # type: ignore 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): async def get_secrets_used(
return await get_secrets_used(*args, **kwags) # type: ignore self,
db: Database,
conn: Optional[Connection] = None,
):
return await get_secrets_used(db=db, conn=conn)
async def invalidate_proof(*args, **kwags): async def invalidate_proof(
return await invalidate_proof(*args, **kwags) # type: ignore 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): async def get_proofs_pending(
return await get_proofs_pending(*args, **kwags) # type: ignore self,
db: Database,
conn: Optional[Connection] = None,
):
return await get_proofs_pending(db=db, conn=conn)
async def set_proof_pending(*args, **kwags): async def set_proof_pending(
return await set_proof_pending(*args, **kwags) # type: ignore 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): async def unset_proof_pending(
return await unset_proof_pending(*args, **kwags) # type: ignore 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): async def store_keyset(
return await store_keyset(*args, **kwags) # type: ignore 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): async def store_lightning_invoice(
return await store_lightning_invoice(*args, **kwags) # type: ignore 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): async def store_promise(
return await store_promise(*args, **kwags) # type: ignore 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): async def get_promise(
return await get_promise(*args, **kwags) # type: ignore 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): async def update_lightning_invoice(
return await update_lightning_invoice(*args, **kwags) # type: ignore 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( async def store_promise(
@@ -174,46 +285,47 @@ async def store_lightning_invoice(
await (conn or db).execute( await (conn or db).execute(
f""" f"""
INSERT INTO {table_with_schema(db, 'invoices')} INSERT INTO {table_with_schema(db, 'invoices')}
(amount, pr, hash, issued, payment_hash) (amount, bolt11, id, issued, payment_hash, out)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", """,
( (
invoice.amount, invoice.amount,
invoice.pr, invoice.bolt11,
invoice.hash, invoice.id,
invoice.issued, invoice.issued,
invoice.payment_hash, invoice.payment_hash,
invoice.out,
), ),
) )
async def get_lightning_invoice( async def get_lightning_invoice(
db: Database, db: Database,
hash: str, id: str,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ):
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'invoices')} SELECT * from {table_with_schema(db, 'invoices')}
WHERE hash = ? WHERE id = ?
""", """,
(hash,), (id,),
) )
row_dict = dict(row)
return Invoice(**row) if row else None return Invoice(**row_dict) if row_dict else None
async def update_lightning_invoice( async def update_lightning_invoice(
db: Database, db: Database,
hash: str, id: str,
issued: bool, issued: bool,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ):
await (conn or db).execute( 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, issued,
hash, id,
), ),
) )

View File

@@ -1,10 +1,10 @@
import asyncio import asyncio
import math 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 loguru import logger
from ..core import bolt11
from ..core.base import ( from ..core.base import (
DLEQ, DLEQ,
BlindedMessage, BlindedMessage,
@@ -19,7 +19,6 @@ from ..core.crypto.keys import derive_pubkey, random_hash
from ..core.crypto.secp import PublicKey from ..core.crypto.secp import PublicKey
from ..core.db import Connection, Database from ..core.db import Connection, Database
from ..core.errors import ( from ..core.errors import (
InvoiceNotPaidError,
KeysetError, KeysetError,
KeysetNotFoundError, KeysetNotFoundError,
LightningError, LightningError,
@@ -29,13 +28,14 @@ from ..core.errors import (
from ..core.helpers import fee_reserve, sum_proofs from ..core.helpers import fee_reserve, sum_proofs
from ..core.settings import settings from ..core.settings import settings
from ..core.split import amount_split from ..core.split import amount_split
from ..lightning.base import Wallet from ..lightning.base import PaymentResponse, Wallet
from ..mint.crud import LedgerCrud from ..mint.crud import LedgerCrud
from .conditions import LedgerSpendingConditions from .conditions import LedgerSpendingConditions
from .lightning import LedgerLightning
from .verification import LedgerVerification from .verification import LedgerVerification
class Ledger(LedgerVerification, LedgerSpendingConditions): class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerLightning):
locks: Dict[str, asyncio.Lock] = {} # holds multiprocessing locks locks: Dict[str, asyncio.Lock] = {} # holds multiprocessing locks
proofs_pending_lock: asyncio.Lock = ( proofs_pending_lock: asyncio.Lock = (
asyncio.Lock() asyncio.Lock()
@@ -46,8 +46,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
db: Database, db: Database,
seed: str, seed: str,
lightning: Wallet, lightning: Wallet,
crud: LedgerCrud,
derivation_path="", derivation_path="",
crud=LedgerCrud,
): ):
self.secrets_used: Set[str] = set() self.secrets_used: Set[str] = set()
self.master_key = seed self.master_key = seed
@@ -146,113 +146,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
assert keyset.public_keys, KeysetError("no public keys for this keyset") assert keyset.public_keys, KeysetError("no public keys for this keyset")
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()} 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 ------- # ------- ECASH -------
async def _invalidate_proofs(self, proofs: List[Proof]) -> None: async def _invalidate_proofs(self, proofs: List[Proof]) -> None:
@@ -343,7 +236,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
Exception: Invoice creation failed. Exception: Invoice creation failed.
Returns: 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") logger.trace("called request_mint")
if settings.mint_max_peg_in and amount > settings.mint_max_peg_in: 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.") raise NotAllowedError("Mint does not allow minting new tokens.")
logger.trace(f"requesting invoice for {amount} satoshis") logger.trace(f"requesting invoice for {amount} satoshis")
payment_request, payment_hash = await self._request_lightning_invoice(amount) invoice_response = await self._request_lightning_invoice(amount)
logger.trace(f"got invoice {payment_request} with hash {payment_hash}") logger.trace(
assert payment_request and payment_hash, LightningError( f"got invoice {invoice_response.payment_request} with check id"
"could not fetch invoice from Lightning backend" 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( invoice = Invoice(
amount=amount, amount=amount,
hash=random_hash(), id=random_hash(),
pr=payment_request, bolt11=invoice_response.payment_request,
payment_hash=payment_hash, # what we got from the backend payment_hash=invoice_response.checking_id, # what we got from the backend
issued=False, 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) await self.crud.store_lightning_invoice(invoice=invoice, db=self.db)
logger.trace(f"crud: stored invoice {invoice.hash} in db") logger.trace(f"crud: stored invoice {invoice.id} in db")
return payment_request, invoice.hash return invoice_response.payment_request, invoice.id
async def mint( async def mint(
self, self,
B_s: List[BlindedMessage], B_s: List[BlindedMessage],
hash: Optional[str] = None, id: Optional[str] = None,
keyset: Optional[MintKeyset] = None, keyset: Optional[MintKeyset] = None,
) -> List[BlindedSignature]: ) -> List[BlindedSignature]:
"""Mints a promise for coins for B_. """Mints a promise for coins for B_.
Args: Args:
B_s (List[BlindedMessage]): Outputs (blinded messages) to sign. 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. keyset (Optional[MintKeyset], optional): Keyset to use. If not provided, uses active keyset. Defaults to None.
Raises: Raises:
Exception: Lightning invvoice is not paid. Exception: Lightning invoice is not paid.
Exception: Lightning is turned on but no payment hash is provided. Exception: Lightning is turned on but no id is provided.
Exception: Something went wrong with the invoice check. Exception: Something went wrong with the invoice check.
Exception: Amount too large. Exception: Amount too large.
@@ -398,21 +294,19 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
amount_outputs = sum([b.amount for b in B_s]) amount_outputs = sum([b.amount for b in B_s])
if settings.lightning: if settings.lightning:
if not hash: if not id:
raise NotAllowedError("no hash provided.") raise NotAllowedError("no id provided.")
self.locks[hash] = ( self.locks[id] = (
self.locks.get(hash) or asyncio.Lock() self.locks.get(id) or asyncio.Lock()
) # create a new lock if it doesn't exist ) # 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 # will raise an exception if the invoice is not paid or tokens are
# already issued or the requested amount is too high # 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") logger.trace(f"crud: setting invoice {id} as issued")
await self.crud.update_lightning_invoice( await self.crud.update_lightning_invoice(id=id, issued=True, db=self.db)
hash=hash, issued=True, db=self.db del self.locks[id]
)
del self.locks[hash]
self._verify_outputs(B_s) self._verify_outputs(B_s)
@@ -439,6 +333,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
logger.trace("melt called") logger.trace("melt called")
# set proofs to pending to avoid race conditions
await self._set_proofs_pending(proofs) await self._set_proofs_pending(proofs)
try: try:
@@ -465,20 +360,19 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
if settings.lightning: if settings.lightning:
logger.trace(f"paying lightning invoice {invoice}") 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 invoice, reserve_fees_sat * 1000
) )
preimage = preimage or ""
logger.trace("paid lightning invoice") logger.trace("paid lightning invoice")
else: else:
status, preimage, paid_fee_msat = True, "preimage", 0 payment = PaymentResponse(ok=True, preimage="preimage", fee_msat=0)
logger.debug( logger.debug(
f"Melt status: {status}: preimage: {preimage}, fee_msat:" f"Melt status: {payment.ok}: preimage: {payment.preimage}, fee_msat:"
f" {paid_fee_msat}" f" {payment.fee_msat}"
) )
if not status: if not payment.ok:
raise LightningError("Lightning payment unsuccessful.") raise LightningError("Lightning payment unsuccessful.")
# melt successful, invalidate proofs # melt successful, invalidate proofs
@@ -486,11 +380,11 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# prepare change to compensate wallet for overpaid fees # prepare change to compensate wallet for overpaid fees
return_promises: List[BlindedSignature] = [] 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( return_promises = await self._generate_change_promises(
total_provided=total_provided, total_provided=total_provided,
invoice_amount=invoice_amount, invoice_amount=invoice_amount,
ln_fee_msat=paid_fee_msat, ln_fee_msat=payment.fee_msat,
outputs=outputs, outputs=outputs,
) )
@@ -501,7 +395,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# delete proofs from pending list # delete proofs from pending list
await self._unset_proofs_pending(proofs) 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: async def get_melt_fees(self, pr: str) -> int:
"""Returns the fee reserve (in sat) that a wallet must add to its proofs """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, # 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 # if id does not exist (not internal), it returns paid = None
amount_msat = 0
if settings.lightning: if settings.lightning:
decoded_invoice = bolt11.decode(pr) 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( logger.trace(
"get_melt_fees: checking lightning invoice:" "get_melt_fees: checking lightning invoice:"
f" {decoded_invoice.payment_hash}" f" {decoded_invoice.payment_hash}"
) )
paid = await self.lightning.get_invoice_status(decoded_invoice.payment_hash) payment = await self.lightning.get_invoice_status(
logger.trace(f"get_melt_fees: paid: {paid}") decoded_invoice.payment_hash
internal = paid.paid is False )
logger.trace(f"get_melt_fees: paid: {payment.paid}")
internal = payment.paid is False
else: else:
amount_msat = 0 amount_msat = 0
internal = True internal = True
fees_msat = fee_reserve(amount_msat, internal) fees_msat = fee_reserve(amount_msat, internal)
fee_sat = math.ceil(fees_msat / 1000) fee_sat = math.ceil(fees_msat / 1000)
return fee_sat return fee_sat
@@ -732,7 +631,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
Raises: Raises:
Exception: At least one proof already in pending table. 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: async with self.proofs_pending_lock:
await self._validate_proofs_pending(proofs, conn) await self._validate_proofs_pending(proofs, conn)
for p in proofs: for p in proofs:

137
cashu/mint/lightning.py Normal file
View 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

View File

@@ -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): async def m000_create_migrations_table(conn: Connection):
await db.execute(f""" await conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'dbversions')} ( CREATE TABLE IF NOT EXISTS {table_with_schema(conn, 'dbversions')} (
db TEXT PRIMARY KEY, db TEXT PRIMARY KEY,
version INT NOT NULL version INT NOT NULL
) )
@@ -11,7 +11,8 @@ async def m000_create_migrations_table(db: Database):
async def m001_initial(db: Database): async def m001_initial(db: Database):
await db.execute(f""" async with db.connect() as conn:
await conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} ( CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
amount {db.big_int} NOT NULL, amount {db.big_int} NOT NULL,
B_b TEXT NOT NULL, B_b TEXT NOT NULL,
@@ -22,7 +23,7 @@ async def m001_initial(db: Database):
); );
""") """)
await db.execute(f""" await conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} ( CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
amount {db.big_int} NOT NULL, amount {db.big_int} NOT NULL,
C TEXT NOT NULL, C TEXT NOT NULL,
@@ -33,7 +34,7 @@ async def m001_initial(db: Database):
); );
""") """)
await db.execute(f""" await conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} ( CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} (
amount {db.big_int} NOT NULL, amount {db.big_int} NOT NULL,
pr TEXT NOT NULL, pr TEXT NOT NULL,
@@ -45,7 +46,10 @@ async def m001_initial(db: Database):
); );
""") """)
await db.execute(f"""
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 CREATE VIEW {table_with_schema(db, 'balance_issued')} AS
SELECT COALESCE(SUM(s), 0) AS balance FROM ( SELECT COALESCE(SUM(s), 0) AS balance FROM (
SELECT SUM(amount) SELECT SUM(amount)
@@ -54,7 +58,7 @@ async def m001_initial(db: Database):
) AS s; ) AS s;
""") """)
await db.execute(f""" await conn.execute(f"""
CREATE VIEW {table_with_schema(db, 'balance_redeemed')} AS CREATE VIEW {table_with_schema(db, 'balance_redeemed')} AS
SELECT COALESCE(SUM(s), 0) AS balance FROM ( SELECT COALESCE(SUM(s), 0) AS balance FROM (
SELECT SUM(amount) SELECT SUM(amount)
@@ -63,7 +67,7 @@ async def m001_initial(db: Database):
) AS s; ) AS s;
""") """)
await db.execute(f""" await conn.execute(f"""
CREATE VIEW {table_with_schema(db, 'balance')} AS CREATE VIEW {table_with_schema(db, 'balance')} AS
SELECT s_issued - s_used FROM ( SELECT s_issued - s_used FROM (
SELECT bi.balance AS s_issued, bu.balance AS s_used SELECT bi.balance AS s_issued, bu.balance AS s_used
@@ -77,7 +81,8 @@ async def m003_mint_keysets(db: Database):
""" """
Stores mint keysets from different mints and epochs. Stores mint keysets from different mints and epochs.
""" """
await db.execute(f""" async with db.connect() as conn:
await conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} ( CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} (
id TEXT NOT NULL, id TEXT NOT NULL,
derivation_path TEXT, derivation_path TEXT,
@@ -90,7 +95,7 @@ async def m003_mint_keysets(db: Database):
); );
""") """)
await db.execute(f""" await conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_pubkeys')} ( CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_pubkeys')} (
id TEXT NOT NULL, id TEXT NOT NULL,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
@@ -106,7 +111,8 @@ async def m004_keysets_add_version(db: Database):
""" """
Column that remembers with which version Column that remembers with which version
""" """
await db.execute( async with db.connect() as conn:
await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT" f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT"
) )
@@ -115,7 +121,8 @@ async def m005_pending_proofs_table(db: Database) -> None:
""" """
Store pending proofs. Store pending proofs.
""" """
await db.execute(f""" async with db.connect() as conn:
await conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} ( CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
C TEXT NOT NULL, C TEXT NOT NULL,
@@ -133,27 +140,28 @@ async def m006_invoices_add_payment_hash(db: Database):
the column hash as a random identifier now the column hash as a random identifier now
(see https://github.com/cashubtc/nuts/pull/14). (see https://github.com/cashubtc/nuts/pull/14).
""" """
await db.execute( async with db.connect() as conn:
f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN payment_hash TEXT" await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN payment_hash"
" TEXT"
) )
await db.execute( await conn.execute(
f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash" f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash"
) )
async def m007_proofs_and_promises_store_id(db: Database): async def m007_proofs_and_promises_store_id(db: Database):
""" """
Column that remembers the payment_hash as we're using Column that stores the id of the proof or promise.
the column hash as a random identifier now
(see https://github.com/cashubtc/nuts/pull/14).
""" """
await db.execute( async with db.connect() as conn:
await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT" f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT"
) )
await db.execute( await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT" f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT"
) )
await db.execute( await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT" f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT"
) )
@@ -162,9 +170,37 @@ async def m008_promises_dleq(db: Database):
""" """
Add columns for DLEQ proof to promises table. Add columns for DLEQ proof to promises table.
""" """
await db.execute( async with db.connect() as conn:
await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN e TEXT" f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN e TEXT"
) )
await db.execute( await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN s TEXT" 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"
)

View File

@@ -1,8 +1,20 @@
from typing import Protocol from typing import Protocol
from ..core.base import MintKeyset, MintKeysets from ..core.base import MintKeyset, MintKeysets
from ..core.db import Database
from ..lightning.base import Wallet
from ..mint.crud import LedgerCrud
class SupportsKeysets(Protocol): class SupportsKeysets(Protocol):
keyset: MintKeyset keyset: MintKeyset
keysets: MintKeysets keysets: MintKeysets
class SupportLightning(Protocol):
lightning: Wallet
class SupportsDb(Protocol):
db: Database
crud: LedgerCrud

View File

@@ -162,10 +162,10 @@ async def mint(
# BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash # 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. # 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 # 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) blinded_signatures = PostMintResponse(promises=promises)
logger.trace(f"< POST /mint: {blinded_signatures}") logger.trace(f"< POST /mint: {blinded_signatures}")
return blinded_signatures return blinded_signatures

View File

@@ -10,6 +10,7 @@ from ..core.db import Database
from ..core.migrations import migrate_databases from ..core.migrations import migrate_databases
from ..core.settings import settings from ..core.settings import settings
from ..mint import migrations from ..mint import migrations
from ..mint.crud import LedgerCrud
from ..mint.ledger import Ledger from ..mint.ledger import Ledger
logger.debug("Enviroment Settings:") logger.debug("Enviroment Settings:")
@@ -26,6 +27,7 @@ ledger = Ledger(
seed=settings.mint_private_key, seed=settings.mint_private_key,
derivation_path=settings.mint_derivation_path, derivation_path=settings.mint_derivation_path,
lightning=lightning_backend, lightning=lightning_backend,
crud=LedgerCrud(),
) )
@@ -50,14 +52,14 @@ async def start_mint_init():
if settings.lightning: if settings.lightning:
logger.info(f"Using backend: {settings.mint_lightning_backend}") logger.info(f"Using backend: {settings.mint_lightning_backend}")
error_message, balance = await ledger.lightning.status() status = await ledger.lightning.status()
if error_message: if status.error_message:
logger.warning( logger.warning(
f"The backend for {ledger.lightning.__class__.__name__} isn't working" f"The backend for {ledger.lightning.__class__.__name__} isn't"
f" properly: '{error_message}'", f" working properly: '{status.error_message}'",
RuntimeWarning, 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(f"Data dir: {settings.cashu_dir}")
logger.info("Mint started.") logger.info("Mint started.")

View File

@@ -6,15 +6,13 @@ from ...core.base import Invoice
class PayResponse(BaseModel): class PayResponse(BaseModel):
amount: int ok: Optional[bool] = None
fee: int
amount_with_fee: int
class InvoiceResponse(BaseModel): class InvoiceResponse(BaseModel):
amount: Optional[int] = None amount: Optional[int] = None
invoice: Optional[Invoice] = None invoice: Optional[Invoice] = None
hash: Optional[str] = None id: Optional[str] = None
class SwapResponse(BaseModel): class SwapResponse(BaseModel):

View File

@@ -11,6 +11,12 @@ from fastapi import APIRouter, Query
from ...core.base import TokenV3 from ...core.base import TokenV3
from ...core.helpers import sum_proofs from ...core.helpers import sum_proofs
from ...core.settings import settings from ...core.settings import settings
from ...lightning.base import (
InvoiceResponse,
PaymentResponse,
PaymentStatus,
StatusResponse,
)
from ...nostr.client.client import NostrClient from ...nostr.client.client import NostrClient
from ...tor.tor import TorProxy from ...tor.tor import TorProxy
from ...wallet.crud import get_lightning_invoices, get_reserved_proofs 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.nostr import receive_nostr, send_nostr
from ...wallet.wallet import Wallet as Wallet from ...wallet.wallet import Wallet as Wallet
from ..lightning.lightning import LightningWallet
from .api_helpers import verify_mints from .api_helpers import verify_mints
from .responses import ( from .responses import (
BalanceResponse, BalanceResponse,
BurnResponse, BurnResponse,
InfoResponse, InfoResponse,
InvoiceResponse,
InvoicesResponse, InvoicesResponse,
LockResponse, LockResponse,
LocksResponse, LocksResponse,
PayResponse,
PendingResponse, PendingResponse,
ReceiveResponse, ReceiveResponse,
RestoreResponse, RestoreResponse,
@@ -44,17 +49,19 @@ from .responses import (
router: APIRouter = APIRouter() router: APIRouter = APIRouter()
async def mint_wallet(mint_url: Optional[str] = None): async def mint_wallet(
wallet: Wallet = await Wallet.with_db( mint_url: Optional[str] = None, raise_connection_error: bool = True
) -> LightningWallet:
lightning_wallet = await LightningWallet.with_db(
mint_url or settings.mint_url, mint_url or settings.mint_url,
db=os.path.join(settings.cashu_dir, settings.wallet_name), db=os.path.join(settings.cashu_dir, settings.wallet_name),
name=settings.wallet_name, name=settings.wallet_name,
) )
await wallet.load_mint() await lightning_wallet.async_init(raise_connection_error=raise_connection_error)
return wallet return lightning_wallet
wallet: Wallet = Wallet( wallet = LightningWallet(
settings.mint_url, settings.mint_url,
db=os.path.join(settings.cashu_dir, settings.wallet_name), db=os.path.join(settings.cashu_dir, settings.wallet_name),
name=settings.wallet_name, name=settings.wallet_name,
@@ -64,87 +71,101 @@ wallet: Wallet = Wallet(
@router.on_event("startup") @router.on_event("startup")
async def start_wallet(): async def start_wallet():
global wallet global wallet
wallet = await Wallet.with_db( wallet = await mint_wallet(settings.mint_url, raise_connection_error=False)
settings.mint_url,
db=os.path.join(settings.cashu_dir, settings.wallet_name),
name=settings.wallet_name,
)
if settings.tor and not TorProxy().check_platform(): if settings.tor and not TorProxy().check_platform():
raise Exception("tor not working.") 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( async def pay(
invoice: str = Query(default=..., description="Lightning invoice to pay"), bolt11: str = Query(default=..., description="Lightning invoice to pay"),
mint: str = Query( mint: str = Query(
default=None, default=None,
description="Mint URL to pay from (None for default mint)", description="Mint URL to pay from (None for default mint)",
), ),
): ) -> PaymentResponse:
if not settings.lightning:
raise Exception("lightning not enabled.")
global wallet global wallet
if mint:
wallet = await mint_wallet(mint) wallet = await mint_wallet(mint)
await wallet.load_proofs(reload=True) payment_response = await wallet.pay_invoice(bolt11)
return payment_response
total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
assert total_amount > 0, "amount has to be larger than zero." @router.get(
assert wallet.available_balance >= total_amount, "balance is too low." "/lightning/payment_state",
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount) name="Request lightning invoice",
await wallet.pay_lightning(send_proofs, invoice, fee_reserve_sat) response_model=PaymentStatus,
return PayResponse(
amount=total_amount - fee_reserve_sat,
fee=fee_reserve_sat,
amount_with_fee=total_amount,
) )
async def payment_state(
payment_hash: str = Query(default=None, description="Id of paid invoice"),
@router.post(
"/invoice", name="Request lightning invoice", response_model=InvoiceResponse
)
async def invoice(
amount: int = Query(default=..., description="Amount to request in invoice"),
hash: str = Query(default=None, description="Hash of paid invoice"),
mint: str = Query( mint: str = Query(
default=None, default=None,
description="Mint URL to create an invoice at (None for default mint)", description="Mint URL to create an invoice at (None for default mint)",
), ),
split: int = Query( ) -> PaymentStatus:
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.")
global wallet global wallet
if mint:
wallet = await mint_wallet(mint) wallet = await mint_wallet(mint)
if not settings.lightning: state = await wallet.get_payment_status(payment_hash)
await wallet.mint(amount, split=optional_split) return state
return InvoiceResponse(
amount=amount,
@router.post(
"/lightning/create_invoice",
name="Request lightning invoice",
response_model=InvoiceResponse,
) )
elif amount and not hash: async def create_invoice(
invoice = await wallet.request_mint(amount) amount: int = Query(default=..., description="Amount to request in invoice"),
return InvoiceResponse( mint: str = Query(
amount=amount, default=None,
invoice=invoice, 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,
) )
elif amount and hash: async def invoice_state(
await wallet.mint(amount, split=optional_split, hash=hash) payment_hash: str = Query(default=None, description="Payment hash of paid invoice"),
return InvoiceResponse( mint: str = Query(
amount=amount, default=None,
hash=hash, 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
) )
return
@router.post( @router.post(
@@ -171,7 +192,7 @@ async def swap(
# pay invoice from outgoing mint # pay invoice from outgoing mint
await outgoing_wallet.load_proofs(reload=True) await outgoing_wallet.load_proofs(reload=True)
total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees( 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" assert total_amount > 0, "amount must be positive"
if outgoing_wallet.available_balance < total_amount: if outgoing_wallet.available_balance < total_amount:
@@ -180,10 +201,10 @@ async def swap(
_, send_proofs = await outgoing_wallet.split_to_send( _, send_proofs = await outgoing_wallet.split_to_send(
outgoing_wallet.proofs, total_amount, set_reserved=True 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 # 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 incoming_wallet.load_proofs(reload=True)
mint_balances = await incoming_wallet.balance_per_minturl() mint_balances = await incoming_wallet.balance_per_minturl()
return SwapResponse( return SwapResponse(
@@ -223,6 +244,8 @@ async def send_command(
), ),
): ):
global wallet global wallet
if mint:
wallet = await mint_wallet(mint)
if not nostr: if not nostr:
balance, token = await send( balance, token = await send(
wallet, amount=amount, lock=lock, legacy=False, split=not nosplit wallet, amount=amount, lock=lock, legacy=False, split=not nosplit

View File

@@ -168,9 +168,12 @@ async def pay(ctx: Context, invoice: str, yes: bool):
wallet.status() wallet.status()
total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice) total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
if not yes: 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( click.confirm(
f"Pay {total_amount - fee_reserve_sat} sat ({total_amount} sat with" message,
" potential fees)?",
abort=True, abort=True,
default=True, default=True,
) )
@@ -187,7 +190,7 @@ async def pay(ctx: Context, invoice: str, yes: bool):
@cli.command("invoice", help="Create Lighting invoice.") @cli.command("invoice", help="Create Lighting invoice.")
@click.argument("amount", type=int) @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( @click.option(
"--split", "--split",
"-s", "-s",
@@ -197,7 +200,7 @@ async def pay(ctx: Context, invoice: str, yes: bool):
) )
@click.pass_context @click.pass_context
@coro @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"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint() await wallet.load_mint()
wallet.status() wallet.status()
@@ -213,16 +216,16 @@ async def invoice(ctx: Context, amount: int, hash: str, split: int):
if not settings.lightning: if not settings.lightning:
await wallet.mint(amount, split=optional_split) await wallet.mint(amount, split=optional_split)
# user requests an invoice # user requests an invoice
elif amount and not hash: elif amount and not id:
invoice = await wallet.request_mint(amount) invoice = await wallet.request_mint(amount)
if invoice.pr: if invoice.bolt11:
print(f"Pay invoice to mint {amount} sat:") print(f"Pay invoice to mint {amount} sat:")
print("") print("")
print(f"Invoice: {invoice.pr}") print(f"Invoice: {invoice.bolt11}")
print("") print("")
print( print(
"If you abort this you can use this command to recheck the" "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 check_until = time.time() + 5 * 60 # check for five minutes
print("") print("")
@@ -235,7 +238,7 @@ async def invoice(ctx: Context, amount: int, hash: str, split: int):
while time.time() < check_until and not paid: while time.time() < check_until and not paid:
time.sleep(3) time.sleep(3)
try: try:
await wallet.mint(amount, split=optional_split, hash=invoice.hash) await wallet.mint(amount, split=optional_split, id=invoice.id)
paid = True paid = True
print(" Invoice paid.") print(" Invoice paid.")
except Exception as e: 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 # user paid invoice and want to check it
elif amount and hash: elif amount and id:
await wallet.mint(amount, split=optional_split, hash=hash) await wallet.mint(amount, split=optional_split, id=id)
wallet.status() wallet.status()
return return
@@ -285,17 +288,17 @@ async def swap(ctx: Context):
# pay invoice from outgoing mint # pay invoice from outgoing mint
total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees( total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees(
invoice.pr invoice.bolt11
) )
if outgoing_wallet.available_balance < total_amount: if outgoing_wallet.available_balance < total_amount:
raise Exception("balance too low") raise Exception("balance too low")
_, send_proofs = await outgoing_wallet.split_to_send( _, send_proofs = await outgoing_wallet.split_to_send(
outgoing_wallet.proofs, total_amount, set_reserved=True 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 # 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 incoming_wallet.load_proofs(reload=True)
await print_mint_balances(incoming_wallet, show_mints=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"Paid: {invoice.paid}")
print(f"Incoming: {invoice.amount > 0}") print(f"Incoming: {invoice.amount > 0}")
print(f"Amount: {abs(invoice.amount)}") print(f"Amount: {abs(invoice.amount)}")
if invoice.hash: if invoice.id:
print(f"Hash: {invoice.hash}") print(f"ID: {invoice.id}")
if invoice.preimage: if invoice.preimage:
print(f"Preimage: {invoice.preimage}") print(f"Preimage: {invoice.preimage}")
if invoice.time_created: if invoice.time_created:
@@ -644,7 +647,7 @@ async def invoices(ctx):
) )
print(f"Paid: {d}") print(f"Paid: {d}")
print("") print("")
print(f"Payment request: {invoice.pr}") print(f"Payment request: {invoice.bolt11}")
print("") print("")
print("--------------------------\n") print("--------------------------\n")
else: else:

View File

@@ -14,8 +14,8 @@ async def store_proof(
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO proofs INSERT INTO proofs
(id, amount, C, secret, time_created, derivation_path, dleq) (id, amount, C, secret, time_created, derivation_path, dleq, mint_id, melt_id)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
proof.id, proof.id,
@@ -25,18 +25,42 @@ async def store_proof(
int(time.time()), int(time.time()),
proof.derivation_path, proof.derivation_path,
json.dumps(proof.dleq.dict()) if proof.dleq else "", json.dumps(proof.dleq.dict()) if proof.dleq else "",
proof.mint_id,
proof.melt_id,
), ),
) )
async def get_proofs( async def get_proofs(
*,
db: Database, db: Database,
melt_id: str = "",
mint_id: str = "",
table: str = "proofs",
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Proof]: ):
rows = await (conn or db).fetchall(""" clauses = []
SELECT * from proofs values: List[Any] = []
""")
return [Proof.from_dict(dict(r)) for r in rows] 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( async def get_reserved_proofs(
@@ -66,8 +90,8 @@ async def invalidate_proof(
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO proofs_used INSERT INTO proofs_used
(amount, C, secret, time_used, id, derivation_path) (amount, C, secret, time_used, id, derivation_path, mint_id, melt_id)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
proof.amount, proof.amount,
@@ -76,14 +100,19 @@ async def invalidate_proof(
int(time.time()), int(time.time()),
proof.id, proof.id,
proof.derivation_path, proof.derivation_path,
proof.mint_id,
proof.melt_id,
), ),
) )
async def update_proof_reserved( async def update_proof(
proof: 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, db: Optional[Database] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
@@ -92,15 +121,22 @@ async def update_proof_reserved(
clauses.append("reserved = ?") clauses.append("reserved = ?")
values.append(reserved) values.append(reserved)
if send_id: if send_id is not None:
clauses.append("send_id = ?") clauses.append("send_id = ?")
values.append(send_id) values.append(send_id)
if reserved: if reserved is not None:
# set the time of reserving
clauses.append("time_reserved = ?") clauses.append("time_reserved = ?")
values.append(int(time.time())) 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 await (conn or db).execute( # type: ignore
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?", f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
(*values, str(proof.secret)), (*values, str(proof.secret)),
@@ -184,44 +220,55 @@ async def store_lightning_invoice(
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO invoices INSERT INTO invoices
(amount, pr, hash, preimage, paid, time_created, time_paid) (amount, bolt11, id, payment_hash, preimage, paid, time_created, time_paid, out)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
invoice.amount, invoice.amount,
invoice.pr, invoice.bolt11,
invoice.hash, invoice.id,
invoice.payment_hash,
invoice.preimage, invoice.preimage,
invoice.paid, invoice.paid,
invoice.time_created, invoice.time_created,
invoice.time_paid, invoice.time_paid,
invoice.out,
), ),
) )
async def get_lightning_invoice( async def get_lightning_invoice(
*,
db: Database, db: Database,
hash: str = "", id: str = "",
payment_hash: str = "",
out: Optional[bool] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Invoice: ) -> Optional[Invoice]:
clauses = [] clauses = []
values: List[Any] = [] values: List[Any] = []
if hash: if id:
clauses.append("hash = ?") clauses.append("id = ?")
values.append(hash) 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 = "" where = ""
if clauses: if clauses:
where = f"WHERE {' AND '.join(clauses)}" where = f"WHERE {' AND '.join(clauses)}"
query = f"""
row = await (conn or db).fetchone(
f"""
SELECT * from invoices SELECT * from invoices
{where} {where}
""", """
row = await (conn or db).fetchone(
query,
tuple(values), tuple(values),
) )
return Invoice(**row) return Invoice(**row) if row else None
async def get_lightning_invoices( async def get_lightning_invoices(
@@ -252,9 +299,10 @@ async def get_lightning_invoices(
async def update_lightning_invoice( async def update_lightning_invoice(
db: Database, db: Database,
hash: str, id: str,
paid: bool, paid: bool,
time_paid: Optional[int] = None, time_paid: Optional[int] = None,
preimage: Optional[str] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
clauses = [] clauses = []
@@ -265,12 +313,15 @@ async def update_lightning_invoice(
if time_paid: if time_paid:
clauses.append("time_paid = ?") clauses.append("time_paid = ?")
values.append(time_paid) values.append(time_paid)
if preimage:
clauses.append("preimage = ?")
values.append(preimage)
await (conn or db).execute( await (conn or db).execute(
f"UPDATE invoices SET {', '.join(clauses)} WHERE hash = ?", f"UPDATE invoices SET {', '.join(clauses)} WHERE id = ?",
( (
*values, *values,
hash, id,
), ),
) )

View File

@@ -2,7 +2,6 @@ import hashlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Optional from typing import List, Optional
from ..core import bolt11 as bolt11
from ..core.base import HTLCWitness, Proof from ..core.base import HTLCWitness, Proof
from ..core.db import Database from ..core.db import Database
from ..core.htlc import ( from ..core.htlc import (

View File

@@ -0,0 +1 @@
from .lightning import LightningWallet # noqa

View 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
)

View File

@@ -1,8 +1,8 @@
from ..core.db import Database from ..core.db import Connection, Database
async def m000_create_migrations_table(db: Database): async def m000_create_migrations_table(conn: Connection):
await db.execute(""" await conn.execute("""
CREATE TABLE IF NOT EXISTS dbversions ( CREATE TABLE IF NOT EXISTS dbversions (
db TEXT PRIMARY KEY, db TEXT PRIMARY KEY,
version INT NOT NULL version INT NOT NULL
@@ -11,7 +11,8 @@ async def m000_create_migrations_table(db: Database):
async def m001_initial(db: Database): async def m001_initial(db: Database):
await db.execute(f""" async with db.connect() as conn:
await conn.execute(f"""
CREATE TABLE IF NOT EXISTS proofs ( CREATE TABLE IF NOT EXISTS proofs (
amount {db.big_int} NOT NULL, amount {db.big_int} NOT NULL,
C TEXT NOT NULL, C TEXT NOT NULL,
@@ -22,7 +23,7 @@ async def m001_initial(db: Database):
); );
""") """)
await db.execute(f""" await conn.execute(f"""
CREATE TABLE IF NOT EXISTS proofs_used ( CREATE TABLE IF NOT EXISTS proofs_used (
amount {db.big_int} NOT NULL, amount {db.big_int} NOT NULL,
C TEXT NOT NULL, C TEXT NOT NULL,
@@ -33,7 +34,7 @@ async def m001_initial(db: Database):
); );
""") """)
await db.execute(""" await conn.execute("""
CREATE VIEW IF NOT EXISTS balance AS CREATE VIEW IF NOT EXISTS balance AS
SELECT COALESCE(SUM(s), 0) AS balance FROM ( SELECT COALESCE(SUM(s), 0) AS balance FROM (
SELECT SUM(amount) AS s SELECT SUM(amount) AS s
@@ -42,7 +43,7 @@ async def m001_initial(db: Database):
); );
""") """)
await db.execute(""" await conn.execute("""
CREATE VIEW IF NOT EXISTS balance_used AS CREATE VIEW IF NOT EXISTS balance_used AS
SELECT COALESCE(SUM(s), 0) AS used FROM ( SELECT COALESCE(SUM(s), 0) AS used FROM (
SELECT SUM(amount) AS s SELECT SUM(amount) AS s
@@ -56,8 +57,8 @@ async def m002_add_proofs_reserved(db: Database):
""" """
Column for marking proofs as reserved when they are being sent. Column for marking proofs as reserved when they are being sent.
""" """
async with db.connect() as conn:
await db.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL") await conn.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL")
async def m003_add_proofs_sendid_and_timestamps(db: Database): 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 Column with unique ID for each initiated send attempt
so proofs can be later grouped together for each send attempt. so proofs can be later grouped together for each send attempt.
""" """
await db.execute("ALTER TABLE proofs ADD COLUMN send_id TEXT") async with db.connect() as conn:
await db.execute("ALTER TABLE proofs ADD COLUMN time_created TIMESTAMP") await conn.execute("ALTER TABLE proofs ADD COLUMN send_id TEXT")
await db.execute("ALTER TABLE proofs ADD COLUMN time_reserved TIMESTAMP") await conn.execute("ALTER TABLE proofs ADD COLUMN time_created TIMESTAMP")
await db.execute("ALTER TABLE proofs_used ADD COLUMN time_used 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): async def m004_p2sh_locks(db: Database):
""" """
DEPRECATED: Stores P2SH addresses and unlock scripts. 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 ( # CREATE TABLE IF NOT EXISTS p2sh (
# address TEXT NOT NULL, # address TEXT NOT NULL,
# script TEXT NOT NULL, # script TEXT NOT NULL,
@@ -92,7 +95,8 @@ async def m005_wallet_keysets(db: Database):
""" """
Stores mint keysets from different mints and epochs. Stores mint keysets from different mints and epochs.
""" """
await db.execute(f""" async with db.connect() as conn:
await conn.execute(f"""
CREATE TABLE IF NOT EXISTS keysets ( CREATE TABLE IF NOT EXISTS keysets (
id TEXT, id TEXT,
mint_url TEXT, mint_url TEXT,
@@ -106,15 +110,16 @@ async def m005_wallet_keysets(db: Database):
); );
""") """)
await db.execute("ALTER TABLE proofs ADD COLUMN id TEXT") await conn.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_used ADD COLUMN id TEXT")
async def m006_invoices(db: Database): async def m006_invoices(db: Database):
""" """
Stores Lightning invoices. Stores Lightning invoices.
""" """
await db.execute(f""" async with db.connect() as conn:
await conn.execute(f"""
CREATE TABLE IF NOT EXISTS invoices ( CREATE TABLE IF NOT EXISTS invoices (
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
pr TEXT NOT NULL, pr TEXT NOT NULL,
@@ -134,37 +139,40 @@ async def m007_nostr(db: Database):
""" """
Stores timestamps of nostr operations. Stores timestamps of nostr operations.
""" """
await db.execute(""" # async with db.connect() as conn:
CREATE TABLE IF NOT EXISTS nostr ( # await conn.execute("""
type TEXT NOT NULL, # CREATE TABLE IF NOT EXISTS nostr (
last TIMESTAMP DEFAULT NULL # type TEXT NOT NULL,
) # last TIMESTAMP DEFAULT NULL
""") # )
await db.execute( # """)
""" # await conn.execute(
INSERT INTO nostr # """
(type, last) # INSERT INTO nostr
VALUES (?, ?) # (type, last)
""", # VALUES (?, ?)
( # """,
"dm", # (
None, # "dm",
), # None,
) # ),
# )
async def m008_keysets_add_public_keys(db: Database): async def m008_keysets_add_public_keys(db: Database):
""" """
Stores public keys of mint in a new column of table keysets. 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): async def m009_privatekey_and_determinstic_key_derivation(db: Database):
await db.execute("ALTER TABLE keysets ADD COLUMN counter INTEGER DEFAULT 0") async with db.connect() as conn:
await db.execute("ALTER TABLE proofs ADD COLUMN derivation_path TEXT") await conn.execute("ALTER TABLE keysets ADD COLUMN counter INTEGER DEFAULT 0")
await db.execute("ALTER TABLE proofs_used ADD COLUMN derivation_path TEXT") await conn.execute("ALTER TABLE proofs ADD COLUMN derivation_path TEXT")
await db.execute(""" await conn.execute("ALTER TABLE proofs_used ADD COLUMN derivation_path TEXT")
await conn.execute("""
CREATE TABLE IF NOT EXISTS seed ( CREATE TABLE IF NOT EXISTS seed (
seed TEXT NOT NULL, seed TEXT NOT NULL,
mnemonic TEXT NOT NULL, mnemonic TEXT NOT NULL,
@@ -172,11 +180,32 @@ async def m009_privatekey_and_determinstic_key_derivation(db: Database):
UNIQUE (seed, mnemonic) UNIQUE (seed, mnemonic)
); );
""") """)
# await db.execute("INSERT INTO secret_derivation (counter) VALUES (0)") # await conn.execute("INSERT INTO secret_derivation (counter) VALUES (0)")
async def m010_add_proofs_dleq(db: Database): async def m010_add_proofs_dleq(db: Database):
""" """
Columns to store DLEQ proofs for proofs. 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")

View File

@@ -2,8 +2,8 @@ import asyncio
import threading import threading
import click import click
from httpx import ConnectError
from loguru import logger from loguru import logger
from requests.exceptions import ConnectionError
from ..core.settings import settings from ..core.settings import settings
from ..nostr.client.client import NostrClient from ..nostr.client.client import NostrClient
@@ -27,11 +27,11 @@ async def nip5_to_pubkey(wallet: Wallet, address: str):
user, host = address.split("@") user, host = address.split("@")
resp_dict = {} resp_dict = {}
try: try:
resp = wallet.s.get( resp = await wallet.httpx.get(
f"https://{host}/.well-known/nostr.json?name={user}", f"https://{host}/.well-known/nostr.json?name={user}",
) )
resp.raise_for_status() resp.raise_for_status()
except ConnectionError: except ConnectError:
raise Exception(f"Could not connect to {host}") raise Exception(f"Could not connect to {host}")
except Exception as e: except Exception as e:
raise e raise e

View File

@@ -3,7 +3,6 @@ from typing import List, Optional
from loguru import logger from loguru import logger
from ..core import bolt11 as bolt11
from ..core.base import ( from ..core.base import (
BlindedMessage, BlindedMessage,
P2PKWitness, P2PKWitness,

View File

@@ -6,7 +6,6 @@ from bip32 import BIP32
from loguru import logger from loguru import logger
from mnemonic import Mnemonic from mnemonic import Mnemonic
from ..core import bolt11 as bolt11
from ..core.crypto.secp import PrivateKey from ..core.crypto.secp import PrivateKey
from ..core.db import Database from ..core.db import Database
from ..core.settings import settings from ..core.settings import settings

View File

@@ -1,19 +1,18 @@
import base64 import base64
import json import json
import math import math
import secrets as scrts
import time import time
import uuid import uuid
from itertools import groupby from itertools import groupby
from posixpath import join from posixpath import join
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
import requests import bolt11
import httpx
from bip32 import BIP32 from bip32 import BIP32
from httpx import Response
from loguru import logger from loguru import logger
from requests import Response
from ..core import bolt11 as bolt11
from ..core.base import ( from ..core.base import (
BlindedMessage, BlindedMessage,
BlindedSignature, BlindedSignature,
@@ -38,7 +37,6 @@ from ..core.base import (
TokenV3Token, TokenV3Token,
WalletKeyset, WalletKeyset,
) )
from ..core.bolt11 import Invoice as InvoiceBolt11
from ..core.crypto import b_dhke from ..core.crypto import b_dhke
from ..core.crypto.secp import PrivateKey, PublicKey from ..core.crypto.secp import PrivateKey, PublicKey
from ..core.db import Database from ..core.db import Database
@@ -59,7 +57,7 @@ from ..wallet.crud import (
store_lightning_invoice, store_lightning_invoice,
store_proof, store_proof,
update_lightning_invoice, update_lightning_invoice,
update_proof_reserved, update_proof,
) )
from . import migrations from . import migrations
from .htlc import WalletHTLC from .htlc import WalletHTLC
@@ -67,7 +65,7 @@ from .p2pk import WalletP2PK
from .secrets import WalletSecrets 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 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 API calls. Sets some HTTP headers and starts a Tor instance if none is
@@ -75,11 +73,8 @@ def async_set_requests(func):
""" """
async def wrapper(self, *args, **kwargs): async def wrapper(self, *args, **kwargs):
self.s.headers.update({"Client-version": settings.version})
if settings.debug:
self.s.verify = False
# set proxy # set proxy
proxies_dict = {}
proxy_url: Union[str, None] = None proxy_url: Union[str, None] = None
if settings.tor and TorProxy().check_platform(): if settings.tor and TorProxy().check_platform():
self.tor = TorProxy(timeout=True) self.tor = TorProxy(timeout=True)
@@ -90,10 +85,17 @@ def async_set_requests(func):
elif settings.http_proxy: elif settings.http_proxy:
proxy_url = settings.http_proxy proxy_url = settings.http_proxy
if proxy_url: if proxy_url:
self.s.proxies.update({"http": proxy_url}) proxies_dict.update({"http": proxy_url})
self.s.proxies.update({"https": 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 await func(self, *args, **kwargs)
return wrapper return wrapper
@@ -106,16 +108,15 @@ class LedgerAPI(object):
mint_info: GetInfoResponse # holds info about mint mint_info: GetInfoResponse # holds info about mint
tor: TorProxy tor: TorProxy
s: requests.Session
db: Database db: Database
httpx: httpx.AsyncClient
def __init__(self, url: str, db: Database): def __init__(self, url: str, db: Database):
self.url = url self.url = url
self.s = requests.Session()
self.db = db self.db = db
self.keysets = {} self.keysets = {}
@async_set_requests @async_set_httpx_client
async def _init_s(self): async def _init_s(self):
"""Dummy function that can be called from outside to use LedgerAPI.s""" """Dummy function that can be called from outside to use LedgerAPI.s"""
return return
@@ -262,7 +263,7 @@ class LedgerAPI(object):
ENDPOINTS ENDPOINTS
""" """
@async_set_requests @async_set_httpx_client
async def _get_keys(self, url: str) -> WalletKeyset: async def _get_keys(self, url: str) -> WalletKeyset:
"""API that gets the current keys of the mint """API that gets the current keys of the mint
@@ -275,7 +276,7 @@ class LedgerAPI(object):
Raises: Raises:
Exception: If no keys are received from the mint Exception: If no keys are received from the mint
""" """
resp = self.s.get( resp = await self.httpx.get(
join(url, "keys"), join(url, "keys"),
) )
self.raise_on_error(resp) self.raise_on_error(resp)
@@ -288,7 +289,7 @@ class LedgerAPI(object):
keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url) keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url)
return keyset return keyset
@async_set_requests @async_set_httpx_client
async def _get_keys_of_keyset(self, url: str, keyset_id: str) -> WalletKeyset: 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. """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 Exception: If no keys are received from the mint
""" """
keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_") keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_")
resp = self.s.get( resp = await self.httpx.get(
join(url, f"keys/{keyset_id_urlsafe}"), join(url, f"keys/{keyset_id_urlsafe}"),
) )
self.raise_on_error(resp) self.raise_on_error(resp)
@@ -317,7 +318,7 @@ class LedgerAPI(object):
keyset = WalletKeyset(id=keyset_id, public_keys=keyset_keys, mint_url=url) keyset = WalletKeyset(id=keyset_id, public_keys=keyset_keys, mint_url=url)
return keyset return keyset
@async_set_requests @async_set_httpx_client
async def _get_keyset_ids(self, url: str) -> List[str]: async def _get_keyset_ids(self, url: str) -> List[str]:
"""API that gets a list of all active keysets of the mint. """API that gets a list of all active keysets of the mint.
@@ -330,7 +331,7 @@ class LedgerAPI(object):
Raises: Raises:
Exception: If no keysets are received from the mint Exception: If no keysets are received from the mint
""" """
resp = self.s.get( resp = await self.httpx.get(
join(url, "keysets"), join(url, "keysets"),
) )
self.raise_on_error(resp) self.raise_on_error(resp)
@@ -339,7 +340,7 @@ class LedgerAPI(object):
assert len(keysets.keysets), Exception("did not receive any keysets") assert len(keysets.keysets), Exception("did not receive any keysets")
return keysets.keysets return keysets.keysets
@async_set_requests @async_set_httpx_client
async def _get_info(self, url: str) -> GetInfoResponse: async def _get_info(self, url: str) -> GetInfoResponse:
"""API that gets the mint info. """API that gets the mint info.
@@ -352,7 +353,7 @@ class LedgerAPI(object):
Raises: Raises:
Exception: If the mint info request fails Exception: If the mint info request fails
""" """
resp = self.s.get( resp = await self.httpx.get(
join(url, "info"), join(url, "info"),
) )
self.raise_on_error(resp) self.raise_on_error(resp)
@@ -360,7 +361,7 @@ class LedgerAPI(object):
mint_info: GetInfoResponse = GetInfoResponse.parse_obj(data) mint_info: GetInfoResponse = GetInfoResponse.parse_obj(data)
return mint_info return mint_info
@async_set_requests @async_set_httpx_client
async def request_mint(self, amount) -> Invoice: async def request_mint(self, amount) -> Invoice:
"""Requests a mint from the server and returns Lightning invoice. """Requests a mint from the server and returns Lightning invoice.
@@ -374,21 +375,29 @@ class LedgerAPI(object):
Exception: If the mint request fails Exception: If the mint request fails
""" """
logger.trace("Requesting mint: GET /mint") 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) self.raise_on_error(resp)
return_dict = resp.json() return_dict = resp.json()
mint_response = GetMintResponse.parse_obj(return_dict) 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( async def mint(
self, outputs: List[BlindedMessage], hash: Optional[str] = None self, outputs: List[BlindedMessage], id: Optional[str] = None
) -> List[BlindedSignature]: ) -> List[BlindedSignature]:
"""Mints new coins and returns a proof of promise. """Mints new coins and returns a proof of promise.
Args: Args:
outputs (List[BlindedMessage]): Outputs to mint new tokens with 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: Returns:
list[Proof]: List of proofs. list[Proof]: List of proofs.
@@ -398,12 +407,12 @@ class LedgerAPI(object):
""" """
outputs_payload = PostMintRequest(outputs=outputs) outputs_payload = PostMintRequest(outputs=outputs)
logger.trace("Checking Lightning invoice. POST /mint") logger.trace("Checking Lightning invoice. POST /mint")
resp = self.s.post( resp = await self.httpx.post(
join(self.url, "mint"), join(self.url, "mint"),
json=outputs_payload.dict(), json=outputs_payload.dict(),
params={ params={
"hash": hash, "hash": id,
"payment_hash": hash, # backwards compatibility pre 0.12.0 "payment_hash": id, # backwards compatibility pre 0.12.0
}, },
) )
self.raise_on_error(resp) self.raise_on_error(resp)
@@ -412,7 +421,7 @@ class LedgerAPI(object):
promises = PostMintResponse.parse_obj(response_dict).promises promises = PostMintResponse.parse_obj(response_dict).promises
return promises return promises
@async_set_requests @async_set_httpx_client
async def split( async def split(
self, self,
proofs: List[Proof], proofs: List[Proof],
@@ -437,7 +446,7 @@ class LedgerAPI(object):
"proofs": {i: proofs_include for i in range(len(proofs))}, "proofs": {i: proofs_include for i in range(len(proofs))},
} }
resp = self.s.post( resp = await self.httpx.post(
join(self.url, "split"), join(self.url, "split"),
json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore
) )
@@ -451,7 +460,7 @@ class LedgerAPI(object):
return promises return promises
@async_set_requests @async_set_httpx_client
async def check_proof_state(self, proofs: List[Proof]): 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. 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))}, "proofs": {i: {"secret"} for i in range(len(proofs))},
} }
resp = self.s.post( resp = await self.httpx.post(
join(self.url, "check"), join(self.url, "check"),
json=payload.dict(include=_check_proof_state_include_fields(proofs)), # type: ignore 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) states = CheckSpendableResponse.parse_obj(return_dict)
return states return states
@async_set_requests @async_set_httpx_client
async def check_fees(self, payment_request: str): async def check_fees(self, payment_request: str):
"""Checks whether the Lightning payment is internal.""" """Checks whether the Lightning payment is internal."""
payload = CheckFeesRequest(pr=payment_request) payload = CheckFeesRequest(pr=payment_request)
resp = self.s.post( resp = await self.httpx.post(
join(self.url, "checkfees"), join(self.url, "checkfees"),
json=payload.dict(), json=payload.dict(),
) )
@@ -487,15 +496,16 @@ class LedgerAPI(object):
return_dict = resp.json() return_dict = resp.json()
return return_dict return return_dict
@async_set_requests @async_set_httpx_client
async def pay_lightning( async def pay_lightning(
self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]] self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]]
): ) -> GetMeltResponse:
""" """
Accepts proofs and a lightning invoice to pay in exchange. Accepts proofs and a lightning invoice to pay in exchange.
""" """
payload = PostMeltRequest(proofs=proofs, pr=invoice, outputs=outputs) payload = PostMeltRequest(proofs=proofs, pr=invoice, outputs=outputs)
logger.debug("Calling melt. POST /melt")
def _meltrequest_include_fields(proofs: List[Proof]): def _meltrequest_include_fields(proofs: List[Proof]):
"""strips away fields from the model that aren't necessary for the /melt""" """strips away fields from the model that aren't necessary for the /melt"""
@@ -506,16 +516,17 @@ class LedgerAPI(object):
"outputs": ..., "outputs": ...,
} }
resp = self.s.post( resp = await self.httpx.post(
join(self.url, "melt"), join(self.url, "melt"),
json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore
timeout=None,
) )
self.raise_on_error(resp) self.raise_on_error(resp)
return_dict = resp.json() return_dict = resp.json()
return GetMeltResponse.parse_obj(return_dict) return GetMeltResponse.parse_obj(return_dict)
@async_set_requests @async_set_httpx_client
async def restore_promises( async def restore_promises(
self, outputs: List[BlindedMessage] self, outputs: List[BlindedMessage]
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]: ) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
@@ -523,7 +534,7 @@ class LedgerAPI(object):
Asks the mint to restore promises corresponding to outputs. Asks the mint to restore promises corresponding to outputs.
""" """
payload = PostMintRequest(outputs=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) self.raise_on_error(resp)
response_dict = resp.json() response_dict = resp.json()
returnObj = PostRestoreResponse.parse_obj(response_dict) returnObj = PostRestoreResponse.parse_obj(response_dict)
@@ -620,7 +631,6 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
Invoice: Lightning invoice Invoice: Lightning invoice
""" """
invoice = await super().request_mint(amount) invoice = await super().request_mint(amount)
invoice.time_created = int(time.time())
await store_lightning_invoice(db=self.db, invoice=invoice) await store_lightning_invoice(db=self.db, invoice=invoice)
return invoice return invoice
@@ -628,14 +638,14 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
self, self,
amount: int, amount: int,
split: Optional[List[int]] = None, split: Optional[List[int]] = None,
hash: Optional[str] = None, id: Optional[str] = None,
) -> List[Proof]: ) -> List[Proof]:
"""Mint tokens of a specific amount after an invoice has been paid. """Mint tokens of a specific amount after an invoice has been paid.
Args: Args:
amount (int): Total amount of tokens to be minted 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`. 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: Raises:
Exception: Raises exception if `amounts` does not sum to `amount` or has unsupported value. 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) outputs, rs = self._construct_outputs(amounts, secrets, rs)
# will raise exception if mint is unsuccessful # 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 # success, bump secret counter in database
await bump_secret_derivation( await bump_secret_derivation(
@@ -676,10 +686,15 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
) )
proofs = await self._construct_proofs(promises, secrets, rs, derivation_paths) proofs = await self._construct_proofs(promises, secrets, rs, derivation_paths)
if hash: if id:
await update_lightning_invoice( 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 return proofs
async def redeem( async def redeem(
@@ -782,7 +797,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
async def pay_lightning( async def pay_lightning(
self, proofs: List[Proof], invoice: str, fee_reserve_sat: int self, proofs: List[Proof], invoice: str, fee_reserve_sat: int
) -> bool: ) -> GetMeltResponse:
"""Pays a lightning invoice and returns the status of the payment. """Pays a lightning invoice and returns the status of the payment.
Args: Args:
@@ -795,41 +810,72 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
# Generate a number of blank outputs for any overpaid fees. As described in # 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 # NUT-08, the mint will imprint these outputs with a value depending on the
# amount of fees we overpaid. # amount of fees we overpaid.
n_return_outputs = calculate_number_of_blank_outputs(fee_reserve_sat) n_change_outputs = calculate_number_of_blank_outputs(fee_reserve_sat)
secrets, rs, derivation_paths = await self.generate_n_secrets(n_return_outputs) change_secrets, change_rs, change_derivation_paths = (
outputs, rs = self._construct_outputs(n_return_outputs * [0], secrets, rs) 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: # store the melt_id in proofs
# the payment was successful 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)
decoded_invoice = bolt11.decode(invoice)
invoice_obj = Invoice( invoice_obj = Invoice(
amount=-sum_proofs(proofs), amount=-sum_proofs(proofs),
pr=invoice, bolt11=invoice,
preimage=status.preimage, payment_hash=decoded_invoice.payment_hash,
paid=True, # preimage=status.preimage,
time_paid=time.time(), paid=False,
hash="", time_paid=int(time.time()),
id=melt_id, # store the same ID in the invoice
out=True, # outgoing invoice
) )
# we have a unique constraint on the hash, so we generate a random one if it doesn't exist # store invoice in db as not paid yet
invoice_obj.hash = invoice_obj.hash or await self._generate_secret()
await store_lightning_invoice(db=self.db, invoice=invoice_obj) await store_lightning_invoice(db=self.db, invoice=invoice_obj)
status = await super().pay_lightning(proofs, invoice, change_outputs)
# 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.")
# 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 # handle change and produce proofs
if status.change: if status.change:
change_proofs = await self._construct_proofs( change_proofs = await self._construct_proofs(
status.change, status.change,
secrets[: len(status.change)], change_secrets[: len(status.change)],
rs[: len(status.change)], change_rs[: len(status.change)],
derivation_paths[: len(status.change)], change_derivation_paths[: len(status.change)],
) )
logger.debug(f"Received change: {sum_proofs(change_proofs)} sat") logger.debug(f"Received change: {sum_proofs(change_proofs)} sat")
return status
await self.invalidate(proofs)
else:
raise Exception("could not pay invoice.")
return status.paid
async def check_proof_state(self, proofs): async def check_proof_state(self, proofs):
return await super().check_proof_state(proofs) return await super().check_proof_state(proofs)
@@ -965,7 +1011,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
async def _store_proofs(self, proofs): async def _store_proofs(self, proofs):
try: try:
async with self.db.connect() as conn: # type: ignore async with self.db.connect() as conn:
for proof in proofs: for proof in proofs:
await store_proof(proof, db=self.db, conn=conn) await store_proof(proof, db=self.db, conn=conn)
except Exception as e: except Exception as e:
@@ -1171,7 +1217,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
proof_to_add = sorted_proofs_of_current_keyset.pop() proof_to_add = sorted_proofs_of_current_keyset.pop()
send_proofs.append(proof_to_add) 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 return send_proofs
async def set_reserved(self, proofs: List[Proof], reserved: bool) -> None: 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()) uuid_str = str(uuid.uuid1())
for proof in proofs: for proof in proofs:
proof.reserved = True proof.reserved = True
await update_proof_reserved( await update_proof(proof, reserved=reserved, send_id=uuid_str, db=self.db)
proof, reserved=reserved, send_id=uuid_str, db=self.db
)
async def invalidate( async def invalidate(
self, proofs: List[Proof], check_spendable=True 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 Decodes the amount from a Lightning invoice and returns the
total amount (amount+fees) to be paid. 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 # check if it's an internal payment
fees = int((await self.check_fees(invoice))["fee"]) fees = int((await self.check_fees(invoice))["fee"])
logger.debug(f"Mint wants {fees} sat as fee reserve.") logger.debug(f"Mint wants {fees} sat as fee reserve.")

View File

@@ -1,6 +1,7 @@
[mypy] [mypy]
python_version = 3.9 python_version = 3.9
# disallow_untyped_defs = True # disallow_untyped_defs = True
; check_untyped_defs = True
ignore_missing_imports = True ignore_missing_imports = True
[mypy-cashu.nostr.*] [mypy-cashu.nostr.*]

1020
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,9 +12,9 @@ SQLAlchemy = "^1.3.24"
click = "^8.1.7" click = "^8.1.7"
pydantic = "^1.10.2" pydantic = "^1.10.2"
bech32 = "^1.2.0" bech32 = "^1.2.0"
fastapi = "^0.101.1" fastapi = "0.103.0"
environs = "^9.5.0" environs = "^9.5.0"
uvicorn = "^0.18.3" uvicorn = "0.23.2"
loguru = "^0.7.0" loguru = "^0.7.0"
ecdsa = "^0.18.0" ecdsa = "^0.18.0"
bitstring = "^3.1.9" bitstring = "^3.1.9"
@@ -29,9 +29,10 @@ setuptools = "^68.1.2"
wheel = "^0.41.1" wheel = "^0.41.1"
importlib-metadata = "^6.8.0" importlib-metadata = "^6.8.0"
psycopg2-binary = { version = "^2.9.7", optional = true } psycopg2-binary = { version = "^2.9.7", optional = true }
httpx = "^0.24.1" httpx = "0.25.0"
bip32 = "^3.4" bip32 = "^3.4"
mnemonic = "^0.20" mnemonic = "^0.20"
bolt11 = "^2.0.5"
[tool.poetry.extras] [tool.poetry.extras]
pgsql = ["psycopg2-binary"] pgsql = ["psycopg2-binary"]

View File

@@ -14,6 +14,7 @@ from cashu.core.migrations import migrate_databases
from cashu.core.settings import settings from cashu.core.settings import settings
from cashu.lightning.fake import FakeWallet from cashu.lightning.fake import FakeWallet
from cashu.mint import migrations as migrations_mint from cashu.mint import migrations as migrations_mint
from cashu.mint.crud import LedgerCrud
from cashu.mint.ledger import Ledger from cashu.mint.ledger import Ledger
SERVER_PORT = 3337 SERVER_PORT = 3337
@@ -64,6 +65,7 @@ async def ledger():
seed=settings.mint_private_key, seed=settings.mint_private_key,
derivation_path=settings.mint_derivation_path, derivation_path=settings.mint_derivation_path,
lightning=FakeWallet(), lightning=FakeWallet(),
crud=LedgerCrud(),
) )
await start_mint_init(ledger) await start_mint_init(ledger)
yield ledger yield ledger

View File

@@ -66,14 +66,14 @@ async def test_get_keyset(ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mint(ledger: Ledger): async def test_mint(ledger: Ledger):
invoice, payment_hash = await ledger.request_mint(8) invoice, id = await ledger.request_mint(8)
blinded_messages_mock = [ blinded_messages_mock = [
BlindedMessage( BlindedMessage(
amount=8, amount=8,
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239", 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 len(promises)
assert promises[0].amount == 8 assert promises[0].amount == 8
assert ( assert (
@@ -84,7 +84,7 @@ async def test_mint(ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mint_invalid_blinded_message(ledger: Ledger): 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 = [ blinded_messages_mock_invalid_key = [
BlindedMessage( BlindedMessage(
amount=8, amount=8,
@@ -92,7 +92,7 @@ async def test_mint_invalid_blinded_message(ledger: Ledger):
) )
] ]
await assert_err( 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", "invalid public key",
) )

View File

@@ -23,23 +23,25 @@ async def wallet1(mint):
async def test_melt(wallet1: Wallet, ledger: Ledger): async def test_melt(wallet1: Wallet, ledger: Ledger):
# mint twice so we have enough to pay the second invoice back # mint twice so we have enough to pay the second invoice back
invoice = await wallet1.request_mint(64) 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) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
assert wallet1.balance == 128 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(
mint_fees = await ledger.get_melt_fees(invoice.pr) invoice.bolt11
)
mint_fees = await ledger.get_melt_fees(invoice.bolt11)
assert mint_fees == fee_reserve_sat assert mint_fees == fee_reserve_sat
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) 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 @pytest.mark.asyncio
async def test_split(wallet1: Wallet, ledger: Ledger): async def test_split(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64) 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) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10)
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(len(send_proofs)) 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 @pytest.mark.asyncio
async def test_check_proof_state(wallet1: Wallet, ledger: Ledger): async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64) 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) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10)

View File

@@ -9,6 +9,7 @@ from cashu.core.base import Proof
from cashu.core.errors import CashuError, KeysetNotFoundError from cashu.core.errors import CashuError, KeysetNotFoundError
from cashu.core.helpers import sum_proofs from cashu.core.helpers import sum_proofs
from cashu.core.settings import settings 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
from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet1
from cashu.wallet.wallet import Wallet as Wallet2 from cashu.wallet.wallet import Wallet as Wallet2
@@ -137,16 +138,28 @@ async def test_get_keyset_ids(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mint(wallet1: Wallet): async def test_mint(wallet1: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
assert wallet1.balance == 64 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 @pytest.mark.asyncio
async def test_mint_amounts(wallet1: Wallet): async def test_mint_amounts(wallet1: Wallet):
"""Mint predefined amounts""" """Mint predefined amounts"""
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
amts = [1, 1, 1, 2, 2, 4, 16] 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.balance == 27
assert wallet1.proof_amounts == amts assert wallet1.proof_amounts == amts
@@ -174,7 +187,7 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split(wallet1: Wallet): async def test_split(wallet1: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
assert wallet1.balance == 64 assert wallet1.balance == 64
p1, p2 = await wallet1.split(wallet1.proofs, 20) p1, p2 = await wallet1.split(wallet1.proofs, 20)
assert wallet1.balance == 64 assert wallet1.balance == 64
@@ -189,7 +202,7 @@ async def test_split(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_to_send(wallet1: Wallet): async def test_split_to_send(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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( keep_proofs, spendable_proofs = await wallet1.split_to_send(
wallet1.proofs, 32, set_reserved=True wallet1.proofs, 32, set_reserved=True
) )
@@ -204,7 +217,7 @@ async def test_split_to_send(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_more_than_balance(wallet1: Wallet): async def test_split_more_than_balance(wallet1: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
await assert_err( await assert_err(
wallet1.split(wallet1.proofs, 128), wallet1.split(wallet1.proofs, 128),
# "Mint Error: inputs do not have same amount as outputs", # "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): async def test_melt(wallet1: Wallet):
# mint twice so we have enough to pay the second invoice back # mint twice so we have enough to pay the second invoice back
invoice = await wallet1.request_mint(64) 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) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
assert wallet1.balance == 128 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) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
await wallet1.pay_lightning( 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 # 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 == 128 - (total_amount - fee_reserve_sat)
assert wallet1.balance == 64
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_to_send_more_than_balance(wallet1: Wallet): async def test_split_to_send_more_than_balance(wallet1: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
await assert_err( await assert_err(
wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True), wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True),
"balance too low.", "balance too low.",
@@ -246,7 +280,7 @@ async def test_split_to_send_more_than_balance(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_double_spend(wallet1: Wallet): async def test_double_spend(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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 wallet1.split(wallet1.proofs, 20)
await assert_err( await assert_err(
wallet1.split(doublespend, 20), wallet1.split(doublespend, 20),
@@ -259,7 +293,7 @@ async def test_double_spend(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_duplicate_proofs_double_spent(wallet1: Wallet): async def test_duplicate_proofs_double_spent(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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( await assert_err(
wallet1.split(wallet1.proofs + doublespend, 20), wallet1.split(wallet1.proofs + doublespend, 20),
"Mint Error: proofs already pending.", "Mint Error: proofs already pending.",
@@ -271,7 +305,7 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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( _, spendable_proofs = await wallet1.split_to_send(
wallet1.proofs, 32, set_reserved=True 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): async def test_invalidate_unspent_proofs(wallet1: Wallet):
"""Try to invalidate proofs that have not been spent yet. Should not work!""" """Try to invalidate proofs that have not been spent yet. Should not work!"""
invoice = await wallet1.request_mint(64) 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) await wallet1.invalidate(wallet1.proofs)
assert wallet1.balance == 64 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): async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet):
"""Try to invalidate proofs that have not been spent yet but force no check.""" """Try to invalidate proofs that have not been spent yet but force no check."""
invoice = await wallet1.request_mint(64) 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) await wallet1.invalidate(wallet1.proofs, check_spendable=False)
assert wallet1.balance == 0 assert wallet1.balance == 0
@@ -306,7 +340,7 @@ async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_invalid_amount(wallet1: Wallet): async def test_split_invalid_amount(wallet1: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
await assert_err( await assert_err(
wallet1.split(wallet1.proofs, -1), wallet1.split(wallet1.proofs, -1),
"amount must be positive.", "amount must be positive.",
@@ -316,7 +350,7 @@ async def test_split_invalid_amount(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_token_state(wallet1: Wallet): async def test_token_state(wallet1: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
assert wallet1.balance == 64 assert wallet1.balance == 64
resp = await wallet1.check_proof_state(wallet1.proofs) resp = await wallet1.check_proof_state(wallet1.proofs)
assert resp.dict()["spendable"] assert resp.dict()["spendable"]

View File

@@ -1,8 +1,10 @@
import asyncio
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from fastapi.testclient import TestClient 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.api.app import app
from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet
from tests.conftest import SERVER_ENDPOINT from tests.conftest import SERVER_ENDPOINT
@@ -23,25 +25,19 @@ async def wallet(mint):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_invoice(wallet: Wallet): async def test_invoice(wallet: Wallet):
with TestClient(app) as client: with TestClient(app) as client:
response = client.post("/invoice?amount=100") response = client.post("/lightning/create_invoice?amount=100")
assert response.status_code == 200 assert response.status_code == 200
if settings.lightning: invoice_response = InvoiceResponse.parse_obj(response.json())
assert response.json()["invoice"] state = PaymentStatus(paid=False)
else: while not state.paid:
assert response.json()["amount"] print("checking invoice state")
response2 = client.get(
f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}"
@pytest.mark.asyncio )
async def test_invoice_with_split(wallet: Wallet): state = PaymentStatus.parse_obj(response2.json())
with TestClient(app) as client: await asyncio.sleep(0.1)
response = client.post("/invoice?amount=10&split=1") print("state:", state)
assert response.status_code == 200 print("paid")
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
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -49,7 +45,7 @@ async def test_balance():
with TestClient(app) as client: with TestClient(app) as client:
response = client.get("/balance") response = client.get("/balance")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["balance"] assert "balance" in response.json()
assert response.json()["keysets"] assert response.json()["keysets"]
assert response.json()["mints"] assert response.json()["mints"]
@@ -89,7 +85,7 @@ async def test_receive_all(wallet: Wallet):
with TestClient(app) as client: with TestClient(app) as client:
response = client.post("/receive?all=true") response = client.post("/receive?all=true")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["initial_balance"] == 0 assert response.json()["initial_balance"]
assert response.json()["balance"] assert response.json()["balance"]
@@ -100,23 +96,20 @@ async def test_burn_all(wallet: Wallet):
assert response.status_code == 200 assert response.status_code == 200
response = client.post("/burn?all=true") response = client.post("/burn?all=true")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["balance"] == 0 assert response.json()["balance"]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_pay(): async def test_pay():
with TestClient(app) as client: with TestClient(app) as client:
invoice = ( invoice = (
"lnbc100n1pjzp22cpp58xvjxvagzywky9xz3vurue822aaax" "lnbc100n1pjjcqzfdq4gdshx6r4ypjx2ur0wd5hgpp58xvj8yn00d5"
"735hzc5pj5fg307y58v5znqdq4vdshx6r4ypjx2ur0wd5hgl" "7uhshwzcwgy9uj3vwf5y2lr5fjf78s4w9l4vhr6xssp5stezsyty9r"
"h6ahauv24wdmac4zk478pmwfzd7sdvm8tje3dmfue3lc2g4l" "hv3lat69g4mhqxqun56jyehhkq3y8zufh83xyfkmmq4usaqwrt5q4f"
"9g40a073h39748uez9p8mxws5vqwjmkqr4wl5l7n4dlhj6z6" "adm44g6crckp0hzvuyv9sja7t65hxj0ucf9y46qstkay7gfnwhuxgr"
"va963cqvufrs4" "krf7djs38rml39l8wpn5ug9shp3n55quxhdecqfwxg23"
) )
response = client.post(f"/pay?invoice={invoice}") response = client.post(f"/lightning/pay_invoice?bolt11={invoice}")
if not settings.lightning:
assert response.status_code == 400
else:
assert response.status_code == 200 assert response.status_code == 200
@@ -159,10 +152,20 @@ async def test_info():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_flow(wallet: Wallet): async def test_flow(wallet: Wallet):
with TestClient(app) as client: with TestClient(app) as client:
if not settings.lightning:
response = client.get("/balance") response = client.get("/balance")
initial_balance = response.json()["balance"] initial_balance = response.json()["balance"]
response = client.post("/invoice?amount=100") 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") response = client.get("/balance")
assert response.json()["balance"] == initial_balance + 100 assert response.json()["balance"] == initial_balance + 100
response = client.post("/send?amount=50") response = client.post("/send?amount=50")

View File

@@ -59,7 +59,7 @@ async def wallet2(mint):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_htlc_secret(wallet1: Wallet): async def test_create_htlc_secret(wallet1: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(preimage=preimage) secret = await wallet1.create_htlc_lock(preimage=preimage)
@@ -69,7 +69,7 @@ async def test_create_htlc_secret(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(preimage=preimage) secret = await wallet1.create_htlc_lock(preimage=preimage)
@@ -82,7 +82,7 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(preimage=preimage) 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 @pytest.mark.asyncio
async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock( 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 @pytest.mark.asyncio
async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # 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 @pytest.mark.asyncio
async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # 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 @pytest.mark.asyncio
async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # 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 wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.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 wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash) await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()

View File

@@ -60,7 +60,7 @@ async def wallet2(mint):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_p2pk_pubkey(wallet1: Wallet): async def test_create_p2pk_pubkey(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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() pubkey = await wallet1.create_p2pk_pubkey()
PublicKey(bytes.fromhex(pubkey), raw=True) PublicKey(bytes.fromhex(pubkey), raw=True)
@@ -68,7 +68,7 @@ async def test_create_p2pk_pubkey(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk(wallet1: Wallet, wallet2: Wallet): async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# p2pk test # p2pk test
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side 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 @pytest.mark.asyncio
async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# p2pk test # p2pk test
secret_lock = await wallet1.create_p2pk_lock( secret_lock = await wallet1.create_p2pk_lock(
@@ -96,7 +96,7 @@ async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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 pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # 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 wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) 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 pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
secret_lock = await wallet1.create_p2pk_lock( 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 @pytest.mark.asyncio
async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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 pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
garbage_pubkey = PrivateKey().pubkey garbage_pubkey = PrivateKey().pubkey
@@ -169,7 +169,7 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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 await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
garbage_pubkey = PrivateKey().pubkey garbage_pubkey = PrivateKey().pubkey
@@ -204,7 +204,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
wallet1: Wallet, wallet2: Wallet wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) 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_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
@@ -235,7 +235,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
assert pubkey_wallet1 != pubkey_wallet2 assert pubkey_wallet1 != pubkey_wallet2
@@ -256,7 +256,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
assert pubkey_wallet1 != pubkey_wallet2 assert pubkey_wallet1 != pubkey_wallet2
@@ -279,7 +279,7 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
assert pubkey_wallet1 != pubkey_wallet2 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 @pytest.mark.asyncio
async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
assert pubkey_wallet1 != pubkey_wallet2 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 @pytest.mark.asyncio
async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# p2pk test # p2pk test
secret_lock = await wallet1.create_p2pk_lock( 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 wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) 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() await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
wrong_pubklic_key = PrivateKey().pubkey wrong_pubklic_key = PrivateKey().pubkey

View File

@@ -147,7 +147,7 @@ async def test_generate_secrets_from_to(wallet3: Wallet):
async def test_restore_wallet_after_mint(wallet3: Wallet): async def test_restore_wallet_after_mint(wallet3: Wallet):
await reset_wallet_db(wallet3) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
await wallet3.mint(64, hash=invoice.hash) await wallet3.mint(64, id=invoice.id)
assert wallet3.balance == 64 assert wallet3.balance == 64
await reset_wallet_db(wallet3) await reset_wallet_db(wallet3)
await wallet3.load_proofs() await wallet3.load_proofs()
@@ -177,7 +177,7 @@ async def test_restore_wallet_after_split_to_send(wallet3: Wallet):
await reset_wallet_db(wallet3) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
await wallet3.mint(64, hash=invoice.hash) await wallet3.mint(64, id=invoice.id)
assert wallet3.balance == 64 assert wallet3.balance == 64
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore _, 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) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
await wallet3.mint(64, hash=invoice.hash) await wallet3.mint(64, id=invoice.id)
assert wallet3.balance == 64 assert wallet3.balance == 64
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore _, 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) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
await wallet3.mint(64, hash=invoice.hash) await wallet3.mint(64, id=invoice.id)
assert wallet3.balance == 64 assert wallet3.balance == 64
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore _, 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) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(2) invoice = await wallet3.request_mint(2)
await wallet3.mint(2, hash=invoice.hash) await wallet3.mint(2, id=invoice.id)
box.add(wallet3.proofs) box.add(wallet3.proofs)
assert wallet3.balance == 2 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) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
await wallet3.mint(64, hash=invoice.hash) await wallet3.mint(64, id=invoice.id)
box.add(wallet3.proofs) box.add(wallet3.proofs)
assert wallet3.balance == 64 assert wallet3.balance == 64