mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
Mint: Recover pending melts at startup (#499)
* wip works with fakewallet * startup refactor * add tests * regtest tests for pending melts * wip CLN * remove db migration * remove foreign key relation to keyset id * fix: get_promise from db and restore DLEQs * test: check for keyset not found error * fix migrations * lower-case all db column names * add more tests for regtest * simlate failure for lightning * test wallet spent state with hodl invoices * retry * regtest with postgres * retry postgres * add sleeps * longer sleep on github * more sleep for github sigh * increase sleep ffs * add sleep loop * try something * do not pay with wallet but with ledger * fix lnbits pending state * fix pipeline to use fake admin from docker
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -35,7 +35,9 @@ jobs:
|
|||||||
poetry-version: ["1.7.1"]
|
poetry-version: ["1.7.1"]
|
||||||
backend-wallet-class:
|
backend-wallet-class:
|
||||||
["LndRestWallet", "CoreLightningRestWallet", "LNbitsWallet"]
|
["LndRestWallet", "CoreLightningRestWallet", "LNbitsWallet"]
|
||||||
|
# mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"]
|
||||||
|
mint-database: ["./test_data/test_mint"]
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
backend-wallet-class: ${{ matrix.backend-wallet-class }}
|
backend-wallet-class: ${{ matrix.backend-wallet-class }}
|
||||||
mint-database: "./test_data/test_mint"
|
mint-database: ${{ matrix.mint-database }}
|
||||||
|
|||||||
4
.github/workflows/regtest.yml
vendored
4
.github/workflows/regtest.yml
vendored
@@ -52,10 +52,6 @@ jobs:
|
|||||||
chmod -R 777 .
|
chmod -R 777 .
|
||||||
bash ./start.sh
|
bash ./start.sh
|
||||||
|
|
||||||
- name: Create fake admin
|
|
||||||
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
|
|
||||||
run: docker exec cashu-lnbits-1 poetry run python tools/create_fake_admin.py
|
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
env:
|
env:
|
||||||
WALLET_NAME: test_wallet
|
WALLET_NAME: test_wallet
|
||||||
|
|||||||
@@ -101,12 +101,12 @@ 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[
|
mint_id: Union[None, str] = (
|
||||||
None, str
|
None # holds the id of the mint operation that created this proof
|
||||||
] = None # holds the id of the mint operation that created this proof
|
)
|
||||||
melt_id: Union[
|
melt_id: Union[None, str] = (
|
||||||
None, str
|
None # holds the id of the melt operation that destroyed this proof
|
||||||
] = None # holds the id of the melt operation that destroyed this proof
|
)
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
@@ -194,6 +194,15 @@ class BlindedSignature(BaseModel):
|
|||||||
C_: str # Hex-encoded signature
|
C_: str # Hex-encoded signature
|
||||||
dleq: Optional[DLEQ] = None # DLEQ proof
|
dleq: Optional[DLEQ] = None # DLEQ proof
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row):
|
||||||
|
return cls(
|
||||||
|
id=row["id"],
|
||||||
|
amount=row["amount"],
|
||||||
|
C_=row["c_"],
|
||||||
|
dleq=DLEQ(e=row["dleq_e"], s=row["dleq_s"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BlindedMessages(BaseModel):
|
class BlindedMessages(BaseModel):
|
||||||
# NOTE: not used in Pydantic validation
|
# NOTE: not used in Pydantic validation
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ class KeysetNotFoundError(KeysetError):
|
|||||||
detail = "keyset not found"
|
detail = "keyset not found"
|
||||||
code = 12001
|
code = 12001
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, keyset_id: Optional[str] = None):
|
||||||
|
if keyset_id:
|
||||||
|
self.detail = f"{self.detail}: {keyset_id}"
|
||||||
super().__init__(self.detail, code=self.code)
|
super().__init__(self.detail, code=self.code)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ class FakeWalletSettings(MintSettings):
|
|||||||
fakewallet_brr: bool = Field(default=True)
|
fakewallet_brr: bool = Field(default=True)
|
||||||
fakewallet_delay_payment: bool = Field(default=False)
|
fakewallet_delay_payment: bool = Field(default=False)
|
||||||
fakewallet_stochastic_invoice: bool = Field(default=False)
|
fakewallet_stochastic_invoice: bool = Field(default=False)
|
||||||
|
fakewallet_payment_state: Optional[bool] = Field(default=None)
|
||||||
mint_cache_secrets: bool = Field(default=True)
|
mint_cache_secrets: bool = Field(default=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -247,17 +247,21 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if r.is_error or "error" in data or not data.get("pays"):
|
if not data.get("pays"):
|
||||||
raise Exception("error in corelightning-rest response")
|
# payment not found
|
||||||
|
logger.error(f"payment not found: {data.get('pays')}")
|
||||||
|
raise Exception("payment not found")
|
||||||
|
|
||||||
|
if r.is_error or "error" in data:
|
||||||
|
message = data.get("error") or data
|
||||||
|
raise Exception(f"error in corelightning-rest response: {message}")
|
||||||
|
|
||||||
pay = data["pays"][0]
|
pay = data["pays"][0]
|
||||||
|
|
||||||
fee_msat, preimage = None, None
|
fee_msat, preimage = None, None
|
||||||
if self.statuses[pay["status"]]:
|
if self.statuses[pay["status"]]:
|
||||||
# cut off "msat" and convert to int
|
# cut off "msat" and convert to int
|
||||||
fee_msat = -int(pay["amount_sent_msat"][:-4]) - int(
|
fee_msat = -int(pay["amount_sent_msat"]) - int(pay["amount_msat"])
|
||||||
pay["amount_msat"][:-4]
|
|
||||||
)
|
|
||||||
preimage = pay["preimage"]
|
preimage = pay["preimage"]
|
||||||
|
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class FakeWallet(LightningBackend):
|
|||||||
return PaymentStatus(paid=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(paid=None)
|
return PaymentStatus(paid=settings.fakewallet_payment_state)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -151,8 +151,18 @@ class LNbitsWallet(LightningBackend):
|
|||||||
if "paid" not in data and "details" not in data:
|
if "paid" not in data and "details" not in data:
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(paid=None)
|
||||||
|
|
||||||
|
paid_value = None
|
||||||
|
if data["paid"]:
|
||||||
|
paid_value = True
|
||||||
|
elif not data["paid"] and data["details"]["pending"]:
|
||||||
|
paid_value = None
|
||||||
|
elif not data["paid"] and not data["details"]["pending"]:
|
||||||
|
paid_value = False
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unexpected value for paid: {data['paid']}")
|
||||||
|
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
paid=data["paid"],
|
paid=paid_value,
|
||||||
fee=Amount(unit=Unit.msat, amount=abs(data["details"]["fee"])),
|
fee=Amount(unit=Unit.msat, amount=abs(data["details"]["fee"])),
|
||||||
preimage=data["preimage"],
|
preimage=data["preimage"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -217,14 +217,20 @@ class LndRestWallet(LightningBackend):
|
|||||||
async for json_line in r.aiter_lines():
|
async for json_line in r.aiter_lines():
|
||||||
try:
|
try:
|
||||||
line = json.loads(json_line)
|
line = json.loads(json_line)
|
||||||
|
|
||||||
|
# check for errors
|
||||||
if line.get("error"):
|
if line.get("error"):
|
||||||
logger.error(
|
message = (
|
||||||
line["error"]["message"]
|
line["error"]["message"]
|
||||||
if "message" in line["error"]
|
if "message" in line["error"]
|
||||||
else line["error"]
|
else line["error"]
|
||||||
)
|
)
|
||||||
|
logger.error(f"LND get_payment_status error: {message}")
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(paid=None)
|
||||||
|
|
||||||
payment = line.get("result")
|
payment = line.get("result")
|
||||||
|
|
||||||
|
# payment exists
|
||||||
if payment is not None and payment.get("status"):
|
if payment is not None and payment.get("status"):
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
paid=statuses[payment["status"]],
|
paid=statuses[payment["status"]],
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ class LedgerCrud(ABC):
|
|||||||
derivation_path: str = "",
|
derivation_path: str = "",
|
||||||
seed: str = "",
|
seed: str = "",
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> List[MintKeyset]:
|
) -> List[MintKeyset]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_spent_proofs(
|
async def get_spent_proofs(
|
||||||
@@ -43,8 +42,7 @@ class LedgerCrud(ABC):
|
|||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> List[Proof]:
|
) -> List[Proof]: ...
|
||||||
...
|
|
||||||
|
|
||||||
async def get_proof_used(
|
async def get_proof_used(
|
||||||
self,
|
self,
|
||||||
@@ -52,8 +50,7 @@ class LedgerCrud(ABC):
|
|||||||
Y: str,
|
Y: str,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Optional[Proof]:
|
) -> Optional[Proof]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def invalidate_proof(
|
async def invalidate_proof(
|
||||||
@@ -61,9 +58,26 @@ class LedgerCrud(ABC):
|
|||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
proof: Proof,
|
proof: Proof,
|
||||||
|
quote_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_all_melt_quotes_from_pending_proofs(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
db: Database,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> List[MeltQuote]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_pending_proofs_for_quote(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
quote_id: str,
|
||||||
|
db: Database,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> List[Proof]: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_proofs_pending(
|
async def get_proofs_pending(
|
||||||
@@ -72,8 +86,7 @@ class LedgerCrud(ABC):
|
|||||||
Ys: List[str],
|
Ys: List[str],
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> List[Proof]:
|
) -> List[Proof]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def set_proof_pending(
|
async def set_proof_pending(
|
||||||
@@ -81,15 +94,18 @@ class LedgerCrud(ABC):
|
|||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
proof: Proof,
|
proof: Proof,
|
||||||
|
quote_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def unset_proof_pending(
|
async def unset_proof_pending(
|
||||||
self, *, proof: Proof, db: Database, conn: Optional[Connection] = None
|
self,
|
||||||
) -> None:
|
*,
|
||||||
...
|
proof: Proof,
|
||||||
|
db: Database,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def store_keyset(
|
async def store_keyset(
|
||||||
@@ -98,16 +114,14 @@ class LedgerCrud(ABC):
|
|||||||
db: Database,
|
db: Database,
|
||||||
keyset: MintKeyset,
|
keyset: MintKeyset,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_balance(
|
async def get_balance(
|
||||||
self,
|
self,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> int:
|
) -> int: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def store_promise(
|
async def store_promise(
|
||||||
@@ -115,24 +129,22 @@ class LedgerCrud(ABC):
|
|||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
amount: int,
|
amount: int,
|
||||||
B_: str,
|
b_: str,
|
||||||
C_: str,
|
c_: str,
|
||||||
id: str,
|
id: str,
|
||||||
e: str = "",
|
e: str = "",
|
||||||
s: str = "",
|
s: str = "",
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_promise(
|
async def get_promise(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
B_: str,
|
b_: str,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Optional[BlindedSignature]:
|
) -> Optional[BlindedSignature]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def store_mint_quote(
|
async def store_mint_quote(
|
||||||
@@ -141,8 +153,7 @@ class LedgerCrud(ABC):
|
|||||||
quote: MintQuote,
|
quote: MintQuote,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_mint_quote(
|
async def get_mint_quote(
|
||||||
@@ -151,8 +162,7 @@ class LedgerCrud(ABC):
|
|||||||
quote_id: str,
|
quote_id: str,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Optional[MintQuote]:
|
) -> Optional[MintQuote]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_mint_quote_by_request(
|
async def get_mint_quote_by_request(
|
||||||
@@ -161,8 +171,7 @@ class LedgerCrud(ABC):
|
|||||||
request: str,
|
request: str,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Optional[MintQuote]:
|
) -> Optional[MintQuote]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update_mint_quote(
|
async def update_mint_quote(
|
||||||
@@ -171,8 +180,7 @@ class LedgerCrud(ABC):
|
|||||||
quote: MintQuote,
|
quote: MintQuote,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
# @abstractmethod
|
# @abstractmethod
|
||||||
# async def update_mint_quote_paid(
|
# async def update_mint_quote_paid(
|
||||||
@@ -191,8 +199,7 @@ class LedgerCrud(ABC):
|
|||||||
quote: MeltQuote,
|
quote: MeltQuote,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_melt_quote(
|
async def get_melt_quote(
|
||||||
@@ -202,8 +209,7 @@ class LedgerCrud(ABC):
|
|||||||
db: Database,
|
db: Database,
|
||||||
checking_id: Optional[str] = None,
|
checking_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Optional[MeltQuote]:
|
) -> Optional[MeltQuote]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update_melt_quote(
|
async def update_melt_quote(
|
||||||
@@ -212,8 +218,7 @@ class LedgerCrud(ABC):
|
|||||||
quote: MeltQuote,
|
quote: MeltQuote,
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class LedgerCrudSqlite(LedgerCrud):
|
class LedgerCrudSqlite(LedgerCrud):
|
||||||
@@ -228,8 +233,8 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
amount: int,
|
amount: int,
|
||||||
B_: str,
|
b_: str,
|
||||||
C_: str,
|
c_: str,
|
||||||
id: str,
|
id: str,
|
||||||
e: str = "",
|
e: str = "",
|
||||||
s: str = "",
|
s: str = "",
|
||||||
@@ -238,13 +243,13 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO {table_with_schema(db, 'promises')}
|
INSERT INTO {table_with_schema(db, 'promises')}
|
||||||
(amount, B_b, C_b, e, s, id, created)
|
(amount, b_, c_, dleq_e, dleq_s, id, created)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
amount,
|
amount,
|
||||||
B_,
|
b_,
|
||||||
C_,
|
c_,
|
||||||
e,
|
e,
|
||||||
s,
|
s,
|
||||||
id,
|
id,
|
||||||
@@ -256,17 +261,17 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
B_: str,
|
b_: str,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Optional[BlindedSignature]:
|
) -> Optional[BlindedSignature]:
|
||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
f"""
|
f"""
|
||||||
SELECT * from {table_with_schema(db, 'promises')}
|
SELECT * from {table_with_schema(db, 'promises')}
|
||||||
WHERE B_b = ?
|
WHERE b_ = ?
|
||||||
""",
|
""",
|
||||||
(str(B_),),
|
(str(b_),),
|
||||||
)
|
)
|
||||||
return BlindedSignature(amount=row[0], C_=row[2], id=row[3]) if row else None
|
return BlindedSignature.from_row(row) if row else None
|
||||||
|
|
||||||
async def get_spent_proofs(
|
async def get_spent_proofs(
|
||||||
self,
|
self,
|
||||||
@@ -286,14 +291,15 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
proof: Proof,
|
proof: Proof,
|
||||||
|
quote_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# we add the proof and secret to the used list
|
# we add the proof and secret to the used list
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO {table_with_schema(db, 'proofs_used')}
|
INSERT INTO {table_with_schema(db, 'proofs_used')}
|
||||||
(amount, C, secret, Y, id, witness, created)
|
(amount, c, secret, y, id, witness, created, melt_quote)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
proof.amount,
|
proof.amount,
|
||||||
@@ -303,9 +309,39 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
proof.id,
|
proof.id,
|
||||||
proof.witness,
|
proof.witness,
|
||||||
timestamp_now(db),
|
timestamp_now(db),
|
||||||
|
quote_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_all_melt_quotes_from_pending_proofs(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
db: Database,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> List[MeltQuote]:
|
||||||
|
rows = await (conn or db).fetchall(
|
||||||
|
f"""
|
||||||
|
SELECT * from {table_with_schema(db, 'melt_quotes')} WHERE quote in (SELECT DISTINCT melt_quote FROM {table_with_schema(db, 'proofs_pending')})
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return [MeltQuote.from_row(r) for r in rows]
|
||||||
|
|
||||||
|
async def get_pending_proofs_for_quote(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
quote_id: str,
|
||||||
|
db: Database,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> List[Proof]:
|
||||||
|
rows = await (conn or db).fetchall(
|
||||||
|
f"""
|
||||||
|
SELECT * from {table_with_schema(db, 'proofs_pending')}
|
||||||
|
WHERE melt_quote = ?
|
||||||
|
""",
|
||||||
|
(quote_id,),
|
||||||
|
)
|
||||||
|
return [Proof(**r) for r in rows]
|
||||||
|
|
||||||
async def get_proofs_pending(
|
async def get_proofs_pending(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -316,7 +352,7 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
rows = await (conn or db).fetchall(
|
rows = await (conn or db).fetchall(
|
||||||
f"""
|
f"""
|
||||||
SELECT * from {table_with_schema(db, 'proofs_pending')}
|
SELECT * from {table_with_schema(db, 'proofs_pending')}
|
||||||
WHERE Y IN ({','.join(['?']*len(Ys))})
|
WHERE y IN ({','.join(['?']*len(Ys))})
|
||||||
""",
|
""",
|
||||||
tuple(Ys),
|
tuple(Ys),
|
||||||
)
|
)
|
||||||
@@ -327,21 +363,25 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
*,
|
*,
|
||||||
db: Database,
|
db: Database,
|
||||||
proof: Proof,
|
proof: Proof,
|
||||||
|
quote_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# we add the proof and secret to the used list
|
# we add the proof and secret to the used list
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO {table_with_schema(db, 'proofs_pending')}
|
INSERT INTO {table_with_schema(db, 'proofs_pending')}
|
||||||
(amount, C, secret, Y, created)
|
(amount, c, secret, y, id, witness, created, melt_quote)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
proof.amount,
|
proof.amount,
|
||||||
proof.C,
|
proof.C,
|
||||||
proof.secret,
|
proof.secret,
|
||||||
proof.Y,
|
proof.Y,
|
||||||
|
proof.id,
|
||||||
|
proof.witness,
|
||||||
timestamp_now(db),
|
timestamp_now(db),
|
||||||
|
quote_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -628,7 +668,7 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
f"""
|
f"""
|
||||||
SELECT * from {table_with_schema(db, 'proofs_used')}
|
SELECT * from {table_with_schema(db, 'proofs_used')}
|
||||||
WHERE Y = ?
|
WHERE y = ?
|
||||||
""",
|
""",
|
||||||
(Y,),
|
(Y,),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,6 +93,83 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
self.pubkey = derive_pubkey(self.seed)
|
self.pubkey = derive_pubkey(self.seed)
|
||||||
self.spent_proofs: Dict[str, Proof] = {}
|
self.spent_proofs: Dict[str, Proof] = {}
|
||||||
|
|
||||||
|
# ------- STARTUP -------
|
||||||
|
|
||||||
|
async def startup_ledger(self):
|
||||||
|
await self._startup_ledger()
|
||||||
|
await self._check_pending_proofs_and_melt_quotes()
|
||||||
|
|
||||||
|
async def _startup_ledger(self):
|
||||||
|
if settings.mint_cache_secrets:
|
||||||
|
await self.load_used_proofs()
|
||||||
|
await self.init_keysets()
|
||||||
|
|
||||||
|
for derivation_path in settings.mint_derivation_path_list:
|
||||||
|
await self.activate_keyset(derivation_path=derivation_path)
|
||||||
|
|
||||||
|
for method in self.backends:
|
||||||
|
for unit in self.backends[method]:
|
||||||
|
logger.info(
|
||||||
|
f"Using {self.backends[method][unit].__class__.__name__} backend for"
|
||||||
|
f" method: '{method.name}' and unit: '{unit.name}'"
|
||||||
|
)
|
||||||
|
status = await self.backends[method][unit].status()
|
||||||
|
if status.error_message:
|
||||||
|
logger.warning(
|
||||||
|
"The backend for"
|
||||||
|
f" {self.backends[method][unit].__class__.__name__} isn't"
|
||||||
|
f" working properly: '{status.error_message}'",
|
||||||
|
RuntimeWarning,
|
||||||
|
)
|
||||||
|
logger.info(f"Backend balance: {status.balance} {unit.name}")
|
||||||
|
|
||||||
|
logger.info(f"Data dir: {settings.cashu_dir}")
|
||||||
|
|
||||||
|
async def _check_pending_proofs_and_melt_quotes(self):
|
||||||
|
"""Startup routine that checks all pending proofs for their melt state and either invalidates
|
||||||
|
them for a successful melt or deletes them if the melt failed.
|
||||||
|
"""
|
||||||
|
# get all pending melt quotes
|
||||||
|
melt_quotes = await self.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=self.db
|
||||||
|
)
|
||||||
|
if not melt_quotes:
|
||||||
|
return
|
||||||
|
for quote in melt_quotes:
|
||||||
|
# get pending proofs for quote
|
||||||
|
pending_proofs = await self.crud.get_pending_proofs_for_quote(
|
||||||
|
quote_id=quote.quote, db=self.db
|
||||||
|
)
|
||||||
|
# check with the backend whether the quote has been paid during downtime
|
||||||
|
payment = await self.backends[Method[quote.method]][
|
||||||
|
Unit[quote.unit]
|
||||||
|
].get_payment_status(quote.checking_id)
|
||||||
|
if payment.paid:
|
||||||
|
logger.info(f"Melt quote {quote.quote} state: paid")
|
||||||
|
quote.paid_time = int(time.time())
|
||||||
|
quote.paid = True
|
||||||
|
if payment.fee:
|
||||||
|
quote.fee_paid = payment.fee.to(Unit[quote.unit]).amount
|
||||||
|
quote.proof = payment.preimage or ""
|
||||||
|
await self.crud.update_melt_quote(quote=quote, db=self.db)
|
||||||
|
# invalidate proofs
|
||||||
|
await self._invalidate_proofs(
|
||||||
|
proofs=pending_proofs, quote_id=quote.quote
|
||||||
|
)
|
||||||
|
# unset pending
|
||||||
|
await self._unset_proofs_pending(pending_proofs)
|
||||||
|
elif payment.failed:
|
||||||
|
logger.info(f"Melt quote {quote.quote} state: failed")
|
||||||
|
|
||||||
|
# unset pending
|
||||||
|
await self._unset_proofs_pending(pending_proofs)
|
||||||
|
elif payment.pending:
|
||||||
|
logger.info(f"Melt quote {quote.quote} state: pending")
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logger.error("Melt quote state unknown")
|
||||||
|
pass
|
||||||
|
|
||||||
# ------- KEYS -------
|
# ------- KEYS -------
|
||||||
|
|
||||||
async def activate_keyset(
|
async def activate_keyset(
|
||||||
@@ -229,7 +306,11 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
# ------- ECASH -------
|
# ------- ECASH -------
|
||||||
|
|
||||||
async def _invalidate_proofs(
|
async def _invalidate_proofs(
|
||||||
self, proofs: List[Proof], conn: Optional[Connection] = None
|
self,
|
||||||
|
*,
|
||||||
|
proofs: List[Proof],
|
||||||
|
quote_id: Optional[str] = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Adds proofs to the set of spent proofs and stores them in the db.
|
"""Adds proofs to the set of spent proofs and stores them in the db.
|
||||||
|
|
||||||
@@ -241,7 +322,9 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
async with get_db_connection(self.db, conn) as conn:
|
async with get_db_connection(self.db, conn) as conn:
|
||||||
# store in db
|
# store in db
|
||||||
for p in proofs:
|
for p in proofs:
|
||||||
await self.crud.invalidate_proof(proof=p, db=self.db, conn=conn)
|
await self.crud.invalidate_proof(
|
||||||
|
proof=p, db=self.db, quote_id=quote_id, conn=conn
|
||||||
|
)
|
||||||
|
|
||||||
async def _generate_change_promises(
|
async def _generate_change_promises(
|
||||||
self,
|
self,
|
||||||
@@ -708,14 +791,15 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# verify inputs and their spending conditions
|
# verify inputs and their spending conditions
|
||||||
|
# note, we do not verify outputs here, as they are only used for returning overpaid fees
|
||||||
|
# we should have used _verify_outputs here already (see above)
|
||||||
await self.verify_inputs_and_outputs(proofs=proofs)
|
await self.verify_inputs_and_outputs(proofs=proofs)
|
||||||
|
|
||||||
# set proofs to pending to avoid race conditions
|
# set proofs to pending to avoid race conditions
|
||||||
await self._set_proofs_pending(proofs)
|
await self._set_proofs_pending(proofs, quote_id=melt_quote.quote)
|
||||||
try:
|
try:
|
||||||
# settle the transaction internally if there is a mint quote with the same payment request
|
# settle the transaction internally if there is a mint quote with the same payment request
|
||||||
melt_quote = await self.melt_mint_settle_internally(melt_quote)
|
melt_quote = await self.melt_mint_settle_internally(melt_quote)
|
||||||
|
|
||||||
# quote not paid yet (not internal), pay it with the backend
|
# quote not paid yet (not internal), pay it with the backend
|
||||||
if not melt_quote.paid:
|
if not melt_quote.paid:
|
||||||
logger.debug(f"Lightning: pay invoice {melt_quote.request}")
|
logger.debug(f"Lightning: pay invoice {melt_quote.request}")
|
||||||
@@ -742,7 +826,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
||||||
|
|
||||||
# melt successful, invalidate proofs
|
# melt successful, invalidate proofs
|
||||||
await self._invalidate_proofs(proofs)
|
await self._invalidate_proofs(proofs=proofs, quote_id=melt_quote.quote)
|
||||||
|
|
||||||
# prepare change to compensate wallet for overpaid fees
|
# prepare change to compensate wallet for overpaid fees
|
||||||
return_promises: List[BlindedSignature] = []
|
return_promises: List[BlindedSignature] = []
|
||||||
@@ -802,7 +886,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
async with get_db_connection(self.db) as conn:
|
async with get_db_connection(self.db) as conn:
|
||||||
# we do this in a single db transaction
|
# we do this in a single db transaction
|
||||||
promises = await self._generate_promises(outputs, keyset, conn)
|
promises = await self._generate_promises(outputs, keyset, conn)
|
||||||
await self._invalidate_proofs(proofs, conn)
|
await self._invalidate_proofs(proofs=proofs, conn=conn)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.trace(f"split failed: {e}")
|
logger.trace(f"split failed: {e}")
|
||||||
@@ -823,7 +907,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
for output in outputs:
|
for output in outputs:
|
||||||
logger.trace(f"looking for promise: {output}")
|
logger.trace(f"looking for promise: {output}")
|
||||||
promise = await self.crud.get_promise(
|
promise = await self.crud.get_promise(
|
||||||
B_=output.B_, db=self.db, conn=conn
|
b_=output.B_, db=self.db, conn=conn
|
||||||
)
|
)
|
||||||
if promise is not None:
|
if promise is not None:
|
||||||
# BEGIN backwards compatibility mints pre `m007_proofs_and_promises_store_id`
|
# BEGIN backwards compatibility mints pre `m007_proofs_and_promises_store_id`
|
||||||
@@ -890,8 +974,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
await self.crud.store_promise(
|
await self.crud.store_promise(
|
||||||
amount=amount,
|
amount=amount,
|
||||||
id=keyset_id,
|
id=keyset_id,
|
||||||
B_=B_.serialize().hex(),
|
b_=B_.serialize().hex(),
|
||||||
C_=C_.serialize().hex(),
|
c_=C_.serialize().hex(),
|
||||||
e=e.serialize(),
|
e=e.serialize(),
|
||||||
s=s.serialize(),
|
s=s.serialize(),
|
||||||
db=self.db,
|
db=self.db,
|
||||||
@@ -950,12 +1034,15 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
)
|
)
|
||||||
return states
|
return states
|
||||||
|
|
||||||
async def _set_proofs_pending(self, proofs: List[Proof]) -> None:
|
async def _set_proofs_pending(
|
||||||
|
self, proofs: List[Proof], quote_id: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
"""If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
|
"""If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
|
||||||
the list of pending proofs or removes them. Used as a mutex for proofs.
|
the list of pending proofs or removes them. Used as a mutex for proofs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
proofs (List[Proof]): Proofs to add to pending table.
|
proofs (List[Proof]): Proofs to add to pending table.
|
||||||
|
quote_id (Optional[str]): Melt quote ID. If it is not set, we assume the pending tokens to be from a swap.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Exception: At least one proof already in pending table.
|
Exception: At least one proof already in pending table.
|
||||||
@@ -967,9 +1054,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
|||||||
try:
|
try:
|
||||||
for p in proofs:
|
for p in proofs:
|
||||||
await self.crud.set_proof_pending(
|
await self.crud.set_proof_pending(
|
||||||
proof=p, db=self.db, conn=conn
|
proof=p, db=self.db, quote_id=quote_id, conn=conn
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to set proofs pending: {e}")
|
||||||
raise TransactionError("Failed to set proofs pending.")
|
raise TransactionError("Failed to set proofs pending.")
|
||||||
|
|
||||||
async def _unset_proofs_pending(self, proofs: List[Proof]) -> None:
|
async def _unset_proofs_pending(self, proofs: List[Proof]) -> None:
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ async def m001_initial(db: Database):
|
|||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
|
||||||
amount {db.big_int} NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
B_b TEXT NOT NULL,
|
b_b TEXT NOT NULL,
|
||||||
C_b TEXT NOT NULL,
|
c_b TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE (B_b)
|
UNIQUE (b_b)
|
||||||
|
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
@@ -33,7 +33,7 @@ async def m001_initial(db: Database):
|
|||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
||||||
amount {db.big_int} NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
C TEXT NOT NULL,
|
c TEXT NOT NULL,
|
||||||
secret TEXT NOT NULL,
|
secret TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE (secret)
|
UNIQUE (secret)
|
||||||
@@ -129,7 +129,7 @@ async def m003_mint_keysets(db: Database):
|
|||||||
f"""
|
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 {db.big_int} NOT NULL,
|
||||||
pubkey TEXT NOT NULL,
|
pubkey TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE (id, pubkey)
|
UNIQUE (id, pubkey)
|
||||||
@@ -157,8 +157,8 @@ async def m005_pending_proofs_table(db: Database) -> None:
|
|||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
||||||
amount INTEGER NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
C TEXT NOT NULL,
|
c TEXT NOT NULL,
|
||||||
secret TEXT NOT NULL,
|
secret TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE (secret)
|
UNIQUE (secret)
|
||||||
@@ -283,7 +283,7 @@ async def m011_add_quote_tables(db: Database):
|
|||||||
request TEXT NOT NULL,
|
request TEXT NOT NULL,
|
||||||
checking_id TEXT NOT NULL,
|
checking_id TEXT NOT NULL,
|
||||||
unit TEXT NOT NULL,
|
unit TEXT NOT NULL,
|
||||||
amount INTEGER NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
paid BOOL NOT NULL,
|
paid BOOL NOT NULL,
|
||||||
issued BOOL NOT NULL,
|
issued BOOL NOT NULL,
|
||||||
created_time TIMESTAMP,
|
created_time TIMESTAMP,
|
||||||
@@ -303,12 +303,12 @@ async def m011_add_quote_tables(db: Database):
|
|||||||
request TEXT NOT NULL,
|
request TEXT NOT NULL,
|
||||||
checking_id TEXT NOT NULL,
|
checking_id TEXT NOT NULL,
|
||||||
unit TEXT NOT NULL,
|
unit TEXT NOT NULL,
|
||||||
amount INTEGER NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
fee_reserve INTEGER,
|
fee_reserve {db.big_int},
|
||||||
paid BOOL NOT NULL,
|
paid BOOL NOT NULL,
|
||||||
created_time TIMESTAMP,
|
created_time TIMESTAMP,
|
||||||
paid_time TIMESTAMP,
|
paid_time TIMESTAMP,
|
||||||
fee_paid INTEGER,
|
fee_paid {db.big_int},
|
||||||
proof TEXT,
|
proof TEXT,
|
||||||
|
|
||||||
UNIQUE (quote)
|
UNIQUE (quote)
|
||||||
@@ -440,11 +440,11 @@ async def m014_proofs_add_Y_column(db: Database):
|
|||||||
await drop_balance_views(db, conn)
|
await drop_balance_views(db, conn)
|
||||||
|
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN Y TEXT"
|
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN y TEXT"
|
||||||
)
|
)
|
||||||
for proof in proofs_used:
|
for proof in proofs_used:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"UPDATE {table_with_schema(db, 'proofs_used')} SET Y = '{proof.Y}'"
|
f"UPDATE {table_with_schema(db, 'proofs_used')} SET y = '{proof.Y}'"
|
||||||
f" WHERE secret = '{proof.secret}'"
|
f" WHERE secret = '{proof.secret}'"
|
||||||
)
|
)
|
||||||
# Copy proofs_used to proofs_used_old and create a new table proofs_used
|
# Copy proofs_used to proofs_used_old and create a new table proofs_used
|
||||||
@@ -461,11 +461,11 @@ async def m014_proofs_add_Y_column(db: Database):
|
|||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
||||||
amount INTEGER NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
C TEXT NOT NULL,
|
c TEXT NOT NULL,
|
||||||
secret TEXT NOT NULL,
|
secret TEXT NOT NULL,
|
||||||
id TEXT,
|
id TEXT,
|
||||||
Y TEXT,
|
y TEXT,
|
||||||
created TIMESTAMP,
|
created TIMESTAMP,
|
||||||
witness TEXT,
|
witness TEXT,
|
||||||
|
|
||||||
@@ -475,19 +475,19 @@ async def m014_proofs_add_Y_column(db: Database):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"INSERT INTO {table_with_schema(db, 'proofs_used')} (amount, C, "
|
f"INSERT INTO {table_with_schema(db, 'proofs_used')} (amount, c, "
|
||||||
"secret, id, Y, created, witness) SELECT amount, C, secret, id, Y,"
|
"secret, id, y, created, witness) SELECT amount, c, secret, id, y,"
|
||||||
f" created, witness FROM {table_with_schema(db, 'proofs_used_old')}"
|
f" created, witness FROM {table_with_schema(db, 'proofs_used_old')}"
|
||||||
)
|
)
|
||||||
await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_used_old')}")
|
await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_used_old')}")
|
||||||
|
|
||||||
# add column Y to proofs_pending
|
# add column y to proofs_pending
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN Y TEXT"
|
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN y TEXT"
|
||||||
)
|
)
|
||||||
for proof in proofs_pending:
|
for proof in proofs_pending:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"UPDATE {table_with_schema(db, 'proofs_pending')} SET Y = '{proof.Y}'"
|
f"UPDATE {table_with_schema(db, 'proofs_pending')} SET y = '{proof.Y}'"
|
||||||
f" WHERE secret = '{proof.secret}'"
|
f" WHERE secret = '{proof.secret}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -507,10 +507,10 @@ async def m014_proofs_add_Y_column(db: Database):
|
|||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
||||||
amount INTEGER NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
C TEXT NOT NULL,
|
c TEXT NOT NULL,
|
||||||
secret TEXT NOT NULL,
|
secret TEXT NOT NULL,
|
||||||
Y TEXT,
|
y TEXT,
|
||||||
id TEXT,
|
id TEXT,
|
||||||
created TIMESTAMP,
|
created TIMESTAMP,
|
||||||
|
|
||||||
@@ -520,8 +520,8 @@ async def m014_proofs_add_Y_column(db: Database):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"INSERT INTO {table_with_schema(db, 'proofs_pending')} (amount, C, "
|
f"INSERT INTO {table_with_schema(db, 'proofs_pending')} (amount, c, "
|
||||||
"secret, Y, id, created) SELECT amount, C, secret, Y, id, created"
|
"secret, y, id, created) SELECT amount, c, secret, y, id, created"
|
||||||
f" FROM {table_with_schema(db, 'proofs_pending_old')}"
|
f" FROM {table_with_schema(db, 'proofs_pending_old')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -531,7 +531,7 @@ async def m014_proofs_add_Y_column(db: Database):
|
|||||||
await create_balance_views(db, conn)
|
await create_balance_views(db, conn)
|
||||||
|
|
||||||
|
|
||||||
async def m015_add_index_Y_to_proofs_used(db: Database):
|
async def m015_add_index_Y_to_proofs_used_and_pending(db: Database):
|
||||||
# create index on proofs_used table for Y
|
# create index on proofs_used table for Y
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
@@ -540,6 +540,12 @@ async def m015_add_index_Y_to_proofs_used(db: Database):
|
|||||||
f" {table_with_schema(db, 'proofs_used')} (Y)"
|
f" {table_with_schema(db, 'proofs_used')} (Y)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS"
|
||||||
|
" proofs_pending_Y_idx ON"
|
||||||
|
f" {table_with_schema(db, 'proofs_pending')} (Y)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m016_recompute_Y_with_new_h2c(db: Database):
|
async def m016_recompute_Y_with_new_h2c(db: Database):
|
||||||
# get all proofs_used and proofs_pending from the database and compute Y for each of them
|
# get all proofs_used and proofs_pending from the database and compute Y for each of them
|
||||||
@@ -570,12 +576,12 @@ async def m016_recompute_Y_with_new_h2c(db: Database):
|
|||||||
f"('{y}', '{secret}')" for y, secret in proofs_used_data
|
f"('{y}', '{secret}')" for y, secret in proofs_used_data
|
||||||
)
|
)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"INSERT INTO tmp_proofs_used (Y, secret) VALUES {values_placeholder}",
|
f"INSERT INTO tmp_proofs_used (y, secret) VALUES {values_placeholder}",
|
||||||
)
|
)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"""
|
f"""
|
||||||
UPDATE {table_with_schema(db, 'proofs_used')}
|
UPDATE {table_with_schema(db, 'proofs_used')}
|
||||||
SET Y = tmp_proofs_used.Y
|
SET y = tmp_proofs_used.y
|
||||||
FROM tmp_proofs_used
|
FROM tmp_proofs_used
|
||||||
WHERE {table_with_schema(db, 'proofs_used')}.secret = tmp_proofs_used.secret
|
WHERE {table_with_schema(db, 'proofs_used')}.secret = tmp_proofs_used.secret
|
||||||
"""
|
"""
|
||||||
@@ -590,12 +596,12 @@ async def m016_recompute_Y_with_new_h2c(db: Database):
|
|||||||
f"('{y}', '{secret}')" for y, secret in proofs_pending_data
|
f"('{y}', '{secret}')" for y, secret in proofs_pending_data
|
||||||
)
|
)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"INSERT INTO tmp_proofs_used (Y, secret) VALUES {values_placeholder}",
|
f"INSERT INTO tmp_proofs_used (y, secret) VALUES {values_placeholder}",
|
||||||
)
|
)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"""
|
f"""
|
||||||
UPDATE {table_with_schema(db, 'proofs_pending')}
|
UPDATE {table_with_schema(db, 'proofs_pending')}
|
||||||
SET Y = tmp_proofs_pending.Y
|
SET y = tmp_proofs_pending.y
|
||||||
FROM tmp_proofs_pending
|
FROM tmp_proofs_pending
|
||||||
WHERE {table_with_schema(db, 'proofs_pending')}.secret = tmp_proofs_pending.secret
|
WHERE {table_with_schema(db, 'proofs_pending')}.secret = tmp_proofs_pending.secret
|
||||||
"""
|
"""
|
||||||
@@ -606,3 +612,109 @@ async def m016_recompute_Y_with_new_h2c(db: Database):
|
|||||||
await conn.execute("DROP TABLE tmp_proofs_used")
|
await conn.execute("DROP TABLE tmp_proofs_used")
|
||||||
if len(proofs_pending_data):
|
if len(proofs_pending_data):
|
||||||
await conn.execute("DROP TABLE tmp_proofs_pending")
|
await conn.execute("DROP TABLE tmp_proofs_pending")
|
||||||
|
|
||||||
|
|
||||||
|
async def m017_foreign_keys_proof_tables(db: Database):
|
||||||
|
"""
|
||||||
|
Create a foreign key relationship between the keyset id in the proof tables and the keyset table.
|
||||||
|
|
||||||
|
Create a foreign key relationship between the keyset id in the promises table and the keyset table.
|
||||||
|
|
||||||
|
Create a foreign key relationship between the quote id in the melt_quotes
|
||||||
|
and the proofs_used and proofs_pending tables.
|
||||||
|
|
||||||
|
NOTE: We do not use ALTER TABLE directly to add the new column with a foreign key relation because SQLIte does not support it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async with db.connect() as conn:
|
||||||
|
# drop the balance views first
|
||||||
|
await drop_balance_views(db, conn)
|
||||||
|
|
||||||
|
# add foreign key constraints to proofs_used table
|
||||||
|
await conn.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used_new')} (
|
||||||
|
amount {db.big_int} NOT NULL,
|
||||||
|
id TEXT,
|
||||||
|
c TEXT NOT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
y TEXT,
|
||||||
|
witness TEXT,
|
||||||
|
created TIMESTAMP,
|
||||||
|
melt_quote TEXT,
|
||||||
|
|
||||||
|
FOREIGN KEY (melt_quote) REFERENCES {table_with_schema(db, 'melt_quotes')}(quote),
|
||||||
|
|
||||||
|
UNIQUE (y)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
f"INSERT INTO {table_with_schema(db, 'proofs_used_new')} (amount, id, c, secret, y, witness, created) SELECT amount, id, c, secret, y, witness, created FROM {table_with_schema(db, 'proofs_used')}"
|
||||||
|
)
|
||||||
|
await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_used')}")
|
||||||
|
await conn.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'proofs_used_new')} RENAME TO {table_with_schema(db, 'proofs_used')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# add foreign key constraints to proofs_pending table
|
||||||
|
await conn.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending_new')} (
|
||||||
|
amount {db.big_int} NOT NULL,
|
||||||
|
id TEXT,
|
||||||
|
c TEXT NOT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
y TEXT,
|
||||||
|
witness TEXT,
|
||||||
|
created TIMESTAMP,
|
||||||
|
melt_quote TEXT,
|
||||||
|
|
||||||
|
FOREIGN KEY (melt_quote) REFERENCES {table_with_schema(db, 'melt_quotes')}(quote),
|
||||||
|
|
||||||
|
UNIQUE (y)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
f"INSERT INTO {table_with_schema(db, 'proofs_pending_new')} (amount, id, c, secret, y, created) SELECT amount, id, c, secret, y, created FROM {table_with_schema(db, 'proofs_pending')}"
|
||||||
|
)
|
||||||
|
await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_pending')}")
|
||||||
|
await conn.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'proofs_pending_new')} RENAME TO {table_with_schema(db, 'proofs_pending')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# add foreign key constraints to promises table
|
||||||
|
await conn.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises_new')} (
|
||||||
|
amount {db.big_int} NOT NULL,
|
||||||
|
id TEXT,
|
||||||
|
b_ TEXT NOT NULL,
|
||||||
|
c_ TEXT NOT NULL,
|
||||||
|
dleq_e TEXT,
|
||||||
|
dleq_s TEXT,
|
||||||
|
created TIMESTAMP,
|
||||||
|
mint_quote TEXT,
|
||||||
|
swap_id TEXT,
|
||||||
|
|
||||||
|
FOREIGN KEY (mint_quote) REFERENCES {table_with_schema(db, 'mint_quotes')}(quote),
|
||||||
|
|
||||||
|
UNIQUE (b_)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await conn.execute(
|
||||||
|
f"INSERT INTO {table_with_schema(db, 'promises_new')} (amount, id, b_, c_, dleq_e, dleq_s, created) SELECT amount, id, b_b, c_b, e, s, created FROM {table_with_schema(db, 'promises')}"
|
||||||
|
)
|
||||||
|
await conn.execute(f"DROP TABLE {table_with_schema(db, 'promises')}")
|
||||||
|
await conn.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'promises_new')} RENAME TO {table_with_schema(db, 'promises')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# recreate the balance views
|
||||||
|
await create_balance_views(db, conn)
|
||||||
|
|
||||||
|
# recreate indices
|
||||||
|
await m015_add_index_Y_to_proofs_used_and_pending(db)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from ..core.base import (
|
|||||||
PostSplitRequest,
|
PostSplitRequest,
|
||||||
PostSplitResponse,
|
PostSplitResponse,
|
||||||
)
|
)
|
||||||
from ..core.errors import CashuError
|
from ..core.errors import KeysetNotFoundError
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from ..mint.startup import ledger
|
from ..mint.startup import ledger
|
||||||
from .limit import limiter
|
from .limit import limiter
|
||||||
@@ -142,7 +142,7 @@ async def keyset_keys(keyset_id: str) -> KeysResponse:
|
|||||||
|
|
||||||
keyset = ledger.keysets.get(keyset_id)
|
keyset = ledger.keysets.get(keyset_id)
|
||||||
if keyset is None:
|
if keyset is None:
|
||||||
raise CashuError(code=0, detail="keyset not found")
|
raise KeysetNotFoundError(keyset_id)
|
||||||
|
|
||||||
keyset_for_response = KeysResponseKeyset(
|
keyset_for_response = KeysResponseKeyset(
|
||||||
id=keyset.id,
|
id=keyset.id,
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ from ..mint import migrations
|
|||||||
from ..mint.crud import LedgerCrudSqlite
|
from ..mint.crud import LedgerCrudSqlite
|
||||||
from ..mint.ledger import Ledger
|
from ..mint.ledger import Ledger
|
||||||
|
|
||||||
|
# kill the program if python runs in non-__debug__ mode
|
||||||
|
# which could lead to asserts not being executed for optimized code
|
||||||
|
if not __debug__:
|
||||||
|
raise Exception("Nutshell cannot run in non-debug mode.")
|
||||||
|
|
||||||
logger.debug("Enviroment Settings:")
|
logger.debug("Enviroment Settings:")
|
||||||
for key, value in settings.dict().items():
|
for key, value in settings.dict().items():
|
||||||
if key in [
|
if key in [
|
||||||
@@ -79,29 +84,6 @@ async def rotate_keys(n_seconds=60):
|
|||||||
|
|
||||||
async def start_mint_init():
|
async def start_mint_init():
|
||||||
await migrate_databases(ledger.db, migrations)
|
await migrate_databases(ledger.db, migrations)
|
||||||
if settings.mint_cache_secrets:
|
await ledger.startup_ledger()
|
||||||
await ledger.load_used_proofs()
|
|
||||||
await ledger.init_keysets()
|
|
||||||
|
|
||||||
for derivation_path in settings.mint_derivation_path_list:
|
|
||||||
await ledger.activate_keyset(derivation_path=derivation_path)
|
|
||||||
|
|
||||||
for method in ledger.backends:
|
|
||||||
for unit in ledger.backends[method]:
|
|
||||||
logger.info(
|
|
||||||
f"Using {ledger.backends[method][unit].__class__.__name__} backend for"
|
|
||||||
f" method: '{method.name}' and unit: '{unit.name}'"
|
|
||||||
)
|
|
||||||
status = await ledger.backends[method][unit].status()
|
|
||||||
if status.error_message:
|
|
||||||
logger.warning(
|
|
||||||
"The backend for"
|
|
||||||
f" {ledger.backends[method][unit].__class__.__name__} isn't"
|
|
||||||
f" working properly: '{status.error_message}'",
|
|
||||||
RuntimeWarning,
|
|
||||||
)
|
|
||||||
logger.info(f"Backend balance: {status.balance} {unit.name}")
|
|
||||||
|
|
||||||
logger.info(f"Data dir: {settings.cashu_dir}")
|
|
||||||
logger.info("Mint started.")
|
logger.info("Mint started.")
|
||||||
# asyncio.create_task(rotate_keys())
|
# asyncio.create_task(rotate_keys())
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class LedgerVerification(
|
|||||||
async with self.db.connect() as conn:
|
async with self.db.connect() as conn:
|
||||||
for output in outputs:
|
for output in outputs:
|
||||||
promise = await self.crud.get_promise(
|
promise = await self.crud.get_promise(
|
||||||
B_=output.B_, db=self.db, conn=conn
|
b_=output.B_, db=self.db, conn=conn
|
||||||
)
|
)
|
||||||
result.append(False if promise is None else True)
|
result.append(False if promise is None else True)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bo
|
|||||||
await wallet.load_mint()
|
await wallet.load_mint()
|
||||||
await print_balance(ctx)
|
await print_balance(ctx)
|
||||||
amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount)
|
amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount)
|
||||||
print(f"Requesting invoice for {wallet.unit.str(amount)} {wallet.unit}.")
|
print(f"Requesting invoice for {wallet.unit.str(amount)}.")
|
||||||
# in case the user wants a specific split, we create a list of amounts
|
# in case the user wants a specific split, we create a list of amounts
|
||||||
optional_split = None
|
optional_split = None
|
||||||
if split:
|
if split:
|
||||||
|
|||||||
@@ -1014,7 +1014,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
|||||||
n_change_outputs * [1], change_secrets, change_rs
|
n_change_outputs * [1], change_secrets, change_rs
|
||||||
)
|
)
|
||||||
|
|
||||||
# store the melt_id in proofs
|
# store the melt_id in proofs db
|
||||||
async with self.db.connect() as conn:
|
async with self.db.connect() as conn:
|
||||||
for p in proofs:
|
for p in proofs:
|
||||||
p.melt_id = quote_id
|
p.melt_id = quote_id
|
||||||
|
|||||||
@@ -94,9 +94,7 @@ def mint():
|
|||||||
async def ledger():
|
async def ledger():
|
||||||
async def start_mint_init(ledger: Ledger):
|
async def start_mint_init(ledger: Ledger):
|
||||||
await migrate_databases(ledger.db, migrations_mint)
|
await migrate_databases(ledger.db, migrations_mint)
|
||||||
if settings.mint_cache_secrets:
|
await ledger.startup_ledger()
|
||||||
await ledger.load_used_proofs()
|
|
||||||
await ledger.init_keysets()
|
|
||||||
|
|
||||||
if not settings.mint_database.startswith("postgres"):
|
if not settings.mint_database.startswith("postgres"):
|
||||||
# clear sqlite database
|
# clear sqlite database
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ is_regtest: bool = not is_fake
|
|||||||
is_deprecated_api_only = settings.debug_mint_only_deprecated
|
is_deprecated_api_only = settings.debug_mint_only_deprecated
|
||||||
is_github_actions = os.getenv("GITHUB_ACTIONS") == "true"
|
is_github_actions = os.getenv("GITHUB_ACTIONS") == "true"
|
||||||
is_postgres = settings.mint_database.startswith("postgres")
|
is_postgres = settings.mint_database.startswith("postgres")
|
||||||
|
SLEEP_TIME = 1 if not is_github_actions else 2
|
||||||
|
|
||||||
docker_lightning_cli = [
|
docker_lightning_cli = [
|
||||||
"docker",
|
"docker",
|
||||||
@@ -156,31 +157,6 @@ def pay_onchain(address: str, sats: int) -> str:
|
|||||||
return run_cmd(cmd)
|
return run_cmd(cmd)
|
||||||
|
|
||||||
|
|
||||||
# def clean_database(settings):
|
|
||||||
# if DB_TYPE == POSTGRES:
|
|
||||||
# db_url = make_url(settings.lnbits_database_url)
|
|
||||||
|
|
||||||
# conn = psycopg2.connect(settings.lnbits_database_url)
|
|
||||||
# conn.autocommit = True
|
|
||||||
# with conn.cursor() as cur:
|
|
||||||
# try:
|
|
||||||
# cur.execute("DROP DATABASE lnbits_test")
|
|
||||||
# except psycopg2.errors.InvalidCatalogName:
|
|
||||||
# pass
|
|
||||||
# cur.execute("CREATE DATABASE lnbits_test")
|
|
||||||
|
|
||||||
# db_url.database = "lnbits_test"
|
|
||||||
# settings.lnbits_database_url = str(db_url)
|
|
||||||
|
|
||||||
# core.db.__init__("database")
|
|
||||||
|
|
||||||
# conn.close()
|
|
||||||
# else:
|
|
||||||
# # FIXME: do this once mock data is removed from test data folder
|
|
||||||
# # os.remove(settings.lnbits_data_folder + "/database.sqlite3")
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
def pay_if_regtest(bolt11: str):
|
def pay_if_regtest(bolt11: str):
|
||||||
if is_regtest:
|
if is_regtest:
|
||||||
pay_real_invoice(bolt11)
|
pay_real_invoice(bolt11)
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
from typing import List
|
import asyncio
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import bolt11
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
from cashu.core.base import Proof
|
from cashu.core.base import MeltQuote, Proof, SpentState
|
||||||
from cashu.core.crypto.aes import AESCipher
|
from cashu.core.crypto.aes import AESCipher
|
||||||
from cashu.core.db import Database
|
from cashu.core.db import Database
|
||||||
from cashu.core.settings import settings
|
from cashu.core.settings import settings
|
||||||
from cashu.mint.crud import LedgerCrudSqlite
|
from cashu.mint.crud import LedgerCrudSqlite
|
||||||
from cashu.mint.ledger import Ledger
|
from cashu.mint.ledger import Ledger
|
||||||
|
from cashu.wallet.wallet import Wallet
|
||||||
|
from tests.conftest import SERVER_ENDPOINT
|
||||||
|
from tests.helpers import (
|
||||||
|
SLEEP_TIME,
|
||||||
|
cancel_invoice,
|
||||||
|
get_hold_invoice,
|
||||||
|
is_fake,
|
||||||
|
is_regtest,
|
||||||
|
pay_if_regtest,
|
||||||
|
settle_invoice,
|
||||||
|
)
|
||||||
|
|
||||||
SEED = "TEST_PRIVATE_KEY"
|
SEED = "TEST_PRIVATE_KEY"
|
||||||
DERIVATION_PATH = "m/0'/0'/0'"
|
DERIVATION_PATH = "m/0'/0'/0'"
|
||||||
@@ -30,6 +44,17 @@ def assert_amt(proofs: List[Proof], expected: int):
|
|||||||
assert [p.amount for p in proofs] == expected
|
assert [p.amount for p in proofs] == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def wallet(ledger: Ledger):
|
||||||
|
wallet1 = await Wallet.with_db(
|
||||||
|
url=SERVER_ENDPOINT,
|
||||||
|
db="test_data/wallet_mint_api_deprecated",
|
||||||
|
name="wallet_mint_api_deprecated",
|
||||||
|
)
|
||||||
|
await wallet1.load_mint()
|
||||||
|
yield wallet1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_init_keysets_with_duplicates(ledger: Ledger):
|
async def test_init_keysets_with_duplicates(ledger: Ledger):
|
||||||
ledger.keysets = {}
|
ledger.keysets = {}
|
||||||
@@ -126,3 +151,251 @@ async def test_decrypt_seed():
|
|||||||
pubkeys_encrypted[1].serialize().hex()
|
pubkeys_encrypted[1].serialize().hex()
|
||||||
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_pending_melts(
|
||||||
|
ledger: Ledger, check_id: str = "checking_id"
|
||||||
|
) -> Tuple[Proof, MeltQuote]:
|
||||||
|
"""Helper function for startup tests for fakewallet. Creates fake pending melt
|
||||||
|
quote and fake proofs that are in the pending table that look like they're being
|
||||||
|
used to pay the pending melt quote."""
|
||||||
|
quote_id = "quote_id"
|
||||||
|
quote = MeltQuote(
|
||||||
|
quote=quote_id,
|
||||||
|
method="bolt11",
|
||||||
|
request="asdasd",
|
||||||
|
checking_id=check_id,
|
||||||
|
unit="sat",
|
||||||
|
paid=False,
|
||||||
|
amount=100,
|
||||||
|
fee_reserve=1,
|
||||||
|
)
|
||||||
|
await ledger.crud.store_melt_quote(
|
||||||
|
quote=quote,
|
||||||
|
db=ledger.db,
|
||||||
|
)
|
||||||
|
pending_proof = Proof(amount=123, C="asdasd", secret="asdasd", id=quote_id)
|
||||||
|
await ledger.crud.set_proof_pending(
|
||||||
|
db=ledger.db,
|
||||||
|
proof=pending_proof,
|
||||||
|
quote_id=quote_id,
|
||||||
|
)
|
||||||
|
# expect a pending melt quote
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert melt_quotes
|
||||||
|
return pending_proof, quote
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||||
|
async def test_startup_fakewallet_pending_quote_success(ledger: Ledger):
|
||||||
|
"""Startup routine test. Expects that a pending proofs are removed form the pending db
|
||||||
|
after the startup routine determines that the associated melt quote was paid."""
|
||||||
|
pending_proof, quote = await create_pending_melts(ledger)
|
||||||
|
states = await ledger.check_proofs_state([pending_proof.Y])
|
||||||
|
assert states[0].state == SpentState.pending
|
||||||
|
settings.fakewallet_payment_state = True
|
||||||
|
# run startup routinge
|
||||||
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
|
# expect that no pending tokens are in db anymore
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert not melt_quotes
|
||||||
|
|
||||||
|
# expect that proofs are spent
|
||||||
|
states = await ledger.check_proofs_state([pending_proof.Y])
|
||||||
|
assert states[0].state == SpentState.spent
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||||
|
async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
|
||||||
|
"""Startup routine test. Expects that a pending proofs are removed form the pending db
|
||||||
|
after the startup routine determines that the associated melt quote failed to pay.
|
||||||
|
|
||||||
|
The failure is simulated by setting the fakewallet_payment_state to False.
|
||||||
|
"""
|
||||||
|
pending_proof, quote = await create_pending_melts(ledger)
|
||||||
|
states = await ledger.check_proofs_state([pending_proof.Y])
|
||||||
|
assert states[0].state == SpentState.pending
|
||||||
|
settings.fakewallet_payment_state = False
|
||||||
|
# run startup routinge
|
||||||
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
|
# expect that no pending tokens are in db anymore
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert not melt_quotes
|
||||||
|
|
||||||
|
# expect that proofs are unspent
|
||||||
|
states = await ledger.check_proofs_state([pending_proof.Y])
|
||||||
|
assert states[0].state == SpentState.unspent
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_regtest, reason="only for fake wallet")
|
||||||
|
async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
|
||||||
|
pending_proof, quote = await create_pending_melts(ledger)
|
||||||
|
states = await ledger.check_proofs_state([pending_proof.Y])
|
||||||
|
assert states[0].state == SpentState.pending
|
||||||
|
settings.fakewallet_payment_state = None
|
||||||
|
# run startup routinge
|
||||||
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
|
# expect that melt quote is still pending
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert melt_quotes
|
||||||
|
|
||||||
|
# expect that proofs are still pending
|
||||||
|
states = await ledger.check_proofs_state([pending_proof.Y])
|
||||||
|
assert states[0].state == SpentState.pending
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
|
async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Ledger):
|
||||||
|
# fill wallet
|
||||||
|
invoice = await wallet.request_mint(64)
|
||||||
|
pay_if_regtest(invoice.bolt11)
|
||||||
|
await wallet.mint(64, id=invoice.id)
|
||||||
|
assert wallet.balance == 64
|
||||||
|
|
||||||
|
# create hodl invoice
|
||||||
|
preimage, invoice_dict = get_hold_invoice(16)
|
||||||
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
|
# wallet pays the invoice
|
||||||
|
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
||||||
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
|
asyncio.create_task(
|
||||||
|
wallet.pay_lightning(
|
||||||
|
proofs=send_proofs,
|
||||||
|
invoice=invoice_payment_request,
|
||||||
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
quote_id=quote.quote,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
# settle_invoice(preimage=preimage)
|
||||||
|
|
||||||
|
# run startup routinge
|
||||||
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
|
# expect that melt quote is still pending
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert melt_quotes
|
||||||
|
|
||||||
|
# expect that proofs are still pending
|
||||||
|
states = await ledger.check_proofs_state([p.Y for p in send_proofs])
|
||||||
|
assert all([s.state == SpentState.pending for s in states])
|
||||||
|
|
||||||
|
# only now settle the invoice
|
||||||
|
settle_invoice(preimage=preimage)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
|
async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Ledger):
|
||||||
|
# fill wallet
|
||||||
|
invoice = await wallet.request_mint(64)
|
||||||
|
pay_if_regtest(invoice.bolt11)
|
||||||
|
await wallet.mint(64, id=invoice.id)
|
||||||
|
assert wallet.balance == 64
|
||||||
|
|
||||||
|
# create hodl invoice
|
||||||
|
preimage, invoice_dict = get_hold_invoice(16)
|
||||||
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
|
# wallet pays the invoice
|
||||||
|
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
||||||
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
|
asyncio.create_task(
|
||||||
|
wallet.pay_lightning(
|
||||||
|
proofs=send_proofs,
|
||||||
|
invoice=invoice_payment_request,
|
||||||
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
quote_id=quote.quote,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
# expect that proofs are pending
|
||||||
|
states = await ledger.check_proofs_state([p.Y for p in send_proofs])
|
||||||
|
assert all([s.state == SpentState.pending for s in states])
|
||||||
|
|
||||||
|
settle_invoice(preimage=preimage)
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
# run startup routinge
|
||||||
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
|
# expect that no melt quote is pending
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert not melt_quotes
|
||||||
|
|
||||||
|
# expect that proofs are spent
|
||||||
|
states = await ledger.check_proofs_state([p.Y for p in send_proofs])
|
||||||
|
assert all([s.state == SpentState.spent for s in states])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
|
async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Ledger):
|
||||||
|
"""Simulate a failure to pay the hodl invoice by canceling it."""
|
||||||
|
# fill wallet
|
||||||
|
invoice = await wallet.request_mint(64)
|
||||||
|
pay_if_regtest(invoice.bolt11)
|
||||||
|
await wallet.mint(64, id=invoice.id)
|
||||||
|
assert wallet.balance == 64
|
||||||
|
|
||||||
|
# create hodl invoice
|
||||||
|
preimage, invoice_dict = get_hold_invoice(16)
|
||||||
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
invoice_obj = bolt11.decode(invoice_payment_request)
|
||||||
|
preimage_hash = invoice_obj.payment_hash
|
||||||
|
|
||||||
|
# wallet pays the invoice
|
||||||
|
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
||||||
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
|
asyncio.create_task(
|
||||||
|
wallet.pay_lightning(
|
||||||
|
proofs=send_proofs,
|
||||||
|
invoice=invoice_payment_request,
|
||||||
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
quote_id=quote.quote,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
# expect that proofs are pending
|
||||||
|
states = await ledger.check_proofs_state([p.Y for p in send_proofs])
|
||||||
|
assert all([s.state == SpentState.pending for s in states])
|
||||||
|
|
||||||
|
cancel_invoice(preimage_hash=preimage_hash)
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
# run startup routinge
|
||||||
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
|
# expect that no melt quote is pending
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert not melt_quotes
|
||||||
|
|
||||||
|
# expect that proofs are unspent
|
||||||
|
states = await ledger.check_proofs_state([p.Y for p in send_proofs])
|
||||||
|
assert all([s.state == SpentState.unspent for s in states])
|
||||||
|
|||||||
80
tests/test_mint_regtest.py
Normal file
80
tests/test_mint_regtest.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
from cashu.core.base import SpentState
|
||||||
|
from cashu.mint.ledger import Ledger
|
||||||
|
from cashu.wallet.wallet import Wallet
|
||||||
|
from tests.conftest import SERVER_ENDPOINT
|
||||||
|
from tests.helpers import (
|
||||||
|
SLEEP_TIME,
|
||||||
|
get_hold_invoice,
|
||||||
|
is_fake,
|
||||||
|
pay_if_regtest,
|
||||||
|
settle_invoice,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def wallet():
|
||||||
|
wallet = await Wallet.with_db(
|
||||||
|
url=SERVER_ENDPOINT,
|
||||||
|
db="test_data/wallet",
|
||||||
|
name="wallet",
|
||||||
|
)
|
||||||
|
await wallet.load_mint()
|
||||||
|
yield wallet
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
|
async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
||||||
|
# fill wallet
|
||||||
|
invoice = await wallet.request_mint(64)
|
||||||
|
pay_if_regtest(invoice.bolt11)
|
||||||
|
await wallet.mint(64, id=invoice.id)
|
||||||
|
assert wallet.balance == 64
|
||||||
|
|
||||||
|
# create hodl invoice
|
||||||
|
preimage, invoice_dict = get_hold_invoice(16)
|
||||||
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
|
# wallet pays the invoice
|
||||||
|
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
||||||
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
|
asyncio.create_task(ledger.melt(proofs=send_proofs, quote=quote.quote))
|
||||||
|
# asyncio.create_task(
|
||||||
|
# wallet.pay_lightning(
|
||||||
|
# proofs=send_proofs,
|
||||||
|
# invoice=invoice_payment_request,
|
||||||
|
# fee_reserve_sat=quote.fee_reserve,
|
||||||
|
# quote_id=quote.quote,
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
# expect that melt quote is still pending
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert melt_quotes
|
||||||
|
|
||||||
|
# expect that proofs are still pending
|
||||||
|
states = await ledger.check_proofs_state([p.Y for p in send_proofs])
|
||||||
|
assert all([s.state == SpentState.pending for s in states])
|
||||||
|
|
||||||
|
# only now settle the invoice
|
||||||
|
settle_invoice(preimage=preimage)
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
# expect that proofs are now spent
|
||||||
|
states = await ledger.check_proofs_state([p.Y for p in send_proofs])
|
||||||
|
assert all([s.state == SpentState.spent for s in states])
|
||||||
|
|
||||||
|
# expect that no melt quote is pending
|
||||||
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
db=ledger.db
|
||||||
|
)
|
||||||
|
assert not melt_quotes
|
||||||
107
tests/test_wallet_regtest.py
Normal file
107
tests/test_wallet_regtest.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import bolt11
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
from cashu.core.base import SpentState
|
||||||
|
from cashu.mint.ledger import Ledger
|
||||||
|
from cashu.wallet.wallet import Wallet
|
||||||
|
from tests.conftest import SERVER_ENDPOINT
|
||||||
|
from tests.helpers import (
|
||||||
|
SLEEP_TIME,
|
||||||
|
cancel_invoice,
|
||||||
|
get_hold_invoice,
|
||||||
|
is_fake,
|
||||||
|
pay_if_regtest,
|
||||||
|
settle_invoice,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def wallet():
|
||||||
|
wallet = await Wallet.with_db(
|
||||||
|
url=SERVER_ENDPOINT,
|
||||||
|
db="test_data/wallet",
|
||||||
|
name="wallet",
|
||||||
|
)
|
||||||
|
await wallet.load_mint()
|
||||||
|
yield wallet
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
|
async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
||||||
|
# fill wallet
|
||||||
|
invoice = await wallet.request_mint(64)
|
||||||
|
pay_if_regtest(invoice.bolt11)
|
||||||
|
await wallet.mint(64, id=invoice.id)
|
||||||
|
assert wallet.balance == 64
|
||||||
|
|
||||||
|
# create hodl invoice
|
||||||
|
preimage, invoice_dict = get_hold_invoice(16)
|
||||||
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
|
# wallet pays the invoice
|
||||||
|
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
||||||
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
|
asyncio.create_task(
|
||||||
|
wallet.pay_lightning(
|
||||||
|
proofs=send_proofs,
|
||||||
|
invoice=invoice_payment_request,
|
||||||
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
quote_id=quote.quote,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
states = await wallet.check_proof_state(send_proofs)
|
||||||
|
assert all([s.state == SpentState.pending for s in states.states])
|
||||||
|
|
||||||
|
settle_invoice(preimage=preimage)
|
||||||
|
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
states = await wallet.check_proof_state(send_proofs)
|
||||||
|
assert all([s.state == SpentState.spent for s in states.states])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
|
async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger):
|
||||||
|
# fill wallet
|
||||||
|
invoice = await wallet.request_mint(64)
|
||||||
|
pay_if_regtest(invoice.bolt11)
|
||||||
|
await wallet.mint(64, id=invoice.id)
|
||||||
|
assert wallet.balance == 64
|
||||||
|
|
||||||
|
# create hodl invoice
|
||||||
|
preimage, invoice_dict = get_hold_invoice(16)
|
||||||
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
invoice_obj = bolt11.decode(invoice_payment_request)
|
||||||
|
preimage_hash = invoice_obj.payment_hash
|
||||||
|
|
||||||
|
# wallet pays the invoice
|
||||||
|
quote = await wallet.get_pay_amount_with_fees(invoice_payment_request)
|
||||||
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||||
|
asyncio.create_task(
|
||||||
|
wallet.pay_lightning(
|
||||||
|
proofs=send_proofs,
|
||||||
|
invoice=invoice_payment_request,
|
||||||
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
|
quote_id=quote.quote,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
states = await wallet.check_proof_state(send_proofs)
|
||||||
|
assert all([s.state == SpentState.pending for s in states.states])
|
||||||
|
|
||||||
|
cancel_invoice(preimage_hash=preimage_hash)
|
||||||
|
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
states = await wallet.check_proof_state(send_proofs)
|
||||||
|
assert all([s.state == SpentState.unspent for s in states.states])
|
||||||
@@ -172,6 +172,11 @@ async def test_restore_wallet_after_mint(wallet3: Wallet):
|
|||||||
await wallet3.restore_promises_from_to(0, 20)
|
await wallet3.restore_promises_from_to(0, 20)
|
||||||
assert wallet3.balance == 64
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
# expect that DLEQ proofs are restored
|
||||||
|
assert all([p.dleq for p in wallet3.proofs])
|
||||||
|
assert all([p.dleq.e for p in wallet3.proofs]) # type: ignore
|
||||||
|
assert all([p.dleq.s for p in wallet3.proofs]) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_restore_wallet_with_invalid_mnemonic(wallet3: Wallet):
|
async def test_restore_wallet_with_invalid_mnemonic(wallet3: Wallet):
|
||||||
|
|||||||
Reference in New Issue
Block a user