mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
Wallet: Lightning interface (#318)
* mint does not start yet * fix import * revert mint db migrations * handle zero fee case * cli: adjust fee message * wallet: replace requests with httpx * clean up * rename http client decorator * fix pending check in main, todo: TEST PROXIES WITH HTTPX * fix up * use httpx for nostr as well * update packages to same versions as https://github.com/lnbits/lnbits/pull/1609/files * fix proof deserialization * check for string * tests passing * adjust wallet api tests * lockfile * add correct responses to Lightning interface and delete melt_id for proofs for which the payent has failed * fix create_invoice checking_id response * migrations atomic * proofs are stored automatically when created * make format * use bolt11 lib * stricter type checking * add fee response to payments * assert fees in test_melt * test that mint_id and melt_id is stored correctly in proofs and proofs_used * remove traces * refactor: Lightning interface into own file and LedgerCrud with typing * fix tests * fix payment response * rename variable
This commit is contained in:
2
Makefile
2
Makefile
@@ -11,7 +11,7 @@ black-check:
|
|||||||
poetry run black . --check
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,369 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from binascii import unhexlify
|
|
||||||
from decimal import Decimal
|
|
||||||
from typing import List, NamedTuple, Optional
|
|
||||||
|
|
||||||
import bitstring # type: ignore
|
|
||||||
import secp256k1
|
|
||||||
from bech32 import CHARSET, bech32_decode, bech32_encode
|
|
||||||
from ecdsa import SECP256k1, VerifyingKey # type: ignore
|
|
||||||
from ecdsa.util import sigdecode_string # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Route(NamedTuple):
|
|
||||||
pubkey: str
|
|
||||||
short_channel_id: str
|
|
||||||
base_fee_msat: int
|
|
||||||
ppm_fee: int
|
|
||||||
cltv: int
|
|
||||||
|
|
||||||
|
|
||||||
class Invoice(object):
|
|
||||||
payment_hash: str
|
|
||||||
amount_msat: int = 0
|
|
||||||
description: Optional[str] = None
|
|
||||||
description_hash: Optional[str] = None
|
|
||||||
payee: Optional[str] = None
|
|
||||||
date: int
|
|
||||||
expiry: int = 3600
|
|
||||||
secret: Optional[str] = None
|
|
||||||
route_hints: List[Route] = []
|
|
||||||
min_final_cltv_expiry: int = 18
|
|
||||||
|
|
||||||
|
|
||||||
def decode(pr: str) -> Invoice:
|
|
||||||
"""bolt11 decoder,
|
|
||||||
based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
hrp, decoded_data = bech32_decode(pr)
|
|
||||||
if hrp is None or decoded_data is None:
|
|
||||||
raise ValueError("Bad bech32 checksum")
|
|
||||||
if not hrp.startswith("ln"):
|
|
||||||
raise ValueError("Does not start with ln")
|
|
||||||
|
|
||||||
bitarray = _u5_to_bitarray(decoded_data)
|
|
||||||
|
|
||||||
# final signature 65 bytes, split it off.
|
|
||||||
if len(bitarray) < 65 * 8:
|
|
||||||
raise ValueError("Too short to contain signature")
|
|
||||||
|
|
||||||
# extract the signature
|
|
||||||
signature = bitarray[-65 * 8 :].tobytes()
|
|
||||||
|
|
||||||
# the tagged fields as a bitstream
|
|
||||||
data = bitstring.ConstBitStream(bitarray[: -65 * 8])
|
|
||||||
|
|
||||||
# build the invoice object
|
|
||||||
invoice = Invoice()
|
|
||||||
|
|
||||||
# decode the amount from the hrp
|
|
||||||
m = re.search(r"[^\d]+", hrp[2:])
|
|
||||||
if m:
|
|
||||||
amountstr = hrp[2 + m.end() :]
|
|
||||||
if amountstr != "":
|
|
||||||
invoice.amount_msat = _unshorten_amount(amountstr)
|
|
||||||
|
|
||||||
# pull out date
|
|
||||||
invoice.date = data.read(35).uint
|
|
||||||
|
|
||||||
while data.pos != data.len:
|
|
||||||
tag, tagdata, data = _pull_tagged(data)
|
|
||||||
data_length = len(tagdata) / 5
|
|
||||||
|
|
||||||
if tag == "d":
|
|
||||||
invoice.description = _trim_to_bytes(tagdata).decode("utf-8")
|
|
||||||
elif tag == "h" and data_length == 52:
|
|
||||||
invoice.description_hash = _trim_to_bytes(tagdata).hex()
|
|
||||||
elif tag == "p" and data_length == 52:
|
|
||||||
invoice.payment_hash = _trim_to_bytes(tagdata).hex()
|
|
||||||
elif tag == "x":
|
|
||||||
invoice.expiry = tagdata.uint
|
|
||||||
elif tag == "n":
|
|
||||||
invoice.payee = _trim_to_bytes(tagdata).hex()
|
|
||||||
# this won't work in most cases, we must extract the payee
|
|
||||||
# from the signature
|
|
||||||
elif tag == "s":
|
|
||||||
invoice.secret = _trim_to_bytes(tagdata).hex()
|
|
||||||
elif tag == "r":
|
|
||||||
s = bitstring.ConstBitStream(tagdata)
|
|
||||||
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
|
|
||||||
route = Route(
|
|
||||||
pubkey=s.read(264).tobytes().hex(),
|
|
||||||
short_channel_id=_readable_scid(s.read(64).intbe),
|
|
||||||
base_fee_msat=s.read(32).intbe,
|
|
||||||
ppm_fee=s.read(32).intbe,
|
|
||||||
cltv=s.read(16).intbe,
|
|
||||||
)
|
|
||||||
invoice.route_hints.append(route)
|
|
||||||
|
|
||||||
# BOLT #11:
|
|
||||||
# A reader MUST check that the `signature` is valid (see the `n` tagged
|
|
||||||
# field specified below).
|
|
||||||
# A reader MUST use the `n` field to validate the signature instead of
|
|
||||||
# performing signature recovery if a valid `n` field is provided.
|
|
||||||
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
|
|
||||||
sig = signature[0:64]
|
|
||||||
if invoice.payee:
|
|
||||||
key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1)
|
|
||||||
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
|
|
||||||
else:
|
|
||||||
keys = VerifyingKey.from_public_key_recovery(
|
|
||||||
sig, message, SECP256k1, hashlib.sha256
|
|
||||||
)
|
|
||||||
signaling_byte = signature[64]
|
|
||||||
key = keys[int(signaling_byte)]
|
|
||||||
invoice.payee = key.to_string("compressed").hex()
|
|
||||||
|
|
||||||
return invoice
|
|
||||||
|
|
||||||
|
|
||||||
def encode(options):
|
|
||||||
"""Convert options into LnAddr and pass it to the encoder"""
|
|
||||||
addr = LnAddr()
|
|
||||||
addr.currency = options["currency"]
|
|
||||||
addr.fallback = options["fallback"] if options["fallback"] else None
|
|
||||||
if options["amount"]:
|
|
||||||
addr.amount = options["amount"]
|
|
||||||
if options["timestamp"]:
|
|
||||||
addr.date = int(options["timestamp"])
|
|
||||||
|
|
||||||
addr.paymenthash = unhexlify(options["paymenthash"])
|
|
||||||
|
|
||||||
if options["description"]:
|
|
||||||
addr.tags.append(("d", options["description"]))
|
|
||||||
if options["description_hash"]:
|
|
||||||
addr.tags.append(("h", options["description_hash"]))
|
|
||||||
if options["expires"]:
|
|
||||||
addr.tags.append(("x", options["expires"]))
|
|
||||||
|
|
||||||
if options["fallback"]:
|
|
||||||
addr.tags.append(("f", options["fallback"]))
|
|
||||||
if options["route"]:
|
|
||||||
for r in options["route"]:
|
|
||||||
splits = r.split("/")
|
|
||||||
route = []
|
|
||||||
while len(splits) >= 5:
|
|
||||||
route.append(
|
|
||||||
(
|
|
||||||
unhexlify(splits[0]),
|
|
||||||
unhexlify(splits[1]),
|
|
||||||
int(splits[2]),
|
|
||||||
int(splits[3]),
|
|
||||||
int(splits[4]),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
splits = splits[5:]
|
|
||||||
assert len(splits) == 0
|
|
||||||
addr.tags.append(("r", route))
|
|
||||||
return lnencode(addr, options["privkey"])
|
|
||||||
|
|
||||||
|
|
||||||
def lnencode(addr, privkey):
|
|
||||||
if addr.amount:
|
|
||||||
amount = Decimal(str(addr.amount))
|
|
||||||
# We can only send down to millisatoshi.
|
|
||||||
if amount * 10**12 % 10:
|
|
||||||
raise ValueError(
|
|
||||||
"Cannot encode {}: too many decimal places".format(addr.amount)
|
|
||||||
)
|
|
||||||
|
|
||||||
amount = addr.currency + shorten_amount(amount)
|
|
||||||
else:
|
|
||||||
amount = addr.currency if addr.currency else ""
|
|
||||||
|
|
||||||
hrp = "ln" + amount + "0n"
|
|
||||||
|
|
||||||
# Start with the timestamp
|
|
||||||
data = bitstring.pack("uint:35", addr.date)
|
|
||||||
|
|
||||||
# Payment hash
|
|
||||||
data += tagged_bytes("p", addr.paymenthash)
|
|
||||||
tags_set = set()
|
|
||||||
|
|
||||||
for k, v in addr.tags:
|
|
||||||
# BOLT #11:
|
|
||||||
#
|
|
||||||
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
|
|
||||||
if k in ("d", "h", "n", "x"):
|
|
||||||
if k in tags_set:
|
|
||||||
raise ValueError("Duplicate '{}' tag".format(k))
|
|
||||||
|
|
||||||
if k == "r":
|
|
||||||
route = bitstring.BitArray()
|
|
||||||
for step in v:
|
|
||||||
pubkey, channel, feebase, feerate, cltv = step
|
|
||||||
route.append(
|
|
||||||
bitstring.BitArray(pubkey)
|
|
||||||
+ bitstring.BitArray(channel)
|
|
||||||
+ bitstring.pack("intbe:32", feebase)
|
|
||||||
+ bitstring.pack("intbe:32", feerate)
|
|
||||||
+ bitstring.pack("intbe:16", cltv)
|
|
||||||
)
|
|
||||||
data += tagged("r", route)
|
|
||||||
elif k == "f":
|
|
||||||
data += encode_fallback(v, addr.currency)
|
|
||||||
elif k == "d":
|
|
||||||
data += tagged_bytes("d", v.encode())
|
|
||||||
elif k == "x":
|
|
||||||
# Get minimal length by trimming leading 5 bits at a time.
|
|
||||||
expirybits = bitstring.pack("intbe:64", v)[4:64]
|
|
||||||
while expirybits.startswith("0b00000"):
|
|
||||||
expirybits = expirybits[5:]
|
|
||||||
data += tagged("x", expirybits)
|
|
||||||
elif k == "h":
|
|
||||||
data += tagged_bytes("h", v)
|
|
||||||
elif k == "n":
|
|
||||||
data += tagged_bytes("n", v)
|
|
||||||
else:
|
|
||||||
# FIXME: Support unknown tags?
|
|
||||||
raise ValueError("Unknown tag {}".format(k))
|
|
||||||
|
|
||||||
tags_set.add(k)
|
|
||||||
|
|
||||||
# BOLT #11:
|
|
||||||
#
|
|
||||||
# A writer MUST include either a `d` or `h` field, and MUST NOT include
|
|
||||||
# both.
|
|
||||||
if "d" in tags_set and "h" in tags_set:
|
|
||||||
raise ValueError("Cannot include both 'd' and 'h'")
|
|
||||||
if not "d" in tags_set and not "h" in tags_set:
|
|
||||||
raise ValueError("Must include either 'd' or 'h'")
|
|
||||||
|
|
||||||
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
|
||||||
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
|
|
||||||
sig = privkey.ecdsa_sign_recoverable(
|
|
||||||
bytearray([ord(c) for c in hrp]) + data.tobytes()
|
|
||||||
)
|
|
||||||
# This doesn't actually serialize, but returns a pair of values :(
|
|
||||||
sig, recid = privkey.ecdsa_recoverable_serialize(sig)
|
|
||||||
data += bytes(sig) + bytes([recid])
|
|
||||||
|
|
||||||
return bech32_encode(hrp, bitarray_to_u5(data))
|
|
||||||
|
|
||||||
|
|
||||||
class LnAddr(object):
|
|
||||||
def __init__(
|
|
||||||
self, paymenthash=None, amount=None, currency="bc", tags=None, date=None
|
|
||||||
):
|
|
||||||
self.date = int(time.time()) if not date else int(date)
|
|
||||||
self.tags = [] if not tags else tags
|
|
||||||
self.unknown_tags = []
|
|
||||||
self.paymenthash = paymenthash
|
|
||||||
self.signature = None
|
|
||||||
self.pubkey = None
|
|
||||||
self.currency = currency
|
|
||||||
self.amount = amount
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
|
||||||
hexlify(self.pubkey.serialize()).decode("utf-8"),
|
|
||||||
self.amount,
|
|
||||||
self.currency,
|
|
||||||
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def shorten_amount(amount):
|
|
||||||
"""Given an amount in bitcoin, shorten it"""
|
|
||||||
# Convert to pico initially
|
|
||||||
amount = int(amount * 10**12)
|
|
||||||
units = ["p", "n", "u", "m", ""]
|
|
||||||
for unit in units:
|
|
||||||
if amount % 1000 == 0:
|
|
||||||
amount //= 1000
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return str(amount) + unit
|
|
||||||
|
|
||||||
|
|
||||||
def _unshorten_amount(amount: str) -> int:
|
|
||||||
"""Given a shortened amount, return millisatoshis"""
|
|
||||||
# BOLT #11:
|
|
||||||
# The following `multiplier` letters are defined:
|
|
||||||
#
|
|
||||||
# * `m` (milli): multiply by 0.001
|
|
||||||
# * `u` (micro): multiply by 0.000001
|
|
||||||
# * `n` (nano): multiply by 0.000000001
|
|
||||||
# * `p` (pico): multiply by 0.000000000001
|
|
||||||
units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3}
|
|
||||||
unit = str(amount)[-1]
|
|
||||||
|
|
||||||
# BOLT #11:
|
|
||||||
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
|
|
||||||
# anything except a `multiplier` in the table above.
|
|
||||||
if not re.fullmatch(r"\d+[pnum]?", str(amount)):
|
|
||||||
raise ValueError("Invalid amount '{}'".format(amount))
|
|
||||||
|
|
||||||
if unit in units:
|
|
||||||
return int(int(amount[:-1]) * 100_000_000_000 / units[unit])
|
|
||||||
else:
|
|
||||||
return int(amount) * 100_000_000_000
|
|
||||||
|
|
||||||
|
|
||||||
def _pull_tagged(stream):
|
|
||||||
tag = stream.read(5).uint
|
|
||||||
length = stream.read(5).uint * 32 + stream.read(5).uint
|
|
||||||
return (CHARSET[tag], stream.read(length * 5), stream)
|
|
||||||
|
|
||||||
|
|
||||||
def is_p2pkh(currency, prefix):
|
|
||||||
return prefix == base58_prefix_map[currency][0]
|
|
||||||
|
|
||||||
|
|
||||||
def is_p2sh(currency, prefix):
|
|
||||||
return prefix == base58_prefix_map[currency][1]
|
|
||||||
|
|
||||||
|
|
||||||
# Tagged field containing BitArray
|
|
||||||
def tagged(char, l):
|
|
||||||
# Tagged fields need to be zero-padded to 5 bits.
|
|
||||||
while l.len % 5 != 0:
|
|
||||||
l.append("0b0")
|
|
||||||
return (
|
|
||||||
bitstring.pack(
|
|
||||||
"uint:5, uint:5, uint:5",
|
|
||||||
CHARSET.find(char),
|
|
||||||
(l.len / 5) / 32,
|
|
||||||
(l.len / 5) % 32,
|
|
||||||
)
|
|
||||||
+ l
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def tagged_bytes(char, l):
|
|
||||||
return tagged(char, bitstring.BitArray(l))
|
|
||||||
|
|
||||||
|
|
||||||
def _trim_to_bytes(barr):
|
|
||||||
# Adds a byte if necessary.
|
|
||||||
b = barr.tobytes()
|
|
||||||
if barr.len % 8 != 0:
|
|
||||||
return b[:-1]
|
|
||||||
return b
|
|
||||||
|
|
||||||
|
|
||||||
def _readable_scid(short_channel_id: int) -> str:
|
|
||||||
return "{blockheight}x{transactionindex}x{outputindex}".format(
|
|
||||||
blockheight=((short_channel_id >> 40) & 0xFFFFFF),
|
|
||||||
transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
|
|
||||||
outputindex=(short_channel_id & 0xFFFF),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
|
|
||||||
ret = bitstring.BitArray()
|
|
||||||
for a in arr:
|
|
||||||
ret += bitstring.pack("uint:5", a)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def bitarray_to_u5(barr):
|
|
||||||
assert barr.len % 5 == 0
|
|
||||||
ret = []
|
|
||||||
s = bitstring.ConstBitStream(barr)
|
|
||||||
while s.pos != s.len:
|
|
||||||
ret.append(s.read(5).uint)
|
|
||||||
return ret
|
|
||||||
@@ -3,7 +3,7 @@ import datetime
|
|||||||
import os
|
import 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}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
137
cashu/mint/lightning.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from ..core.base import (
|
||||||
|
Invoice,
|
||||||
|
)
|
||||||
|
from ..core.db import Connection, Database
|
||||||
|
from ..core.errors import (
|
||||||
|
InvoiceNotPaidError,
|
||||||
|
LightningError,
|
||||||
|
)
|
||||||
|
from ..lightning.base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||||
|
from ..mint.crud import LedgerCrud
|
||||||
|
from .protocols import SupportLightning, SupportsDb
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerLightning(SupportLightning, SupportsDb):
|
||||||
|
"""Lightning functions for the ledger."""
|
||||||
|
|
||||||
|
lightning: Wallet
|
||||||
|
crud: LedgerCrud
|
||||||
|
db: Database
|
||||||
|
|
||||||
|
async def _request_lightning_invoice(self, amount: int) -> InvoiceResponse:
|
||||||
|
"""Generate a Lightning invoice using the funding source backend.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
amount (int): Amount of invoice (in Satoshis)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Error with funding source.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[str, str]: Bolt11 invoice and payment id (for lookup)
|
||||||
|
"""
|
||||||
|
logger.trace(
|
||||||
|
"_request_lightning_invoice: Requesting Lightning invoice for"
|
||||||
|
f" {amount} satoshis."
|
||||||
|
)
|
||||||
|
status = await self.lightning.status()
|
||||||
|
logger.trace(
|
||||||
|
"_request_lightning_invoice: Lightning wallet balance:"
|
||||||
|
f" {status.balance_msat}"
|
||||||
|
)
|
||||||
|
if status.error_message:
|
||||||
|
raise LightningError(
|
||||||
|
f"Lightning wallet not responding: {status.error_message}"
|
||||||
|
)
|
||||||
|
payment = await self.lightning.create_invoice(amount, "Cashu deposit")
|
||||||
|
logger.trace(
|
||||||
|
f"_request_lightning_invoice: Lightning invoice: {payment.payment_request}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payment.ok:
|
||||||
|
raise LightningError(f"Lightning wallet error: {payment.error_message}")
|
||||||
|
assert payment.payment_request and payment.checking_id, LightningError(
|
||||||
|
"could not fetch invoice from Lightning backend"
|
||||||
|
)
|
||||||
|
return payment
|
||||||
|
|
||||||
|
async def _check_lightning_invoice(
|
||||||
|
self, *, amount: int, id: str, conn: Optional[Connection] = None
|
||||||
|
) -> PaymentStatus:
|
||||||
|
"""Checks with the Lightning backend whether an invoice with `id` was paid.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
amount (int): Amount of the outputs the wallet wants in return (in Satoshis).
|
||||||
|
id (str): Id to look up Lightning invoice by.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Invoice not found.
|
||||||
|
Exception: Tokens for invoice already issued.
|
||||||
|
Exception: Amount larger than invoice amount.
|
||||||
|
Exception: Invoice not paid yet
|
||||||
|
e: Update database and pass through error.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if invoice has been paid, else False
|
||||||
|
"""
|
||||||
|
invoice: Union[Invoice, None] = await self.crud.get_lightning_invoice(
|
||||||
|
id=id, db=self.db, conn=conn
|
||||||
|
)
|
||||||
|
if invoice is None:
|
||||||
|
raise LightningError("invoice not found.")
|
||||||
|
if invoice.issued:
|
||||||
|
raise LightningError("tokens already issued for this invoice.")
|
||||||
|
if amount > invoice.amount:
|
||||||
|
raise LightningError(
|
||||||
|
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
|
||||||
|
)
|
||||||
|
assert invoice.payment_hash, "invoice has no payment hash."
|
||||||
|
# set this invoice as issued
|
||||||
|
await self.crud.update_lightning_invoice(
|
||||||
|
id=id, issued=True, db=self.db, conn=conn
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = await self.lightning.get_invoice_status(invoice.payment_hash)
|
||||||
|
if status.paid:
|
||||||
|
return status
|
||||||
|
else:
|
||||||
|
raise InvoiceNotPaidError()
|
||||||
|
except Exception as e:
|
||||||
|
# unset issued
|
||||||
|
await self.crud.update_lightning_invoice(
|
||||||
|
id=id, issued=False, db=self.db, conn=conn
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
async def _pay_lightning_invoice(
|
||||||
|
self, invoice: str, fee_limit_msat: int
|
||||||
|
) -> PaymentResponse:
|
||||||
|
"""Pays a Lightning invoice via the funding source backend.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
invoice (str): Bolt11 Lightning invoice
|
||||||
|
fee_limit_msat (int): Maximum fee reserve for payment (in Millisatoshi)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Funding source error.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, string, int]: Returns payment status, preimage of invoice, paid fees (in Millisatoshi)
|
||||||
|
"""
|
||||||
|
status = await self.lightning.status()
|
||||||
|
if status.error_message:
|
||||||
|
raise LightningError(
|
||||||
|
f"Lightning wallet not responding: {status.error_message}"
|
||||||
|
)
|
||||||
|
payment = await self.lightning.pay_invoice(
|
||||||
|
invoice, fee_limit_msat=fee_limit_msat
|
||||||
|
)
|
||||||
|
logger.trace(f"_pay_lightning_invoice: Lightning payment status: {payment.ok}")
|
||||||
|
# make sure that fee is positive and not None
|
||||||
|
payment.fee_msat = abs(payment.fee_msat) if payment.fee_msat else 0
|
||||||
|
return payment
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
from ..core.db import Database, table_with_schema
|
from ..core.db import Connection, Database, table_with_schema
|
||||||
|
|
||||||
|
|
||||||
async def m000_create_migrations_table(db: Database):
|
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,120 +11,127 @@ 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:
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
|
await conn.execute(f"""
|
||||||
amount {db.big_int} NOT NULL,
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
|
||||||
B_b TEXT NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
C_b TEXT NOT NULL,
|
B_b TEXT NOT NULL,
|
||||||
|
C_b TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE (B_b)
|
UNIQUE (B_b)
|
||||||
|
|
||||||
);
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
await conn.execute(f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
||||||
|
amount {db.big_int} NOT NULL,
|
||||||
|
C TEXT NOT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (secret)
|
||||||
|
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
await conn.execute(f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} (
|
||||||
|
amount {db.big_int} NOT NULL,
|
||||||
|
pr TEXT NOT NULL,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
issued BOOL NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (hash)
|
||||||
|
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
async def m002_add_balance_views(db: Database):
|
||||||
|
async with db.connect() as conn:
|
||||||
|
await conn.execute(f"""
|
||||||
|
CREATE VIEW {table_with_schema(db, 'balance_issued')} AS
|
||||||
|
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||||
|
SELECT SUM(amount)
|
||||||
|
FROM {table_with_schema(db, 'promises')}
|
||||||
|
WHERE amount > 0
|
||||||
|
) AS s;
|
||||||
""")
|
""")
|
||||||
|
|
||||||
await db.execute(f"""
|
await conn.execute(f"""
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
CREATE VIEW {table_with_schema(db, 'balance_redeemed')} AS
|
||||||
amount {db.big_int} NOT NULL,
|
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||||
C TEXT NOT NULL,
|
SELECT SUM(amount)
|
||||||
secret TEXT NOT NULL,
|
FROM {table_with_schema(db, 'proofs_used')}
|
||||||
|
WHERE amount > 0
|
||||||
UNIQUE (secret)
|
) AS s;
|
||||||
|
|
||||||
);
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
await db.execute(f"""
|
await conn.execute(f"""
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} (
|
CREATE VIEW {table_with_schema(db, 'balance')} AS
|
||||||
amount {db.big_int} NOT NULL,
|
SELECT s_issued - s_used FROM (
|
||||||
pr TEXT NOT NULL,
|
SELECT bi.balance AS s_issued, bu.balance AS s_used
|
||||||
hash TEXT NOT NULL,
|
FROM {table_with_schema(db, 'balance_issued')} bi
|
||||||
issued BOOL NOT NULL,
|
CROSS JOIN {table_with_schema(db, 'balance_redeemed')} bu
|
||||||
|
) AS balance;
|
||||||
UNIQUE (hash)
|
|
||||||
|
|
||||||
);
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
await db.execute(f"""
|
|
||||||
CREATE VIEW {table_with_schema(db, 'balance_issued')} AS
|
|
||||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
|
||||||
SELECT SUM(amount)
|
|
||||||
FROM {table_with_schema(db, 'promises')}
|
|
||||||
WHERE amount > 0
|
|
||||||
) AS s;
|
|
||||||
""")
|
|
||||||
|
|
||||||
await db.execute(f"""
|
|
||||||
CREATE VIEW {table_with_schema(db, 'balance_redeemed')} AS
|
|
||||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
|
||||||
SELECT SUM(amount)
|
|
||||||
FROM {table_with_schema(db, 'proofs_used')}
|
|
||||||
WHERE amount > 0
|
|
||||||
) AS s;
|
|
||||||
""")
|
|
||||||
|
|
||||||
await db.execute(f"""
|
|
||||||
CREATE VIEW {table_with_schema(db, 'balance')} AS
|
|
||||||
SELECT s_issued - s_used FROM (
|
|
||||||
SELECT bi.balance AS s_issued, bu.balance AS s_used
|
|
||||||
FROM {table_with_schema(db, 'balance_issued')} bi
|
|
||||||
CROSS JOIN {table_with_schema(db, 'balance_redeemed')} bu
|
|
||||||
) AS balance;
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
async def m003_mint_keysets(db: Database):
|
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:
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} (
|
await conn.execute(f"""
|
||||||
id TEXT NOT NULL,
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} (
|
||||||
derivation_path TEXT,
|
id TEXT NOT NULL,
|
||||||
valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
derivation_path TEXT,
|
||||||
valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||||
first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||||
active BOOL DEFAULT TRUE,
|
first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||||
|
active BOOL DEFAULT TRUE,
|
||||||
|
|
||||||
UNIQUE (derivation_path)
|
UNIQUE (derivation_path)
|
||||||
|
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
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,
|
||||||
pubkey TEXT NOT NULL,
|
pubkey TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE (id, pubkey)
|
UNIQUE (id, pubkey)
|
||||||
|
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
async def m004_keysets_add_version(db: Database):
|
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:
|
||||||
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT"
|
await conn.execute(
|
||||||
)
|
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m005_pending_proofs_table(db: Database) -> None:
|
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:
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
await conn.execute(f"""
|
||||||
amount INTEGER NOT NULL,
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
||||||
C TEXT NOT NULL,
|
amount INTEGER NOT NULL,
|
||||||
secret TEXT NOT NULL,
|
C TEXT NOT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE (secret)
|
UNIQUE (secret)
|
||||||
|
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
async def m006_invoices_add_payment_hash(db: Database):
|
async def m006_invoices_add_payment_hash(db: Database):
|
||||||
@@ -133,38 +140,67 @@ async def m006_invoices_add_payment_hash(db: Database):
|
|||||||
the column hash as a random identifier now
|
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"
|
||||||
await db.execute(
|
" TEXT"
|
||||||
f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash"
|
)
|
||||||
)
|
await conn.execute(
|
||||||
|
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:
|
||||||
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT"
|
await conn.execute(
|
||||||
)
|
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT"
|
||||||
await db.execute(
|
)
|
||||||
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT"
|
await conn.execute(
|
||||||
)
|
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT"
|
||||||
await db.execute(
|
)
|
||||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT"
|
await conn.execute(
|
||||||
)
|
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m008_promises_dleq(db: Database):
|
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:
|
||||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN e TEXT"
|
await conn.execute(
|
||||||
)
|
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN e TEXT"
|
||||||
await db.execute(
|
)
|
||||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN s TEXT"
|
await conn.execute(
|
||||||
)
|
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN s TEXT"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m009_add_out_to_invoices(db: Database):
|
||||||
|
# column in invoices for marking whether the invoice is incoming (out=False) or outgoing (out=True)
|
||||||
|
async with db.connect() as conn:
|
||||||
|
# we have to drop the balance views first and recreate them later
|
||||||
|
await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance_issued')}")
|
||||||
|
await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance_redeemed')}")
|
||||||
|
await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance')}")
|
||||||
|
|
||||||
|
# rename column pr to bolt11
|
||||||
|
await conn.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'invoices')} RENAME COLUMN pr TO"
|
||||||
|
" bolt11"
|
||||||
|
)
|
||||||
|
# rename column hash to payment_hash
|
||||||
|
await conn.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'invoices')} RENAME COLUMN hash TO id"
|
||||||
|
)
|
||||||
|
|
||||||
|
# recreate balance views
|
||||||
|
await m002_add_balance_views(db)
|
||||||
|
|
||||||
|
async with db.connect() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN out BOOL"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
from typing import Protocol
|
from 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
wallet = await mint_wallet(mint)
|
if mint:
|
||||||
await wallet.load_proofs(reload=True)
|
wallet = await mint_wallet(mint)
|
||||||
|
payment_response = await wallet.pay_invoice(bolt11)
|
||||||
total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
|
return payment_response
|
||||||
assert total_amount > 0, "amount has to be larger than zero."
|
|
||||||
assert wallet.available_balance >= total_amount, "balance is too low."
|
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
|
||||||
await wallet.pay_lightning(send_proofs, invoice, fee_reserve_sat)
|
|
||||||
return PayResponse(
|
|
||||||
amount=total_amount - fee_reserve_sat,
|
|
||||||
fee=fee_reserve_sat,
|
|
||||||
amount_with_fee=total_amount,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.get(
|
||||||
"/invoice", name="Request lightning invoice", response_model=InvoiceResponse
|
"/lightning/payment_state",
|
||||||
|
name="Request lightning invoice",
|
||||||
|
response_model=PaymentStatus,
|
||||||
)
|
)
|
||||||
async def invoice(
|
async def payment_state(
|
||||||
amount: int = Query(default=..., description="Amount to request in invoice"),
|
payment_hash: str = Query(default=None, description="Id of paid 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
|
||||||
wallet = await mint_wallet(mint)
|
if mint:
|
||||||
if not settings.lightning:
|
wallet = await mint_wallet(mint)
|
||||||
await wallet.mint(amount, split=optional_split)
|
state = await wallet.get_payment_status(payment_hash)
|
||||||
return InvoiceResponse(
|
return state
|
||||||
amount=amount,
|
|
||||||
)
|
|
||||||
elif amount and not hash:
|
@router.post(
|
||||||
invoice = await wallet.request_mint(amount)
|
"/lightning/create_invoice",
|
||||||
return InvoiceResponse(
|
name="Request lightning invoice",
|
||||||
amount=amount,
|
response_model=InvoiceResponse,
|
||||||
invoice=invoice,
|
)
|
||||||
)
|
async def create_invoice(
|
||||||
elif amount and hash:
|
amount: int = Query(default=..., description="Amount to request in invoice"),
|
||||||
await wallet.mint(amount, split=optional_split, hash=hash)
|
mint: str = Query(
|
||||||
return InvoiceResponse(
|
default=None,
|
||||||
amount=amount,
|
description="Mint URL to create an invoice at (None for default mint)",
|
||||||
hash=hash,
|
),
|
||||||
)
|
) -> InvoiceResponse:
|
||||||
return
|
global wallet
|
||||||
|
if mint:
|
||||||
|
wallet = await mint_wallet(mint)
|
||||||
|
invoice = await wallet.create_invoice(amount)
|
||||||
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/lightning/invoice_state",
|
||||||
|
name="Request lightning invoice",
|
||||||
|
response_model=PaymentStatus,
|
||||||
|
)
|
||||||
|
async def invoice_state(
|
||||||
|
payment_hash: str = Query(default=None, description="Payment hash of paid invoice"),
|
||||||
|
mint: str = Query(
|
||||||
|
default=None,
|
||||||
|
description="Mint URL to create an invoice at (None for default mint)",
|
||||||
|
),
|
||||||
|
) -> PaymentStatus:
|
||||||
|
global wallet
|
||||||
|
if mint:
|
||||||
|
wallet = await mint_wallet(mint)
|
||||||
|
state = await wallet.get_invoice_status(payment_hash)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/lightning/balance",
|
||||||
|
name="Balance",
|
||||||
|
summary="Display balance.",
|
||||||
|
response_model=StatusResponse,
|
||||||
|
)
|
||||||
|
async def lightning_balance() -> StatusResponse:
|
||||||
|
try:
|
||||||
|
await wallet.load_proofs(reload=True)
|
||||||
|
except Exception as exc:
|
||||||
|
return StatusResponse(error_message=str(exc), balance_msat=0)
|
||||||
|
return StatusResponse(
|
||||||
|
error_message=None, balance_msat=wallet.available_balance * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@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
|
||||||
@@ -284,7 +307,7 @@ async def burn(
|
|||||||
if not (all or token or force or delete) or (token and all):
|
if not (all or token or force or delete) or (token and all):
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"enter a token or use --all to burn all pending tokens, --force to"
|
"enter a token or use --all to burn all pending tokens, --force to"
|
||||||
" check all tokensor --delete with send ID to force-delete pending"
|
" check all tokens or --delete with send ID to force-delete pending"
|
||||||
" token from list if mint is unavailable.",
|
" token from list if mint is unavailable.",
|
||||||
)
|
)
|
||||||
if all:
|
if all:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
1
cashu/wallet/lightning/__init__.py
Normal file
1
cashu/wallet/lightning/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .lightning import LightningWallet # noqa
|
||||||
BIN
cashu/wallet/lightning/data/lightning.db/wallet.sqlite3
Normal file
BIN
cashu/wallet/lightning/data/lightning.db/wallet.sqlite3
Normal file
Binary file not shown.
153
cashu/wallet/lightning/lightning.py
Normal file
153
cashu/wallet/lightning/lightning.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import bolt11
|
||||||
|
|
||||||
|
from ...core.helpers import sum_promises
|
||||||
|
from ...core.settings import settings
|
||||||
|
from ...lightning.base import (
|
||||||
|
InvoiceResponse,
|
||||||
|
PaymentResponse,
|
||||||
|
PaymentStatus,
|
||||||
|
StatusResponse,
|
||||||
|
)
|
||||||
|
from ...wallet.crud import get_lightning_invoice, get_proofs
|
||||||
|
from ..wallet import Wallet
|
||||||
|
|
||||||
|
|
||||||
|
class LightningWallet(Wallet):
|
||||||
|
"""
|
||||||
|
Lightning wallet interface for Cashu
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def async_init(self, raise_connection_error: bool = True):
|
||||||
|
"""Async init for lightning wallet"""
|
||||||
|
settings.tor = False
|
||||||
|
await self.load_proofs()
|
||||||
|
try:
|
||||||
|
await self.load_mint()
|
||||||
|
except Exception as e:
|
||||||
|
if raise_connection_error:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if not args and not kwargs:
|
||||||
|
pass
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
async def create_invoice(self, amount: int) -> InvoiceResponse:
|
||||||
|
"""Create lightning invoice
|
||||||
|
|
||||||
|
Args:
|
||||||
|
amount (int): amount in satoshis
|
||||||
|
Returns:
|
||||||
|
str: invoice
|
||||||
|
"""
|
||||||
|
invoice = await self.request_mint(amount)
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True, payment_request=invoice.bolt11, checking_id=invoice.payment_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pay_invoice(self, pr: str) -> PaymentResponse:
|
||||||
|
"""Pay lightning invoice
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pr (str): bolt11 payment request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful
|
||||||
|
"""
|
||||||
|
total_amount, fee_reserve_sat = await self.get_pay_amount_with_fees(pr)
|
||||||
|
assert total_amount > 0, "amount is not positive"
|
||||||
|
if self.available_balance < total_amount:
|
||||||
|
print("Error: Balance too low.")
|
||||||
|
return PaymentResponse(ok=False)
|
||||||
|
_, send_proofs = await self.split_to_send(self.proofs, total_amount)
|
||||||
|
try:
|
||||||
|
resp = await self.pay_lightning(send_proofs, pr, fee_reserve_sat)
|
||||||
|
if resp.change:
|
||||||
|
fees_paid_sat = fee_reserve_sat - sum_promises(resp.change)
|
||||||
|
else:
|
||||||
|
fees_paid_sat = fee_reserve_sat
|
||||||
|
|
||||||
|
invoice_obj = bolt11.decode(pr)
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=invoice_obj.payment_hash,
|
||||||
|
preimage=resp.preimage,
|
||||||
|
fee_msat=fees_paid_sat * 1000,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print("Exception:", e)
|
||||||
|
return PaymentResponse(ok=False, error_message=str(e))
|
||||||
|
|
||||||
|
async def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
|
"""Get lightning invoice status (incoming)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
invoice (str): lightning invoice
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: status
|
||||||
|
"""
|
||||||
|
invoice = await get_lightning_invoice(
|
||||||
|
db=self.db, payment_hash=payment_hash, out=False
|
||||||
|
)
|
||||||
|
if not invoice:
|
||||||
|
return PaymentStatus(paid=None)
|
||||||
|
if invoice.paid:
|
||||||
|
return PaymentStatus(paid=True)
|
||||||
|
try:
|
||||||
|
# to check the invoice state, we try minting tokens
|
||||||
|
await self.mint(invoice.amount, id=invoice.id)
|
||||||
|
return PaymentStatus(paid=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return PaymentStatus(paid=False)
|
||||||
|
|
||||||
|
async def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
|
"""Get lightning payment status (outgoing)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
payment_hash (str): lightning invoice payment_hash
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: status
|
||||||
|
"""
|
||||||
|
|
||||||
|
# NOTE: consider adding this in wallet.py and update invoice state to paid in DB
|
||||||
|
|
||||||
|
invoice = await get_lightning_invoice(
|
||||||
|
db=self.db, payment_hash=payment_hash, out=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not invoice:
|
||||||
|
return PaymentStatus(paid=False) # "invoice not found (in db)"
|
||||||
|
if invoice.paid:
|
||||||
|
return PaymentStatus(paid=True, preimage=invoice.preimage) # "paid (in db)"
|
||||||
|
proofs = await get_proofs(db=self.db, melt_id=invoice.id)
|
||||||
|
if not proofs:
|
||||||
|
return PaymentStatus(paid=False) # "proofs not fount (in db)"
|
||||||
|
proofs_states = await self.check_proof_state(proofs)
|
||||||
|
if (
|
||||||
|
not proofs_states
|
||||||
|
or not proofs_states.spendable
|
||||||
|
or not proofs_states.pending
|
||||||
|
):
|
||||||
|
return PaymentStatus(paid=False) # "states not fount"
|
||||||
|
|
||||||
|
if all(proofs_states.spendable) and all(proofs_states.pending):
|
||||||
|
return PaymentStatus(paid=None) # "pending (with check)"
|
||||||
|
if not any(proofs_states.spendable) and not any(proofs_states.pending):
|
||||||
|
# NOTE: consider adding this check in wallet.py and mark the invoice as paid if all proofs are spent
|
||||||
|
return PaymentStatus(paid=True) # "paid (with check)"
|
||||||
|
if all(proofs_states.spendable) and not any(proofs_states.pending):
|
||||||
|
return PaymentStatus(paid=False) # "failed (with check)"
|
||||||
|
return PaymentStatus(paid=None) # "undefined state"
|
||||||
|
|
||||||
|
async def get_balance(self) -> StatusResponse:
|
||||||
|
"""Get lightning balance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: balance in satoshis
|
||||||
|
"""
|
||||||
|
return StatusResponse(
|
||||||
|
error_message=None, balance_msat=self.available_balance * 1000
|
||||||
|
)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
from ..core.db import Database
|
from ..core.db import Connection, Database
|
||||||
|
|
||||||
|
|
||||||
async def m000_create_migrations_table(db: Database):
|
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,53 +11,54 @@ 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:
|
||||||
CREATE TABLE IF NOT EXISTS proofs (
|
await conn.execute(f"""
|
||||||
amount {db.big_int} NOT NULL,
|
CREATE TABLE IF NOT EXISTS proofs (
|
||||||
C TEXT NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
secret TEXT NOT NULL,
|
C TEXT NOT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE (secret)
|
UNIQUE (secret)
|
||||||
|
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
await conn.execute(f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS proofs_used (
|
||||||
|
amount {db.big_int} NOT NULL,
|
||||||
|
C TEXT NOT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (secret)
|
||||||
|
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE VIEW IF NOT EXISTS balance AS
|
||||||
|
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||||
|
SELECT SUM(amount) AS s
|
||||||
|
FROM proofs
|
||||||
|
WHERE amount > 0
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
await db.execute(f"""
|
await conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS proofs_used (
|
CREATE VIEW IF NOT EXISTS balance_used AS
|
||||||
amount {db.big_int} NOT NULL,
|
SELECT COALESCE(SUM(s), 0) AS used FROM (
|
||||||
C TEXT NOT NULL,
|
SELECT SUM(amount) AS s
|
||||||
secret TEXT NOT NULL,
|
FROM proofs_used
|
||||||
|
WHERE amount > 0
|
||||||
UNIQUE (secret)
|
|
||||||
|
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
await db.execute("""
|
|
||||||
CREATE VIEW IF NOT EXISTS balance AS
|
|
||||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
|
||||||
SELECT SUM(amount) AS s
|
|
||||||
FROM proofs
|
|
||||||
WHERE amount > 0
|
|
||||||
);
|
|
||||||
""")
|
|
||||||
|
|
||||||
await db.execute("""
|
|
||||||
CREATE VIEW IF NOT EXISTS balance_used AS
|
|
||||||
SELECT COALESCE(SUM(s), 0) AS used FROM (
|
|
||||||
SELECT SUM(amount) AS s
|
|
||||||
FROM proofs_used
|
|
||||||
WHERE amount > 0
|
|
||||||
);
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
async def m002_add_proofs_reserved(db: Database):
|
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,91 +95,117 @@ 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:
|
||||||
CREATE TABLE IF NOT EXISTS keysets (
|
await conn.execute(f"""
|
||||||
id TEXT,
|
CREATE TABLE IF NOT EXISTS keysets (
|
||||||
mint_url TEXT,
|
id TEXT,
|
||||||
valid_from TIMESTAMP DEFAULT {db.timestamp_now},
|
mint_url TEXT,
|
||||||
valid_to TIMESTAMP DEFAULT {db.timestamp_now},
|
valid_from TIMESTAMP DEFAULT {db.timestamp_now},
|
||||||
first_seen TIMESTAMP DEFAULT {db.timestamp_now},
|
valid_to TIMESTAMP DEFAULT {db.timestamp_now},
|
||||||
active BOOL DEFAULT TRUE,
|
first_seen TIMESTAMP DEFAULT {db.timestamp_now},
|
||||||
|
active BOOL DEFAULT TRUE,
|
||||||
|
|
||||||
UNIQUE (id, mint_url)
|
UNIQUE (id, mint_url)
|
||||||
|
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
await db.execute("ALTER TABLE proofs ADD COLUMN id TEXT")
|
await 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:
|
||||||
CREATE TABLE IF NOT EXISTS invoices (
|
await conn.execute(f"""
|
||||||
amount INTEGER NOT NULL,
|
CREATE TABLE IF NOT EXISTS invoices (
|
||||||
pr TEXT NOT NULL,
|
amount INTEGER NOT NULL,
|
||||||
hash TEXT,
|
pr TEXT NOT NULL,
|
||||||
preimage TEXT,
|
hash TEXT,
|
||||||
paid BOOL DEFAULT FALSE,
|
preimage TEXT,
|
||||||
time_created TIMESTAMP DEFAULT {db.timestamp_now},
|
paid BOOL DEFAULT FALSE,
|
||||||
time_paid TIMESTAMP DEFAULT {db.timestamp_now},
|
time_created TIMESTAMP DEFAULT {db.timestamp_now},
|
||||||
|
time_paid TIMESTAMP DEFAULT {db.timestamp_now},
|
||||||
|
|
||||||
UNIQUE (hash)
|
UNIQUE (hash)
|
||||||
|
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
async def m007_nostr(db: Database):
|
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")
|
||||||
CREATE TABLE IF NOT EXISTS seed (
|
await conn.execute("""
|
||||||
seed TEXT NOT NULL,
|
CREATE TABLE IF NOT EXISTS seed (
|
||||||
mnemonic TEXT NOT NULL,
|
seed TEXT NOT NULL,
|
||||||
|
mnemonic TEXT NOT NULL,
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,19 +65,16 @@ 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
|
||||||
already running and and sets local proxy to use it.
|
already running and and sets local proxy to use it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def wrapper(self, *args, **kwargs):
|
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:
|
||||||
invoice_obj = Invoice(
|
for p in proofs:
|
||||||
amount=-sum_proofs(proofs),
|
p.melt_id = melt_id
|
||||||
pr=invoice,
|
await update_proof(p, melt_id=melt_id, conn=conn)
|
||||||
preimage=status.preimage,
|
|
||||||
paid=True,
|
|
||||||
time_paid=time.time(),
|
|
||||||
hash="",
|
|
||||||
)
|
|
||||||
# we have a unique constraint on the hash, so we generate a random one if it doesn't exist
|
|
||||||
invoice_obj.hash = invoice_obj.hash or await self._generate_secret()
|
|
||||||
await store_lightning_invoice(db=self.db, invoice=invoice_obj)
|
|
||||||
|
|
||||||
# handle change and produce proofs
|
decoded_invoice = bolt11.decode(invoice)
|
||||||
if status.change:
|
invoice_obj = Invoice(
|
||||||
change_proofs = await self._construct_proofs(
|
amount=-sum_proofs(proofs),
|
||||||
status.change,
|
bolt11=invoice,
|
||||||
secrets[: len(status.change)],
|
payment_hash=decoded_invoice.payment_hash,
|
||||||
rs[: len(status.change)],
|
# preimage=status.preimage,
|
||||||
derivation_paths[: len(status.change)],
|
paid=False,
|
||||||
)
|
time_paid=int(time.time()),
|
||||||
logger.debug(f"Received change: {sum_proofs(change_proofs)} sat")
|
id=melt_id, # store the same ID in the invoice
|
||||||
|
out=True, # outgoing invoice
|
||||||
|
)
|
||||||
|
# store invoice in db as not paid yet
|
||||||
|
await store_lightning_invoice(db=self.db, invoice=invoice_obj)
|
||||||
|
|
||||||
await self.invalidate(proofs)
|
status = await super().pay_lightning(proofs, invoice, change_outputs)
|
||||||
|
|
||||||
else:
|
# if payment fails
|
||||||
|
if not status.paid:
|
||||||
|
# remove the melt_id in proofs
|
||||||
|
for p in proofs:
|
||||||
|
p.melt_id = None
|
||||||
|
await update_proof(p, melt_id=None, db=self.db)
|
||||||
raise Exception("could not pay invoice.")
|
raise Exception("could not pay invoice.")
|
||||||
return status.paid
|
|
||||||
|
# invoice was paid successfully
|
||||||
|
# we don't have to recheck the spendable sate of these tokens when invalidating
|
||||||
|
await self.invalidate(proofs, check_spendable=False)
|
||||||
|
|
||||||
|
# update paid status in db
|
||||||
|
logger.trace(f"Settings invoice {melt_id} to paid.")
|
||||||
|
await update_lightning_invoice(
|
||||||
|
db=self.db,
|
||||||
|
id=melt_id,
|
||||||
|
paid=True,
|
||||||
|
time_paid=int(time.time()),
|
||||||
|
preimage=status.preimage,
|
||||||
|
)
|
||||||
|
|
||||||
|
# handle change and produce proofs
|
||||||
|
if status.change:
|
||||||
|
change_proofs = await self._construct_proofs(
|
||||||
|
status.change,
|
||||||
|
change_secrets[: len(status.change)],
|
||||||
|
change_rs[: len(status.change)],
|
||||||
|
change_derivation_paths[: len(status.change)],
|
||||||
|
)
|
||||||
|
logger.debug(f"Received change: {sum_proofs(change_proofs)} sat")
|
||||||
|
return status
|
||||||
|
|
||||||
async def check_proof_state(self, proofs):
|
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.")
|
||||||
|
|||||||
1
mypy.ini
1
mypy.ini
@@ -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
1020
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,9 @@ SQLAlchemy = "^1.3.24"
|
|||||||
click = "^8.1.7"
|
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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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,24 +96,21 @@ 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 == 200
|
||||||
assert response.status_code == 400
|
|
||||||
else:
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -159,21 +152,31 @@ 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("/lightning/create_invoice?amount=100")
|
||||||
response = client.post("/invoice?amount=100")
|
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||||
response = client.get("/balance")
|
state = PaymentStatus(paid=False)
|
||||||
assert response.json()["balance"] == initial_balance + 100
|
while not state.paid:
|
||||||
response = client.post("/send?amount=50")
|
print("checking invoice state")
|
||||||
response = client.get("/balance")
|
response2 = client.get(
|
||||||
assert response.json()["balance"] == initial_balance + 50
|
f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}"
|
||||||
response = client.post("/send?amount=50")
|
)
|
||||||
response = client.get("/balance")
|
state = PaymentStatus.parse_obj(response2.json())
|
||||||
assert response.json()["balance"] == initial_balance
|
await asyncio.sleep(0.1)
|
||||||
response = client.get("/pending")
|
print("state:", state)
|
||||||
token = response.json()["pending_token"]["0"]["token"]
|
|
||||||
amount = response.json()["pending_token"]["0"]["amount"]
|
response = client.get("/balance")
|
||||||
response = client.post(f"/receive?token={token}")
|
assert response.json()["balance"] == initial_balance + 100
|
||||||
response = client.get("/balance")
|
response = client.post("/send?amount=50")
|
||||||
assert response.json()["balance"] == initial_balance + amount
|
response = client.get("/balance")
|
||||||
|
assert response.json()["balance"] == initial_balance + 50
|
||||||
|
response = client.post("/send?amount=50")
|
||||||
|
response = client.get("/balance")
|
||||||
|
assert response.json()["balance"] == initial_balance
|
||||||
|
response = client.get("/pending")
|
||||||
|
token = response.json()["pending_token"]["0"]["token"]
|
||||||
|
amount = response.json()["pending_token"]["0"]["amount"]
|
||||||
|
response = client.post(f"/receive?token={token}")
|
||||||
|
response = client.get("/balance")
|
||||||
|
assert response.json()["balance"] == initial_balance + amount
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ async def wallet2(mint):
|
|||||||
@pytest.mark.asyncio
|
@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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user