mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-19 16:44:20 +01:00
Tests: split wallet test from mint test pipeline (#748)
* split wallet test from mint test pipeline * regtest mint and wallet * fix * fix * move mint tests * real invoice in regtest mpp
This commit is contained in:
237
tests/mint/test_mint.py
Normal file
237
tests/mint/test_mint.py
Normal file
@@ -0,0 +1,237 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from cashu.core.base import BlindedMessage, MintKeyset, Proof, Unit
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
from cashu.core.helpers import calculate_number_of_blank_outputs
|
||||
from cashu.core.models import PostMintQuoteRequest
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
assert exc.args[0] == msg, Exception(
|
||||
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pubkeys(ledger: Ledger):
|
||||
assert ledger.keyset.public_keys
|
||||
assert (
|
||||
ledger.keyset.public_keys[1].serialize().hex()
|
||||
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
assert (
|
||||
ledger.keyset.public_keys[2 ** (settings.max_order - 1)].serialize().hex()
|
||||
== "023c84c0895cc0e827b348ea0a62951ca489a5e436f3ea7545f3c1d5f1bea1c866"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_privatekeys(ledger: Ledger):
|
||||
assert ledger.keyset.private_keys
|
||||
assert (
|
||||
ledger.keyset.private_keys[1].serialize()
|
||||
== "8300050453f08e6ead1296bb864e905bd46761beed22b81110fae0751d84604d"
|
||||
)
|
||||
assert (
|
||||
ledger.keyset.private_keys[2 ** (settings.max_order - 1)].serialize()
|
||||
== "b0477644cb3d82ffcc170bc0a76e0409727232e87c5ae51d64a259936228c7be"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keysets(ledger: Ledger):
|
||||
assert len(ledger.keysets)
|
||||
assert len(list(ledger.keysets.keys()))
|
||||
assert ledger.keyset.id == "009a1f293253e41e"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset(ledger: Ledger):
|
||||
keyset = ledger.get_keyset()
|
||||
assert isinstance(keyset, dict)
|
||||
assert len(keyset) == settings.max_order
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await pay_if_regtest(quote.request)
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
promises = await ledger.mint(outputs=blinded_messages_mock, quote_id=quote.quote)
|
||||
assert len(promises)
|
||||
assert promises[0].amount == 8
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_quote(ledger: Ledger):
|
||||
await assert_err(
|
||||
ledger.get_mint_quote(quote_id="invalid_quote_id"),
|
||||
"quote not found",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_invalid_quote(ledger: Ledger):
|
||||
await assert_err(
|
||||
ledger.get_melt_quote(quote_id="invalid_quote_id"),
|
||||
"quote not found",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await pay_if_regtest(quote.request)
|
||||
blinded_messages_mock_invalid_key = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bff02d7fa2365379b0840afe249a7a9d71237",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
await assert_err(
|
||||
ledger.mint(outputs=blinded_messages_mock_invalid_key, quote_id=quote.quote),
|
||||
"invalid public key",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_promises(ledger: Ledger):
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
promises = await ledger._generate_promises(blinded_messages_mock)
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
assert promises[0].amount == 8
|
||||
assert promises[0].id == "009a1f293253e41e"
|
||||
|
||||
# DLEQ proof present
|
||||
assert promises[0].dleq
|
||||
assert promises[0].dleq.s
|
||||
assert promises[0].dleq.e
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises(ledger: Ledger):
|
||||
# Example slightly adapted from NUT-08 because we want to ensure the dynamic change
|
||||
# token amount works: `n_blank_outputs != n_returned_promises != 4`.
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 2_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee = 100
|
||||
|
||||
expected_returned_promises = 7 # Amounts = [4, 8, 32, 64, 256, 512, 1024]
|
||||
expected_returned_fees = 1900
|
||||
|
||||
n_blank_outputs = calculate_number_of_blank_outputs(fee_reserve)
|
||||
blinded_msgs = [step1_alice(str(n)) for n in range(n_blank_outputs)]
|
||||
outputs = [
|
||||
BlindedMessage(
|
||||
amount=1,
|
||||
B_=b.serialize().hex(),
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
for b, _ in blinded_msgs
|
||||
]
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
fee_provided=fee_reserve, fee_paid=actual_fee, outputs=outputs
|
||||
)
|
||||
|
||||
assert len(promises) == expected_returned_promises
|
||||
assert sum([promise.amount for promise in promises]) == expected_returned_fees
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises_legacy_wallet(ledger: Ledger):
|
||||
# Check if mint handles a legacy wallet implementation (always sends 4 blank
|
||||
# outputs) as well.
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 2_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee = 100
|
||||
|
||||
expected_returned_promises = 4 # Amounts = [64, 256, 512, 1024]
|
||||
expected_returned_fees = 1856
|
||||
|
||||
n_blank_outputs = 4
|
||||
blinded_msgs = [step1_alice(str(n)) for n in range(n_blank_outputs)]
|
||||
outputs = [
|
||||
BlindedMessage(
|
||||
amount=1,
|
||||
B_=b.serialize().hex(),
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
for b, _ in blinded_msgs
|
||||
]
|
||||
|
||||
promises = await ledger._generate_change_promises(fee_reserve, actual_fee, outputs)
|
||||
|
||||
assert len(promises) == expected_returned_promises
|
||||
assert sum([promise.amount for promise in promises]) == expected_returned_fees
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises_returns_empty_if_no_outputs(ledger: Ledger):
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 1_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee_msat = 100_000
|
||||
outputs = None
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
fee_reserve, actual_fee_msat, outputs
|
||||
)
|
||||
assert len(promises) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_balance(ledger: Ledger):
|
||||
unit = Unit["sat"]
|
||||
active_keyset: MintKeyset = next(
|
||||
filter(lambda k: k.active and k.unit == unit, ledger.keysets.values())
|
||||
)
|
||||
balance = await ledger.get_balance(active_keyset)
|
||||
assert balance == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maximum_balance(ledger: Ledger):
|
||||
settings.mint_max_balance = 1000
|
||||
await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await assert_err(
|
||||
ledger.mint_quote(PostMintQuoteRequest(amount=8000, unit="sat")),
|
||||
"Mint has reached maximum balance.",
|
||||
)
|
||||
settings.mint_max_balance = 0
|
||||
583
tests/mint/test_mint_api.py
Normal file
583
tests/mint/test_mint_api.py
Normal file
@@ -0,0 +1,583 @@
|
||||
import bolt11
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuoteState, MintQuoteState
|
||||
from cashu.core.models import (
|
||||
GetInfoResponse,
|
||||
MintMethodSetting,
|
||||
PostCheckStateRequest,
|
||||
PostCheckStateResponse,
|
||||
PostMeltQuoteResponse,
|
||||
PostMintQuoteResponse,
|
||||
PostRestoreRequest,
|
||||
PostRestoreResponse,
|
||||
)
|
||||
from cashu.core.nuts import nut20
|
||||
from cashu.core.nuts.nuts import MINT_NUT
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.crud import bump_secret_derivation
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
BASE_URL = "http://localhost:3337"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(ledger: Ledger):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=BASE_URL,
|
||||
db="test_data/wallet_mint_api",
|
||||
name="wallet_mint_api",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_info(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/info")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.pubkey
|
||||
assert response.json()["pubkey"] == ledger.pubkey.serialize().hex()
|
||||
info = GetInfoResponse(**response.json())
|
||||
assert info.nuts
|
||||
assert info.nuts[MINT_NUT]["disabled"] is False
|
||||
setting = MintMethodSetting.parse_obj(info.nuts[MINT_NUT]["methods"][0])
|
||||
assert setting.method == "bolt11"
|
||||
assert setting.unit == "sat"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_keys(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/keys")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
expected = {
|
||||
"keysets": [
|
||||
{
|
||||
"id": keyset.id,
|
||||
"unit": keyset.unit.name,
|
||||
"keys": {
|
||||
str(k): v.serialize().hex()
|
||||
for k, v in keyset.public_keys.items() # type: ignore
|
||||
},
|
||||
}
|
||||
for keyset in ledger.keysets.values()
|
||||
]
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_keysets(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/keysets")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
expected = {
|
||||
"keysets": [
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"unit": "sat",
|
||||
"active": True,
|
||||
"input_fee_ppk": 0,
|
||||
},
|
||||
{
|
||||
"id": "00c074b96c7e2b0e",
|
||||
"unit": "usd",
|
||||
"active": True,
|
||||
"input_fee_ppk": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_keyset_keys(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/keys/009a1f293253e41e")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
expected = {
|
||||
"keysets": [
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"unit": "sat",
|
||||
"keys": {
|
||||
str(k): v.serialize().hex()
|
||||
for k, v in ledger.keysets["009a1f293253e41e"].public_keys.items() # type: ignore
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_keyset_keys_old_keyset_id(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/keys/009a1f293253e41e")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
expected = {
|
||||
"keysets": [
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"unit": "sat",
|
||||
"keys": {
|
||||
str(k): v.serialize().hex()
|
||||
for k, v in ledger.keysets["009a1f293253e41e"].public_keys.items() # type: ignore
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_swap(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(2)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
payload = {"inputs": inputs_payload, "outputs": outputs_payload}
|
||||
response = httpx.post(f"{BASE_URL}/v1/swap", json=payload, timeout=None)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert len(result["signatures"]) == 2
|
||||
assert result["signatures"][0]["amount"] == 32
|
||||
assert result["signatures"][1]["amount"] == 32
|
||||
assert result["signatures"][0]["id"] == "009a1f293253e41e"
|
||||
assert result["signatures"][0]["dleq"]
|
||||
assert "e" in result["signatures"][0]["dleq"]
|
||||
assert "s" in result["signatures"][0]["dleq"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_mint_quote(ledger: Ledger):
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/quote/bolt11",
|
||||
json={"unit": "sat", "amount": 100, "pubkey": "02" + "00" * 32},
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result["quote"]
|
||||
assert result["request"]
|
||||
assert result["pubkey"] == "02" + "00" * 32
|
||||
|
||||
# deserialize the response
|
||||
resp_quote = PostMintQuoteResponse(**result)
|
||||
assert resp_quote.quote == result["quote"]
|
||||
assert resp_quote.state == MintQuoteState.unpaid.value
|
||||
assert resp_quote.amount == 100
|
||||
assert resp_quote.unit == "sat"
|
||||
assert resp_quote.request == result["request"]
|
||||
|
||||
# check if DEPRECATED paid flag is also returned
|
||||
assert result["paid"] is False
|
||||
assert resp_quote.paid is False
|
||||
|
||||
invoice = bolt11.decode(result["request"])
|
||||
assert invoice.amount_msat == 100 * 1000
|
||||
|
||||
expiry = None
|
||||
if invoice.expiry is not None:
|
||||
expiry = invoice.date + invoice.expiry
|
||||
|
||||
assert result["expiry"] == expiry
|
||||
|
||||
# pay the invoice
|
||||
await pay_if_regtest(result["request"])
|
||||
|
||||
# get mint quote again from api
|
||||
response = httpx.get(
|
||||
f"{BASE_URL}/v1/mint/quote/bolt11/{result['quote']}",
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result2 = response.json()
|
||||
assert result2["quote"] == result["quote"]
|
||||
# deserialize the response
|
||||
resp_quote = PostMintQuoteResponse(**result2)
|
||||
assert resp_quote.quote == result["quote"]
|
||||
assert resp_quote.state == MintQuoteState.paid.value
|
||||
assert resp_quote.amount == 100
|
||||
assert resp_quote.unit == "sat"
|
||||
assert resp_quote.request == result["request"]
|
||||
|
||||
# check if DEPRECATED paid flag is also returned
|
||||
assert result2["paid"] is True
|
||||
assert resp_quote.paid is True
|
||||
assert resp_quote.pubkey == "02" + "00" * 32
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_mint(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
assert mint_quote.privkey
|
||||
signature = nut20.sign_mint_quote(mint_quote.quote, outputs, mint_quote.privkey)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/bolt11",
|
||||
json={
|
||||
"quote": mint_quote.quote,
|
||||
"outputs": outputs_payload,
|
||||
"signature": signature,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert len(result["signatures"]) == 2
|
||||
assert result["signatures"][0]["amount"] == 32
|
||||
assert result["signatures"][1]["amount"] == 32
|
||||
assert result["signatures"][0]["id"] == "009a1f293253e41e"
|
||||
assert result["signatures"][0]["dleq"]
|
||||
assert "e" in result["signatures"][0]["dleq"]
|
||||
assert "s" in result["signatures"][0]["dleq"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_mint_bolt11_no_signature(ledger: Ledger, wallet: Wallet):
|
||||
"""
|
||||
For backwards compatibility, we do not require a NUT-20 signature
|
||||
for minting with bolt11.
|
||||
"""
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/quote/bolt11",
|
||||
json={
|
||||
"unit": "sat",
|
||||
"amount": 64,
|
||||
# no pubkey
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result["pubkey"] is None
|
||||
await pay_if_regtest(result["request"])
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/bolt11",
|
||||
json={
|
||||
"quote": result["quote"],
|
||||
"outputs": outputs_payload,
|
||||
# no signature
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
is_regtest,
|
||||
reason="regtest",
|
||||
)
|
||||
async def test_melt_quote_internal(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
request = mint_quote.request
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/quote/bolt11",
|
||||
json={"unit": "sat", "request": request},
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result["quote"]
|
||||
assert result["amount"] == 64
|
||||
# TODO: internal invoice, fee should be 0
|
||||
assert result["fee_reserve"] == 0
|
||||
|
||||
# deserialize the response
|
||||
resp_quote = PostMeltQuoteResponse(**result)
|
||||
assert resp_quote.quote == result["quote"]
|
||||
assert resp_quote.payment_preimage is None
|
||||
assert resp_quote.change is None
|
||||
assert resp_quote.state == MeltQuoteState.unpaid.value
|
||||
assert resp_quote.amount == 64
|
||||
assert resp_quote.unit == "sat"
|
||||
assert resp_quote.request == request
|
||||
|
||||
# check if DEPRECATED paid flag is also returned
|
||||
assert result["paid"] is False
|
||||
assert resp_quote.paid is False
|
||||
|
||||
invoice_obj = bolt11.decode(request)
|
||||
|
||||
expiry = None
|
||||
if invoice_obj.expiry is not None:
|
||||
expiry = invoice_obj.date + invoice_obj.expiry
|
||||
|
||||
assert result["expiry"] == expiry
|
||||
|
||||
# # get melt quote again from api
|
||||
# response = httpx.get(
|
||||
# f"{BASE_URL}/v1/melt/quote/bolt11/{result['quote']}",
|
||||
# )
|
||||
# assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
# result2 = response.json()
|
||||
# assert result2["quote"] == result["quote"]
|
||||
|
||||
# # deserialize the response
|
||||
# resp_quote = PostMeltQuoteResponse(**result2)
|
||||
# assert resp_quote.quote == result["quote"]
|
||||
# assert resp_quote.payment_preimage is not None
|
||||
# assert len(resp_quote.payment_preimage) == 64
|
||||
# assert resp_quote.change is not None
|
||||
# assert resp_quote.state == MeltQuoteState.paid.value
|
||||
|
||||
# # check if DEPRECATED paid flag is also returned
|
||||
# assert result2["paid"] is True
|
||||
# assert resp_quote.paid is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
is_fake,
|
||||
reason="only works on regtest",
|
||||
)
|
||||
async def test_melt_quote_external(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
invoice_dict = get_real_invoice(64)
|
||||
request = invoice_dict["payment_request"]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/quote/bolt11",
|
||||
json={"unit": "sat", "request": request},
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result["quote"]
|
||||
assert result["amount"] == 64
|
||||
# external invoice, fee should be 2
|
||||
assert result["fee_reserve"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_melt_internal(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
|
||||
# create invoice to melt to
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
invoice_payment_request = mint_quote.request
|
||||
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 64
|
||||
assert quote.fee_reserve == 0
|
||||
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/bolt11",
|
||||
json={
|
||||
"quote": quote.quote,
|
||||
"inputs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("payment_preimage") is None
|
||||
assert result["paid"] is True
|
||||
|
||||
# deserialize the response
|
||||
resp_quote = PostMeltQuoteResponse(**result)
|
||||
assert resp_quote.quote == quote.quote
|
||||
|
||||
# internal invoice, no preimage, no change
|
||||
assert resp_quote.payment_preimage is None
|
||||
assert resp_quote.change == []
|
||||
assert resp_quote.state == MeltQuoteState.paid.value
|
||||
assert resp_quote.amount == 64
|
||||
assert resp_quote.unit == "sat"
|
||||
assert resp_quote.request == invoice_payment_request
|
||||
|
||||
# check if DEPRECATED paid flag is also returned
|
||||
assert result["paid"] is True
|
||||
assert resp_quote.paid is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
is_fake,
|
||||
reason="only works on regtest",
|
||||
)
|
||||
async def test_melt_external(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
|
||||
invoice_dict = get_real_invoice(62)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 62
|
||||
assert quote.fee_reserve == 2
|
||||
|
||||
keep, send = await wallet.swap_to_send(wallet.proofs, 64)
|
||||
inputs_payload = [p.to_dict() for p in send]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/bolt11",
|
||||
json={
|
||||
"quote": quote.quote,
|
||||
"inputs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
response.raise_for_status()
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("payment_preimage") is not None
|
||||
assert result["paid"] is True
|
||||
assert result["change"]
|
||||
# we get back 2 sats because Lightning was free to pay on regtest
|
||||
assert result["change"][0]["amount"] == 2
|
||||
|
||||
# deserialize the response
|
||||
resp_quote = PostMeltQuoteResponse(**result)
|
||||
assert resp_quote.quote == quote.quote
|
||||
assert resp_quote.amount == 62
|
||||
assert resp_quote.unit == "sat"
|
||||
assert resp_quote.request == invoice_payment_request
|
||||
assert resp_quote.payment_preimage is not None
|
||||
assert len(resp_quote.payment_preimage) == 64
|
||||
assert resp_quote.change is not None
|
||||
assert resp_quote.change[0].amount == 2
|
||||
assert resp_quote.state == MeltQuoteState.paid.value
|
||||
|
||||
# check if DEPRECATED paid flag is also returned
|
||||
assert result["paid"] is True
|
||||
assert resp_quote.paid is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_check_state(ledger: Ledger):
|
||||
payload = PostCheckStateRequest(Ys=["asdasdasd", "asdasdasd1"])
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/checkstate",
|
||||
json=payload.dict(),
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
response = PostCheckStateResponse.parse_obj(response.json())
|
||||
assert response
|
||||
assert len(response.states) == 2
|
||||
assert response.states[0].state.unspent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_restore(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
secret_counter = await bump_secret_derivation(
|
||||
db=wallet.db, keyset_id=wallet.keyset_id, by=0, skip=True
|
||||
)
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(
|
||||
secret_counter - 1, secret_counter - 1
|
||||
)
|
||||
outputs, rs = wallet._construct_outputs([64], secrets, rs)
|
||||
|
||||
payload = PostRestoreRequest(outputs=outputs)
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/restore",
|
||||
json=payload.dict(),
|
||||
)
|
||||
data = response.json()
|
||||
assert "signatures" in data
|
||||
assert "outputs" in data
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
response = PostRestoreResponse.parse_obj(response.json())
|
||||
assert response
|
||||
assert response
|
||||
assert len(response.signatures) == 1
|
||||
assert len(response.outputs) == 1
|
||||
assert response.outputs == outputs
|
||||
128
tests/mint/test_mint_api_cache.py
Normal file
128
tests/mint/test_mint_api_cache.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.nuts import nut20
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
BASE_URL = "http://localhost:3337"
|
||||
invoice_32sat = "lnbc320n1pnsuamsdqqxqrrsssp5w3tlpw2zss396qh28l3a07u35zdx8nmknzryk89ackn23eywdu2spp5ckt298t835ejzh2xepyxlg57f54q27ffc2zjsjh3t5pmx4wghpcqne0vycw5dfalx5y45d2jtwqfwz437hduyccn9nxk2feay0ytxldjpf3fcjrcf5k2s56q3erj86ymlqdp703y89vt4lr4lun5z5duulcqwuwutn"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(ledger: Ledger):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=BASE_URL,
|
||||
db="test_data/wallet_mint_api",
|
||||
name="wallet_mint_api",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_redis_cache_enabled,
|
||||
reason="settings.mint_redis_cache_enabled is False",
|
||||
)
|
||||
async def test_api_mint_cached_responses(wallet: Wallet):
|
||||
# Testing mint
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
|
||||
quote_id = mint_quote.quote
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10010, 10011)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
assert mint_quote.privkey
|
||||
signature = nut20.sign_mint_quote(quote_id, outputs, mint_quote.privkey)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/bolt11",
|
||||
json={"quote": quote_id, "outputs": outputs_payload, "signature": signature},
|
||||
timeout=None,
|
||||
)
|
||||
response1 = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/bolt11",
|
||||
json={"quote": quote_id, "outputs": outputs_payload, "signature": signature},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.status_code = }"
|
||||
assert response1.status_code == 200, f"{response1.status_code = }"
|
||||
assert response.text == response1.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_redis_cache_enabled,
|
||||
reason="settings.mint_redis_cache_enabled is False",
|
||||
)
|
||||
async def test_api_swap_cached_responses(wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
|
||||
minted = await wallet.mint(64, mint_quote.quote)
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10010, 10011)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
assert mint_quote.privkey
|
||||
signature = nut20.sign_mint_quote(mint_quote.quote, outputs, mint_quote.privkey)
|
||||
inputs_payload = [i.dict() for i in minted]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/swap",
|
||||
json={
|
||||
"inputs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
"signature": signature,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
response1 = httpx.post(
|
||||
f"{BASE_URL}/v1/swap",
|
||||
json={"inputs": inputs_payload, "outputs": outputs_payload},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.status_code = }"
|
||||
assert response1.status_code == 200, f"{response1.status_code = }"
|
||||
assert response.text == response1.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_redis_cache_enabled,
|
||||
reason="settings.mint_redis_cache_enabled is False",
|
||||
)
|
||||
async def test_api_melt_cached_responses(wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
melt_quote = await wallet.melt_quote(invoice_32sat)
|
||||
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
minted = await wallet.mint(64, mint_quote.quote)
|
||||
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10010, 10010)
|
||||
outputs, rs = wallet._construct_outputs([32], secrets, rs)
|
||||
inputs_payload = [i.dict() for i in minted]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/bolt11",
|
||||
json={
|
||||
"quote": melt_quote.quote,
|
||||
"inputs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
response1 = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/bolt11",
|
||||
json={
|
||||
"quote": melt_quote.quote,
|
||||
"inputs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.status_code = }"
|
||||
assert response1.status_code == 200, f"{response1.status_code = }"
|
||||
assert response.text == response1.text
|
||||
357
tests/mint/test_mint_api_deprecated.py
Normal file
357
tests/mint/test_mint_api_deprecated.py
Normal file
@@ -0,0 +1,357 @@
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Proof, Unit
|
||||
from cashu.core.models import (
|
||||
CheckSpendableRequest_deprecated,
|
||||
CheckSpendableResponse_deprecated,
|
||||
GetMintResponse_deprecated,
|
||||
PostRestoreRequest,
|
||||
PostRestoreResponse,
|
||||
)
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.crud import bump_secret_derivation
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
BASE_URL = "http://localhost:3337"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(ledger: Ledger):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=BASE_URL,
|
||||
db="test_data/wallet_mint_api_deprecated",
|
||||
name="wallet_mint_api_deprecated",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_info(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/info")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.pubkey
|
||||
assert response.json()["pubkey"] == ledger.pubkey.serialize().hex()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keys(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/keys")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
assert response.json() == {
|
||||
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keysets(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/keysets")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
sat_keysets = {k: v for k, v in ledger.keysets.items() if v.unit == Unit.sat}
|
||||
assert response.json()["keysets"] == list(sat_keysets.keys())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keyset_keys(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/keys/009a1f293253e41e")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
assert response.json() == {
|
||||
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(20000, 20001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
# strip "id" from outputs_payload, which is not used in the deprecated split endpoint
|
||||
for o in outputs_payload:
|
||||
o.pop("id")
|
||||
payload = {"proofs": inputs_payload, "outputs": outputs_payload}
|
||||
response = httpx.post(f"{BASE_URL}/split", json=payload, timeout=None)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result["promises"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_deprecated_with_amount(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(80000, 80001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
# strip "id" from outputs_payload, which is not used in the deprecated split endpoint
|
||||
for o in outputs_payload:
|
||||
o.pop("id")
|
||||
# we supply an amount here, which should cause the very old deprecated split endpoint to be used
|
||||
payload = {"proofs": inputs_payload, "outputs": outputs_payload, "amount": 32}
|
||||
response = httpx.post(f"{BASE_URL}/split", json=payload, timeout=None)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
# old deprecated output format
|
||||
assert result["fst"]
|
||||
assert result["snd"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_mint_validation(ledger):
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=-21")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=0")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=2100000000000001")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=1")
|
||||
assert "detail" not in response.json()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger, wallet: Wallet):
|
||||
quote_response = httpx.get(
|
||||
f"{BASE_URL}/mint",
|
||||
params={"amount": 64},
|
||||
timeout=None,
|
||||
)
|
||||
mint_quote = GetMintResponse_deprecated.parse_obj(quote_response.json())
|
||||
await pay_if_regtest(mint_quote.pr)
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/mint",
|
||||
json={"outputs": outputs_payload},
|
||||
params={"hash": mint_quote.hash},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert len(result["promises"]) == 2
|
||||
assert result["promises"][0]["amount"] == 32
|
||||
assert result["promises"][1]["amount"] == 32
|
||||
assert result["promises"][0]["id"] == "009a1f293253e41e"
|
||||
assert result["promises"][0]["dleq"]
|
||||
assert "e" in result["promises"][0]["dleq"]
|
||||
assert "s" in result["promises"][0]["dleq"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_internal(ledger: Ledger, wallet: Wallet):
|
||||
# fill wallet
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
|
||||
# create invoice to melt to
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
|
||||
invoice_payment_request = mint_quote.request
|
||||
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 64
|
||||
assert quote.fee_reserve == 0
|
||||
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/melt",
|
||||
json={
|
||||
"pr": invoice_payment_request,
|
||||
"proofs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("preimage") is None
|
||||
assert result["paid"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_internal_no_change_outputs(ledger: Ledger, wallet: Wallet):
|
||||
# Clients without NUT-08 will not send change outputs
|
||||
# internal invoice
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
|
||||
# create invoice to melt to
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
invoice_payment_request = mint_quote.request
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 64
|
||||
assert quote.fee_reserve == 0
|
||||
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/melt",
|
||||
json={
|
||||
"pr": invoice_payment_request,
|
||||
"proofs": inputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("preimage") is None
|
||||
assert result["paid"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
is_fake,
|
||||
reason="only works on regtest",
|
||||
)
|
||||
async def test_melt_external(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
|
||||
# create invoice to melt to
|
||||
# use 2 sat less because we need to pay the fee
|
||||
invoice_dict = get_real_invoice(62)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 62
|
||||
assert quote.fee_reserve == 2
|
||||
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/melt",
|
||||
json={
|
||||
"pr": invoice_payment_request,
|
||||
"proofs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("preimage") is not None
|
||||
assert result["paid"] is True
|
||||
assert result["change"]
|
||||
# we get back 2 sats because Lightning was free to pay on regtest
|
||||
assert result["change"][0]["amount"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_checkfees(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/checkfees",
|
||||
json={
|
||||
"pr": mint_quote.request,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
# internal invoice, so no fee
|
||||
assert result["fee"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(not is_regtest, reason="only works on regtest")
|
||||
async def test_checkfees_external(ledger: Ledger, wallet: Wallet):
|
||||
# external invoice
|
||||
invoice_dict = get_real_invoice(62)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/checkfees",
|
||||
json={"pr": invoice_payment_request},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
# external invoice, so fee
|
||||
assert result["fee"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_check_state(ledger: Ledger):
|
||||
proofs = [
|
||||
Proof(id="1234", amount=0, secret="asdasdasd", C="asdasdasd"),
|
||||
Proof(id="1234", amount=0, secret="asdasdasd1", C="asdasdasd1"),
|
||||
]
|
||||
payload = CheckSpendableRequest_deprecated(proofs=proofs)
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/check",
|
||||
json=payload.dict(),
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
states = CheckSpendableResponse_deprecated.parse_obj(response.json())
|
||||
assert states.spendable
|
||||
assert len(states.spendable) == 2
|
||||
assert states.pending
|
||||
assert len(states.pending) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_restore(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
secret_counter = await bump_secret_derivation(
|
||||
db=wallet.db, keyset_id=wallet.keyset_id, by=0, skip=True
|
||||
)
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(
|
||||
secret_counter - 1, secret_counter - 1
|
||||
)
|
||||
outputs, rs = wallet._construct_outputs([64], secrets, rs)
|
||||
|
||||
payload = PostRestoreRequest(outputs=outputs)
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/restore",
|
||||
json=payload.dict(),
|
||||
)
|
||||
data = response.json()
|
||||
assert "promises" in data
|
||||
assert "outputs" in data
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
response = PostRestoreResponse.parse_obj(response.json())
|
||||
assert response
|
||||
assert response.promises
|
||||
assert len(response.promises) == 1
|
||||
assert len(response.outputs) == 1
|
||||
assert response.outputs == outputs
|
||||
291
tests/mint/test_mint_db.py
Normal file
291
tests/mint/test_mint_db.py
Normal file
@@ -0,0 +1,291 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import WebSocket
|
||||
|
||||
from cashu.core.base import MeltQuoteState, MintQuoteState
|
||||
from cashu.core.json_rpc.base import (
|
||||
JSONRPCMethods,
|
||||
JSONRPCNotficationParams,
|
||||
JSONRPCNotification,
|
||||
JSONRPCSubscriptionKinds,
|
||||
)
|
||||
from cashu.core.models import PostMeltQuoteRequest
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
assert_err,
|
||||
is_github_actions,
|
||||
pay_if_regtest,
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(ledger: Ledger):
|
||||
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_github_actions, reason="GITHUB_ACTIONS")
|
||||
async def test_mint_proofs_pending(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
proofs = wallet.proofs.copy()
|
||||
|
||||
proofs_states_before_split = await wallet.check_proof_state(proofs)
|
||||
assert all([s.unspent for s in proofs_states_before_split.states])
|
||||
|
||||
await ledger.db_write._verify_spent_proofs_and_set_pending(proofs)
|
||||
|
||||
proof_states = await wallet.check_proof_state(proofs)
|
||||
assert all([s.pending for s in proof_states.states])
|
||||
await assert_err(wallet.split(wallet.proofs, 20), "proofs are pending.")
|
||||
|
||||
await ledger.db_write._unset_proofs_pending(proofs)
|
||||
|
||||
await wallet.split(proofs, 20)
|
||||
|
||||
proofs_states_after_split = await wallet.check_proof_state(proofs)
|
||||
assert all([s.spent for s in proofs_states_after_split.states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_quote(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.amount == 128
|
||||
assert quote.unit == "sat"
|
||||
assert not quote.paid
|
||||
# assert quote.paid_time is None
|
||||
assert quote.created_time
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_quote_state_transitions(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.unpaid
|
||||
|
||||
# set pending again
|
||||
async def set_state(quote, state):
|
||||
quote.state = state
|
||||
|
||||
# set pending
|
||||
await assert_err(
|
||||
set_state(quote, MintQuoteState.pending),
|
||||
"Cannot change state of an unpaid mint quote",
|
||||
)
|
||||
|
||||
# set unpaid
|
||||
await assert_err(
|
||||
set_state(quote, MintQuoteState.unpaid),
|
||||
"Cannot change state of an unpaid mint quote",
|
||||
)
|
||||
|
||||
# set paid
|
||||
quote.state = MintQuoteState.paid
|
||||
|
||||
# set unpaid
|
||||
await assert_err(
|
||||
set_state(quote, MintQuoteState.unpaid),
|
||||
"Cannot change state of a paid mint quote to unpaid.",
|
||||
)
|
||||
|
||||
# set pending
|
||||
quote.state = MintQuoteState.pending
|
||||
|
||||
# set paid again
|
||||
quote.state = MintQuoteState.paid
|
||||
|
||||
# set pending again
|
||||
quote.state = MintQuoteState.pending
|
||||
|
||||
# set issued
|
||||
quote.state = MintQuoteState.issued
|
||||
|
||||
# set pending again
|
||||
await assert_err(
|
||||
set_state(quote, MintQuoteState.pending),
|
||||
"Cannot change state of an issued mint quote.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_mint_quote_by_request(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
quote = await ledger.crud.get_mint_quote(request=mint_quote.request, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.amount == 128
|
||||
assert quote.unit == "sat"
|
||||
assert not quote.paid
|
||||
# assert quote.paid_time is None
|
||||
assert quote.created_time
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_quote(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.quote == melt_quote.quote
|
||||
assert quote.amount == 128
|
||||
assert quote.unit == "sat"
|
||||
assert not quote.paid
|
||||
# assert quote.paid_time is None
|
||||
assert quote.created_time
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_quote_set_pending(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
assert melt_quote is not None
|
||||
assert melt_quote.state == MeltQuoteState.unpaid.value
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.quote == melt_quote.quote
|
||||
assert quote.unpaid
|
||||
previous_state = quote.state
|
||||
await ledger.db_write._set_melt_quote_pending(quote)
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.pending
|
||||
|
||||
# set unpending
|
||||
await ledger.db_write._unset_melt_quote_pending(quote, previous_state)
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.state == previous_state
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_quote_state_transitions(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.quote == melt_quote.quote
|
||||
assert quote.unpaid
|
||||
|
||||
# set pending
|
||||
quote.state = MeltQuoteState.pending
|
||||
|
||||
# set unpaid
|
||||
quote.state = MeltQuoteState.unpaid
|
||||
|
||||
# set paid
|
||||
quote.state = MeltQuoteState.paid
|
||||
|
||||
# set pending again
|
||||
async def set_state(quote, state):
|
||||
quote.state = state
|
||||
|
||||
await assert_err(
|
||||
set_state(quote, MeltQuoteState.pending),
|
||||
"Cannot change state of a paid melt quote.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_quote_set_pending(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
mint_quote = await ledger.crud.get_mint_quote(
|
||||
quote_id=mint_quote.quote, db=ledger.db
|
||||
)
|
||||
assert mint_quote is not None
|
||||
assert mint_quote.unpaid
|
||||
|
||||
# pay_if_regtest pays on regtest, get_mint_quote pays on FakeWallet
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
_ = await ledger.get_mint_quote(mint_quote.quote)
|
||||
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.paid
|
||||
|
||||
previous_state = MintQuoteState.paid
|
||||
await ledger.db_write._set_mint_quote_pending(quote.quote)
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.pending
|
||||
|
||||
# try to mint while pending
|
||||
await assert_err(
|
||||
wallet.mint(128, quote_id=mint_quote.quote), "Mint quote already pending."
|
||||
)
|
||||
|
||||
# set unpending
|
||||
await ledger.db_write._unset_mint_quote_pending(quote.quote, previous_state)
|
||||
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.state == previous_state
|
||||
assert quote.paid
|
||||
|
||||
# # set paid and mint again
|
||||
# quote.state = MintQuoteState.paid
|
||||
# await ledger.crud.update_mint_quote(quote=quote, db=ledger.db)
|
||||
|
||||
await wallet.mint(quote.amount, quote_id=quote.quote)
|
||||
|
||||
# check if quote is issued
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.issued
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_events_add_client(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
assert melt_quote is not None
|
||||
assert melt_quote.state == MeltQuoteState.unpaid.value
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.quote == melt_quote.quote
|
||||
assert quote.unpaid
|
||||
|
||||
# add event client
|
||||
websocket_mock = AsyncMock(spec=WebSocket)
|
||||
client = ledger.events.add_client(websocket_mock, ledger.db, ledger.crud)
|
||||
asyncio.create_task(client.start())
|
||||
await asyncio.sleep(0.1)
|
||||
websocket_mock.accept.assert_called_once()
|
||||
|
||||
# add subscription
|
||||
client.add_subscription(
|
||||
JSONRPCSubscriptionKinds.BOLT11_MELT_QUOTE, [quote.quote], "subId"
|
||||
)
|
||||
quote_pending = await ledger.db_write._set_melt_quote_pending(quote)
|
||||
await asyncio.sleep(0.1)
|
||||
notification = JSONRPCNotification(
|
||||
method=JSONRPCMethods.SUBSCRIBE.value,
|
||||
params=JSONRPCNotficationParams(
|
||||
subId="subId", payload=quote_pending.dict()
|
||||
).dict(),
|
||||
)
|
||||
websocket_mock.send_text.assert_called_with(notification.json())
|
||||
|
||||
# remove subscription
|
||||
client.remove_subscription("subId")
|
||||
330
tests/mint/test_mint_db_operations.py
Normal file
330
tests/mint/test_mint_db_operations.py
Normal file
@@ -0,0 +1,330 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core import db
|
||||
from cashu.core.db import Connection
|
||||
from cashu.core.migrations import backup_database
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import is_github_actions, is_postgres, is_regtest, pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
async def assert_err_multiple(f, msgs: List[str]):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
for msg in msgs:
|
||||
if msg in str(exc.args[0]):
|
||||
return
|
||||
raise Exception(f"Expected error: {msgs}, got: {exc.args[0]}")
|
||||
raise Exception(f"Expected error: {msgs}, got no error")
|
||||
|
||||
|
||||
@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
|
||||
await wallet.db.engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_tables(ledger: Ledger):
|
||||
async with ledger.db.connect() as conn:
|
||||
if ledger.db.type == db.SQLITE:
|
||||
tables_res = await conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table';"
|
||||
)
|
||||
elif ledger.db.type in {db.POSTGRES, db.COCKROACH}:
|
||||
tables_res = await conn.execute(
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_schema ="
|
||||
" 'public';"
|
||||
)
|
||||
tables_all: List[Tuple[str]] = tables_res.all() # type: ignore
|
||||
tables = [t[0] for t in tables_all]
|
||||
tables_expected = [
|
||||
"dbversions",
|
||||
"keysets",
|
||||
"proofs_used",
|
||||
"proofs_pending",
|
||||
"melt_quotes",
|
||||
"mint_quotes",
|
||||
"mint_pubkeys",
|
||||
"promises",
|
||||
]
|
||||
for table in tables_expected:
|
||||
assert table in tables
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
is_github_actions and is_postgres,
|
||||
reason=(
|
||||
"Fails on GitHub Actions because pg_dump is not the same version as postgres"
|
||||
),
|
||||
)
|
||||
async def test_backup_db_migration(ledger: Ledger):
|
||||
settings.db_backup_path = "./test_data/backups/"
|
||||
filepath = await backup_database(ledger.db, 999)
|
||||
assert os.path.exists(filepath)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timestamp_now(ledger: Ledger):
|
||||
ts = ledger.db.timestamp_now_str()
|
||||
if ledger.db.type == db.SQLITE:
|
||||
assert isinstance(ts, str)
|
||||
assert int(ts) <= time.time()
|
||||
elif ledger.db.type in {db.POSTGRES, db.COCKROACH}:
|
||||
assert isinstance(ts, str)
|
||||
datetime.datetime.strptime(ts, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_connect(ledger: Ledger):
|
||||
async with ledger.db.connect() as conn:
|
||||
assert isinstance(conn, Connection)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_get_connection(ledger: Ledger):
|
||||
async with ledger.db.get_connection() as conn:
|
||||
assert isinstance(conn, Connection)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_get_connection_locked(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
|
||||
async def get_connection():
|
||||
"""This code makes sure that only the error of the second connection is raised (which we check in the assert_err)"""
|
||||
try:
|
||||
async with ledger.db.get_connection(lock_table="mint_quotes"):
|
||||
try:
|
||||
async with ledger.db.get_connection(
|
||||
lock_table="mint_quotes", lock_timeout=0.1
|
||||
) as conn2:
|
||||
# write something with conn1, we never reach this point if the lock works
|
||||
await conn2.execute(
|
||||
f"INSERT INTO mint_quotes (quote, amount) VALUES ('{mint_quote.quote}', 100);"
|
||||
)
|
||||
except Exception as exc:
|
||||
# this is expected to raise
|
||||
raise Exception(f"conn2: {exc}")
|
||||
|
||||
except Exception as exc:
|
||||
if str(exc).startswith("conn2"):
|
||||
raise exc
|
||||
else:
|
||||
raise Exception("not expected to happen")
|
||||
|
||||
await assert_err(get_connection(), "failed to acquire database lock")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
not not is_postgres,
|
||||
reason="SQLite does not support row locking",
|
||||
)
|
||||
async def test_db_get_connection_lock_row(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
|
||||
async def get_connection():
|
||||
"""This code makes sure that only the error of the second connection is raised (which we check in the assert_err)"""
|
||||
try:
|
||||
async with ledger.db.get_connection(
|
||||
lock_table="mint_quotes",
|
||||
lock_select_statement=f"quote='{mint_quote.quote}'",
|
||||
lock_timeout=0.1,
|
||||
) as conn1:
|
||||
await conn1.execute(
|
||||
f"UPDATE mint_quotes SET amount=100 WHERE quote='{mint_quote.quote}';"
|
||||
)
|
||||
try:
|
||||
async with ledger.db.get_connection(
|
||||
lock_table="mint_quotes",
|
||||
lock_select_statement=f"quote='{mint_quote.quote}'",
|
||||
lock_timeout=0.1,
|
||||
) as conn2:
|
||||
# write something with conn1, we never reach this point if the lock works
|
||||
await conn2.execute(
|
||||
f"UPDATE mint_quotes SET amount=101 WHERE quote='{mint_quote.quote}';"
|
||||
)
|
||||
except Exception as exc:
|
||||
# this is expected to raise
|
||||
raise Exception(f"conn2: {exc}")
|
||||
except Exception as exc:
|
||||
if "conn2" in str(exc):
|
||||
raise exc
|
||||
else:
|
||||
raise Exception(f"not expected to happen: {exc}")
|
||||
|
||||
await assert_err(get_connection(), "failed to acquire database lock")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
is_github_actions and is_regtest and not is_postgres,
|
||||
reason=("Fails on GitHub Actions for regtest + SQLite"),
|
||||
)
|
||||
async def test_db_verify_spent_proofs_and_set_pending_race_condition(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
# fill wallet
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
|
||||
await assert_err_multiple(
|
||||
asyncio.gather(
|
||||
ledger.db_write._verify_spent_proofs_and_set_pending(wallet.proofs),
|
||||
ledger.db_write._verify_spent_proofs_and_set_pending(wallet.proofs),
|
||||
),
|
||||
[
|
||||
"failed to acquire database lock",
|
||||
"proofs are pending",
|
||||
], # depending on how fast the database is, it can be either
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
is_github_actions and is_regtest and not is_postgres,
|
||||
reason=("Fails on GitHub Actions for regtest + SQLite"),
|
||||
)
|
||||
async def test_db_verify_spent_proofs_and_set_pending_delayed_no_race_condition(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
# fill wallet
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
|
||||
async def delayed_verify_spent_proofs_and_set_pending():
|
||||
await asyncio.sleep(0.1)
|
||||
await ledger.db_write._verify_spent_proofs_and_set_pending(wallet.proofs)
|
||||
|
||||
await assert_err(
|
||||
asyncio.gather(
|
||||
ledger.db_write._verify_spent_proofs_and_set_pending(wallet.proofs),
|
||||
delayed_verify_spent_proofs_and_set_pending(),
|
||||
),
|
||||
"proofs are pending",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
is_github_actions and is_regtest and not is_postgres,
|
||||
reason=("Fails on GitHub Actions for regtest + SQLite"),
|
||||
)
|
||||
async def test_db_verify_spent_proofs_and_set_pending_no_race_condition_different_proofs(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
# fill wallet
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote, split=[32, 32])
|
||||
assert wallet.balance == 64
|
||||
assert len(wallet.proofs) == 2
|
||||
|
||||
asyncio.gather(
|
||||
ledger.db_write._verify_spent_proofs_and_set_pending(wallet.proofs[:1]),
|
||||
ledger.db_write._verify_spent_proofs_and_set_pending(wallet.proofs[1:]),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
not is_postgres,
|
||||
reason="SQLite does not support row locking",
|
||||
)
|
||||
async def test_db_get_connection_lock_different_row(wallet: Wallet, ledger: Ledger):
|
||||
if ledger.db.type == db.SQLITE:
|
||||
pytest.skip("SQLite does not support row locking")
|
||||
# this should work since we lock two different rows
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
mint_quote_2 = await wallet.request_mint(64)
|
||||
|
||||
async def get_connection2():
|
||||
"""This code makes sure that only the error of the second connection is raised (which we check in the assert_err)"""
|
||||
try:
|
||||
async with ledger.db.get_connection(
|
||||
lock_table="mint_quotes",
|
||||
lock_select_statement=f"quote='{mint_quote.quote}'",
|
||||
lock_timeout=0.1,
|
||||
):
|
||||
try:
|
||||
async with ledger.db.get_connection(
|
||||
lock_table="mint_quotes",
|
||||
lock_select_statement=f"quote='{mint_quote_2.quote}'",
|
||||
lock_timeout=0.1,
|
||||
) as conn2:
|
||||
# write something with conn1, this time we should reach this block with postgres
|
||||
quote = await ledger.crud.get_mint_quote(
|
||||
quote_id=mint_quote_2.quote, db=ledger.db, conn=conn2
|
||||
)
|
||||
assert quote is not None
|
||||
quote.amount = 100
|
||||
await ledger.crud.update_mint_quote(
|
||||
quote=quote, db=ledger.db, conn=conn2
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
# this is expected to raise
|
||||
raise Exception(f"conn2: {exc}")
|
||||
|
||||
except Exception as exc:
|
||||
if "conn2" in str(exc):
|
||||
raise exc
|
||||
else:
|
||||
raise Exception(f"not expected to happen: {exc}")
|
||||
|
||||
await get_connection2()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
is_github_actions and is_regtest and not is_postgres,
|
||||
reason=("Fails on GitHub Actions for regtest + SQLite"),
|
||||
)
|
||||
async def test_db_lock_table(wallet: Wallet, ledger: Ledger):
|
||||
# fill wallet
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet.balance == 64
|
||||
|
||||
async with ledger.db.connect(lock_table="proofs_pending", lock_timeout=0.1) as conn:
|
||||
assert isinstance(conn, Connection)
|
||||
await assert_err(
|
||||
ledger.db_write._verify_spent_proofs_and_set_pending(wallet.proofs),
|
||||
"failed to acquire database lock",
|
||||
)
|
||||
276
tests/mint/test_mint_fees.py
Normal file
276
tests/mint/test_mint_fees.py
Normal file
@@ -0,0 +1,276 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.models import PostMeltQuoteRequest
|
||||
from cashu.core.split import amount_split
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(ledger: Ledger):
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
def set_ledger_keyset_fees(
|
||||
fee_ppk: int, ledger: Ledger, wallet: Optional[Wallet] = None
|
||||
):
|
||||
for keyset in ledger.keysets.values():
|
||||
keyset.input_fee_ppk = fee_ppk
|
||||
|
||||
if wallet:
|
||||
for wallet_keyset in wallet.keysets.values():
|
||||
wallet_keyset.input_fee_ppk = fee_ppk
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_fees_for_proofs(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, split=[1] * 64, quote_id=mint_quote.quote)
|
||||
|
||||
# two proofs
|
||||
|
||||
set_ledger_keyset_fees(100, ledger)
|
||||
proofs = [wallet1.proofs[0], wallet1.proofs[1]]
|
||||
fees = ledger.get_fees_for_proofs(proofs)
|
||||
assert fees == 1
|
||||
|
||||
set_ledger_keyset_fees(1234, ledger)
|
||||
fees = ledger.get_fees_for_proofs(proofs)
|
||||
assert fees == 3
|
||||
|
||||
set_ledger_keyset_fees(0, ledger)
|
||||
fees = ledger.get_fees_for_proofs(proofs)
|
||||
assert fees == 0
|
||||
|
||||
set_ledger_keyset_fees(1, ledger)
|
||||
fees = ledger.get_fees_for_proofs(proofs)
|
||||
assert fees == 1
|
||||
|
||||
# ten proofs
|
||||
|
||||
ten_proofs = wallet1.proofs[:10]
|
||||
set_ledger_keyset_fees(100, ledger)
|
||||
fees = ledger.get_fees_for_proofs(ten_proofs)
|
||||
assert fees == 1
|
||||
|
||||
set_ledger_keyset_fees(101, ledger)
|
||||
fees = ledger.get_fees_for_proofs(ten_proofs)
|
||||
assert fees == 2
|
||||
|
||||
# three proofs
|
||||
|
||||
three_proofs = wallet1.proofs[:3]
|
||||
set_ledger_keyset_fees(333, ledger)
|
||||
fees = ledger.get_fees_for_proofs(three_proofs)
|
||||
assert fees == 1
|
||||
|
||||
set_ledger_keyset_fees(334, ledger)
|
||||
fees = ledger.get_fees_for_proofs(three_proofs)
|
||||
assert fees == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_wallet_selection_with_fee(wallet1: Wallet, ledger: Ledger):
|
||||
# set fees to 100 ppk
|
||||
set_ledger_keyset_fees(100, ledger, wallet1)
|
||||
|
||||
# THIS TEST IS A FAKE, WE SET THE WALLET FEES MANUALLY IN set_ledger_keyset_fees
|
||||
# check if all wallet keysets have the correct fees
|
||||
for keyset in wallet1.keysets.values():
|
||||
assert keyset.input_fee_ppk == 100
|
||||
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10)
|
||||
assert sum_proofs(send_proofs) == 10
|
||||
|
||||
send_proofs_with_fees, _ = await wallet1.select_to_send(
|
||||
wallet1.proofs, 10, include_fees=True
|
||||
)
|
||||
assert sum_proofs(send_proofs_with_fees) == 11
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_wallet_swap_to_send_with_fee(wallet1: Wallet, ledger: Ledger):
|
||||
# set fees to 100 ppk
|
||||
set_ledger_keyset_fees(100, ledger, wallet1)
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(
|
||||
64, quote_id=mint_quote.quote, split=[32, 32]
|
||||
) # make sure we need to swap
|
||||
|
||||
# quirk: this should call a `/v1/swap` with the mint but the mint will
|
||||
# throw an error since the fees are only changed in the `ledger` instance, not in the uvicorn API server
|
||||
# this *should* succeed if the fees were set in the API server
|
||||
# at least, we can verify that the wallet is correctly computing the fees
|
||||
# by asserting for this super specific error message from the (API server) mint
|
||||
await assert_err(
|
||||
wallet1.select_to_send(wallet1.proofs, 10),
|
||||
"Mint Error: inputs (32) - fees (0) vs outputs (31) are not balanced.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_with_fees(wallet1: Wallet, ledger: Ledger):
|
||||
# set fees to 100 ppk
|
||||
set_ledger_keyset_fees(100, ledger)
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10)
|
||||
fees = ledger.get_fees_for_proofs(send_proofs)
|
||||
assert fees == 1
|
||||
outputs = await wallet1.construct_outputs(amount_split(9))
|
||||
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
assert [p.amount for p in promises] == [p.amount for p in outputs]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_with_high_fees(wallet1: Wallet, ledger: Ledger):
|
||||
# set fees to 100 ppk
|
||||
set_ledger_keyset_fees(1234, ledger)
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10)
|
||||
fees = ledger.get_fees_for_proofs(send_proofs)
|
||||
assert fees == 3
|
||||
outputs = await wallet1.construct_outputs(amount_split(7))
|
||||
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
assert [p.amount for p in promises] == [p.amount for p in outputs]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_not_enough_fees(wallet1: Wallet, ledger: Ledger):
|
||||
# set fees to 100 ppk
|
||||
set_ledger_keyset_fees(100, ledger)
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10)
|
||||
fees = ledger.get_fees_for_proofs(send_proofs)
|
||||
assert fees == 1
|
||||
# with 10 sat input, we request 10 sat outputs but fees are 1 sat so the swap will fail
|
||||
outputs = await wallet1.construct_outputs(amount_split(10))
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs), "are not balanced"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_melt_internal(wallet1: Wallet, ledger: Ledger):
|
||||
# set fees to 100 ppk
|
||||
set_ledger_keyset_fees(100, ledger, wallet1)
|
||||
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(128, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 128
|
||||
|
||||
# create a mint quote so that we can melt to it internally
|
||||
invoice_to_pay = await wallet1.request_mint(64)
|
||||
invoice_payment_request = invoice_to_pay.request
|
||||
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
|
||||
)
|
||||
assert not melt_quote.paid
|
||||
assert melt_quote.amount == 64
|
||||
assert melt_quote.fee_reserve == 0
|
||||
|
||||
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
|
||||
|
||||
# let's first try to melt without enough funds
|
||||
send_proofs, fees = await wallet1.select_to_send(wallet1.proofs, 64)
|
||||
# this should fail because we need 64 + 1 sat fees
|
||||
assert sum_proofs(send_proofs) == 64
|
||||
await assert_err(
|
||||
ledger.melt(proofs=send_proofs, quote=melt_quote.quote),
|
||||
"not enough inputs provided for melt",
|
||||
)
|
||||
|
||||
# the wallet respects the fees for coin selection
|
||||
send_proofs, fees = await wallet1.select_to_send(
|
||||
wallet1.proofs, 64, include_fees=True
|
||||
)
|
||||
# includes 1 sat fees
|
||||
assert sum_proofs(send_proofs) == 65
|
||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||
|
||||
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_post_payment.paid, "melt quote should be paid"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_melt_external_with_fees(wallet1: Wallet, ledger: Ledger):
|
||||
# set fees to 100 ppk
|
||||
set_ledger_keyset_fees(100, ledger, wallet1)
|
||||
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(128, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 128
|
||||
|
||||
invoice_dict = get_real_invoice(64)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
mint_quote = await wallet1.melt_quote(invoice_payment_request)
|
||||
total_amount = mint_quote.amount + mint_quote.fee_reserve
|
||||
send_proofs, fee = await wallet1.select_to_send(
|
||||
wallet1.proofs, total_amount, include_fees=True
|
||||
)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
|
||||
)
|
||||
|
||||
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
|
||||
|
||||
assert not melt_quote.paid, "melt quote should not be paid"
|
||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||
|
||||
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_post_payment.paid, "melt quote should be paid"
|
||||
528
tests/mint/test_mint_init.py
Normal file
528
tests/mint/test_mint_init.py
Normal file
@@ -0,0 +1,528 @@
|
||||
import asyncio
|
||||
from typing import List, Tuple
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuote, MeltQuoteState, Method, Proof, Unit
|
||||
from cashu.core.crypto.aes import AESCipher
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.settings import settings
|
||||
from cashu.lightning.base import PaymentResult, PaymentStatus
|
||||
from cashu.mint.crud import LedgerCrudSqlite
|
||||
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,
|
||||
get_real_invoice,
|
||||
is_fake,
|
||||
is_regtest,
|
||||
pay_if_regtest,
|
||||
settle_invoice,
|
||||
)
|
||||
|
||||
SEED = "TEST_PRIVATE_KEY"
|
||||
DERIVATION_PATH = "m/0'/0'/0'"
|
||||
DECRYPTON_KEY = "testdecryptionkey"
|
||||
ENCRYPTED_SEED = "U2FsdGVkX1_7UU_-nVBMBWDy_9yDu4KeYb7MH8cJTYQGD4RWl82PALH8j-HKzTrI"
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
assert exc.args[0] == msg, Exception(
|
||||
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
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
|
||||
async def test_init_keysets(ledger: Ledger):
|
||||
ledger.keysets = {}
|
||||
await ledger.init_keysets()
|
||||
assert len(ledger.keysets) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_encrypt():
|
||||
aes = AESCipher(DECRYPTON_KEY)
|
||||
encrypted = aes.encrypt(SEED.encode())
|
||||
assert aes.decrypt(encrypted) == SEED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_decrypt():
|
||||
aes = AESCipher(DECRYPTON_KEY)
|
||||
assert aes.decrypt(ENCRYPTED_SEED) == SEED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decrypt_seed():
|
||||
ledger = Ledger(
|
||||
db=Database("mint", settings.mint_database),
|
||||
seed=SEED,
|
||||
seed_decryption_key=None,
|
||||
derivation_path=DERIVATION_PATH,
|
||||
backends={},
|
||||
crud=LedgerCrudSqlite(),
|
||||
)
|
||||
await ledger.init_keysets()
|
||||
assert ledger.keyset.seed == SEED
|
||||
private_key_1 = (
|
||||
ledger.keysets[list(ledger.keysets.keys())[0]].private_keys[1].serialize()
|
||||
)
|
||||
assert (
|
||||
private_key_1
|
||||
== "8300050453f08e6ead1296bb864e905bd46761beed22b81110fae0751d84604d"
|
||||
)
|
||||
pubkeys = ledger.keysets[list(ledger.keysets.keys())[0]].public_keys
|
||||
assert pubkeys
|
||||
assert (
|
||||
pubkeys[1].serialize().hex()
|
||||
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
|
||||
ledger_encrypted = Ledger(
|
||||
db=Database("mint", settings.mint_database),
|
||||
seed=ENCRYPTED_SEED,
|
||||
seed_decryption_key=DECRYPTON_KEY,
|
||||
derivation_path=DERIVATION_PATH,
|
||||
backends={},
|
||||
crud=LedgerCrudSqlite(),
|
||||
)
|
||||
await ledger_encrypted.init_keysets()
|
||||
assert ledger_encrypted.keyset.seed == SEED
|
||||
private_key_1 = (
|
||||
ledger_encrypted.keysets[list(ledger_encrypted.keysets.keys())[0]]
|
||||
.private_keys[1]
|
||||
.serialize()
|
||||
)
|
||||
assert (
|
||||
private_key_1
|
||||
== "8300050453f08e6ead1296bb864e905bd46761beed22b81110fae0751d84604d"
|
||||
)
|
||||
pubkeys_encrypted = ledger_encrypted.keysets[
|
||||
list(ledger_encrypted.keysets.keys())[0]
|
||||
].public_keys
|
||||
assert pubkeys_encrypted
|
||||
assert (
|
||||
pubkeys_encrypted[1].serialize().hex()
|
||||
== "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",
|
||||
state=MeltQuoteState.pending,
|
||||
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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
# 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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
# 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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.PENDING.name
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
# 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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only for fake wallet")
|
||||
async def test_startup_fakewallet_pending_quote_unknown(ledger: Ledger):
|
||||
# unknown state simulates a failure th check the lightning backend
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.UNKNOWN.name
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
# 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
|
||||
assert melt_quotes[0].state == MeltQuoteState.pending
|
||||
|
||||
# expect that proofs are still pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].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
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
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.melt_quote(invoice_payment_request)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
_, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount)
|
||||
asyncio.create_task(
|
||||
wallet.melt(
|
||||
proofs=send_proofs,
|
||||
invoice=invoice_payment_request,
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
# 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.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.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
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
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.melt_quote(invoice_payment_request)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
_, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount)
|
||||
asyncio.create_task(
|
||||
wallet.melt(
|
||||
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.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
settle_invoice(preimage=preimage)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
# 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.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.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
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
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.melt_quote(invoice_payment_request)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
_, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount)
|
||||
asyncio.create_task(
|
||||
wallet.melt(
|
||||
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.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
cancel_invoice(preimage_hash=preimage_hash)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
# 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.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.unspent for s in states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_startup_regtest_pending_quote_unknown(wallet: Wallet, ledger: Ledger):
|
||||
"""Simulate an unknown payment by executing a pending payment, then
|
||||
manipulating the melt_quote in the mint's db so that its checking_id
|
||||
points to an unknown payment."""
|
||||
|
||||
# fill wallet
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
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.melt_quote(invoice_payment_request)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
_, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount)
|
||||
asyncio.create_task(
|
||||
wallet.melt(
|
||||
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.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
# before we cancel the payment, we manipulate the melt_quote's checking_id so
|
||||
# that the mint fails to look up the payment and treats the payment as failed during startup
|
||||
melt_quote = await ledger.crud.get_melt_quote_by_request(
|
||||
db=ledger.db, request=invoice_payment_request
|
||||
)
|
||||
assert melt_quote
|
||||
assert melt_quote.pending
|
||||
|
||||
# manipulate the checking_id 32 bytes hexadecmial
|
||||
melt_quote.checking_id = "a" * 64
|
||||
await ledger.crud.update_melt_quote(quote=melt_quote, db=ledger.db)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
# 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
|
||||
assert melt_quotes[0].state == MeltQuoteState.pending
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
# clean up
|
||||
cancel_invoice(preimage_hash=preimage_hash)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_check_nonexisting_melt_quote(wallet: Wallet, ledger: Ledger):
|
||||
invoice_obj = get_real_invoice(16)
|
||||
invoice_payment_request = str(invoice_obj["payment_request"])
|
||||
checking_id = invoice_obj["r_hash"]
|
||||
quote = MeltQuote(
|
||||
quote="nonexistingquote",
|
||||
method="bolt11",
|
||||
request=invoice_payment_request,
|
||||
checking_id=checking_id,
|
||||
unit="sat",
|
||||
amount=64,
|
||||
state=MeltQuoteState.pending,
|
||||
fee_reserve=0,
|
||||
created_time=0,
|
||||
)
|
||||
await ledger.crud.store_melt_quote(quote=quote, db=ledger.db)
|
||||
|
||||
# assert that there is one pending melt quote
|
||||
melt_quotes = await ledger.crud.get_melt_quote(
|
||||
db=ledger.db, checking_id=quote.checking_id
|
||||
)
|
||||
|
||||
assert melt_quotes
|
||||
assert melt_quotes.state == MeltQuoteState.pending
|
||||
|
||||
# run startup routine
|
||||
await ledger._check_pending_proofs_and_melt_quotes()
|
||||
|
||||
status: PaymentStatus = await ledger.backends[Method.bolt11][
|
||||
Unit.sat
|
||||
].get_payment_status(quote.checking_id)
|
||||
|
||||
assert status.unknown
|
||||
|
||||
# this should NOT remove the pending melt quote
|
||||
await ledger.get_melt_quote(quote.quote, rollback_unknown=False)
|
||||
|
||||
# assert melt quote unpaid
|
||||
melt_quotes = await ledger.crud.get_melt_quote(
|
||||
db=ledger.db, checking_id=quote.checking_id
|
||||
)
|
||||
assert melt_quotes
|
||||
assert melt_quotes.state == MeltQuoteState.pending
|
||||
|
||||
# this should remove the pending melt quote
|
||||
await ledger.get_melt_quote(quote.quote, rollback_unknown=True)
|
||||
|
||||
# assert melt quote unpaid
|
||||
melt_quotes = await ledger.crud.get_melt_quote(
|
||||
db=ledger.db, checking_id=quote.checking_id
|
||||
)
|
||||
assert melt_quotes
|
||||
assert melt_quotes.state == MeltQuoteState.unpaid
|
||||
107
tests/mint/test_mint_keysets.py
Normal file
107
tests/mint/test_mint_keysets.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import pytest
|
||||
|
||||
from cashu.core.base import MintKeyset, Unit
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from tests.mint.test_mint_init import (
|
||||
DECRYPTON_KEY,
|
||||
DERIVATION_PATH,
|
||||
ENCRYPTED_SEED,
|
||||
SEED,
|
||||
)
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keyset_0_15_0():
|
||||
keyset = MintKeyset(seed=SEED, derivation_path=DERIVATION_PATH, version="0.15.0")
|
||||
assert len(keyset.public_keys_hex) == settings.max_order
|
||||
assert keyset.seed == "TEST_PRIVATE_KEY"
|
||||
assert keyset.derivation_path == "m/0'/0'/0'"
|
||||
assert (
|
||||
keyset.public_keys_hex[1]
|
||||
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
assert keyset.id == "009a1f293253e41e"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keyset_0_14_0():
|
||||
keyset = MintKeyset(seed=SEED, derivation_path=DERIVATION_PATH, version="0.14.0")
|
||||
assert len(keyset.public_keys_hex) == settings.max_order
|
||||
assert keyset.seed == "TEST_PRIVATE_KEY"
|
||||
assert keyset.derivation_path == "m/0'/0'/0'"
|
||||
assert (
|
||||
keyset.public_keys_hex[1]
|
||||
== "036d6f3adf897e88e16ece3bffb2ce57a0b635fa76f2e46dbe7c636a937cd3c2f2"
|
||||
)
|
||||
assert keyset.id == "xnI+Y0j7cT1/"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keyset_0_11_0():
|
||||
keyset = MintKeyset(seed=SEED, derivation_path=DERIVATION_PATH, version="0.11.0")
|
||||
assert len(keyset.public_keys_hex) == settings.max_order
|
||||
assert keyset.seed == "TEST_PRIVATE_KEY"
|
||||
assert keyset.derivation_path == "m/0'/0'/0'"
|
||||
assert (
|
||||
keyset.public_keys_hex[1]
|
||||
== "026b714529f157d4c3de5a93e3a67618475711889b6434a497ae6ad8ace6682120"
|
||||
)
|
||||
assert keyset.id == "Zkdws9zWxNc4"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keyset_0_15_0_encrypted():
|
||||
settings.mint_seed_decryption_key = DECRYPTON_KEY
|
||||
keyset = MintKeyset(
|
||||
encrypted_seed=ENCRYPTED_SEED,
|
||||
derivation_path=DERIVATION_PATH,
|
||||
version="0.15.0",
|
||||
)
|
||||
assert len(keyset.public_keys_hex) == settings.max_order
|
||||
assert keyset.seed == "TEST_PRIVATE_KEY"
|
||||
assert keyset.derivation_path == "m/0'/0'/0'"
|
||||
assert (
|
||||
keyset.public_keys_hex[1]
|
||||
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
assert keyset.id == "009a1f293253e41e"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keyset_rotation(ledger: Ledger):
|
||||
keyset_sat = next(
|
||||
filter(lambda k: k.unit == Unit["sat"] and k.active, ledger.keysets.values())
|
||||
)
|
||||
new_keyset_sat = await ledger.rotate_next_keyset(
|
||||
unit=Unit["sat"], max_order=20, input_fee_ppk=1
|
||||
)
|
||||
|
||||
keyset_sat_derivation = keyset_sat.derivation_path.split("/")
|
||||
new_keyset_sat_derivation = keyset_sat.derivation_path.split("/")
|
||||
|
||||
assert (
|
||||
keyset_sat_derivation[:-1] == new_keyset_sat_derivation[:-1]
|
||||
), "keyset derivation does not match up to the counter branch"
|
||||
assert (
|
||||
int(new_keyset_sat_derivation[-1].replace("'", ""))
|
||||
- int(keyset_sat_derivation[-1].replace("'", ""))
|
||||
== 0
|
||||
), "counters should differ by exactly 1"
|
||||
|
||||
assert new_keyset_sat.input_fee_ppk == 1
|
||||
assert len(new_keyset_sat.private_keys.values()) == 20
|
||||
|
||||
old_keyset = (await ledger.crud.get_keyset(db=ledger.db, id=keyset_sat.id))[0]
|
||||
assert not old_keyset.active, "old keyset is still active"
|
||||
250
tests/mint/test_mint_lightning_blink.py
Normal file
250
tests/mint/test_mint_lightning_blink.py
Normal file
@@ -0,0 +1,250 @@
|
||||
import pytest
|
||||
import respx
|
||||
from httpx import Response
|
||||
|
||||
from cashu.core.base import Amount, MeltQuote, MeltQuoteState, Unit
|
||||
from cashu.core.models import PostMeltQuoteRequest
|
||||
from cashu.core.settings import settings
|
||||
from cashu.lightning.blink import MINIMUM_FEE_MSAT, BlinkWallet # type: ignore
|
||||
|
||||
settings.mint_blink_key = "123"
|
||||
blink = BlinkWallet(unit=Unit.sat)
|
||||
payment_request = (
|
||||
"lnbc10u1pjap7phpp50s9lzr3477j0tvacpfy2ucrs4q0q6cvn232ex7nt2zqxxxj8gxrsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsss"
|
||||
"p575z0n39w2j7zgnpqtdlrgz9rycner4eptjm3lz363dzylnrm3h4s9qyyssqfz8jglcshnlcf0zkw4qu8fyr564lg59x5al724kms3h6gpuhx9xrfv27tgx3l3u3cyf6"
|
||||
"3r52u0xmac6max8mdupghfzh84t4hfsvrfsqwnuszf"
|
||||
) # 1000 sat
|
||||
|
||||
payment_request_10k = (
|
||||
"lnbc100u1pjaxuyzpp5wn37d3mx38haqs7nd5he4j7pq4r806e6s83jdksxrd77pnanm3zqdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrss"
|
||||
"sp5ayy0uuhwgy8hwphvy7ptzpg2dfn8vt3vlgsk53rsvj76jvafhujs9qyyssqc8aj03s5au3tgu6pj0rm0ws4a838s8ffe3y3qkj77esh7qmgsz7qlvdlzgj6dvx7tx7"
|
||||
"zn6k352z85rvdqvlszrevvzakp96a4pvyn2cpgaaks6"
|
||||
)
|
||||
|
||||
payment_request_4973 = (
|
||||
"lnbc49730n1pjaxuxnpp5zw0ry2w2heyuv7wk4r6z38vvgnaudfst0hl2p5xnv0mjkxtavg2qdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrr"
|
||||
"sssp5x8tv2ka0m95hgek25kauw540m0dx727stqqr07l8h37v5283sn5q9qyyssqeevcs6vxcdnerk5w5mwfmntsf8nze7nxrf97dywmga7v0742vhmxtjrulgu3kah4f"
|
||||
"2r6025j974jpjg4mkqhv2gdls5k7e5cvwdf4wcp3ytsvx"
|
||||
)
|
||||
payment_request_1 = (
|
||||
"lnbc10n1pjaxujrpp5sqehn6h5p8xpa0c0lvj5vy3a537gxfk5e7h2ate2alfw3y5cm6xqdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsss"
|
||||
"p5fkxsvyl0r32mvnhv9cws4rp986v0wjl2lp93zzl8jejnuwzvpynq9qyyssqqmsnatsz87qrgls98c97dfa6l2z3rzg2x6kxmrvpz886rwjylmd56y3qxzfulrq03kkh"
|
||||
"hwk6r32wes6pjt2zykhnsjn30c6uhuk0wugp3x74al"
|
||||
)
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_blink_status():
|
||||
mock_response = {
|
||||
"data": {
|
||||
"me": {
|
||||
"defaultAccount": {
|
||||
"wallets": [
|
||||
{"walletCurrency": "USD", "id": "123", "balance": 32142},
|
||||
{
|
||||
"walletCurrency": "BTC",
|
||||
"id": "456",
|
||||
"balance": 100000,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
status = await blink.status()
|
||||
assert status.balance == 100000
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_blink_create_invoice():
|
||||
mock_response = {
|
||||
"data": {
|
||||
"lnInvoiceCreateOnBehalfOfRecipient": {
|
||||
"invoice": {"paymentRequest": payment_request}
|
||||
}
|
||||
}
|
||||
}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
invoice = await blink.create_invoice(Amount(Unit.sat, 1000))
|
||||
assert invoice.checking_id == invoice.payment_request
|
||||
assert invoice.ok
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_blink_pay_invoice():
|
||||
mock_response = {
|
||||
"data": {
|
||||
"lnInvoicePaymentSend": {
|
||||
"status": "SUCCESS",
|
||||
"transaction": {
|
||||
"settlementFee": 10,
|
||||
"settlementVia": {
|
||||
"preImage": "123",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
quote = MeltQuote(
|
||||
request=payment_request,
|
||||
quote="asd",
|
||||
method="bolt11",
|
||||
checking_id=payment_request,
|
||||
unit="sat",
|
||||
amount=100,
|
||||
fee_reserve=12,
|
||||
state=MeltQuoteState.unpaid,
|
||||
)
|
||||
payment = await blink.pay_invoice(quote, 1000)
|
||||
assert payment.settled
|
||||
assert payment.fee
|
||||
assert payment.fee.amount == 10
|
||||
assert payment.error_message is None
|
||||
assert payment.checking_id == payment_request
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_blink_pay_invoice_failure():
|
||||
mock_response = {
|
||||
"data": {
|
||||
"lnInvoicePaymentSend": {
|
||||
"status": "FAILURE",
|
||||
"errors": [
|
||||
{"message": "This is the error", "codee": "ROUTE_FINDING_ERROR"},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
quote = MeltQuote(
|
||||
request=payment_request,
|
||||
quote="asd",
|
||||
method="bolt11",
|
||||
checking_id=payment_request,
|
||||
unit="sat",
|
||||
amount=100,
|
||||
fee_reserve=12,
|
||||
state=MeltQuoteState.unpaid,
|
||||
)
|
||||
payment = await blink.pay_invoice(quote, 1000)
|
||||
assert not payment.settled
|
||||
assert payment.fee is None
|
||||
assert payment.error_message
|
||||
assert "This is the error" in payment.error_message
|
||||
assert payment.checking_id == payment_request
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_blink_get_invoice_status():
|
||||
mock_response = {
|
||||
"data": {
|
||||
"lnInvoicePaymentStatus": {
|
||||
"status": "PAID",
|
||||
"errors": [],
|
||||
}
|
||||
}
|
||||
}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
status = await blink.get_invoice_status("123")
|
||||
assert status.settled
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_blink_get_payment_status():
|
||||
mock_response = {
|
||||
"data": {
|
||||
"me": {
|
||||
"defaultAccount": {
|
||||
"walletById": {
|
||||
"transactionsByPaymentHash": [
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"settlementFee": 10,
|
||||
"direction": "SEND",
|
||||
"settlementVia": {
|
||||
"preImage": "123",
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
status = await blink.get_payment_status(payment_request)
|
||||
assert status.settled
|
||||
assert status.fee
|
||||
assert status.fee.amount == 10
|
||||
assert status.preimage == "123"
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_blink_get_payment_quote():
|
||||
# response says 1 sat fees but invoice (1000 sat) * 0.5% is 5 sat so we expect MINIMUM_FEE_MSAT/1000 sat
|
||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 1}}}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
melt_quote_request = PostMeltQuoteRequest(
|
||||
unit=Unit.sat.name, request=payment_request
|
||||
)
|
||||
quote = await blink.get_payment_quote(melt_quote_request)
|
||||
assert quote.checking_id == payment_request
|
||||
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
||||
assert quote.fee == Amount(Unit.sat, MINIMUM_FEE_MSAT // 1000) # msat
|
||||
|
||||
# response says 10 sat fees but invoice (1000 sat) * 0.5% is 5 sat so we expect 10 sat
|
||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
melt_quote_request = PostMeltQuoteRequest(
|
||||
unit=Unit.sat.name, request=payment_request
|
||||
)
|
||||
quote = await blink.get_payment_quote(melt_quote_request)
|
||||
assert quote.checking_id == payment_request
|
||||
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
||||
assert quote.fee == Amount(Unit.sat, 10) # sat
|
||||
|
||||
# response says 10 sat fees but invoice (4973 sat) * 0.5% is 24.865 sat so we expect 25 sat
|
||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
melt_quote_request_4973 = PostMeltQuoteRequest(
|
||||
unit=Unit.sat.name, request=payment_request_4973
|
||||
)
|
||||
quote = await blink.get_payment_quote(melt_quote_request_4973)
|
||||
assert quote.checking_id == payment_request_4973
|
||||
assert quote.amount == Amount(Unit.sat, 4973) # sat
|
||||
assert quote.fee == Amount(Unit.sat, 25) # sat
|
||||
|
||||
# response says 0 sat fees but invoice (1 sat) * 0.5% is 0.005 sat so we expect MINIMUM_FEE_MSAT/1000 sat
|
||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 0}}}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
melt_quote_request_1 = PostMeltQuoteRequest(
|
||||
unit=Unit.sat.name, request=payment_request_1
|
||||
)
|
||||
quote = await blink.get_payment_quote(melt_quote_request_1)
|
||||
assert quote.checking_id == payment_request_1
|
||||
assert quote.amount == Amount(Unit.sat, 1) # sat
|
||||
assert quote.fee == Amount(Unit.sat, MINIMUM_FEE_MSAT // 1000) # msat
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_blink_get_payment_quote_backend_error():
|
||||
# response says error but invoice (1000 sat) * 0.5% is 5 sat so we expect 10 sat (MINIMUM_FEE_MSAT)
|
||||
mock_response = {"data": {"lnInvoiceFeeProbe": {"errors": [{"message": "error"}]}}}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
melt_quote_request = PostMeltQuoteRequest(
|
||||
unit=Unit.sat.name, request=payment_request
|
||||
)
|
||||
quote = await blink.get_payment_quote(melt_quote_request)
|
||||
assert quote.checking_id == payment_request
|
||||
assert quote.amount == Amount(Unit.sat, 1000) # sat
|
||||
assert quote.fee == Amount(Unit.sat, MINIMUM_FEE_MSAT // 1000) # msat
|
||||
371
tests/mint/test_mint_melt.py
Normal file
371
tests/mint/test_mint_melt.py
Normal file
@@ -0,0 +1,371 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuote, MeltQuoteState, Proof
|
||||
from cashu.core.errors import LightningPaymentFailedError
|
||||
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
|
||||
from cashu.core.settings import settings
|
||||
from cashu.lightning.base import PaymentResult
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
is_regtest,
|
||||
)
|
||||
|
||||
SEED = "TEST_PRIVATE_KEY"
|
||||
DERIVATION_PATH = "m/0'/0'/0'"
|
||||
DECRYPTON_KEY = "testdecryptionkey"
|
||||
ENCRYPTED_SEED = "U2FsdGVkX1_7UU_-nVBMBWDy_9yDu4KeYb7MH8cJTYQGD4RWl82PALH8j-HKzTrI"
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
assert exc.args[0] == msg, Exception(
|
||||
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
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
|
||||
|
||||
|
||||
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",
|
||||
state=MeltQuoteState.pending,
|
||||
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_fakewallet_pending_quote_get_melt_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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||
|
||||
# get_melt_quote should check the payment status and update the db
|
||||
quote2 = await ledger.get_melt_quote(quote_id=quote.quote)
|
||||
assert quote2.state == MeltQuoteState.paid
|
||||
|
||||
# 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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].spent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_fakewallet_pending_quote_get_melt_quote_pending(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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.PENDING.name
|
||||
|
||||
# get_melt_quote should check the payment status and update the db
|
||||
quote2 = await ledger.get_melt_quote(quote_id=quote.quote)
|
||||
assert quote2.state == MeltQuoteState.pending
|
||||
|
||||
# expect that pending tokens are still in db
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert melt_quotes
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_fakewallet_pending_quote_get_melt_quote_failed(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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||
|
||||
# get_melt_quote should check the payment status and update the db
|
||||
quote2 = await ledger.get_melt_quote(quote_id=quote.quote)
|
||||
assert quote2.state == MeltQuoteState.unpaid
|
||||
|
||||
# expect that pending tokens are still in db
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert not melt_quotes
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].unspent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_fakewallet_pending_quote_get_melt_quote_unknown(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.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.UNKNOWN.name
|
||||
|
||||
# get_melt_quote(..., rollback_unknown=True) should check the payment status and update the db
|
||||
quote2 = await ledger.get_melt_quote(quote_id=quote.quote, rollback_unknown=True)
|
||||
assert quote2.state == MeltQuoteState.unpaid
|
||||
|
||||
# expect that pending tokens are still in db
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert not melt_quotes
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].unspent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_settled(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(mint_quote.quote) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
# invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0"
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
# quote = await ledger.get_melt_quote(quote_id)
|
||||
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.SETTLED.name
|
||||
melt_response = await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
assert melt_response.state == MeltQuoteState.paid.value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_failed_failed(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(mint_quote.quote) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
# invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0"
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
# quote = await ledger.get_melt_quote(quote_id)
|
||||
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
|
||||
try:
|
||||
await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
raise AssertionError("Expected LightningPaymentFailedError")
|
||||
except LightningPaymentFailedError:
|
||||
pass
|
||||
|
||||
settings.fakewallet_payment_state = PaymentResult.UNKNOWN.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
|
||||
try:
|
||||
await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
raise AssertionError("Expected LightningPaymentFailedError")
|
||||
except LightningPaymentFailedError:
|
||||
pass
|
||||
|
||||
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.UNKNOWN.name
|
||||
try:
|
||||
await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
raise AssertionError("Expected LightningPaymentFailedError")
|
||||
except LightningPaymentFailedError:
|
||||
pass
|
||||
|
||||
settings.fakewallet_payment_state = PaymentResult.UNKNOWN.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.UNKNOWN.name
|
||||
try:
|
||||
await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
raise AssertionError("Expected LightningPaymentFailedError")
|
||||
except LightningPaymentFailedError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_failed_settled(
|
||||
ledger: Ledger, wallet: Wallet
|
||||
):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(mint_quote.quote) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
|
||||
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||
|
||||
melt_response = await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
assert melt_response.state == MeltQuoteState.pending.value
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in wallet.proofs])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_failed_pending(
|
||||
ledger: Ledger, wallet: Wallet
|
||||
):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(mint_quote.quote) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
|
||||
settings.fakewallet_payment_state = PaymentResult.PENDING.name
|
||||
|
||||
melt_response = await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
assert melt_response.state == MeltQuoteState.pending.value
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in wallet.proofs])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_exception_exception(
|
||||
ledger: Ledger, wallet: Wallet
|
||||
):
|
||||
"""Simulates the case where pay_invoice and get_payment_status raise an exception (due to network issues for example)."""
|
||||
settings.mint_disable_melt_on_error = True
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(mint_quote.quote) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
# invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0"
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
# quote = await ledger.get_melt_quote(quote_id)
|
||||
settings.fakewallet_payment_state_exception = True
|
||||
settings.fakewallet_pay_invoice_state_exception = True
|
||||
|
||||
# we expect a pending melt quote because something has gone wrong (for example has lost connection to backend)
|
||||
resp = await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
assert resp.state == MeltQuoteState.pending.value
|
||||
|
||||
# the mint should be locked now and not allow any other melts until it is restarted
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
await assert_err(
|
||||
ledger.melt(proofs=wallet.proofs, quote=quote_id),
|
||||
"Melt is disabled. Please contact the operator.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_mint_melt_different_units(ledger: Ledger, wallet: Wallet):
|
||||
"""Mint and melt different units."""
|
||||
# load the wallet
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
amount = 32
|
||||
|
||||
# mint quote in sat
|
||||
sat_mint_quote = await ledger.mint_quote(
|
||||
quote_request=PostMintQuoteRequest(amount=amount, unit="sat")
|
||||
)
|
||||
sat_invoice = sat_mint_quote.request
|
||||
assert sat_mint_quote.paid is False
|
||||
|
||||
# melt quote in usd
|
||||
usd_melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="usd", request=sat_invoice)
|
||||
)
|
||||
assert usd_melt_quote.paid is False
|
||||
|
||||
# pay melt quote with usd
|
||||
await ledger.melt(proofs=wallet.proofs, quote=usd_melt_quote.quote)
|
||||
|
||||
output_amounts = [32]
|
||||
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(len(output_amounts))
|
||||
outputs, rs = wallet._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# mint in sat
|
||||
mint_resp = await ledger.mint(outputs=outputs, quote_id=sat_mint_quote.quote)
|
||||
|
||||
assert len(mint_resp) == len(outputs)
|
||||
452
tests/mint/test_mint_operations.py
Normal file
452
tests/mint/test_mint_operations.py
Normal file
@@ -0,0 +1,452 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuoteState, MintQuoteState
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
|
||||
from cashu.core.nuts import nut20
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(ledger: Ledger):
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_melt_internal(wallet1: Wallet, ledger: Ledger):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(128, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 128
|
||||
|
||||
# create a mint quote so that we can melt to it internally
|
||||
mint_quote_to_pay = await wallet1.request_mint(64)
|
||||
invoice_payment_request = mint_quote_to_pay.request
|
||||
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
|
||||
)
|
||||
assert not melt_quote.paid
|
||||
assert melt_quote.state == MeltQuoteState.unpaid.value
|
||||
|
||||
assert melt_quote.amount == 64
|
||||
assert melt_quote.fee_reserve == 0
|
||||
|
||||
if not settings.debug_mint_only_deprecated:
|
||||
melt_quote_response_pre_payment = await wallet1.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_response_pre_payment
|
||||
assert (
|
||||
not melt_quote_response_pre_payment.state == MeltQuoteState.paid
|
||||
), "melt quote should not be paid"
|
||||
assert melt_quote_response_pre_payment.amount == 64
|
||||
|
||||
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
|
||||
assert melt_quote_pre_payment.unpaid
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 64)
|
||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||
|
||||
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_post_payment.paid, "melt quote should be paid"
|
||||
assert melt_quote_post_payment.paid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_melt_external(wallet1: Wallet, ledger: Ledger):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(128, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 128
|
||||
|
||||
invoice_dict = get_real_invoice(64)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
melt_quote = await wallet1.melt_quote(invoice_payment_request)
|
||||
assert not melt_quote.paid, "mint quote should not be paid"
|
||||
assert melt_quote.state == MeltQuoteState.unpaid
|
||||
|
||||
total_amount = melt_quote.amount + melt_quote.fee_reserve
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, total_amount)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
|
||||
)
|
||||
|
||||
if not settings.debug_mint_only_deprecated:
|
||||
melt_quote_response_pre_payment = await wallet1.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_response_pre_payment
|
||||
assert (
|
||||
melt_quote_response_pre_payment.state == MeltQuoteState.unpaid
|
||||
), "melt quote should not be paid"
|
||||
assert melt_quote_response_pre_payment.amount == melt_quote.amount
|
||||
|
||||
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
|
||||
assert melt_quote_pre_payment.unpaid
|
||||
|
||||
assert not melt_quote.paid, "melt quote should not be paid"
|
||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||
|
||||
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_post_payment.paid, "melt quote should be paid"
|
||||
assert melt_quote_post_payment.paid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||
wallet_mint_quote = await wallet1.request_mint(128)
|
||||
await ledger.get_mint_quote(wallet_mint_quote.quote)
|
||||
mint_quote = await ledger.get_mint_quote(wallet_mint_quote.quote)
|
||||
|
||||
assert mint_quote.paid, "mint quote should be paid"
|
||||
|
||||
if not settings.debug_mint_only_deprecated:
|
||||
mint_quote = await wallet1.get_mint_quote(mint_quote.quote)
|
||||
assert mint_quote.state == MintQuoteState.paid, "mint quote should be paid"
|
||||
|
||||
output_amounts = [128]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
assert wallet_mint_quote.privkey
|
||||
signature = nut20.sign_mint_quote(
|
||||
mint_quote.quote, outputs, wallet_mint_quote.privkey
|
||||
)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote.quote, signature=signature)
|
||||
|
||||
await assert_err(
|
||||
ledger.mint(outputs=outputs, quote_id=mint_quote.quote),
|
||||
"outputs have already been signed before.",
|
||||
)
|
||||
|
||||
mint_quote_after_payment = await ledger.get_mint_quote(mint_quote.quote)
|
||||
assert mint_quote_after_payment.issued, "mint quote should be issued"
|
||||
assert mint_quote_after_payment.issued
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_mint_external(wallet1: Wallet, ledger: Ledger):
|
||||
quote = await wallet1.request_mint(128)
|
||||
|
||||
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||
assert not mint_quote.paid, "mint quote already paid"
|
||||
assert mint_quote.unpaid
|
||||
|
||||
if not settings.debug_mint_only_deprecated:
|
||||
mint_quote = await wallet1.get_mint_quote(quote.quote)
|
||||
assert not mint_quote.paid, "mint quote should not be paid"
|
||||
|
||||
await assert_err(
|
||||
wallet1.mint(128, quote_id=quote.quote),
|
||||
"quote not paid",
|
||||
)
|
||||
|
||||
await pay_if_regtest(quote.request)
|
||||
|
||||
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||
assert mint_quote.paid, "mint quote should be paid"
|
||||
assert mint_quote.paid
|
||||
|
||||
output_amounts = [128]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
assert quote.privkey
|
||||
signature = nut20.sign_mint_quote(quote.quote, outputs, quote.privkey)
|
||||
await ledger.mint(outputs=outputs, quote_id=quote.quote, signature=signature)
|
||||
|
||||
mint_quote_after_payment = await ledger.get_mint_quote(quote.quote)
|
||||
assert mint_quote_after_payment.issued, "mint quote should be issued"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10)
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(len(send_proofs))
|
||||
outputs, rs = wallet1._construct_outputs(
|
||||
[p.amount for p in send_proofs], secrets, rs
|
||||
)
|
||||
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
assert [p.amount for p in promises] == [p.amount for p in outputs]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10, set_reserved=False)
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=[]),
|
||||
"no outputs provided",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_with_input_less_than_outputs(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 10, set_reserved=False
|
||||
)
|
||||
|
||||
too_many_proofs = send_proofs + send_proofs
|
||||
|
||||
# generate more outputs than inputs
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(too_many_proofs)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(
|
||||
[p.amount for p in too_many_proofs], secrets, rs
|
||||
)
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"are not balanced",
|
||||
)
|
||||
|
||||
# make sure we can still spend our tokens
|
||||
keep_proofs, send_proofs = await wallet1.split(wallet1.proofs, 10)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(128, quote_id=mint_quote.quote)
|
||||
|
||||
inputs = wallet1.proofs
|
||||
|
||||
# less outputs than inputs
|
||||
output_amounts = [8]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=inputs, outputs=outputs),
|
||||
"are not balanced",
|
||||
)
|
||||
|
||||
# make sure we can still spend our tokens
|
||||
keep_proofs, send_proofs = await wallet1.split(inputs, 10)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(128, split=[64, 64], quote_id=mint_quote.quote)
|
||||
inputs1 = wallet1.proofs[:1]
|
||||
inputs2 = wallet1.proofs[1:]
|
||||
|
||||
assert inputs1[0].amount == 64
|
||||
assert inputs2[0].amount == 64
|
||||
|
||||
output_amounts = [64]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
await ledger.swap(proofs=inputs1, outputs=outputs)
|
||||
|
||||
# try to spend other proofs with the same outputs again
|
||||
await assert_err(
|
||||
ledger.swap(proofs=inputs2, outputs=outputs),
|
||||
"outputs have already been signed before.",
|
||||
)
|
||||
|
||||
# try to spend inputs2 again with new outputs
|
||||
output_amounts = [64]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
await ledger.swap(proofs=inputs2, outputs=outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
output_amounts = [128]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
assert mint_quote.privkey
|
||||
signature = nut20.sign_mint_quote(mint_quote.quote, outputs, mint_quote.privkey)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote.quote, signature=signature)
|
||||
|
||||
# now try to mint with the same outputs again
|
||||
mint_quote_2 = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote_2.request)
|
||||
|
||||
assert mint_quote_2.privkey
|
||||
signature = nut20.sign_mint_quote(mint_quote_2.quote, outputs, mint_quote_2.privkey)
|
||||
await assert_err(
|
||||
ledger.mint(outputs=outputs, quote_id=mint_quote_2.quote, signature=signature),
|
||||
"outputs have already been signed before.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(130)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(130, quote_id=mint_quote.quote)
|
||||
|
||||
output_amounts = [128]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# we use the outputs once for minting
|
||||
mint_quote_2 = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote_2.request)
|
||||
assert mint_quote_2.privkey
|
||||
signature = nut20.sign_mint_quote(mint_quote_2.quote, outputs, mint_quote_2.privkey)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote_2.quote, signature=signature)
|
||||
|
||||
# use the same outputs for melting
|
||||
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
|
||||
)
|
||||
await assert_err(
|
||||
ledger.melt(proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs),
|
||||
"outputs have already been signed before.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_with_less_inputs_than_invoice(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(32)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(32, quote_id=mint_quote.quote)
|
||||
|
||||
# outputs for fee return
|
||||
output_amounts = [1, 1, 1, 1]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# create a mint quote to pay
|
||||
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
|
||||
# prepare melt quote
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
|
||||
)
|
||||
|
||||
assert melt_quote.amount + melt_quote.fee_reserve > sum_proofs(wallet1.proofs)
|
||||
|
||||
# try to pay with not enough inputs
|
||||
await assert_err(
|
||||
ledger.melt(proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs),
|
||||
"not enough inputs provided for melt",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_with_more_inputs_than_invoice(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(130)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(130, split=[64, 64, 2], quote_id=mint_quote.quote)
|
||||
|
||||
# outputs for fee return
|
||||
output_amounts = [1, 1, 1, 1]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# create a mint quote to pay
|
||||
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
|
||||
# prepare melt quote
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
|
||||
)
|
||||
# fees are 0 because it's internal
|
||||
assert melt_quote.fee_reserve == 0
|
||||
|
||||
# make sure we have more inputs than the melt quote needs
|
||||
assert sum_proofs(wallet1.proofs) >= melt_quote.amount + melt_quote.fee_reserve
|
||||
melt_resp = await ledger.melt(
|
||||
proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs
|
||||
)
|
||||
# we get 2 sats back because we overpaid
|
||||
assert melt_resp.change
|
||||
assert sum([o.amount for o in melt_resp.change]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10)
|
||||
|
||||
proof_states = await ledger.db_read.get_proofs_states(Ys=[p.Y for p in send_proofs])
|
||||
assert all([p.state.value == "UNSPENT" for p in proof_states])
|
||||
|
||||
|
||||
# TODO: test keeps running forever, needs to be fixed
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_websocket_quote_updates(wallet1: Wallet, ledger: Ledger):
|
||||
# mint_quote = await wallet1.request_mint(64)
|
||||
# ws = websocket.create_connection(
|
||||
# f"ws://localhost:{SERVER_PORT}/v1/quote/{invoice.id}"
|
||||
# )
|
||||
# await asyncio.sleep(0.1)
|
||||
# await pay_if_regtest(mint_quote.request)
|
||||
# await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
# await asyncio.sleep(0.1)
|
||||
# data = str(ws.recv())
|
||||
# ws.close()
|
||||
# n_lines = len(data.split("\n"))
|
||||
# assert n_lines == 1
|
||||
323
tests/mint/test_mint_p2pk.py
Normal file
323
tests/mint/test_mint_p2pk.py
Normal file
@@ -0,0 +1,323 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import P2PKWitness
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(ledger: Ledger):
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_inputs_require_sigall_detection(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test the ledger function that detects if any inputs require SIG_ALL."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create two proofs: one with SIG_INPUTS and one with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
|
||||
# Create a proof with SIG_INPUTS
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
_, send_proofs_inputs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
|
||||
# Create a new mint quote for the second mint operation
|
||||
mint_quote_2 = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote_2.request)
|
||||
await ledger.get_mint_quote(mint_quote_2.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote_2.quote)
|
||||
|
||||
# Create a proof with SIG_ALL
|
||||
secret_lock_all = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, send_proofs_all = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_all
|
||||
)
|
||||
|
||||
# Test that _inputs_require_sigall correctly detects SIG_ALL flag
|
||||
assert not ledger._inputs_require_sigall(
|
||||
send_proofs_inputs
|
||||
), "Should not detect SIG_ALL"
|
||||
assert ledger._inputs_require_sigall(send_proofs_all), "Should detect SIG_ALL"
|
||||
|
||||
# Test with a mixed list of proofs (should detect SIG_ALL if any proof has it)
|
||||
mixed_proofs = send_proofs_inputs + send_proofs_all
|
||||
assert ledger._inputs_require_sigall(
|
||||
mixed_proofs
|
||||
), "Should detect SIG_ALL in mixed list"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_verify_p2pk_signature_validation(
|
||||
wallet1: Wallet1, ledger: Ledger
|
||||
):
|
||||
"""Test the signature validation for P2PK inputs."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Create locked tokens
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Sign the tokens
|
||||
signed_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
assert len(signed_proofs) > 0, "Should have signed proofs"
|
||||
|
||||
# Verify that a valid witness was added to the proofs
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness is not None, "Proof should have a witness"
|
||||
witness = P2PKWitness.from_witness(proof.witness)
|
||||
assert len(witness.signatures) > 0, "Witness should have a signature"
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# The swap should succeed because the signatures are valid
|
||||
promises = await ledger.swap(proofs=signed_proofs, outputs=outputs)
|
||||
assert len(promises) == len(
|
||||
outputs
|
||||
), "Should have the same number of promises as outputs"
|
||||
|
||||
# Test for a failure
|
||||
# Create a fake witness with an incorrect signature
|
||||
fake_signature = "0" * 128 # Just a fake 64-byte hex string
|
||||
for proof in send_proofs:
|
||||
proof.witness = P2PKWitness(signatures=[fake_signature]).json()
|
||||
|
||||
# The swap should fail because the signatures are invalid
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"signature threshold not met",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_verify_incorrect_signature(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test rejection of incorrect signatures for P2PK inputs."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Create locked tokens
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create a fake witness with an incorrect signature
|
||||
fake_signature = "0" * 128 # Just a fake 64-byte hex string
|
||||
for proof in send_proofs:
|
||||
proof.witness = P2PKWitness(signatures=[fake_signature]).json()
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# The swap should fail because the signatures are invalid
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"signature threshold not met",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_verify_sigall_validation(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test validation of SIG_ALL signature that covers both inputs and outputs."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
|
||||
# Create locked tokens
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Create the message to sign (all inputs + all outputs)
|
||||
message_to_sign = "".join([p.secret for p in send_proofs] + [o.B_ for o in outputs])
|
||||
|
||||
# Sign the message with the wallet's private key
|
||||
signature = wallet1.schnorr_sign_message(message_to_sign)
|
||||
|
||||
# Add the signature to the first proof only (as required for SIG_ALL)
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[signature]).json()
|
||||
|
||||
# The swap should succeed because the SIG_ALL signature is valid
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(
|
||||
outputs
|
||||
), "Should have the same number of promises as outputs"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_verify_incorrect_sigall_signature(
|
||||
wallet1: Wallet1, ledger: Ledger
|
||||
):
|
||||
"""Test rejection of incorrect SIG_ALL signatures."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
|
||||
# Create locked tokens
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Create a fake witness with an incorrect signature
|
||||
fake_signature = "0" * 128 # Just a fake 64-byte hex string
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[fake_signature]).json()
|
||||
|
||||
# The swap should fail because the SIG_ALL signature is invalid
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"signature threshold not met",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_swap_p2pk_without_signature(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test ledger swap with p2pk locked tokens without providing signatures."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# Create a p2pk lock with wallet's own public key
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Attempt to swap WITHOUT adding signatures - this should fail
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"Witness is missing for p2pk signature",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_swap_p2pk_with_signature(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test ledger swap with p2pk locked tokens with proper signatures."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# Create a p2pk lock with wallet's own public key
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Sign the p2pk inputs before sending to the ledger
|
||||
signed_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
# Extract signed proofs and put them back in the send_proofs list
|
||||
signed_proofs_secrets = [p.secret for p in signed_proofs]
|
||||
for p in send_proofs:
|
||||
if p.secret in signed_proofs_secrets:
|
||||
send_proofs[send_proofs.index(p)] = signed_proofs[
|
||||
signed_proofs_secrets.index(p.secret)
|
||||
]
|
||||
|
||||
# Now swap with signatures - this should succeed
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
|
||||
# Verify the result
|
||||
assert len(promises) == len(outputs)
|
||||
assert [p.amount for p in promises] == [o.amount for o in outputs]
|
||||
635
tests/mint/test_mint_p2pk_comprehensive.py
Normal file
635
tests/mint/test_mint_p2pk_comprehensive.py
Normal file
@@ -0,0 +1,635 @@
|
||||
import copy
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import BlindedMessage, P2PKWitness
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import P2PKSecret, SigFlags
|
||||
from cashu.core.secret import Secret, SecretKind, Tags
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(ledger: Ledger):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1_p2pk_comprehensive",
|
||||
name="wallet1",
|
||||
)
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(ledger: Ledger):
|
||||
wallet2 = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet2_p2pk_comprehensive",
|
||||
name="wallet2",
|
||||
)
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
await wallet2.load_mint()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet3(ledger: Ledger):
|
||||
wallet3 = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet3_p2pk_comprehensive",
|
||||
name="wallet3",
|
||||
)
|
||||
await migrate_databases(wallet3.db, migrations)
|
||||
await wallet3.load_mint()
|
||||
yield wallet3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_inputs_basic(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test basic P2PK with SIG_INPUTS."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Verify wallet1 has tokens
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# Create locked tokens from wallet1 to wallet2
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Verify that sent tokens have P2PK secrets with SIG_INPUTS flag
|
||||
for proof in send_proofs:
|
||||
p2pk_secret = Secret.deserialize(proof.secret)
|
||||
assert p2pk_secret.kind == SecretKind.P2PK.value
|
||||
assert P2PKSecret.from_secret(p2pk_secret).sigflag == SigFlags.SIG_INPUTS
|
||||
|
||||
# Try to redeem without signatures (should fail)
|
||||
unsigned_proofs = copy.deepcopy(send_proofs)
|
||||
for proof in unsigned_proofs:
|
||||
proof.witness = None
|
||||
await assert_err(
|
||||
ledger.swap(
|
||||
proofs=unsigned_proofs, outputs=await create_test_outputs(wallet2, 16)
|
||||
),
|
||||
"Witness is missing for p2pk signature",
|
||||
)
|
||||
|
||||
# Redeem with proper signatures
|
||||
signed_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)
|
||||
assert all(p.witness is not None for p in signed_proofs)
|
||||
|
||||
# Now swap should succeed
|
||||
outputs = await create_test_outputs(wallet2, 16)
|
||||
promises = await ledger.swap(proofs=signed_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all_valid(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test P2PK with SIG_ALL where the signature covers both inputs and outputs."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create locked tokens with SIG_ALL
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, sig_all=True)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Verify that sent tokens have P2PK secrets with SIG_ALL flag
|
||||
for proof in send_proofs:
|
||||
p2pk_secret = Secret.deserialize(proof.secret)
|
||||
assert p2pk_secret.kind == SecretKind.P2PK.value
|
||||
assert P2PKSecret.from_secret(p2pk_secret).sigflag == SigFlags.SIG_ALL
|
||||
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet2, 16)
|
||||
|
||||
# Create a message from concatenated inputs and outputs
|
||||
message_to_sign = "".join([p.secret for p in send_proofs] + [o.B_ for o in outputs])
|
||||
|
||||
# Sign with wallet2's private key
|
||||
signature = wallet2.schnorr_sign_message(message_to_sign)
|
||||
|
||||
# Add the signature to the first proof only (since it's SIG_ALL)
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[signature]).json()
|
||||
|
||||
# Swap should succeed
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all_invalid(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test P2PK with SIG_ALL where the signature is invalid."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create locked tokens with SIG_ALL
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, sig_all=True)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet2, 16)
|
||||
|
||||
# Add an invalid signature
|
||||
fake_signature = "0" * 128 # Just a fake 64-byte hex string
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[fake_signature]).json()
|
||||
|
||||
# Swap should fail
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs), "signature threshold not met"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all_mixed(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test that attempting to use mixed SIG_ALL and SIG_INPUTS proofs fails."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(128, quote_id=mint_quote.quote)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet2, 32) # 16 + 16
|
||||
|
||||
# Create a proof with SIG_ALL
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
secret_lock_all = await wallet1.create_p2pk_lock(pubkey_wallet2, sig_all=True)
|
||||
_, proofs_sig_all = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_all
|
||||
)
|
||||
# sign proofs_sig_all
|
||||
signed_proofs_sig_all = wallet2.add_witness_swap_sig_all(proofs_sig_all, outputs)
|
||||
|
||||
# Mint more tokens to wallet1 for the SIG_INPUTS test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a proof with SIG_INPUTS
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey_wallet2, sig_all=False)
|
||||
_, proofs_sig_inputs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
# sign proofs_sig_inputs
|
||||
signed_proofs_sig_inputs = wallet2.sign_p2pk_sig_inputs(proofs_sig_inputs)
|
||||
|
||||
# Combine the proofs
|
||||
mixed_proofs = signed_proofs_sig_all + signed_proofs_sig_inputs
|
||||
|
||||
# Add an invalid signature to the SIG_ALL proof
|
||||
mixed_proofs[0].witness = P2PKWitness(signatures=["0" * 128]).json()
|
||||
|
||||
# Try to use the mixed proofs (should fail)
|
||||
await assert_err(
|
||||
ledger.swap(proofs=mixed_proofs, outputs=outputs),
|
||||
"not all secrets are equal.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_2_of_3(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with 2-of-3 multisig."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(6400)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(6400, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys from all wallets
|
||||
pubkey1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey2 = await wallet2.create_p2pk_pubkey()
|
||||
pubkey3 = await wallet3.create_p2pk_pubkey()
|
||||
|
||||
# Create 2-of-3 multisig tokens locked to all three wallets
|
||||
tags = Tags([["pubkeys", pubkey2, pubkey3]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey1, tags=tags, n_sigs=2)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Sign with wallet1 (first signature)
|
||||
signed_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
# Try to redeem with only 1 signature (should fail)
|
||||
await assert_err(
|
||||
ledger.swap(proofs=signed_proofs, outputs=outputs),
|
||||
"not enough pubkeys (3) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
# Mint new tokens for the second test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create new locked tokens
|
||||
_, send_proofs2 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Sign with wallet1 (first signature)
|
||||
signed_proofs2 = wallet1.sign_p2pk_sig_inputs(send_proofs2)
|
||||
|
||||
# Add signature from wallet2 (second signature)
|
||||
signed_proofs2 = wallet2.sign_p2pk_sig_inputs(signed_proofs2)
|
||||
|
||||
# Now redemption should succeed with 2 of 3 signatures
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
promises = await ledger.swap(proofs=signed_proofs2, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
# Mint new tokens for the third test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create new locked tokens
|
||||
_, send_proofs3 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Alternative: sign with wallet1 and wallet3
|
||||
signed_proofs3 = wallet1.sign_p2pk_sig_inputs(send_proofs3)
|
||||
signed_proofs3 = wallet3.sign_p2pk_sig_inputs(signed_proofs3)
|
||||
|
||||
# This should also succeed
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
promises2 = await ledger.swap(proofs=signed_proofs3, outputs=outputs)
|
||||
assert len(promises2) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_timelock(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test P2PK with a timelock that expires."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create tokens with a 2-second timelock
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# Set a past timestamp to ensure test works consistently
|
||||
past_time = int(time.time()) - 10
|
||||
tags = Tags([["locktime", str(past_time)]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, tags=tags)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Store current time to check if locktime passed
|
||||
locktime = 0
|
||||
for proof in send_proofs:
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||
locktime = p2pk_secret.locktime
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Verify that current time is past the locktime
|
||||
assert locktime is not None, "Locktime should not be None"
|
||||
assert (
|
||||
int(time.time()) > locktime
|
||||
), f"Current time ({int(time.time())}) should be greater than locktime ({locktime})"
|
||||
|
||||
# Try to redeem without signature after locktime (should succeed)
|
||||
unsigned_proofs = copy.deepcopy(send_proofs)
|
||||
for proof in unsigned_proofs:
|
||||
proof.witness = None
|
||||
|
||||
promises = await ledger.swap(proofs=unsigned_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_timelock_with_refund_before_locktime(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with a timelock and refund pubkeys before locktime."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # Receiver
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey() # Refund key
|
||||
|
||||
# Create tokens with a 2-second timelock and refund key
|
||||
future_time = int(time.time()) + 60 # 60 seconds in the future
|
||||
refund_tags = Tags([["refund", pubkey_wallet3], ["locktime", str(future_time)]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, tags=refund_tags)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Try to redeem without any signature before locktime (should fail)
|
||||
unsigned_proofs = copy.deepcopy(send_proofs)
|
||||
for proof in unsigned_proofs:
|
||||
proof.witness = None
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=unsigned_proofs, outputs=outputs),
|
||||
"Witness is missing for p2pk signature",
|
||||
)
|
||||
|
||||
# Try to redeem with refund key signature before locktime (should fail)
|
||||
refund_signed_proofs = wallet3.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=refund_signed_proofs, outputs=outputs),
|
||||
"signature threshold not met", # Refund key can't be used before locktime
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_timelock_with_receiver_signature(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with a timelock and refund pubkeys with receiver signature."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # Receiver
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey() # Refund key
|
||||
|
||||
# Create tokens with a 2-second timelock and refund key
|
||||
future_time = int(time.time()) + 60 # 60 seconds in the future
|
||||
refund_tags = Tags([["refund", pubkey_wallet3], ["locktime", str(future_time)]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, tags=refund_tags)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Try to redeem with the correct receiver signature (should succeed)
|
||||
receiver_signed_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
promises = await ledger.swap(proofs=receiver_signed_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_timelock_with_refund_after_locktime(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with a timelock and refund pubkeys after locktime."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # Receiver
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey() # Refund key
|
||||
|
||||
# Create tokens with a past timestamp for locktime testing
|
||||
past_time = int(time.time()) - 10 # 10 seconds in the past
|
||||
refund_tags_past = Tags([["refund", pubkey_wallet3], ["locktime", str(past_time)]])
|
||||
secret_lock_past = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=refund_tags_past
|
||||
)
|
||||
_, send_proofs3 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_past
|
||||
)
|
||||
|
||||
# Try to redeem with refund key after locktime (should succeed)
|
||||
refund_signed_proofs2 = wallet3.sign_p2pk_sig_inputs(send_proofs3)
|
||||
|
||||
# This should work because locktime has passed and refund key is used
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
promises2 = await ledger.swap(proofs=refund_signed_proofs2, outputs=outputs)
|
||||
assert len(promises2) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_n_sigs_refund(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with a timelock and multiple refund pubkeys with n_sigs_refund."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # Receiver
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # Refund key 1
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey() # Refund key 2
|
||||
|
||||
# Create tokens with a future timelock and 2-of-2 refund requirement
|
||||
future_time = int(time.time()) + 60 # 60 seconds in the future
|
||||
refund_tags = Tags(
|
||||
[
|
||||
["refund", pubkey_wallet2, pubkey_wallet3],
|
||||
["n_sigs_refund", "2"],
|
||||
["locktime", str(future_time)],
|
||||
]
|
||||
)
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet1, tags=refund_tags)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Mint new tokens for receiver test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create new locked tokens
|
||||
_, send_proofs2 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Try to redeem with receiver key (should succeed before locktime)
|
||||
receiver_signed_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs2)
|
||||
promises = await ledger.swap(proofs=receiver_signed_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
# Mint new tokens for the refund test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create tokens with a past locktime for refund testing
|
||||
past_time = int(time.time()) - 10 # 10 seconds in the past
|
||||
refund_tags_past = Tags(
|
||||
[
|
||||
["refund", pubkey_wallet2, pubkey_wallet3],
|
||||
["n_sigs_refund", "2"],
|
||||
["locktime", str(past_time)],
|
||||
]
|
||||
)
|
||||
secret_lock_past = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet1, tags=refund_tags_past
|
||||
)
|
||||
_, send_proofs3 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_past
|
||||
)
|
||||
|
||||
# Try to redeem with only one refund key signature (should fail)
|
||||
refund_signed_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs3)
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=refund_signed_proofs, outputs=outputs),
|
||||
"not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
# Mint new tokens for the final test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create tokens with same past locktime
|
||||
_, send_proofs4 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_past
|
||||
)
|
||||
|
||||
# Add both refund signatures
|
||||
refund_signed_proofs2 = wallet2.sign_p2pk_sig_inputs(send_proofs4)
|
||||
refund_signed_proofs2 = wallet3.sign_p2pk_sig_inputs(refund_signed_proofs2)
|
||||
|
||||
# Now it should succeed with 2-of-2 refund signatures
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
promises2 = await ledger.swap(proofs=refund_signed_proofs2, outputs=outputs)
|
||||
assert len(promises2) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_invalid_pubkey_check(
|
||||
wallet1: Wallet, wallet2: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test that an invalid public key is properly rejected."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create an invalid pubkey string (too short)
|
||||
invalid_pubkey = "03aaff"
|
||||
|
||||
# Try to create a P2PK lock with invalid pubkey
|
||||
# This should fail in create_p2pk_lock, but if it doesn't, let's handle it gracefully
|
||||
try:
|
||||
secret_lock = await wallet1.create_p2pk_lock(invalid_pubkey)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Verify it fails during validation
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"failed to deserialize pubkey", # Generic error for pubkey issues
|
||||
)
|
||||
except Exception as e:
|
||||
# If it fails during creation, that's fine too
|
||||
assert (
|
||||
"pubkey" in str(e).lower() or "key" in str(e).lower()
|
||||
), f"Expected error about invalid public key, got: {str(e)}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all_with_multiple_pubkeys(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test SIG_ALL combined with multiple pubkeys/n_sigs."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey2 = await wallet2.create_p2pk_pubkey()
|
||||
pubkey3 = await wallet3.create_p2pk_pubkey()
|
||||
|
||||
# Create tokens with SIG_ALL and 2-of-3 multisig
|
||||
tags = Tags([["pubkeys", pubkey2, pubkey3]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey1, tags=tags, n_sigs=2, sig_all=True
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Create message to sign (all inputs + all outputs)
|
||||
message_to_sign = "".join([p.secret for p in send_proofs] + [o.B_ for o in outputs])
|
||||
|
||||
# Sign with wallet1's key
|
||||
signature1 = wallet1.schnorr_sign_message(message_to_sign)
|
||||
|
||||
# Sign with wallet2's key
|
||||
signature2 = wallet2.schnorr_sign_message(message_to_sign)
|
||||
|
||||
# Add both signatures to the first proof only (SIG_ALL)
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[signature1, signature2]).json()
|
||||
|
||||
# This should succeed with 2 valid signatures
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
async def create_test_outputs(wallet: Wallet, amount: int) -> List[BlindedMessage]:
|
||||
"""Helper to create blinded outputs for testing."""
|
||||
output_amounts = [amount]
|
||||
secrets, rs, _ = await wallet.generate_n_secrets(len(output_amounts))
|
||||
outputs, _ = wallet._construct_outputs(output_amounts, secrets, rs)
|
||||
return outputs
|
||||
323
tests/mint/test_mint_regtest.py
Normal file
323
tests/mint/test_mint_regtest.py
Normal file
@@ -0,0 +1,323 @@
|
||||
import asyncio
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Amount, MeltQuote, MeltQuoteState, Method, Unit
|
||||
from cashu.core.models import PostMeltQuoteRequest
|
||||
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,
|
||||
get_real_invoice,
|
||||
get_real_invoice_cln,
|
||||
is_fake,
|
||||
pay_if_regtest,
|
||||
pay_real_invoice,
|
||||
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_lightning_create_invoice(ledger: Ledger):
|
||||
invoice = await ledger.backends[Method.bolt11][Unit.sat].create_invoice(
|
||||
Amount(Unit.sat, 1000)
|
||||
)
|
||||
assert invoice.ok
|
||||
assert invoice.payment_request
|
||||
assert invoice.checking_id
|
||||
|
||||
# TEST 2: check the invoice status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_invoice_status(
|
||||
invoice.checking_id
|
||||
)
|
||||
assert status.pending
|
||||
|
||||
# settle the invoice
|
||||
await pay_if_regtest(invoice.payment_request)
|
||||
|
||||
# TEST 3: check the invoice status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_invoice_status(
|
||||
invoice.checking_id
|
||||
)
|
||||
assert status.settled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_get_payment_quote(ledger: Ledger):
|
||||
invoice_dict = get_real_invoice(64)
|
||||
request = invoice_dict["payment_request"]
|
||||
payment_quote = await ledger.backends[Method.bolt11][Unit.sat].get_payment_quote(
|
||||
PostMeltQuoteRequest(request=request, unit=Unit.sat.name)
|
||||
)
|
||||
assert payment_quote.amount == Amount(Unit.sat, 64)
|
||||
assert payment_quote.checking_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_pay_invoice(ledger: Ledger):
|
||||
invoice_dict = get_real_invoice(64)
|
||||
request = invoice_dict["payment_request"]
|
||||
quote = MeltQuote(
|
||||
quote="test",
|
||||
method=Method.bolt11.name,
|
||||
unit=Unit.sat.name,
|
||||
state=MeltQuoteState.unpaid,
|
||||
request=request,
|
||||
checking_id="test",
|
||||
amount=64,
|
||||
fee_reserve=0,
|
||||
)
|
||||
payment = await ledger.backends[Method.bolt11][Unit.sat].pay_invoice(quote, 1000)
|
||||
assert payment.settled
|
||||
assert payment.preimage
|
||||
assert payment.checking_id
|
||||
assert not payment.error_message
|
||||
|
||||
# TEST 2: check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
payment.checking_id
|
||||
)
|
||||
assert status.settled
|
||||
assert status.preimage
|
||||
assert not status.error_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_pay_invoice_failure(ledger: Ledger):
|
||||
# create an invoice with the external CLN node and pay it with the external LND – so that our mint backend can't pay it
|
||||
request = get_real_invoice_cln(64)
|
||||
# pay the invoice so that the attempt later fails
|
||||
pay_real_invoice(request)
|
||||
|
||||
# we call get_payment_quote to get a checking_id that we will use to check for the failed pending state later with get_payment_status
|
||||
payment_quote = await ledger.backends[Method.bolt11][Unit.sat].get_payment_quote(
|
||||
PostMeltQuoteRequest(request=request, unit=Unit.sat.name)
|
||||
)
|
||||
checking_id = payment_quote.checking_id
|
||||
|
||||
# TEST 1: check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
checking_id
|
||||
)
|
||||
assert status.unknown
|
||||
|
||||
# TEST 2: pay the invoice
|
||||
quote = MeltQuote(
|
||||
quote="test",
|
||||
method=Method.bolt11.name,
|
||||
unit=Unit.sat.name,
|
||||
state=MeltQuoteState.unpaid,
|
||||
request=request,
|
||||
checking_id="test",
|
||||
amount=64,
|
||||
fee_reserve=0,
|
||||
)
|
||||
payment = await ledger.backends[Method.bolt11][Unit.sat].pay_invoice(quote, 1000)
|
||||
|
||||
assert payment.failed
|
||||
assert not payment.preimage
|
||||
assert payment.error_message
|
||||
assert not payment.checking_id
|
||||
|
||||
# TEST 3: check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
checking_id
|
||||
)
|
||||
|
||||
assert status.failed or status.unknown
|
||||
assert not status.preimage
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_pay_invoice_pending_success(ledger: Ledger):
|
||||
# create a hold invoice
|
||||
preimage, invoice_dict = get_hold_invoice(64)
|
||||
request = str(invoice_dict["payment_request"])
|
||||
|
||||
# we call get_payment_quote to get a checking_id that we will use to check for the failed pending state later with get_payment_status
|
||||
payment_quote = await ledger.backends[Method.bolt11][Unit.sat].get_payment_quote(
|
||||
PostMeltQuoteRequest(request=request, unit=Unit.sat.name)
|
||||
)
|
||||
checking_id = payment_quote.checking_id
|
||||
|
||||
# pay the invoice
|
||||
quote = MeltQuote(
|
||||
quote="test",
|
||||
method=Method.bolt11.name,
|
||||
unit=Unit.sat.name,
|
||||
state=MeltQuoteState.unpaid,
|
||||
request=request,
|
||||
checking_id=checking_id,
|
||||
amount=64,
|
||||
fee_reserve=0,
|
||||
)
|
||||
|
||||
async def pay():
|
||||
payment = await ledger.backends[Method.bolt11][Unit.sat].pay_invoice(
|
||||
quote, 1000
|
||||
)
|
||||
return payment
|
||||
|
||||
task = asyncio.create_task(pay())
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
quote.checking_id
|
||||
)
|
||||
assert status.pending
|
||||
|
||||
# settle the invoice
|
||||
settle_invoice(preimage=preimage)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# collect the payment
|
||||
payment = await task
|
||||
assert payment.settled
|
||||
assert payment.preimage
|
||||
assert payment.checking_id
|
||||
assert not payment.error_message
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
quote.checking_id
|
||||
)
|
||||
assert status.settled
|
||||
assert status.preimage
|
||||
assert not status.error_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_pay_invoice_pending_failure(ledger: Ledger):
|
||||
# create a hold invoice
|
||||
preimage, invoice_dict = get_hold_invoice(64)
|
||||
request = str(invoice_dict["payment_request"])
|
||||
payment_hash = bolt11.decode(request).payment_hash
|
||||
|
||||
# we call get_payment_quote to get a checking_id that we will use to check for the failed pending state later with get_payment_status
|
||||
payment_quote = await ledger.backends[Method.bolt11][Unit.sat].get_payment_quote(
|
||||
PostMeltQuoteRequest(request=request, unit=Unit.sat.name)
|
||||
)
|
||||
checking_id = payment_quote.checking_id
|
||||
|
||||
# pay the invoice
|
||||
quote = MeltQuote(
|
||||
quote="test",
|
||||
method=Method.bolt11.name,
|
||||
unit=Unit.sat.name,
|
||||
state=MeltQuoteState.unpaid,
|
||||
request=request,
|
||||
checking_id=checking_id,
|
||||
amount=64,
|
||||
fee_reserve=0,
|
||||
)
|
||||
|
||||
async def pay():
|
||||
payment = await ledger.backends[Method.bolt11][Unit.sat].pay_invoice(
|
||||
quote, 1000
|
||||
)
|
||||
return payment
|
||||
|
||||
task = asyncio.create_task(pay())
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
quote.checking_id
|
||||
)
|
||||
assert status.pending
|
||||
|
||||
# cancel the invoice
|
||||
cancel_invoice(payment_hash)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# collect the payment
|
||||
payment = await task
|
||||
assert payment.failed
|
||||
assert not payment.preimage
|
||||
# assert payment.error_message
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
quote.checking_id
|
||||
)
|
||||
assert (
|
||||
status.failed or status.unknown
|
||||
) # some backends send unknown instead of failed if they can't find the payment
|
||||
assert not status.preimage
|
||||
# assert status.error_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
||||
# fill wallet
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
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.melt_quote(invoice_payment_request)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
_, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount)
|
||||
asyncio.create_task(ledger.melt(proofs=send_proofs, quote=quote.quote))
|
||||
# asyncio.create_task(
|
||||
# wallet.melt(
|
||||
# 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.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.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.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.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
|
||||
Reference in New Issue
Block a user