mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-06 18:34: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:
555
tests/wallet/test_wallet.py
Normal file
555
tests/wallet/test_wallet.py
Normal file
@@ -0,0 +1,555 @@
|
||||
import copy
|
||||
from typing import List, Union
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuote, MeltQuoteState, MintQuoteState, Proof
|
||||
from cashu.core.errors import CashuError, KeysetNotFoundError
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.crud import (
|
||||
get_bolt11_melt_quote,
|
||||
get_bolt11_mint_quote,
|
||||
get_keysets,
|
||||
get_proofs,
|
||||
)
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
get_real_invoice,
|
||||
is_deprecated_api_only,
|
||||
is_fake,
|
||||
is_github_actions,
|
||||
is_regtest,
|
||||
pay_if_regtest,
|
||||
)
|
||||
|
||||
|
||||
async def assert_err(f, msg: Union[str, CashuError]):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
error_message: str = str(exc.args[0])
|
||||
if isinstance(msg, CashuError):
|
||||
if msg.detail not in error_message:
|
||||
raise Exception(
|
||||
f"CashuError. Expected error: {msg.detail}, got: {error_message}"
|
||||
)
|
||||
return
|
||||
if msg not in error_message:
|
||||
raise Exception(f"Expected error: {msg}, got: {error_message}")
|
||||
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")
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert sum([p.amount for p in proofs]) == expected
|
||||
|
||||
|
||||
async def reset_wallet_db(wallet: Wallet):
|
||||
await wallet.db.execute("DELETE FROM proofs")
|
||||
await wallet.db.execute("DELETE FROM proofs_used")
|
||||
await wallet.db.execute("DELETE FROM keysets")
|
||||
await wallet.load_mint()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet2.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet2",
|
||||
name="wallet2",
|
||||
)
|
||||
await wallet2.load_mint()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keys(wallet1: Wallet):
|
||||
assert wallet1.keysets[wallet1.keyset_id].public_keys
|
||||
assert len(wallet1.keysets[wallet1.keyset_id].public_keys) == settings.max_order
|
||||
keysets = await wallet1._get_keys()
|
||||
keyset = keysets[0]
|
||||
assert keyset.id is not None
|
||||
# assert keyset.id_deprecated == "eGnEWtdJ0PIM"
|
||||
assert keyset.id == "009a1f293253e41e"
|
||||
assert isinstance(keyset.id, str)
|
||||
assert len(keyset.id) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset(wallet1: Wallet):
|
||||
assert wallet1.keysets[wallet1.keyset_id].public_keys
|
||||
assert len(wallet1.keysets[wallet1.keyset_id].public_keys) == settings.max_order
|
||||
# let's get the keys first so we can get a keyset ID that we use later
|
||||
keysets = await wallet1._get_keys()
|
||||
keyset = keysets[0]
|
||||
# gets the keys of a specific keyset
|
||||
assert keyset.id is not None
|
||||
assert keyset.public_keys is not None
|
||||
keys2 = await wallet1._get_keyset(keyset.id)
|
||||
assert keys2.public_keys is not None
|
||||
assert len(keyset.public_keys) == len(keys2.public_keys)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset_from_db(wallet1: Wallet):
|
||||
# first load it from the mint
|
||||
# await wallet1.activate_keyset()
|
||||
# NOTE: conftest already called wallet.load_mint() which got the keys from the mint
|
||||
keyset1 = copy.copy(wallet1.keysets[wallet1.keyset_id])
|
||||
|
||||
# then load it from the db
|
||||
await wallet1.activate_keyset()
|
||||
keyset2 = copy.copy(wallet1.keysets[wallet1.keyset_id])
|
||||
|
||||
assert keyset1.public_keys == keyset2.public_keys
|
||||
assert keyset1.id == keyset2.id
|
||||
|
||||
# load it directly from the db
|
||||
keysets_local = await get_keysets(db=wallet1.db, id=keyset1.id)
|
||||
assert keysets_local[0]
|
||||
keyset3 = keysets_local[0]
|
||||
assert keyset1.public_keys == keyset3.public_keys
|
||||
assert keyset1.id == keyset3.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info(wallet1: Wallet):
|
||||
info = await wallet1._get_info()
|
||||
assert info.name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_keyset(wallet1: Wallet):
|
||||
await assert_err(
|
||||
wallet1._get_keyset("nonexistent"),
|
||||
KeysetNotFoundError(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keysets(wallet1: Wallet):
|
||||
keysets = await wallet1._get_keysets()
|
||||
assert isinstance(keysets, list)
|
||||
assert len(keysets) > 0
|
||||
assert wallet1.keyset_id in [k.id for k in keysets]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_mint(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
assert mint_quote.request
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
if not settings.debug_mint_only_deprecated:
|
||||
mint_quote = await wallet1.get_mint_quote(mint_quote.quote)
|
||||
assert mint_quote.request == mint_quote.request
|
||||
assert mint_quote.state == MintQuoteState.paid
|
||||
|
||||
expected_proof_amounts = wallet1.split_wallet_state(64)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# verify that proofs in proofs_used db have the same mint_id as the invoice in the db
|
||||
mint_quote_2 = await get_bolt11_mint_quote(db=wallet1.db, quote=mint_quote.quote)
|
||||
assert mint_quote_2
|
||||
proofs_minted = await get_proofs(
|
||||
db=wallet1.db, mint_id=mint_quote_2.quote, table="proofs"
|
||||
)
|
||||
assert len(proofs_minted) == len(expected_proof_amounts)
|
||||
assert all([p.amount in expected_proof_amounts for p in proofs_minted])
|
||||
assert all([p.mint_id == mint_quote_2.quote for p in proofs_minted])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_amounts(wallet1: Wallet):
|
||||
"""Mint predefined amounts"""
|
||||
amts = [1, 1, 1, 2, 2, 4, 16]
|
||||
mint_quote = await wallet1.request_mint(sum(amts))
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(amount=sum(amts), split=amts, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 27
|
||||
assert wallet1.proof_amounts == amts
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_amounts_wrong_sum(wallet1: Wallet):
|
||||
"""Mint predefined amounts"""
|
||||
|
||||
amts = [1, 1, 1, 2, 2, 4, 16]
|
||||
mint_quote = await wallet1.request_mint(sum(amts))
|
||||
await assert_err(
|
||||
wallet1.mint(amount=sum(amts) + 1, split=amts, quote_id=mint_quote.quote),
|
||||
"split must sum to amount",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_amounts_wrong_order(wallet1: Wallet):
|
||||
"""Mint amount that is not part in 2^n"""
|
||||
amts = [1, 2, 3]
|
||||
mint_quote = await wallet1.request_mint(sum(amts))
|
||||
allowed_amounts = wallet1.get_allowed_amounts()
|
||||
await assert_err(
|
||||
wallet1.mint(amount=sum(amts), split=[1, 2, 3], quote_id=mint_quote.quote),
|
||||
f"Can only mint amounts supported by the mint: {allowed_amounts}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 64
|
||||
# the outputs we keep that we expect after the split
|
||||
expected_proof_amounts = wallet1.split_wallet_state(44)
|
||||
p1, p2 = await wallet1.split(wallet1.proofs, 20)
|
||||
assert wallet1.balance == 64
|
||||
assert sum_proofs(p1) == 44
|
||||
# what we keep should have the expected amounts
|
||||
assert [p.amount for p in p1] == expected_proof_amounts
|
||||
assert sum_proofs(p2) == 20
|
||||
# what we send should be the optimal split
|
||||
assert [p.amount for p in p2] == [4, 16]
|
||||
assert all([p.id == wallet1.keyset_id for p in p1])
|
||||
assert all([p.id == wallet1.keyset_id for p in p2])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_swap_to_send(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# this will select 32 sats and them (nothing to keep)
|
||||
keep_proofs, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, set_reserved=True
|
||||
)
|
||||
assert_amt(send_proofs, 32)
|
||||
assert_amt(keep_proofs, 0)
|
||||
|
||||
spendable_proofs = wallet1.coinselect(wallet1.proofs, 32)
|
||||
assert sum_proofs(spendable_proofs) == 32
|
||||
|
||||
assert sum_proofs(send_proofs) == 32
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 32
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_more_than_balance(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs, 128),
|
||||
# "Mint Error: inputs do not have same amount as outputs",
|
||||
"amount too large.",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt(wallet1: Wallet):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
topup_mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(topup_mint_quote.request)
|
||||
await wallet1.mint(128, quote_id=topup_mint_quote.quote)
|
||||
assert wallet1.balance == 128
|
||||
|
||||
invoice_payment_request = ""
|
||||
if is_regtest:
|
||||
invoice_dict = get_real_invoice(64)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
if is_fake:
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
invoice_payment_request = mint_quote.request
|
||||
|
||||
quote = await wallet1.melt_quote(invoice_payment_request)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
|
||||
if is_regtest:
|
||||
# we expect a fee reserve of 2 sat for regtest
|
||||
assert total_amount == 66
|
||||
assert quote.fee_reserve == 2
|
||||
if is_fake:
|
||||
# we expect a fee reserve of 0 sat for fake
|
||||
assert total_amount == 64
|
||||
assert quote.fee_reserve == 0
|
||||
|
||||
if not settings.debug_mint_only_deprecated:
|
||||
quote_resp = await wallet1.get_melt_quote(quote.quote)
|
||||
assert quote_resp
|
||||
assert quote_resp.amount == quote.amount
|
||||
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, total_amount)
|
||||
|
||||
melt_response = await wallet1.melt(
|
||||
proofs=send_proofs,
|
||||
invoice=invoice_payment_request,
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
|
||||
if is_regtest:
|
||||
assert melt_response.change, "No change returned"
|
||||
assert len(melt_response.change) == 1, "More than one change returned"
|
||||
# NOTE: we assume that we will get a token back from the same keyset as the ones we melted
|
||||
# this could be wrong if we melted tokens from an old keyset but the returned ones are
|
||||
# from a newer one.
|
||||
assert melt_response.change[0].id == send_proofs[0].id, "Wrong keyset returned"
|
||||
|
||||
# verify that proofs in proofs_used db have the same melt_id as the invoice in the db
|
||||
melt_quote_db = await get_bolt11_melt_quote(
|
||||
db=wallet1.db, request=invoice_payment_request
|
||||
)
|
||||
assert melt_quote_db, "No melt quote in db"
|
||||
|
||||
# compare melt quote from API against db
|
||||
if not settings.debug_mint_only_deprecated:
|
||||
melt_quote_api_resp = await wallet1.get_melt_quote(melt_quote_db.quote)
|
||||
assert melt_quote_api_resp, "No melt quote from API"
|
||||
assert melt_quote_api_resp.quote == melt_quote_db.quote, "Wrong quote ID"
|
||||
assert melt_quote_api_resp.amount == melt_quote_db.amount, "Wrong amount"
|
||||
assert melt_quote_api_resp.fee_reserve == melt_quote_db.fee_reserve, "Wrong fee"
|
||||
assert melt_quote_api_resp.request == melt_quote_db.request, "Wrong request"
|
||||
assert melt_quote_api_resp.state == melt_quote_db.state, "Wrong state"
|
||||
assert melt_quote_api_resp.unit == melt_quote_db.unit, "Wrong unit"
|
||||
|
||||
proofs_used = await get_proofs(
|
||||
db=wallet1.db, melt_id=melt_quote_db.quote, table="proofs_used"
|
||||
)
|
||||
|
||||
assert len(proofs_used) == len(send_proofs), "Not all proofs used"
|
||||
assert all([p.melt_id == melt_quote_db.quote for p in proofs_used]), "Wrong melt_id"
|
||||
|
||||
# the payment was without fees so we need to remove it from the total amount
|
||||
assert wallet1.balance == 128 - (total_amount - quote.fee_reserve), "Wrong balance"
|
||||
assert wallet1.balance == 64, "Wrong balance"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_deprecated_api_only, reason="Deprecated API only")
|
||||
async def test_get_melt_quote_state(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(128, quote_id=mint_quote.quote)
|
||||
invoice_payment_request = ""
|
||||
if is_regtest:
|
||||
invoice_dict = get_real_invoice(64)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
if is_fake:
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
invoice_payment_request = mint_quote.request
|
||||
quote = await wallet1.melt_quote(invoice_payment_request)
|
||||
assert quote.state == MeltQuoteState.unpaid
|
||||
assert quote.request == invoice_payment_request
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, total_amount)
|
||||
melt_response = await wallet1.melt(
|
||||
proofs=send_proofs,
|
||||
invoice=invoice_payment_request,
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
melt_quote_wallet = MeltQuote.from_resp_wallet(
|
||||
melt_response,
|
||||
mint="test",
|
||||
unit=quote.unit or "sat",
|
||||
request=quote.request or invoice_payment_request,
|
||||
)
|
||||
|
||||
# compare melt quote from API against db
|
||||
melt_quote_api_resp = await wallet1.get_melt_quote(melt_quote_wallet.quote)
|
||||
assert melt_quote_api_resp, "No melt quote from API"
|
||||
assert melt_quote_api_resp.quote == melt_quote_wallet.quote, "Wrong quote ID"
|
||||
assert melt_quote_api_resp.amount == melt_quote_wallet.amount, "Wrong amount"
|
||||
assert melt_quote_api_resp.fee_reserve == melt_quote_wallet.fee_reserve, "Wrong fee"
|
||||
assert melt_quote_api_resp.request == melt_quote_wallet.request, "Wrong request"
|
||||
assert melt_quote_api_resp.state == melt_quote_wallet.state, "Wrong state"
|
||||
assert melt_quote_api_resp.unit == melt_quote_wallet.unit, "Wrong unit"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_swap_to_send_more_than_balance(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await assert_err(
|
||||
wallet1.swap_to_send(wallet1.proofs, 128, set_reserved=True),
|
||||
"Balance too low",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_double_spend(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
doublespend = await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await wallet1.split(wallet1.proofs, 20)
|
||||
await assert_err(
|
||||
wallet1.split(doublespend, 20),
|
||||
"Token already spent.",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_proofs_double_spent(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
doublespend = await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs + doublespend, 20),
|
||||
"Mint Error: Duplicate inputs provided",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_github_actions, reason="GITHUB_ACTIONS")
|
||||
async def test_split_race_condition(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
# run two splits in parallel
|
||||
import asyncio
|
||||
|
||||
await assert_err_multiple(
|
||||
asyncio.gather(
|
||||
wallet1.split(wallet1.proofs, 20),
|
||||
wallet1.split(wallet1.proofs, 20),
|
||||
),
|
||||
["proofs are pending.", "already spent."],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
_, spendable_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, set_reserved=True
|
||||
)
|
||||
await wallet2.redeem(spendable_proofs)
|
||||
assert wallet2.balance == 32
|
||||
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 32
|
||||
await wallet1.invalidate(spendable_proofs)
|
||||
assert wallet1.balance == 32
|
||||
assert wallet1.available_balance == 32
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalidate_all_proofs(wallet1: Wallet):
|
||||
"""Try to invalidate proofs that have not been spent yet. Should not work!"""
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await wallet1.invalidate(wallet1.proofs)
|
||||
assert wallet1.balance == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalidate_unspent_proofs_with_checking(wallet1: Wallet):
|
||||
"""Try to invalidate proofs that have not been spent yet but force no check."""
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await wallet1.invalidate(wallet1.proofs, check_spendable=True)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalidate_batch_many_proofs(wallet1: Wallet):
|
||||
"""Try to invalidate proofs that have not been spent yet but force no check."""
|
||||
amount_to_mint = 500 # nutshell default value is 1000
|
||||
mint_quote = await wallet1.request_mint(amount_to_mint)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
proofs = await wallet1.mint(
|
||||
amount_to_mint, quote_id=mint_quote.quote, split=[1] * amount_to_mint
|
||||
)
|
||||
assert len(proofs) == amount_to_mint
|
||||
|
||||
states = await wallet1.check_proof_state(proofs)
|
||||
assert all([s.unspent for s in states.states])
|
||||
spent_proofs = await wallet1.get_spent_proofs_check_states_batched(proofs)
|
||||
assert len(spent_proofs) == 0
|
||||
assert wallet1.balance == amount_to_mint
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_invalid_amount(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs, -1),
|
||||
"amount can't be negative",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_state(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 64
|
||||
resp = await wallet1.check_proof_state(wallet1.proofs)
|
||||
assert resp.states[0].state.value == "UNSPENT"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def testactivate_keyset_specific_keyset(wallet1: Wallet):
|
||||
await wallet1.activate_keyset()
|
||||
assert list(wallet1.keysets.keys()) == ["009a1f293253e41e"]
|
||||
await wallet1.activate_keyset(keyset_id=wallet1.keyset_id)
|
||||
await wallet1.activate_keyset(keyset_id="009a1f293253e41e")
|
||||
# expect deprecated keyset id to be present
|
||||
await assert_err(
|
||||
wallet1.activate_keyset(keyset_id="nonexistent"),
|
||||
KeysetNotFoundError("nonexistent"),
|
||||
)
|
||||
199
tests/wallet/test_wallet_api.py
Normal file
199
tests/wallet/test_wallet_api.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from cashu.lightning.base import InvoiceResponse, PaymentResult, PaymentStatus
|
||||
from cashu.wallet.api.app import app
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import is_regtest
|
||||
|
||||
|
||||
@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.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoice(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/lightning/create_invoice?amount=100")
|
||||
assert response.status_code == 200
|
||||
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||
state = PaymentStatus(result=PaymentResult.PENDING)
|
||||
while state.pending:
|
||||
print("checking invoice state")
|
||||
response2 = client.get(
|
||||
f"/lightning/invoice_state?payment_request={invoice_response.payment_request}"
|
||||
)
|
||||
state = PaymentStatus.parse_obj(response2.json())
|
||||
await asyncio.sleep(0.1)
|
||||
print("state:", state)
|
||||
print("paid")
|
||||
await wallet.load_proofs()
|
||||
assert wallet.available_balance >= 100
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_balance():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/balance")
|
||||
assert response.status_code == 200
|
||||
assert "balance" in response.json()
|
||||
assert response.json()["keysets"]
|
||||
assert response.json()["mints"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_send(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/send?amount=10")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["balance"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_without_split(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/send?amount=2&offline=true")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["balance"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_too_much(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/send?amount=110000")
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_pending():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/pending")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_receive_all(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/receive?all=true")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["initial_balance"]
|
||||
assert response.json()["balance"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_burn_all(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/send?amount=20")
|
||||
assert response.status_code == 200
|
||||
response = client.post("/burn?all=true")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["balance"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_pay():
|
||||
with TestClient(app) as client:
|
||||
invoice = (
|
||||
"lnbc100n1pjjcqzfdq4gdshx6r4ypjx2ur0wd5hgpp58xvj8yn00d5"
|
||||
"7uhshwzcwgy9uj3vwf5y2lr5fjf78s4w9l4vhr6xssp5stezsyty9r"
|
||||
"hv3lat69g4mhqxqun56jyehhkq3y8zufh83xyfkmmq4usaqwrt5q4f"
|
||||
"adm44g6crckp0hzvuyv9sja7t65hxj0ucf9y46qstkay7gfnwhuxgr"
|
||||
"krf7djs38rml39l8wpn5ug9shp3n55quxhdecqfwxg23"
|
||||
)
|
||||
response = client.post(f"/lightning/pay_invoice?bolt11={invoice}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_lock():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/lock")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_locks():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/locks")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoices():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/invoices")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallets():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/wallets")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_info():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/info")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["version"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="regtest")
|
||||
@pytest.mark.asyncio
|
||||
async def test_flow(wallet: Wallet):
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/balance")
|
||||
initial_balance = response.json()["balance"]
|
||||
response = client.post("/lightning/create_invoice?amount=100")
|
||||
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||
state = PaymentStatus(result=PaymentResult.PENDING)
|
||||
while state.pending:
|
||||
print("checking invoice state")
|
||||
response2 = client.get(
|
||||
f"/lightning/invoice_state?payment_request={invoice_response.payment_request}"
|
||||
)
|
||||
state = PaymentStatus.parse_obj(response2.json())
|
||||
await asyncio.sleep(0.1)
|
||||
print("state:", state)
|
||||
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + 100
|
||||
response = client.post("/send?amount=50")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + 50
|
||||
response = client.post("/send?amount=50")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance
|
||||
response = client.get("/pending")
|
||||
token = response.json()["pending_token"]["0"]["token"]
|
||||
amount = response.json()["pending_token"]["0"]["amount"]
|
||||
response = client.post(f"/receive?token={token}")
|
||||
response = client.get("/balance")
|
||||
assert response.json()["balance"] == initial_balance + amount
|
||||
251
tests/wallet/test_wallet_auth.py
Normal file
251
tests/wallet/test_wallet_auth.py
Normal file
@@ -0,0 +1,251 @@
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Unit
|
||||
from cashu.core.crypto.keys import random_hash
|
||||
from cashu.core.crypto.secp import PrivateKey
|
||||
from cashu.core.errors import (
|
||||
BlindAuthFailedError,
|
||||
BlindAuthRateLimitExceededError,
|
||||
ClearAuthFailedError,
|
||||
)
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.auth.auth import WalletAuth
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import assert_err
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet():
|
||||
dirpath = Path("test_data/wallet")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
wallet = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet",
|
||||
name="wallet",
|
||||
)
|
||||
await wallet.load_mint()
|
||||
yield wallet
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_require_auth,
|
||||
reason="settings.mint_require_auth is False",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_auth_password(wallet: Wallet):
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=wallet.url,
|
||||
db=wallet.db.db_location,
|
||||
username="asd@asd.com",
|
||||
password="asdasd",
|
||||
)
|
||||
|
||||
requires_auth = await auth_wallet.init_auth_wallet(
|
||||
wallet.mint_info, mint_auth_proofs=False
|
||||
)
|
||||
assert requires_auth
|
||||
|
||||
# expect JWT (CAT) with format ey*.ey*
|
||||
assert auth_wallet.oidc_client.access_token
|
||||
assert auth_wallet.oidc_client.access_token.split(".")[0].startswith("ey")
|
||||
assert auth_wallet.oidc_client.access_token.split(".")[1].startswith("ey")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_require_auth,
|
||||
reason="settings.mint_require_auth is False",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_auth_wrong_password(wallet: Wallet):
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=wallet.url,
|
||||
db=wallet.db.db_location,
|
||||
username="asd@asd.com",
|
||||
password="wrong_password",
|
||||
)
|
||||
|
||||
await assert_err(auth_wallet.init_auth_wallet(wallet.mint_info), "401 Unauthorized")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_require_auth,
|
||||
reason="settings.mint_require_auth is False",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_auth_mint(wallet: Wallet):
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=wallet.url,
|
||||
db=wallet.db.db_location,
|
||||
username="asd@asd.com",
|
||||
password="asdasd",
|
||||
)
|
||||
|
||||
requires_auth = await auth_wallet.init_auth_wallet(wallet.mint_info)
|
||||
assert requires_auth
|
||||
|
||||
await auth_wallet.load_proofs()
|
||||
assert len(auth_wallet.proofs) == auth_wallet.mint_info.bat_max_mint
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_require_auth,
|
||||
reason="settings.mint_require_auth is False",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_auth_mint_manually(wallet: Wallet):
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=wallet.url,
|
||||
db=wallet.db.db_location,
|
||||
username="asd@asd.com",
|
||||
password="asdasd",
|
||||
)
|
||||
|
||||
requires_auth = await auth_wallet.init_auth_wallet(
|
||||
wallet.mint_info, mint_auth_proofs=False
|
||||
)
|
||||
assert requires_auth
|
||||
assert len(auth_wallet.proofs) == 0
|
||||
|
||||
await auth_wallet.mint_blind_auth()
|
||||
assert len(auth_wallet.proofs) == auth_wallet.mint_info.bat_max_mint
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_require_auth,
|
||||
reason="settings.mint_require_auth is False",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_auth_mint_manually_invalid_cat(wallet: Wallet):
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=wallet.url,
|
||||
db=wallet.db.db_location,
|
||||
username="asd@asd.com",
|
||||
password="asdasd",
|
||||
)
|
||||
|
||||
requires_auth = await auth_wallet.init_auth_wallet(
|
||||
wallet.mint_info, mint_auth_proofs=False
|
||||
)
|
||||
assert requires_auth
|
||||
assert len(auth_wallet.proofs) == 0
|
||||
|
||||
# invalidate CAT in the database
|
||||
auth_wallet.oidc_client.access_token = random_hash()
|
||||
|
||||
# this is the code executed in auth_wallet.mint_blind_auth():
|
||||
clear_auth_token = auth_wallet.oidc_client.access_token
|
||||
if not clear_auth_token:
|
||||
raise Exception("No clear auth token available.")
|
||||
|
||||
amounts = auth_wallet.mint_info.bat_max_mint * [1] # 1 AUTH tokens
|
||||
secrets = [hashlib.sha256(os.urandom(32)).hexdigest() for _ in amounts]
|
||||
rs = [PrivateKey(privkey=os.urandom(32), raw=True) for _ in amounts]
|
||||
outputs, rs = auth_wallet._construct_outputs(amounts, secrets, rs)
|
||||
|
||||
# should fail because of invalid CAT
|
||||
await assert_err(
|
||||
auth_wallet.blind_mint_blind_auth(clear_auth_token, outputs),
|
||||
ClearAuthFailedError.detail,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_require_auth,
|
||||
reason="settings.mint_require_auth is False",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_auth_invoice(wallet: Wallet):
|
||||
# should fail, wallet error
|
||||
await assert_err(wallet.mint_quote(10, Unit.sat), "Mint requires blind auth")
|
||||
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=wallet.url,
|
||||
db=wallet.db.db_location,
|
||||
username="asd@asd.com",
|
||||
password="asdasd",
|
||||
)
|
||||
requires_auth = await auth_wallet.init_auth_wallet(wallet.mint_info)
|
||||
assert requires_auth
|
||||
|
||||
await auth_wallet.load_proofs()
|
||||
assert len(auth_wallet.proofs) == auth_wallet.mint_info.bat_max_mint
|
||||
|
||||
wallet.auth_db = auth_wallet.db
|
||||
wallet.auth_keyset_id = auth_wallet.keyset_id
|
||||
|
||||
# should succeed
|
||||
await wallet.mint_quote(10, Unit.sat)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_require_auth,
|
||||
reason="settings.mint_require_auth is False",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_auth_invoice_invalid_bat(wallet: Wallet):
|
||||
# should fail, wallet error
|
||||
await assert_err(wallet.mint_quote(10, Unit.sat), "Mint requires blind auth")
|
||||
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=wallet.url,
|
||||
db=wallet.db.db_location,
|
||||
username="asd@asd.com",
|
||||
password="asdasd",
|
||||
)
|
||||
requires_auth = await auth_wallet.init_auth_wallet(wallet.mint_info)
|
||||
assert requires_auth
|
||||
|
||||
await auth_wallet.load_proofs()
|
||||
assert len(auth_wallet.proofs) == auth_wallet.mint_info.bat_max_mint
|
||||
|
||||
# invalidate blind auth proofs
|
||||
for p in auth_wallet.proofs:
|
||||
await auth_wallet.db.execute(
|
||||
f"UPDATE proofs SET secret = '{random_hash()}' WHERE secret = '{p.secret}'"
|
||||
)
|
||||
|
||||
wallet.auth_db = auth_wallet.db
|
||||
wallet.auth_keyset_id = auth_wallet.keyset_id
|
||||
|
||||
# blind auth failed
|
||||
await assert_err(wallet.mint_quote(10, Unit.sat), BlindAuthFailedError.detail)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not settings.mint_require_auth,
|
||||
reason="settings.mint_require_auth is False",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_auth_rate_limit(wallet: Wallet):
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=wallet.url,
|
||||
db=wallet.db.db_location,
|
||||
username="asd@asd.com",
|
||||
password="asdasd",
|
||||
)
|
||||
requires_auth = await auth_wallet.init_auth_wallet(
|
||||
wallet.mint_info, mint_auth_proofs=False
|
||||
)
|
||||
assert requires_auth
|
||||
|
||||
errored = False
|
||||
for _ in range(100):
|
||||
try:
|
||||
await auth_wallet.mint_blind_auth()
|
||||
except Exception as e:
|
||||
assert BlindAuthRateLimitExceededError.detail in str(e)
|
||||
errored = True
|
||||
break
|
||||
|
||||
assert errored
|
||||
|
||||
# should have minted at least twice
|
||||
assert len(auth_wallet.proofs) > auth_wallet.mint_info.bat_max_mint
|
||||
587
tests/wallet/test_wallet_cli.py
Normal file
587
tests/wallet/test_wallet_cli.py
Normal file
@@ -0,0 +1,587 @@
|
||||
import asyncio
|
||||
from typing import Tuple
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from cashu.core.base import TokenV4
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.cli.cli import cli
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.helpers import (
|
||||
get_real_invoice,
|
||||
is_deprecated_api_only,
|
||||
is_fake,
|
||||
is_regtest,
|
||||
pay_if_regtest,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def cli_prefix():
|
||||
yield ["--wallet", "test_cli_wallet", "--host", settings.mint_url, "--tests"]
|
||||
|
||||
|
||||
def get_bolt11_and_invoice_id_from_invoice_command(output: str) -> Tuple[str, str]:
|
||||
invoice = [
|
||||
line.split(" ")[1] for line in output.split("\n") if line.startswith("Invoice")
|
||||
][0]
|
||||
invoice_id = [
|
||||
line.split(" ")[-1] for line in output.split("\n") if line.startswith("You can")
|
||||
][0]
|
||||
return invoice, invoice_id
|
||||
|
||||
|
||||
def get_invoice_from_invoices_command(output: str) -> dict[str, str]:
|
||||
splitted = output.split("\n")
|
||||
removed_empty_and_hiphens = [
|
||||
value for value in splitted if value and not value.startswith("-----")
|
||||
]
|
||||
# filter only lines that have ": " in them
|
||||
removed_empty_and_hiphens = [
|
||||
value for value in removed_empty_and_hiphens if ": " in value
|
||||
]
|
||||
dict_output = {
|
||||
f"{value.split(': ')[0]}": value.split(": ")[1]
|
||||
for value in removed_empty_and_hiphens
|
||||
}
|
||||
|
||||
return dict_output
|
||||
|
||||
|
||||
async def reset_invoices(wallet: Wallet):
|
||||
await wallet.db.execute("DELETE FROM bolt11_melt_quotes")
|
||||
await wallet.db.execute("DELETE FROM bolt11_mint_quotes")
|
||||
|
||||
|
||||
async def init_wallet():
|
||||
settings.debug = False
|
||||
wallet = await Wallet.with_db(
|
||||
url=settings.mint_url,
|
||||
db="test_data/test_cli_wallet",
|
||||
name="test_cli_wallet",
|
||||
)
|
||||
await wallet.load_proofs()
|
||||
return wallet
|
||||
|
||||
|
||||
def test_info(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "info"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INFO")
|
||||
print(result.output)
|
||||
result.output.startswith(f"Version: {settings.version}")
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_info_with_mint(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "info", "--mint"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INFO --MINT")
|
||||
print(result.output)
|
||||
assert "Mint name" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_info_with_mnemonic(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "info", "--mnemonic"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INFO --MNEMONIC")
|
||||
print(result.output)
|
||||
assert "Mnemonic" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_balance(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "balance"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("------ BALANCE ------")
|
||||
print(result.output)
|
||||
w = asyncio.run(init_wallet())
|
||||
assert f"Balance: {w.available_balance} sat" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_fake, reason="only works with FakeWallet")
|
||||
def test_pay_invoice_regtest(mint, cli_prefix):
|
||||
invoice_dict = get_real_invoice(10)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "pay", invoice_payment_request, "-y"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("PAY INVOICE")
|
||||
print(result.output)
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
def test_invoice(mint, cli_prefix):
|
||||
if settings.debug_mint_only_deprecated:
|
||||
pytest.skip("only works with v1 API")
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "1000"],
|
||||
)
|
||||
|
||||
wallet = asyncio.run(init_wallet())
|
||||
assert wallet.available_balance >= 1000
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_invoice_return_immediately(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "-n", "1000"],
|
||||
)
|
||||
|
||||
assert result.exception is None
|
||||
|
||||
invoice, invoice_id = get_bolt11_and_invoice_id_from_invoice_command(result.output)
|
||||
asyncio.run(pay_if_regtest(invoice))
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "1000", "--id", invoice_id],
|
||||
)
|
||||
assert result.exception is None
|
||||
|
||||
wallet = asyncio.run(init_wallet())
|
||||
assert wallet.available_balance >= 1000
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API")
|
||||
def test_invoice_with_memo(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "-n", "1000", "-m", "test memo"],
|
||||
)
|
||||
assert result.exception is None
|
||||
|
||||
# find word starting with ln in the output
|
||||
lines = result.output.split("\n")
|
||||
invoice_str = ""
|
||||
for line in lines:
|
||||
for word in line.split(" "):
|
||||
if word.startswith("ln"):
|
||||
invoice_str = word
|
||||
break
|
||||
if not invoice_str:
|
||||
raise Exception("No invoice found in the output")
|
||||
invoice_obj = bolt11.decode(invoice_str)
|
||||
assert invoice_obj.amount_msat == 1000_000
|
||||
assert invoice_obj.description == "test memo"
|
||||
|
||||
|
||||
def test_invoice_with_split(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
*cli_prefix,
|
||||
"invoice",
|
||||
"10",
|
||||
"-s",
|
||||
"1",
|
||||
"-n",
|
||||
],
|
||||
)
|
||||
assert result.exception is None
|
||||
|
||||
invoice, invoice_id = get_bolt11_and_invoice_id_from_invoice_command(result.output)
|
||||
asyncio.run(pay_if_regtest(invoice))
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoice", "10", "-s", "1", "--id", invoice_id],
|
||||
)
|
||||
assert result.exception is None
|
||||
|
||||
assert result.exception is None
|
||||
wallet = asyncio.run(init_wallet())
|
||||
assert wallet.proof_amounts.count(1) >= 10
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_fake, reason="only on fakewallet")
|
||||
def test_invoices_with_minting(cli_prefix):
|
||||
# arrange
|
||||
wallet1 = asyncio.run(init_wallet())
|
||||
asyncio.run(reset_invoices(wallet=wallet1))
|
||||
mint_quote = asyncio.run(wallet1.request_mint(64))
|
||||
asyncio.run(pay_if_regtest(mint_quote.request))
|
||||
# act
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices", "--mint"],
|
||||
)
|
||||
|
||||
# assert
|
||||
print("INVOICES --mint")
|
||||
assert result.exception is None
|
||||
assert result.exit_code == 0
|
||||
assert "Received 64 sat" in result.output
|
||||
|
||||
|
||||
def test_invoices_without_minting(cli_prefix):
|
||||
# arrange
|
||||
wallet1 = asyncio.run(init_wallet())
|
||||
asyncio.run(reset_invoices(wallet=wallet1))
|
||||
mint_quote = asyncio.run(wallet1.request_mint(64))
|
||||
|
||||
# act
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices"],
|
||||
)
|
||||
|
||||
# assert
|
||||
print("INVOICES")
|
||||
assert result.exception is None
|
||||
assert result.exit_code == 0
|
||||
assert "No invoices found." not in result.output
|
||||
assert "ID" in result.output
|
||||
assert "State" in result.output
|
||||
assert get_invoice_from_invoices_command(result.output)["ID"] == mint_quote.quote
|
||||
assert get_invoice_from_invoices_command(result.output)["State"] == str(
|
||||
mint_quote.state
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_fake, reason="only on fakewallet")
|
||||
def test_invoices_with_onlypaid_option(cli_prefix):
|
||||
# arrange
|
||||
wallet1 = asyncio.run(init_wallet())
|
||||
asyncio.run(reset_invoices(wallet=wallet1))
|
||||
asyncio.run(wallet1.request_mint(64))
|
||||
|
||||
# act
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices", "--only-paid"],
|
||||
)
|
||||
|
||||
# assert
|
||||
print("INVOICES --only-paid")
|
||||
assert result.exception is None
|
||||
assert result.exit_code == 0
|
||||
assert "No invoices found." in result.output
|
||||
|
||||
|
||||
def test_invoices_with_onlypaid_option_without_minting(cli_prefix):
|
||||
# arrange
|
||||
wallet1 = asyncio.run(init_wallet())
|
||||
asyncio.run(reset_invoices(wallet=wallet1))
|
||||
asyncio.run(wallet1.request_mint(64))
|
||||
|
||||
# act
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices", "--only-paid"],
|
||||
)
|
||||
|
||||
# assert
|
||||
print("INVOICES --only-paid")
|
||||
assert result.exception is None
|
||||
assert result.exit_code == 0
|
||||
assert "No invoices found." in result.output
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_fake, reason="only on fakewallet")
|
||||
def test_invoices_with_onlyunpaid_option(cli_prefix):
|
||||
# arrange
|
||||
wallet1 = asyncio.run(init_wallet())
|
||||
asyncio.run(reset_invoices(wallet=wallet1))
|
||||
asyncio.run(wallet1.request_mint(64))
|
||||
|
||||
# act
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices", "--only-unpaid", "--mint"],
|
||||
)
|
||||
|
||||
# assert
|
||||
print("INVOICES --only-unpaid --mint")
|
||||
assert result.exception is None
|
||||
assert result.exit_code == 0
|
||||
assert "No invoices found." in result.output
|
||||
|
||||
|
||||
def test_invoices_with_onlyunpaid_option_without_minting(cli_prefix):
|
||||
# arrange
|
||||
wallet1 = asyncio.run(init_wallet())
|
||||
asyncio.run(reset_invoices(wallet=wallet1))
|
||||
mint_quote = asyncio.run(wallet1.request_mint(64))
|
||||
|
||||
# act
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices", "--only-unpaid"],
|
||||
)
|
||||
|
||||
# assert
|
||||
print("INVOICES --only-unpaid")
|
||||
assert result.exception is None
|
||||
assert result.exit_code == 0
|
||||
assert "No invoices found." not in result.output
|
||||
assert "ID" in result.output
|
||||
assert "State" in result.output
|
||||
assert get_invoice_from_invoices_command(result.output)["ID"] == mint_quote.quote
|
||||
assert get_invoice_from_invoices_command(result.output)["State"] == str(
|
||||
mint_quote.state
|
||||
)
|
||||
|
||||
|
||||
def test_invoices_with_both_onlypaid_and_onlyunpaid_options(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices", "--only-paid", "--only-unpaid"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INVOICES --only-paid --only-unpaid")
|
||||
assert result.exit_code == 0
|
||||
assert (
|
||||
"You should only choose one option: either --only-paid or --only-unpaid"
|
||||
in result.output
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_fake, reason="only on fakewallet")
|
||||
def test_invoices_with_pending_option(cli_prefix):
|
||||
# arrange
|
||||
wallet1 = asyncio.run(init_wallet())
|
||||
asyncio.run(reset_invoices(wallet=wallet1))
|
||||
asyncio.run(wallet1.request_mint(64))
|
||||
|
||||
# act
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices", "--pending", "--mint"],
|
||||
)
|
||||
|
||||
# assert
|
||||
print("INVOICES --pending --mint")
|
||||
assert result.exception is None
|
||||
assert result.exit_code == 0
|
||||
assert "No invoices found." in result.output
|
||||
|
||||
|
||||
def test_invoices_with_pending_option_without_minting(cli_prefix):
|
||||
# arrange
|
||||
wallet1 = asyncio.run(init_wallet())
|
||||
asyncio.run(reset_invoices(wallet=wallet1))
|
||||
mint_quote = asyncio.run(wallet1.request_mint(64))
|
||||
|
||||
# act
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "invoices", "--pending"],
|
||||
)
|
||||
|
||||
# assert
|
||||
print("INVOICES --pending")
|
||||
assert result.exception is None
|
||||
assert result.exit_code == 0
|
||||
assert "No invoices found." not in result.output
|
||||
assert "ID" in result.output
|
||||
assert "State" in result.output
|
||||
assert get_invoice_from_invoices_command(result.output)["ID"] == mint_quote.quote
|
||||
assert get_invoice_from_invoices_command(result.output)["State"] == str(
|
||||
mint_quote.state
|
||||
)
|
||||
|
||||
|
||||
def test_wallets(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "wallets"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("WALLETS")
|
||||
# on github this is empty
|
||||
if len(result.output):
|
||||
assert "wallet" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_send(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "send", "10"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("test_send", result.output)
|
||||
token_str = result.output.split("\n")[0]
|
||||
assert "cashuB" in token_str, "output does not have a token"
|
||||
token = TokenV4.deserialize(token_str).to_tokenv3()
|
||||
assert token.token[0].proofs[0].dleq is None, "dleq included"
|
||||
|
||||
|
||||
def test_send_with_dleq(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "send", "10", "--dleq"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("test_send_with_dleq", result.output)
|
||||
token_str = result.output.split("\n")[0]
|
||||
assert "cashuB" in token_str, "output does not have a token"
|
||||
token = TokenV4.deserialize(token_str).to_tokenv3()
|
||||
assert token.token[0].proofs[0].dleq is not None, "no dleq included"
|
||||
|
||||
|
||||
def test_send_legacy(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "send", "10", "--legacy"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("test_send_legacy", result.output)
|
||||
# this is the legacy token in the output
|
||||
token_str = result.output.split("\n")[0]
|
||||
assert token_str.startswith("cashuAey"), "output is not as expected"
|
||||
|
||||
|
||||
def test_send_offline(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "send", "2", "--offline"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("SEND")
|
||||
print("test_send_without_split", result.output)
|
||||
assert "cashuB" in result.output, "output does not have a token"
|
||||
|
||||
|
||||
def test_send_too_much(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "send", "100000"],
|
||||
)
|
||||
assert "Balance too low" in str(result.exception)
|
||||
|
||||
|
||||
def test_receive_tokenv3(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjAwOWExZjI5MzI1M2U0MWUiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICI0NzlkY2E0MzUzNzU4MTM4N2Q1ODllMDU1MGY0Y2Q2MjFmNjE0MDM1MGY5M2Q4ZmI1OTA2YjJlMGRiNmRjYmI3IiwgIkMiOiAiMDM1MGQ0ZmI0YzdiYTMzNDRjMWRjYWU1ZDExZjNlNTIzZGVkOThmNGY4ODdkNTQwZmYyMDRmNmVlOWJjMjkyZjQ1In0sIHsiaWQiOiAiMDA5YTFmMjkzMjUzZTQxZSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIjZjNjAzNDgwOGQyNDY5N2IyN2YxZTEyMDllNjdjNjVjNmE2MmM2Zjc3NGI4NWVjMGQ5Y2Y3MjE0M2U0NWZmMDEiLCAiQyI6ICIwMjZkNDlhYTE0MmFlNjM1NWViZTJjZGQzYjFhOTdmMjE1MDk2NTlkMDE3YWU0N2FjNDY3OGE4NWVkY2E4MGMxYmQifV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyJ9XX0=" # noqa
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
*cli_prefix,
|
||||
"receive",
|
||||
token,
|
||||
],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("RECEIVE")
|
||||
print(result.output)
|
||||
|
||||
|
||||
def test_nostr_send(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
*cli_prefix,
|
||||
"send",
|
||||
"1",
|
||||
"-n",
|
||||
"aafa164a8ab54a6b6c67bbac98a5d5aec7ea4075af8928a11478ab9d74aec4ca",
|
||||
"-y",
|
||||
],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("NOSTR_SEND")
|
||||
print(result.output)
|
||||
|
||||
|
||||
def test_pending(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "pending"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print(result.output)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_selfpay(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "selfpay"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print(result.output)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_send_with_lock(mint, cli_prefix):
|
||||
# call "cashu locks" first and get the lock
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "locks"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("test_send_with_lock", result.output)
|
||||
# iterate through all words and get the word that starts with "P2PK:"
|
||||
lock = None
|
||||
for word in result.output.split(" "):
|
||||
# strip the word
|
||||
word = word.strip()
|
||||
if word.startswith("P2PK:"):
|
||||
lock = word
|
||||
break
|
||||
assert lock is not None, "no lock found"
|
||||
pubkey = lock.split(":")[1]
|
||||
|
||||
# now lock the token
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "send", "10", "--lock", lock],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("test_send_with_lock", result.output)
|
||||
token_str = result.output.split("\n")[0]
|
||||
assert "cashuB" in token_str, "output does not have a token"
|
||||
token = TokenV4.deserialize(token_str).to_tokenv3()
|
||||
assert pubkey in token.token[0].proofs[0].secret
|
||||
552
tests/wallet/test_wallet_htlc.py
Normal file
552
tests/wallet/test_wallet_htlc.py
Normal file
@@ -0,0 +1,552 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import hashlib
|
||||
import secrets
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import HTLCWitness, Proof
|
||||
from cashu.core.crypto.secp import PrivateKey
|
||||
from cashu.core.htlc import HTLCSecret
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import SigFlags
|
||||
from cashu.core.secret import SecretKind
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
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")
|
||||
|
||||
|
||||
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 wallet1():
|
||||
wallet1 = await Wallet1.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_1", "wallet1"
|
||||
)
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet2.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_2", "wallet2"
|
||||
)
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet2.load_mint()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_htlc_secret(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
assert secret.data == preimage_hash
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
for p in send_proofs:
|
||||
assert HTLCSecret.deserialize(p.secret).data == preimage_hash
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
for p in send_proofs:
|
||||
p.witness = HTLCWitness(preimage=preimage).json()
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=f"{preimage[:-5]}11111"
|
||||
) # wrong preimage
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
for p in send_proofs:
|
||||
p.witness = HTLCWitness(preimage=preimage).json()
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs), "Mint Error: HTLC preimage does not match"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage, hashlock_pubkeys=[pubkey_wallet1]
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
for p in send_proofs:
|
||||
p.witness = HTLCWitness(preimage=preimage).json()
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: no signatures in proof.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage, hashlock_pubkeys=[pubkey_wallet1]
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(
|
||||
preimage=preimage, signatures=[f"{s[:-5]}11111"]
|
||||
).json() # wrong signature
|
||||
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage, hashlock_pubkeys=[pubkey_wallet1]
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s]).json()
|
||||
|
||||
await wallet1.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_2_of_1_signatures(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
|
||||
hashlock_n_sigs=1,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
|
||||
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_2_of_2_signatures(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
|
||||
hashlock_n_sigs=2,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
|
||||
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_2_of_2_signatures_with_duplicate_pubkeys(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = pubkey_wallet1
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
|
||||
hashlock_n_sigs=2,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
|
||||
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: pubkeys must be unique.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_3_of_3_signatures_but_only_2_provided(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
|
||||
hashlock_n_sigs=3,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
|
||||
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (2) or signatures (2) present for n_sigs (3).",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_with_2_of_3_signatures_with_2_valid_and_1_invalid_provided(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
privatekey_wallet3 = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
assert privatekey_wallet3.pubkey
|
||||
pubkey_wallet3 = privatekey_wallet3.pubkey.serialize().hex()
|
||||
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
|
||||
hashlock_n_sigs=2,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures3 = [f"{s[:-5]}11111" for s in signatures1] # wrong signature
|
||||
for p, s1, s2, s3 in zip(send_proofs, signatures1, signatures2, signatures3):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2, s3]).json()
|
||||
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
locktime_seconds=2,
|
||||
locktime_pubkeys=[pubkey_wallet1],
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s]).json()
|
||||
|
||||
# should error because we used wallet2 signatures for the hash lock
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met",
|
||||
)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
# should succeed since lock time has passed and we provided wallet1 signature for timelock
|
||||
await wallet1.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
locktime_seconds=2,
|
||||
locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2],
|
||||
locktime_n_sigs=2,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(
|
||||
preimage=preimage, signatures=[f"{s[:-5]}11111"]
|
||||
).json() # wrong signature
|
||||
|
||||
# should error because we used wallet2 signatures for the hash lock
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
# should fail since lock time has passed and we provided not enough signatures for the timelock locktime_n_sigs
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met. 1 < 2.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_timelock_2_of_2_signatures(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Testing the 2-of-2 timelock (refund) signature case."""
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
locktime_seconds=2,
|
||||
locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2],
|
||||
locktime_n_sigs=2,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
send_proofs_copy = send_proofs.copy()
|
||||
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s]).json()
|
||||
|
||||
# should error because we used wallet2 signatures for the hash lock
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
# locktime has passed
|
||||
|
||||
# should fail. lock time has passed but we provided only wallet1 signature for timelock, we need 2 though
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
# let's add the second signature
|
||||
send_proofs_copy = wallet2.sign_p2pk_sig_inputs(send_proofs_copy)
|
||||
|
||||
# now we can redeem it
|
||||
await wallet1.redeem(send_proofs_copy)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_sigall_behavior(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Test HTLC with SIG_ALL flag, requiring signatures on both inputs and outputs."""
|
||||
# Mint tokens for testing
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Setup HTLC parameters
|
||||
preimage = "00000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
|
||||
# Create HTLC lock with SIG_ALL flag
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
hashlock_n_sigs=1,
|
||||
)
|
||||
|
||||
# Modify the secret to use SIG_ALL
|
||||
secret.tags["sigflag"] = SigFlags.SIG_ALL.value
|
||||
|
||||
# Send tokens with this HTLC lock
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
# verify sigflag is SIG_ALL
|
||||
assert HTLCSecret.from_secret(secret).kind == SecretKind.HTLC.value
|
||||
assert HTLCSecret.from_secret(secret).sigflag == SigFlags.SIG_ALL
|
||||
|
||||
# first redeem fails because no preimage
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs), "Mint Error: no HTLC preimage provided"
|
||||
)
|
||||
|
||||
# we add the preimage to the proof
|
||||
for p in send_proofs:
|
||||
p.witness = HTLCWitness(preimage=preimage).json()
|
||||
|
||||
# Should succeed, redeem adds signatures to the proof
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_n_sigs_refund_locktime(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Test HTLC with n_sigs_refund parameter requiring multiple signatures for refund after locktime."""
|
||||
# Create a third wallet for the third signature
|
||||
wallet3 = await Wallet.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_htlc_3", "wallet3"
|
||||
)
|
||||
await migrate_databases(wallet3.db, migrations)
|
||||
wallet3.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet3.load_mint()
|
||||
|
||||
# Mint tokens for testing
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Setup parameters
|
||||
preimage = "00000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey()
|
||||
|
||||
# Wrong preimage - making it so we can only spend via locktime
|
||||
wrong_preimage = "11111111111111111111111111111111"
|
||||
|
||||
# Create HTLC with:
|
||||
# 1. Timelock in the past
|
||||
# 2. Three refund pubkeys with 2-of-3 signature requirement
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=wrong_preimage, # this ensures we can't redeem via preimage
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
locktime_seconds=-200000,
|
||||
locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
|
||||
locktime_n_sigs=2, # require 2 of 3 signatures for refund
|
||||
)
|
||||
|
||||
# # Send tokens with this lock
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
|
||||
# # First, try correct preimage but should fail as we're using wrong preimage hash
|
||||
# for p in send_proofs:
|
||||
# p.witness = HTLCWitness(preimage=preimage).json()
|
||||
|
||||
# await assert_err(
|
||||
# wallet2.redeem(send_proofs), "Mint Error: HTLC preimage does not match"
|
||||
# )
|
||||
|
||||
# # Wait for locktime to pass
|
||||
# await asyncio.sleep(2)
|
||||
|
||||
# Try redeeming with only 1 signature after locktime
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs_copy)
|
||||
for p, sig in zip(send_proofs_copy, signatures1):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[sig]).json()
|
||||
|
||||
# Should fail because we need 2 signatures
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs_copy),
|
||||
"Mint Error: not enough pubkeys (3) or signatures (1) present for n_sigs (2)",
|
||||
)
|
||||
|
||||
# Make a fresh copy and add 2 signatures
|
||||
send_proofs_copy2 = copy.deepcopy(send_proofs)
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs_copy2)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs_copy2)
|
||||
|
||||
for p, sig1, sig2 in zip(send_proofs_copy2, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[sig1, sig2]).json()
|
||||
|
||||
# Should succeed with 2 of 3 signatures after locktime
|
||||
await wallet1.redeem(send_proofs_copy2)
|
||||
140
tests/wallet/test_wallet_lightning.py
Normal file
140
tests/wallet/test_wallet_lightning.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from typing import List, Union
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.errors import CashuError
|
||||
from cashu.wallet.lightning import LightningWallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
get_real_invoice,
|
||||
is_deprecated_api_only,
|
||||
is_fake,
|
||||
is_regtest,
|
||||
pay_if_regtest,
|
||||
)
|
||||
|
||||
|
||||
async def assert_err(f, msg: Union[str, CashuError]):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
error_message: str = str(exc.args[0])
|
||||
if isinstance(msg, CashuError):
|
||||
if msg.detail not in error_message:
|
||||
raise Exception(
|
||||
f"CashuError. Expected error: {msg.detail}, got: {error_message}"
|
||||
)
|
||||
return
|
||||
if msg not in error_message:
|
||||
raise Exception(f"Expected error: {msg}, got: {error_message}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
async def reset_wallet_db(wallet: LightningWallet):
|
||||
await wallet.db.execute("DELETE FROM proofs")
|
||||
await wallet.db.execute("DELETE FROM proofs_used")
|
||||
await wallet.db.execute("DELETE FROM keysets")
|
||||
await wallet.load_mint()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet():
|
||||
wallet = await LightningWallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet.async_init()
|
||||
yield wallet
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_invoice(wallet: LightningWallet):
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
assert invoice.payment_request.startswith("ln")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API")
|
||||
async def test_create_invoice_with_description(wallet: LightningWallet):
|
||||
invoice = await wallet.create_invoice(64, "test description")
|
||||
assert invoice.payment_request
|
||||
assert invoice.payment_request.startswith("ln")
|
||||
invoiceObj = bolt11.decode(invoice.payment_request)
|
||||
assert invoiceObj.description == "test description"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_check_invoice_internal(wallet: LightningWallet):
|
||||
# fill wallet
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
status = await wallet.get_invoice_status(invoice.payment_request)
|
||||
assert status.settled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_check_invoice_external(wallet: LightningWallet):
|
||||
# fill wallet
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
status = await wallet.get_invoice_status(invoice.payment_request)
|
||||
assert not status.settled
|
||||
await pay_if_regtest(invoice.payment_request)
|
||||
status = await wallet.get_invoice_status(invoice.payment_request)
|
||||
assert status.settled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_pay_invoice_internal(wallet: LightningWallet):
|
||||
# fill wallet
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
await wallet.get_invoice_status(invoice.payment_request)
|
||||
assert wallet.available_balance >= 64
|
||||
|
||||
# pay invoice
|
||||
invoice2 = await wallet.create_invoice(16)
|
||||
assert invoice2.payment_request
|
||||
status = await wallet.pay_invoice(invoice2.payment_request)
|
||||
|
||||
assert status.settled
|
||||
|
||||
# check payment
|
||||
status = await wallet.get_payment_status(invoice2.payment_request)
|
||||
assert status.settled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_pay_invoice_external(wallet: LightningWallet):
|
||||
# fill wallet
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
await pay_if_regtest(invoice.payment_request)
|
||||
status = await wallet.get_invoice_status(invoice.payment_request)
|
||||
assert status.settled
|
||||
assert wallet.available_balance >= 64
|
||||
|
||||
# pay invoice
|
||||
invoice_real = get_real_invoice(16)
|
||||
status = await wallet.pay_invoice(invoice_real["payment_request"])
|
||||
|
||||
assert status.settled
|
||||
|
||||
# check payment)
|
||||
assert status.settled
|
||||
704
tests/wallet/test_wallet_p2pk.py
Normal file
704
tests/wallet/test_wallet_p2pk.py
Normal file
@@ -0,0 +1,704 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import secrets
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from coincurve import PrivateKey as CoincurvePrivateKey
|
||||
|
||||
from cashu.core.base import P2PKWitness, Proof
|
||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import P2PKSecret, SigFlags
|
||||
from cashu.core.secret import Secret, SecretKind, Tags
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import is_deprecated_api_only, 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")
|
||||
|
||||
|
||||
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 wallet1():
|
||||
wallet1 = await Wallet1.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_1", "wallet1"
|
||||
)
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet2.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_2", "wallet2"
|
||||
)
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet2.load_mint()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_p2pk_pubkey(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
PublicKey(bytes.fromhex(pubkey), raw=True)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
proof_states = await wallet2.check_proof_state(send_proofs)
|
||||
assert all([p.spent for p in proof_states.states])
|
||||
|
||||
if not is_deprecated_api_only:
|
||||
for state in proof_states.states:
|
||||
assert state.witness is not None
|
||||
witness_obj = json.loads(state.witness)
|
||||
assert len(witness_obj["signatures"]) == 1
|
||||
assert len(witness_obj["signatures"][0]) == 128
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, sig_all=True
|
||||
) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# receiver side: wrong private key
|
||||
wallet2.private_key = PrivateKey() # wrong private key
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_short_locktime_receive_with_wrong_private_key(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, locktime_seconds=2
|
||||
) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# receiver side: wrong private key
|
||||
wallet2.private_key = PrivateKey() # wrong private key
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# should succeed because even with the wrong private key we
|
||||
# can redeem the tokens after the locktime
|
||||
await wallet2.redeem(send_proofs_copy)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
assert garbage_pubkey
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
locktime_seconds=2, # locktime
|
||||
tags=Tags([["refund", pubkey_wallet2]]), # refund pubkey
|
||||
) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
# receiver side: can't redeem since we used a garbage pubkey
|
||||
# and locktime has not passed
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# we can now redeem because of the refund locktime
|
||||
await wallet2.redeem(send_proofs_copy)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
garbage_pubkey_2 = PrivateKey().pubkey
|
||||
assert garbage_pubkey
|
||||
assert garbage_pubkey_2
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
locktime_seconds=2, # locktime
|
||||
tags=Tags([["refund", garbage_pubkey_2.serialize().hex()]]), # refund pubkey
|
||||
) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
# receiver side: can't redeem since we used a garbage pubkey
|
||||
# and locktime has not passed
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# we still can't redeem it because we used garbage_pubkey_2 as a refund pubkey
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs_copy),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_second_refund_pubkey(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
assert garbage_pubkey
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
locktime_seconds=2, # locktime
|
||||
tags=Tags(
|
||||
[["refund", pubkey_wallet2, pubkey_wallet1]]
|
||||
), # multiple refund pubkeys
|
||||
) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
# receiver side: can't redeem since we used a garbage pubkey
|
||||
# and locktime has not passed
|
||||
# WALLET WILL ADD A SIGNATURE BECAUSE IT SEES ITS REFUND PUBKEY (it adds a signature even though the locktime hasn't passed)
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# we can now redeem because of the refund locktime
|
||||
await wallet1.redeem(send_proofs_copy)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_2_of_2_refund_pubkeys(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
"""Testing the case where we expect a 2-of-2 signature from the refund pubkeys"""
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
assert garbage_pubkey
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
locktime_seconds=2, # locktime
|
||||
tags=Tags(
|
||||
[["refund", pubkey_wallet2, pubkey_wallet1], ["n_sigs_refund", "2"]],
|
||||
), # multiple refund pubkeys
|
||||
) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# we need to copy the send_proofs because the redeem function
|
||||
# modifies the send_proofs in place by adding the signatures
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
send_proofs_copy2 = copy.deepcopy(send_proofs)
|
||||
# receiver side: can't redeem since we used a garbage pubkey
|
||||
# and locktime has not passed
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# now is the refund time, but we can't redeem it because we need 2 signatures
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs_copy),
|
||||
"not enough pubkeys (2) or signatures (1) present for n_sigs (2)",
|
||||
)
|
||||
|
||||
# let's add the second signature
|
||||
send_proofs_copy2 = wallet2.sign_p2pk_sig_inputs(send_proofs_copy2)
|
||||
|
||||
# now we can redeem it
|
||||
await wallet1.redeem(send_proofs_copy2)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
|
||||
)
|
||||
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# add signatures of wallet1
|
||||
send_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
# here we add the signatures of wallet2
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
|
||||
)
|
||||
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# add signatures of wallet2 – this is a duplicate signature
|
||||
send_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)
|
||||
# wallet does not add a second signature if it finds its own signature already in the witness
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_two_signatures_same_pubkey(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
# we generate two different signatures from the same private key
|
||||
mint_quote = await wallet2.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet2.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
|
||||
)
|
||||
|
||||
_, send_proofs = await wallet2.swap_to_send(
|
||||
wallet2.proofs, 1, secret_lock=secret_lock
|
||||
)
|
||||
assert len(send_proofs) == 1
|
||||
proof = send_proofs[0]
|
||||
# create coincurve private key so we can sign the message
|
||||
coincurve_privatekey2 = CoincurvePrivateKey(
|
||||
bytes.fromhex(wallet2.private_key.serialize())
|
||||
)
|
||||
# check if private keys are the same
|
||||
assert coincurve_privatekey2.to_hex() == wallet2.private_key.serialize()
|
||||
|
||||
msg = hashlib.sha256(proof.secret.encode("utf-8")).digest()
|
||||
coincurve_signature = coincurve_privatekey2.sign_schnorr(msg)
|
||||
|
||||
# add signatures of wallet2 – this is a duplicate signature
|
||||
send_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
# the signatures from coincurve are not the same as the ones from wallet2
|
||||
assert coincurve_signature.hex() != proof.p2pksigs[0]
|
||||
|
||||
# verify both signatures:
|
||||
assert PublicKey(bytes.fromhex(pubkey_wallet2), raw=True).schnorr_verify(
|
||||
msg, bytes.fromhex(proof.p2pksigs[0]), None, raw=True
|
||||
)
|
||||
assert PublicKey(bytes.fromhex(pubkey_wallet2), raw=True).schnorr_verify(
|
||||
msg, coincurve_signature, None, raw=True
|
||||
)
|
||||
|
||||
# add coincurve signature, and the wallet2 signature will be added during .redeem
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[coincurve_signature.hex()]).json()
|
||||
|
||||
# here we add the signatures of wallet2
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs), "Mint Error: signature threshold not met. 1 < 2."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2)",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
assert garbage_pubkey
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2,
|
||||
tags=Tags([["pubkeys", pubkey_wallet1, garbage_pubkey.serialize().hex()]]),
|
||||
n_sigs=3,
|
||||
)
|
||||
# create locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# add signatures of wallet1
|
||||
send_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
# here we add the signatures of wallet2
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (3) or signatures (2) present for n_sigs (3)",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet2]]), n_sigs=2
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
await assert_err(wallet2.redeem(send_proofs), "Mint Error: pubkeys must be unique.")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_with_wrong_first_private_key(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
wrong_pubklic_key = PrivateKey().pubkey
|
||||
assert wrong_pubklic_key
|
||||
wrong_public_key_hex = wrong_pubklic_key.serialize().hex()
|
||||
|
||||
assert wrong_public_key_hex != pubkey_wallet2
|
||||
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=Tags([["pubkeys", wrong_public_key_hex]]), n_sigs=2
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
|
||||
def test_tags():
|
||||
tags = Tags(
|
||||
[["key1", "value1"], ["key2", "value2", "value2_1"], ["key2", "value3"]]
|
||||
)
|
||||
assert tags.get_tag("key1") == "value1"
|
||||
assert tags["key1"] == "value1"
|
||||
assert tags.get_tag("key2") == "value2"
|
||||
assert tags["key2"] == "value2"
|
||||
assert tags.get_tag("key3") is None
|
||||
assert tags["key3"] is None
|
||||
assert tags.get_tag_all("key2") == ["value2", "value2_1", "value3"]
|
||||
|
||||
# set multiple values of the same key
|
||||
tags["key3"] = "value3"
|
||||
assert tags.get_tag_all("key3") == ["value3"]
|
||||
tags["key4"] = ["value4", "value4_2"]
|
||||
assert tags.get_tag_all("key4") == ["value4", "value4_2"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_secret_initialized_with_tags(wallet1: Wallet):
|
||||
tags = Tags([["locktime", "100"], ["n_sigs", "3"], ["sigflag", "SIG_ALL"]])
|
||||
pubkey = PrivateKey().pubkey
|
||||
assert pubkey
|
||||
secret = await wallet1.create_p2pk_lock(
|
||||
data=pubkey.serialize().hex(),
|
||||
tags=tags,
|
||||
)
|
||||
assert secret.locktime == 100
|
||||
assert secret.n_sigs == 3
|
||||
assert secret.sigflag == SigFlags.SIG_ALL
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_secret_initialized_with_arguments(wallet1: Wallet):
|
||||
pubkey = PrivateKey().pubkey
|
||||
assert pubkey
|
||||
secret = await wallet1.create_p2pk_lock(
|
||||
data=pubkey.serialize().hex(),
|
||||
locktime_seconds=100,
|
||||
n_sigs=3,
|
||||
sig_all=True,
|
||||
)
|
||||
assert secret.locktime
|
||||
assert secret.locktime > 1689000000
|
||||
assert secret.n_sigs == 3
|
||||
assert secret.sigflag == SigFlags.SIG_ALL
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_verify_is_p2pk_input(wallet1: Wallet1):
|
||||
"""Test the wallet correctly identifies P2PK inputs."""
|
||||
# Mint tokens to the wallet
|
||||
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 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
|
||||
)
|
||||
|
||||
# Now get a proof and check if it's detected as P2PK
|
||||
proof = send_proofs[0]
|
||||
|
||||
# This tests the internal method that recognizes a P2PK input
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
assert secret.kind == SecretKind.P2PK.value, "Secret should be of kind P2PK"
|
||||
|
||||
# We can verify that we can convert it to a P2PKSecret
|
||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||
assert p2pk_secret.data == pubkey, "P2PK secret data should contain the pubkey"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_verify_p2pk_sigflag_is_sig_inputs(wallet1: Wallet1):
|
||||
"""Test the wallet correctly identifies the SIG_INPUTS flag."""
|
||||
# Mint tokens to the wallet
|
||||
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 p2pk lock with SIG_INPUTS (default)
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Check if sigflag is correctly identified as SIG_INPUTS
|
||||
proof = send_proofs[0]
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||
|
||||
assert p2pk_secret.sigflag == SigFlags.SIG_INPUTS, "Sigflag should be SIG_INPUTS"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_verify_p2pk_sigflag_is_sig_all(wallet1: Wallet1):
|
||||
"""Test the wallet correctly identifies the SIG_ALL flag."""
|
||||
# Mint tokens to the wallet
|
||||
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 p2pk lock with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Check if sigflag is correctly identified as SIG_ALL
|
||||
proof = send_proofs[0]
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||
|
||||
assert p2pk_secret.sigflag == SigFlags.SIG_ALL, "Sigflag should be SIG_ALL"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_3_of_3_refund_pubkeys(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
"""Testing the case where we expect a 3-of-3 signature from the refund pubkeys"""
|
||||
# Create a third wallet for this test
|
||||
wallet3 = await Wallet.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_3", "wallet3"
|
||||
)
|
||||
await migrate_databases(wallet3.db, migrations)
|
||||
wallet3.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet3.load_mint()
|
||||
|
||||
# Get tokens and create public keys for all wallets
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey()
|
||||
|
||||
# Create an unspendable lock with refund conditions requiring 3 signatures
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
assert garbage_pubkey
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
locktime_seconds=2, # locktime
|
||||
tags=Tags(
|
||||
[
|
||||
["refund", pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
|
||||
["n_sigs_refund", "3"],
|
||||
],
|
||||
), # multiple refund pubkeys with required 3 signatures
|
||||
)
|
||||
|
||||
# Send tokens with this lock
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create copies for different test scenarios
|
||||
send_proofs_copy1 = copy.deepcopy(send_proofs)
|
||||
send_proofs_copy2 = copy.deepcopy(send_proofs)
|
||||
|
||||
# Verify tokens can't be redeemed before locktime
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
|
||||
# Wait for locktime to expire
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Try with only 1 signature (wallet1) - should fail
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs_copy1),
|
||||
"not enough pubkeys (3) or signatures (1) present for n_sigs (3)",
|
||||
)
|
||||
|
||||
# Add second signature (wallet2)
|
||||
send_proofs_copy2 = wallet2.sign_p2pk_sig_inputs(send_proofs_copy2)
|
||||
|
||||
# Try with 2 signatures - should still fail
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs_copy2),
|
||||
"not enough pubkeys (3) or signatures (2) present for n_sigs (3)",
|
||||
)
|
||||
|
||||
# Add the third signature (wallet3)
|
||||
send_proofs_copy2 = wallet3.sign_p2pk_sig_inputs(send_proofs_copy2)
|
||||
|
||||
# Now with 3 signatures it should succeed
|
||||
await wallet1.redeem(send_proofs_copy2)
|
||||
468
tests/wallet/test_wallet_p2pk_methods.py
Normal file
468
tests/wallet/test_wallet_p2pk_methods.py
Normal file
@@ -0,0 +1,468 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import P2PKWitness
|
||||
from cashu.core.crypto.secp import PrivateKey
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import P2PKSecret, SigFlags
|
||||
from cashu.core.secret import SecretKind, Tags
|
||||
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():
|
||||
wallet1 = await Wallet.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_methods_1", "wallet1"
|
||||
)
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_methods_2", "wallet2"
|
||||
)
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet2.load_mint()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_p2pk_lock_default(wallet1: Wallet):
|
||||
"""Test creating a P2PK lock with default parameters."""
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Verify created lock properties
|
||||
assert isinstance(secret_lock, P2PKSecret)
|
||||
assert secret_lock.kind == SecretKind.P2PK.value
|
||||
assert secret_lock.data == pubkey
|
||||
assert secret_lock.locktime is None
|
||||
assert secret_lock.sigflag == SigFlags.SIG_INPUTS
|
||||
assert secret_lock.n_sigs == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_p2pk_lock_with_options(wallet1: Wallet):
|
||||
"""Test creating a P2PK lock with all options specified."""
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey,
|
||||
locktime_seconds=3600,
|
||||
sig_all=True,
|
||||
n_sigs=2,
|
||||
tags=Tags([["custom_tag", "custom_value"]]),
|
||||
)
|
||||
|
||||
# Verify created lock properties
|
||||
assert isinstance(secret_lock, P2PKSecret)
|
||||
assert secret_lock.kind == SecretKind.P2PK.value
|
||||
assert secret_lock.data == pubkey
|
||||
assert secret_lock.locktime is not None
|
||||
assert secret_lock.sigflag == SigFlags.SIG_ALL
|
||||
assert secret_lock.n_sigs == 2
|
||||
assert secret_lock.tags.get_tag("custom_tag") == "custom_value"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_signatures_proofs_sig_inputs(wallet1: Wallet):
|
||||
"""Test signing proofs with the private key."""
|
||||
# Mint tokens
|
||||
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 proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 32, secret_lock=secret_lock)
|
||||
|
||||
# Test signatures_proofs_sig_inputs
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(proofs)
|
||||
|
||||
# Verify signatures were created
|
||||
assert len(signatures) == len(proofs)
|
||||
assert all(isinstance(sig, str) for sig in signatures)
|
||||
assert all(len(sig) == 128 for sig in signatures) # 64-byte hex signatures
|
||||
|
||||
# Verify the signatures are valid
|
||||
for proof, signature in zip(proofs, signatures):
|
||||
message = proof.secret.encode("utf-8")
|
||||
sig_bytes = bytes.fromhex(signature)
|
||||
# Make sure wallet has a pubkey
|
||||
assert wallet1.private_key.pubkey is not None
|
||||
assert wallet1.private_key.pubkey.schnorr_verify(
|
||||
hashlib.sha256(message).digest(), sig_bytes, None, raw=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schnorr_sign_message(wallet1: Wallet):
|
||||
"""Test signing an arbitrary message."""
|
||||
# Define a test message
|
||||
message = "test message to sign"
|
||||
|
||||
# Sign the message
|
||||
signature = wallet1.schnorr_sign_message(message)
|
||||
|
||||
# Verify signature format
|
||||
assert isinstance(signature, str)
|
||||
assert len(signature) == 128 # 64-byte hex signature
|
||||
|
||||
# Verify signature is valid
|
||||
sig_bytes = bytes.fromhex(signature)
|
||||
# Make sure wallet has a pubkey
|
||||
assert wallet1.private_key.pubkey is not None
|
||||
assert wallet1.private_key.pubkey.schnorr_verify(
|
||||
hashlib.sha256(message.encode("utf-8")).digest(), sig_bytes, None, raw=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inputs_require_sigall_detection(wallet1: Wallet):
|
||||
"""Test detection of SIG_ALL flag in proof inputs."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create proofs with SIG_INPUTS
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
_, proofs_sig_inputs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
|
||||
# Create proofs with SIG_ALL
|
||||
mint_quote_2 = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote_2.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote_2.quote)
|
||||
secret_lock_all = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, proofs_sig_all = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_all
|
||||
)
|
||||
|
||||
# Test detection of SIG_ALL
|
||||
assert not wallet1._inputs_require_sigall(proofs_sig_inputs)
|
||||
assert wallet1._inputs_require_sigall(proofs_sig_all)
|
||||
|
||||
# Test mixed list of proofs
|
||||
mixed_proofs = proofs_sig_inputs + proofs_sig_all
|
||||
assert wallet1._inputs_require_sigall(mixed_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_witness_swap_sig_all(wallet1: Wallet):
|
||||
"""Test adding a witness to the first proof for SIG_ALL."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create proofs with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 16, secret_lock=secret_lock)
|
||||
|
||||
# Create some outputs
|
||||
output_amounts = [16]
|
||||
secrets, rs, _ = await wallet1.generate_n_secrets(len(output_amounts))
|
||||
outputs, _ = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Add witness
|
||||
signed_proofs = wallet1.add_witness_swap_sig_all(proofs, outputs)
|
||||
|
||||
# Verify the first proof has a witness
|
||||
assert signed_proofs[0].witness is not None
|
||||
witness = P2PKWitness.from_witness(signed_proofs[0].witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
# Verify the signature includes both inputs and outputs
|
||||
message_to_sign = "".join([p.secret for p in proofs] + [o.B_ for o in outputs])
|
||||
signature = wallet1.schnorr_sign_message(message_to_sign)
|
||||
assert witness.signatures[0] == signature
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sign_proofs_inplace_swap(wallet1: Wallet):
|
||||
"""Test signing proofs in place for a swap operation."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create SIG_ALL proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 16, secret_lock=secret_lock)
|
||||
|
||||
# Create some outputs
|
||||
output_amounts = [16]
|
||||
secrets, rs, _ = await wallet1.generate_n_secrets(len(output_amounts))
|
||||
outputs, _ = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Sign proofs
|
||||
signed_proofs = wallet1.sign_proofs_inplace_swap(proofs, outputs)
|
||||
|
||||
# Verify the first proof has a witness with a signature
|
||||
assert signed_proofs[0].witness is not None
|
||||
witness = P2PKWitness.from_witness(signed_proofs[0].witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_signatures_to_proofs(wallet1: Wallet):
|
||||
"""Test adding signatures to proofs."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create P2PK proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 16, secret_lock=secret_lock)
|
||||
|
||||
# Generate signatures
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(proofs)
|
||||
|
||||
# Add signatures to proofs
|
||||
signed_proofs = wallet1.add_signatures_to_proofs(proofs, signatures)
|
||||
|
||||
# Verify signatures were added to the proofs
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness is not None
|
||||
witness = P2PKWitness.from_witness(proof.witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
# Test adding same signatures to already signed proofs (should not duplicate)
|
||||
signed_proofs = wallet1.add_signatures_to_proofs(signed_proofs, signatures)
|
||||
|
||||
# Verify the signatures were not duplicated
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness
|
||||
witness = P2PKWitness.from_witness(proof.witness)
|
||||
# Should still have 1 signature because duplicates aren't added
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_proofs_locked_to_our_pubkey(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Test filtering proofs locked to our public key."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(640)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(640, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys for both wallets
|
||||
pubkey1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey2 = await wallet2.create_p2pk_pubkey()
|
||||
|
||||
# Create proofs locked to wallet1's pubkey
|
||||
secret_lock1 = await wallet1.create_p2pk_lock(pubkey1)
|
||||
_, proofs1 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock1
|
||||
)
|
||||
|
||||
# Create proofs locked to wallet2's pubkey
|
||||
secret_lock2 = await wallet1.create_p2pk_lock(pubkey2)
|
||||
_, proofs2 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock2
|
||||
)
|
||||
|
||||
# Create proofs with multiple pubkeys
|
||||
secret_lock3 = await wallet1.create_p2pk_lock(
|
||||
pubkey1, tags=Tags([["pubkeys", pubkey2]])
|
||||
)
|
||||
_, proofs3 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock3
|
||||
)
|
||||
|
||||
# Sign the proofs to avoid witness errors
|
||||
signed_proofs1 = wallet1.sign_p2pk_sig_inputs(proofs1)
|
||||
signed_proofs2 = wallet2.sign_p2pk_sig_inputs(proofs2)
|
||||
signed_proofs3 = wallet1.sign_p2pk_sig_inputs(proofs3)
|
||||
signed_proofs3 = wallet2.sign_p2pk_sig_inputs(signed_proofs3)
|
||||
|
||||
# Ensure pubkeys are available
|
||||
assert wallet1.private_key.pubkey is not None
|
||||
assert wallet2.private_key.pubkey is not None
|
||||
|
||||
# Filter using wallet1
|
||||
filtered1 = wallet1.filter_proofs_locked_to_our_pubkey(
|
||||
signed_proofs1 + signed_proofs2 + signed_proofs3
|
||||
)
|
||||
# wallet1 should find proofs1 and proofs3
|
||||
assert len(filtered1) == len(signed_proofs1) + len(signed_proofs3)
|
||||
|
||||
# Filter using wallet2
|
||||
filtered2 = wallet2.filter_proofs_locked_to_our_pubkey(
|
||||
signed_proofs1 + signed_proofs2 + signed_proofs3
|
||||
)
|
||||
# wallet2 should find proofs2 and proofs3
|
||||
assert len(filtered2) == len(signed_proofs2) + len(signed_proofs3)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sign_p2pk_sig_inputs(wallet1: Wallet):
|
||||
"""Test signing P2PK SIG_INPUTS proofs."""
|
||||
# Mint tokens
|
||||
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 mix of P2PK and non-P2PK proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
|
||||
# Regular proofs (not P2PK)
|
||||
_, regular_proofs = await wallet1.swap_to_send(wallet1.proofs, 16)
|
||||
|
||||
# P2PK SIG_INPUTS proofs
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
_, p2pk_input_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
|
||||
# P2PK SIG_ALL proofs - these won't be signed by sign_p2pk_sig_inputs
|
||||
secret_lock_all = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, p2pk_all_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_all
|
||||
)
|
||||
|
||||
# P2PK locked to a different pubkey - these won't be signed
|
||||
garbage_pubkey_p = PrivateKey().pubkey
|
||||
assert garbage_pubkey_p is not None
|
||||
garbage_pubkey = garbage_pubkey_p.serialize().hex()
|
||||
secret_lock_other = await wallet1.create_p2pk_lock(garbage_pubkey)
|
||||
_, p2pk_other_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_other
|
||||
)
|
||||
|
||||
# Mix all proofs
|
||||
mixed_proofs = (
|
||||
regular_proofs + p2pk_input_proofs + p2pk_all_proofs + p2pk_other_proofs
|
||||
)
|
||||
|
||||
# Sign the mixed proofs
|
||||
signed_proofs = wallet1.sign_p2pk_sig_inputs(mixed_proofs)
|
||||
|
||||
# Only P2PK SIG_INPUTS proofs locked to our pubkey should be signed
|
||||
assert len(signed_proofs) == len(p2pk_input_proofs)
|
||||
|
||||
# Verify the signatures were added
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness is not None
|
||||
witness = P2PKWitness.from_witness(proof.witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_witnesses_sig_inputs(wallet1: Wallet):
|
||||
"""Test adding witnesses to P2PK SIG_INPUTS proofs."""
|
||||
# Mint tokens
|
||||
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 mix of P2PK and non-P2PK proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
|
||||
# Regular proofs (not P2PK)
|
||||
_, regular_proofs = await wallet1.swap_to_send(wallet1.proofs, 16)
|
||||
|
||||
# P2PK SIG_INPUTS proofs
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
_, p2pk_input_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
|
||||
# Mix all proofs and make a copy for comparison
|
||||
mixed_proofs = regular_proofs + p2pk_input_proofs
|
||||
mixed_proofs_copy = copy.deepcopy(mixed_proofs)
|
||||
|
||||
# Add witnesses to the proofs
|
||||
signed_proofs = wallet1.add_witnesses_sig_inputs(mixed_proofs)
|
||||
|
||||
# Verify that only P2PK proofs have witnesses added
|
||||
for i, (orig_proof, signed_proof) in enumerate(
|
||||
zip(mixed_proofs_copy, signed_proofs)
|
||||
):
|
||||
if i < len(regular_proofs):
|
||||
# Regular proofs should be unchanged
|
||||
assert signed_proof.witness == orig_proof.witness
|
||||
else:
|
||||
# P2PK proofs should have witnesses added
|
||||
assert signed_proof.witness is not None
|
||||
witness = P2PKWitness.from_witness(signed_proof.witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edge_cases(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Test various edge cases for the WalletP2PK methods."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Case 1: Empty list of proofs
|
||||
assert wallet1.signatures_proofs_sig_inputs([]) == []
|
||||
assert wallet1.add_signatures_to_proofs([], []) == []
|
||||
assert wallet1.filter_proofs_locked_to_our_pubkey([]) == []
|
||||
assert wallet1.sign_p2pk_sig_inputs([]) == []
|
||||
assert wallet1.add_witnesses_sig_inputs([]) == []
|
||||
|
||||
# Case 2: Mismatched number of proofs and signatures
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 16, secret_lock=secret_lock)
|
||||
assert len(proofs) == 1
|
||||
# Create fake signatures but we have only one proof - this should fail
|
||||
signatures = ["fake_signature1", "fake_signature2"]
|
||||
assert len(signatures) != len(proofs)
|
||||
|
||||
# This should raise an assertion error
|
||||
with pytest.raises(AssertionError, match="wrong number of signatures"):
|
||||
wallet1.add_signatures_to_proofs(proofs, signatures)
|
||||
|
||||
# Case 3: SIG_ALL with proofs locked to different public keys
|
||||
assert wallet1.private_key.pubkey is not None
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
assert garbage_pubkey is not None
|
||||
secret_lock_other = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), sig_all=True
|
||||
)
|
||||
_, other_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_other
|
||||
)
|
||||
|
||||
output_amounts = [16]
|
||||
secrets, rs, _ = await wallet1.generate_n_secrets(len(output_amounts))
|
||||
outputs, _ = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# wallet1 shouldn't add signatures because proofs are locked to a different pubkey
|
||||
signed_proofs = wallet1.add_witness_swap_sig_all(other_proofs, outputs)
|
||||
# Check each proof for None witness
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness is None
|
||||
293
tests/wallet/test_wallet_regtest.py
Normal file
293
tests/wallet/test_wallet_regtest.py
Normal file
@@ -0,0 +1,293 @@
|
||||
import asyncio
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.crud import get_proofs
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
SLEEP_TIME,
|
||||
cancel_invoice,
|
||||
get_hold_invoice,
|
||||
is_fake,
|
||||
pay_if_regtest,
|
||||
settle_invoice,
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet():
|
||||
wallet = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet",
|
||||
name="wallet",
|
||||
)
|
||||
await wallet.load_mint()
|
||||
yield wallet
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
||||
# fill wallet
|
||||
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)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.pending for s in states.states])
|
||||
|
||||
settle_invoice(preimage=preimage)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.spent for s in states.states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger):
|
||||
# fill wallet
|
||||
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)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.pending for s in states.states])
|
||||
|
||||
cancel_invoice(preimage_hash=preimage_hash)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.unspent for s in states.states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_get_melt_quote_melt_fail_restore_pending_batch_check(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
# simulates a payment that fails on the mint and whether the wallet is able to
|
||||
# restore the state of all proofs (set unreserved)
|
||||
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, set_reserved=True
|
||||
)
|
||||
|
||||
# verify that the proofs are reserved
|
||||
proofs_db = await get_proofs(db=wallet.db, melt_id=quote.quote)
|
||||
assert all([p.reserved for p in proofs_db])
|
||||
|
||||
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)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.pending for s in states.states])
|
||||
|
||||
# fail the payment, melt will unset the proofs as reserved
|
||||
cancel_invoice(preimage_hash=preimage_hash)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# test get_spent_proofs_check_states_batched: verify that no proofs are spent
|
||||
spent_proofs = await wallet.get_spent_proofs_check_states_batched(send_proofs)
|
||||
assert len(spent_proofs) == 0
|
||||
|
||||
proofs_db_later = await get_proofs(db=wallet.db, melt_id=quote.quote)
|
||||
assert all([p.reserved is False for p in proofs_db_later])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_get_melt_quote_wallet_crash_melt_fail_restore_pending_batch_check(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
# simulates a payment failure but the wallet crashed, we confirm that wallet.get_melt_quote() will correctly
|
||||
# recover the state of the proofs and set them as unreserved
|
||||
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, set_reserved=True
|
||||
)
|
||||
assert len(send_proofs) == 2
|
||||
|
||||
task = 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)
|
||||
|
||||
# verify that the proofs are reserved
|
||||
proofs_db = await get_proofs(db=wallet.db, melt_id=quote.quote)
|
||||
assert len(proofs_db) == 2
|
||||
assert all([p.reserved for p in proofs_db])
|
||||
|
||||
# simulate a and kill the task
|
||||
task.cancel()
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.pending for s in states.states])
|
||||
|
||||
# fail the payment, melt will unset the proofs as reserved
|
||||
cancel_invoice(preimage_hash=preimage_hash)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# get the melt quote, this should restore the state of the proofs
|
||||
melt_quote = await wallet.get_melt_quote(quote.quote)
|
||||
assert melt_quote
|
||||
assert melt_quote.unpaid
|
||||
|
||||
# verify that get_melt_quote unset all proofs as not pending anymore
|
||||
proofs_db_later = await get_proofs(db=wallet.db, melt_id=quote.quote)
|
||||
assert len(proofs_db_later) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_wallet_crash_melt_succeed_restore_pending_batch_check(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
# simulates a payment that succeeds but the wallet crashes in the mean time
|
||||
# we then call get_spent_proofs_check_states_batched to check the proof states
|
||||
# and the wallet should then invalidate the reserved proofs
|
||||
|
||||
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, set_reserved=True
|
||||
)
|
||||
|
||||
# verify that the proofs are reserved
|
||||
proofs_db = await get_proofs(db=wallet.db, melt_id=quote.quote)
|
||||
assert all([p.reserved for p in proofs_db])
|
||||
|
||||
task = 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)
|
||||
|
||||
# simulate a and kill the task
|
||||
task.cancel()
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
# verify that the proofs are still reserved
|
||||
proofs_db = await get_proofs(db=wallet.db, melt_id=quote.quote)
|
||||
assert all([p.reserved for p in proofs_db])
|
||||
|
||||
# verify that the proofs are still pending
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.pending for s in states.states])
|
||||
|
||||
# succeed the payment
|
||||
settle_invoice(preimage=preimage)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# get the melt quote
|
||||
melt_quote = await wallet.get_melt_quote(quote.quote)
|
||||
assert melt_quote
|
||||
assert melt_quote.paid
|
||||
|
||||
# verify that get_melt_quote unset all proofs as not pending anymore
|
||||
proofs_db_later = await get_proofs(db=wallet.db, melt_id=quote.quote)
|
||||
assert len(proofs_db_later) == 0
|
||||
272
tests/wallet/test_wallet_regtest_mpp.py
Normal file
272
tests/wallet/test_wallet_regtest_mpp.py
Normal file
@@ -0,0 +1,272 @@
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import List
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuote, MeltQuoteState, Method, Proof
|
||||
from cashu.lightning.base import PaymentResponse
|
||||
from cashu.lightning.clnrest import CLNRestWallet
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
SLEEP_TIME,
|
||||
assert_err,
|
||||
cancel_invoice,
|
||||
get_hold_invoice,
|
||||
get_real_invoice,
|
||||
is_fake,
|
||||
partial_pay_real_invoice,
|
||||
pay_if_regtest,
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet():
|
||||
wallet = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet",
|
||||
name="wallet",
|
||||
)
|
||||
await wallet.load_mint()
|
||||
yield wallet
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger):
|
||||
# make sure that mpp is supported by the bolt11-sat backend
|
||||
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
|
||||
pytest.skip("backend does not support mpp")
|
||||
|
||||
# make sure wallet knows the backend supports mpp
|
||||
assert wallet.mint_info.supports_mpp("bolt11", wallet.unit)
|
||||
|
||||
# top up wallet twice so we have enough for two payments
|
||||
topup_mint_quote = await wallet.request_mint(128)
|
||||
await pay_if_regtest(topup_mint_quote.request)
|
||||
proofs1 = await wallet.mint(128, quote_id=topup_mint_quote.quote)
|
||||
assert wallet.balance == 128
|
||||
|
||||
# this is the invoice we want to pay in two parts
|
||||
invoice_dict = get_real_invoice(64)
|
||||
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||
|
||||
async def _mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
||||
# wallet pays 32 sat of the invoice
|
||||
quote = await wallet.melt_quote(invoice, amount_msat=amount * 1000)
|
||||
assert quote.amount == amount
|
||||
await wallet.melt(
|
||||
proofs,
|
||||
invoice,
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
|
||||
def mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
||||
asyncio.run(_mint_pay_mpp(invoice, amount, proofs))
|
||||
|
||||
# call pay_mpp twice in parallel to pay the full invoice
|
||||
t1 = threading.Thread(
|
||||
target=mint_pay_mpp, args=(invoice_payment_request, 32, proofs1)
|
||||
)
|
||||
t2 = threading.Thread(
|
||||
target=partial_pay_real_invoice, args=(invoice_payment_request, 32, 1)
|
||||
)
|
||||
|
||||
t1.start()
|
||||
t2.start()
|
||||
t1.join()
|
||||
t2.join()
|
||||
|
||||
assert wallet.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_pay_mpp_incomplete_payment(wallet: Wallet, ledger: Ledger):
|
||||
# make sure that mpp is supported by the bolt11-sat backend
|
||||
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
|
||||
pytest.skip("backend does not support mpp")
|
||||
|
||||
# This test cannot be done with CLN because we only have one mint
|
||||
# and CLN hates multiple partial payment requests
|
||||
if isinstance(ledger.backends[Method["bolt11"]][wallet.unit], CLNRestWallet):
|
||||
pytest.skip("CLN cannot perform this test")
|
||||
|
||||
# make sure wallet knows the backend supports mpp
|
||||
assert wallet.mint_info.supports_mpp("bolt11", wallet.unit)
|
||||
|
||||
# top up wallet twice so we have enough for three payments
|
||||
topup_mint_quote = await wallet.request_mint(128)
|
||||
await pay_if_regtest(topup_mint_quote.request)
|
||||
proofs1 = await wallet.mint(128, quote_id=topup_mint_quote.quote)
|
||||
assert wallet.balance == 128
|
||||
|
||||
topup_mint_quote = await wallet.request_mint(128)
|
||||
await pay_if_regtest(topup_mint_quote.request)
|
||||
proofs2 = await wallet.mint(128, quote_id=topup_mint_quote.quote)
|
||||
assert wallet.balance == 256
|
||||
|
||||
topup_mint_quote = await wallet.request_mint(128)
|
||||
await pay_if_regtest(topup_mint_quote.request)
|
||||
proofs3 = await wallet.mint(128, quote_id=topup_mint_quote.quote)
|
||||
assert wallet.balance == 384
|
||||
|
||||
# this is the invoice we want to pay in two parts
|
||||
invoice_dict = get_real_invoice(64)
|
||||
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||
|
||||
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
|
||||
await asyncio.sleep(delay)
|
||||
# wallet pays 32 sat of the invoice
|
||||
quote = await wallet.melt_quote(
|
||||
invoice_payment_request, amount_msat=amount * 1000
|
||||
)
|
||||
assert quote.amount == amount
|
||||
await wallet.melt(
|
||||
proofs,
|
||||
invoice_payment_request,
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
|
||||
# instead: call pay_mpp twice in the background, sleep for a bit, then check if the payment was successful (it should not be)
|
||||
asyncio.create_task(pay_mpp(32, proofs1))
|
||||
asyncio.create_task(pay_mpp(16, proofs2, delay=0.5))
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# payment is still pending because the full amount has not been paid
|
||||
assert wallet.balance == 384
|
||||
|
||||
# send the remaining 16 sat to complete the payment
|
||||
asyncio.create_task(pay_mpp(16, proofs3, delay=0.5))
|
||||
await asyncio.sleep(2)
|
||||
|
||||
assert wallet.balance <= 384 - 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_internal_mpp_melt_quotes(wallet: Wallet, ledger: Ledger):
|
||||
# make sure that mpp is supported by the bolt11-sat backend
|
||||
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
|
||||
pytest.skip("backend does not support mpp")
|
||||
|
||||
# create a mint quote
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
|
||||
# try and create a multi-part melt quote
|
||||
await assert_err(
|
||||
wallet.melt_quote(mint_quote.request, 100 * 1000), "internal mpp not allowed"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_pay_mpp_cancel_payment(wallet: Wallet, ledger: Ledger):
|
||||
# make sure that mpp is supported by the bolt11-sat backend
|
||||
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
|
||||
pytest.skip("backend does not support mpp")
|
||||
|
||||
# make sure wallet knows the backend supports mpp
|
||||
assert wallet.mint_info.supports_mpp("bolt11", wallet.unit)
|
||||
|
||||
# top up wallet so we have enough for the payment
|
||||
topup_mint_quote = await wallet.request_mint(128)
|
||||
await pay_if_regtest(topup_mint_quote.request)
|
||||
proofs1 = await wallet.mint(128, quote_id=topup_mint_quote.quote)
|
||||
assert wallet.balance == 128
|
||||
|
||||
# create a hold invoice that we can cancel
|
||||
preimage, invoice_dict = get_hold_invoice(64)
|
||||
invoice_payment_request = str(invoice_dict.get("payment_request", ""))
|
||||
invoice_obj = bolt11.decode(invoice_payment_request)
|
||||
payment_hash = invoice_obj.payment_hash
|
||||
|
||||
async def _mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
||||
# wallet pays 32 sat of the invoice
|
||||
quote = await wallet.melt_quote(invoice, amount_msat=amount * 1000)
|
||||
assert quote.amount == amount
|
||||
await wallet.melt(
|
||||
proofs,
|
||||
invoice,
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
|
||||
def mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
||||
asyncio.run(_mint_pay_mpp(invoice, amount, proofs))
|
||||
|
||||
# start the MPP payment
|
||||
t1 = threading.Thread(
|
||||
target=mint_pay_mpp, args=(invoice_payment_request, 32, proofs1)
|
||||
)
|
||||
t1.start()
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# cancel the invoice
|
||||
cancel_invoice(payment_hash)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method["bolt11"]][wallet.unit].get_payment_status(
|
||||
payment_hash
|
||||
)
|
||||
assert status.failed # some backends return unknown instead of failed
|
||||
assert not status.preimage # no preimage since payment failed
|
||||
|
||||
# check that the proofs are unspent since payment failed
|
||||
states = await wallet.check_proof_state(proofs1)
|
||||
assert all([s.unspent for s in states.states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_pay_mpp_cancel_payment_pay_partial_invoice(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
# create a hold invoice that we can cancel
|
||||
preimage, invoice_dict = get_hold_invoice(64)
|
||||
invoice_payment_request = str(invoice_dict.get("payment_request", ""))
|
||||
invoice_obj = bolt11.decode(invoice_payment_request)
|
||||
payment_hash = invoice_obj.payment_hash
|
||||
|
||||
# Use a shared container to store the result
|
||||
result_container = []
|
||||
|
||||
async def _mint_pay_mpp(invoice: str, amount: int) -> PaymentResponse:
|
||||
ret = await ledger.backends[Method["bolt11"]][wallet.unit].pay_invoice(
|
||||
MeltQuote(
|
||||
request=invoice,
|
||||
amount=amount,
|
||||
fee_reserve=0,
|
||||
quote="",
|
||||
method="bolt11",
|
||||
checking_id="",
|
||||
unit=wallet.unit.name,
|
||||
state=MeltQuoteState.pending,
|
||||
),
|
||||
0,
|
||||
)
|
||||
return ret
|
||||
|
||||
# Create a wrapper function that will store the result
|
||||
def thread_func():
|
||||
result = asyncio.run(_mint_pay_mpp(invoice_payment_request, 32))
|
||||
result_container.append(result)
|
||||
|
||||
t1 = threading.Thread(target=thread_func)
|
||||
t1.start()
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# cancel the invoice
|
||||
cancel_invoice(payment_hash)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
t1.join()
|
||||
# Get the result from the container
|
||||
assert result_container[0].failed
|
||||
57
tests/wallet/test_wallet_requests.py
Normal file
57
tests/wallet/test_wallet_requests.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import respx
|
||||
from httpx import Request, Response
|
||||
|
||||
from cashu.core.base import BlindedSignature
|
||||
from cashu.core.crypto.b_dhke import hash_to_curve
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
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_swap_outputs_are_sorted(wallet1: Wallet):
|
||||
await wallet1.load_mint()
|
||||
mint_quote = await wallet1.request_mint(16)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(16, quote_id=mint_quote.quote, split=[16])
|
||||
assert wallet1.balance == 16
|
||||
|
||||
test_url = f"{wallet1.url}/v1/swap"
|
||||
key = hash_to_curve("test".encode("utf-8"))
|
||||
mock_blind_signature = BlindedSignature(
|
||||
id=wallet1.keyset_id,
|
||||
amount=8,
|
||||
C_=key.serialize().hex(),
|
||||
)
|
||||
mock_response_data = {"signatures": [mock_blind_signature.dict()]}
|
||||
with respx.mock() as mock:
|
||||
route = mock.post(test_url).mock(
|
||||
return_value=Response(200, json=mock_response_data)
|
||||
)
|
||||
await wallet1.select_to_send(wallet1.proofs, 5)
|
||||
|
||||
assert route.called
|
||||
assert route.call_count == 1
|
||||
request: Request = route.calls[0].request
|
||||
assert request.method == "POST"
|
||||
assert request.url == test_url
|
||||
request_data = json.loads(request.content.decode("utf-8"))
|
||||
output_amounts = [o["amount"] for o in request_data["outputs"]]
|
||||
# assert that output amounts are sorted
|
||||
assert output_amounts == sorted(output_amounts)
|
||||
394
tests/wallet/test_wallet_restore.py
Normal file
394
tests/wallet/test_wallet_restore.py
Normal file
@@ -0,0 +1,394 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.crypto.secp import PrivateKey
|
||||
from cashu.core.errors import CashuError
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg: Union[str, CashuError]):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
error_message: str = str(exc.args[0])
|
||||
if isinstance(msg, CashuError):
|
||||
if msg.detail not in error_message:
|
||||
raise Exception(
|
||||
f"CashuError. Expected error: {msg.detail}, got: {error_message}"
|
||||
)
|
||||
return
|
||||
if msg not in error_message:
|
||||
raise Exception(f"Expected error: {msg}, got: {error_message}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
async def reset_wallet_db(wallet: Wallet):
|
||||
await wallet.db.execute("DELETE FROM proofs")
|
||||
await wallet.db.execute("DELETE FROM proofs_used")
|
||||
await wallet.db.execute("DELETE FROM keysets")
|
||||
await wallet.load_mint()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1():
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet2.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet2",
|
||||
name="wallet2",
|
||||
)
|
||||
await wallet2.load_mint()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet3():
|
||||
dirpath = Path("test_data/wallet3")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
wallet3 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet3",
|
||||
name="wallet3",
|
||||
)
|
||||
await wallet3.db.execute("DELETE FROM proofs")
|
||||
await wallet3.db.execute("DELETE FROM proofs_used")
|
||||
await wallet3.load_mint()
|
||||
yield wallet3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bump_secret_derivation(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture"
|
||||
" humble"
|
||||
)
|
||||
secrets1, rs1, derivation_paths1 = await wallet3.generate_n_secrets(5)
|
||||
secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(0, 4)
|
||||
assert wallet3.keyset_id == "009a1f293253e41e"
|
||||
assert secrets1 == secrets2
|
||||
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
||||
assert derivation_paths1 == derivation_paths2
|
||||
for s in secrets1:
|
||||
print(f'"{s}",')
|
||||
assert secrets1 == [
|
||||
"485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
|
||||
"8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
|
||||
"bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
|
||||
"59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
|
||||
"576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
|
||||
]
|
||||
assert [r.private_key.hex() for r in rs1 if r.private_key] == [
|
||||
"ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679",
|
||||
"967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248",
|
||||
"b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899",
|
||||
"fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29",
|
||||
"5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9",
|
||||
]
|
||||
|
||||
for d in derivation_paths1:
|
||||
print(f'"{d}",')
|
||||
assert derivation_paths1 == [
|
||||
"m/129372'/0'/864559728'/0'",
|
||||
"m/129372'/0'/864559728'/1'",
|
||||
"m/129372'/0'/864559728'/2'",
|
||||
"m/129372'/0'/864559728'/3'",
|
||||
"m/129372'/0'/864559728'/4'",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bump_secret_derivation_two_steps(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture"
|
||||
" humble"
|
||||
)
|
||||
secrets1_1, rs1_1, derivation_paths1 = await wallet3.generate_n_secrets(2)
|
||||
secrets1_2, rs1_2, derivation_paths2 = await wallet3.generate_n_secrets(3)
|
||||
secrets1 = secrets1_1 + secrets1_2
|
||||
rs1 = rs1_1 + rs1_2
|
||||
secrets2, rs2, derivation_paths = await wallet3.generate_secrets_from_to(0, 4)
|
||||
assert secrets1 == secrets2
|
||||
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_secrets_from_to(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture"
|
||||
" humble"
|
||||
)
|
||||
secrets1, rs1, derivation_paths1 = await wallet3.generate_secrets_from_to(0, 4)
|
||||
assert len(secrets1) == 5
|
||||
secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(2, 4)
|
||||
assert len(secrets2) == 3
|
||||
assert secrets1[2:] == secrets2
|
||||
assert [r.private_key for r in rs1[2:]] == [r.private_key for r in rs2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_mint(wallet3: Wallet):
|
||||
await reset_wallet_db(wallet3)
|
||||
mint_quote = await wallet3.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet3.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet3.balance == 64
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs()
|
||||
wallet3.proofs = []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 20)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
# expect that DLEQ proofs are restored
|
||||
assert all([p.dleq for p in wallet3.proofs])
|
||||
assert all([p.dleq.e for p in wallet3.proofs]) # type: ignore
|
||||
assert all([p.dleq.s for p in wallet3.proofs]) # type: ignore
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_with_invalid_mnemonic(wallet3: Wallet):
|
||||
await assert_err(
|
||||
wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture"
|
||||
" picture"
|
||||
),
|
||||
"Invalid mnemonic",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_swap_to_send(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture"
|
||||
" humble"
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
mint_quote = await wallet3.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet3.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.swap_to_send(
|
||||
wallet3.proofs, 32, set_reserved=True
|
||||
) # type: ignore
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs()
|
||||
wallet3.proofs = []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 100)
|
||||
assert wallet3.balance == 96
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"hello rug want adapt talent together lunar method bean expose beef position"
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
mint_quote = await wallet3.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet3.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.swap_to_send(
|
||||
wallet3.proofs, 32, set_reserved=True
|
||||
) # type: ignore
|
||||
|
||||
await wallet2.redeem(spendable_proofs)
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 100)
|
||||
assert wallet3.balance == 96
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 32
|
||||
|
||||
|
||||
class ProofBox:
|
||||
proofs: Dict[str, Proof] = {}
|
||||
|
||||
def add(self, proofs: List[Proof]) -> None:
|
||||
for proof in proofs:
|
||||
if proof.secret in self.proofs:
|
||||
if self.proofs[proof.secret].C != proof.C:
|
||||
print("Proofs are not equal")
|
||||
print(self.proofs[proof.secret])
|
||||
print(proof)
|
||||
else:
|
||||
self.proofs[proof.secret] = proof
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"lucky broken tell exhibit shuffle tomato ethics virus rabbit spread measure"
|
||||
" text"
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
mint_quote = await wallet3.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet3.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.swap_to_send(
|
||||
wallet3.proofs, 32, set_reserved=True
|
||||
) # type: ignore
|
||||
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 100)
|
||||
assert wallet3.balance == 128
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_send_twice(
|
||||
wallet3: Wallet,
|
||||
):
|
||||
box = ProofBox()
|
||||
wallet3.private_key = PrivateKey()
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
mint_quote = await wallet3.request_mint(2)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet3.mint(2, quote_id=mint_quote.quote)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
keep_proofs, spendable_proofs = await wallet3.swap_to_send(
|
||||
wallet3.proofs, 1, set_reserved=True
|
||||
) # type: ignore
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.available_balance == 1
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.available_balance == 2
|
||||
assert wallet3.balance == 2
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 10)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 4
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
# again
|
||||
|
||||
_, spendable_proofs = await wallet3.swap_to_send(
|
||||
wallet3.proofs, 1, set_reserved=True
|
||||
) # type: ignore
|
||||
box.add(wallet3.proofs)
|
||||
|
||||
assert wallet3.available_balance == 1
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.available_balance == 2
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 15)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 6
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
|
||||
wallet3: Wallet,
|
||||
):
|
||||
box = ProofBox()
|
||||
await wallet3._init_private_key(
|
||||
"casual demise flight cradle feature hub link slim remember anger front asthma"
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
mint_quote = await wallet3.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet3.mint(64, quote_id=mint_quote.quote)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
keep_proofs, spendable_proofs = await wallet3.swap_to_send(
|
||||
wallet3.proofs, 10, set_reserved=True
|
||||
) # type: ignore
|
||||
box.add(wallet3.proofs)
|
||||
|
||||
assert wallet3.available_balance == 64 - 10
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.available_balance == 64
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 20)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 84
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
# again
|
||||
|
||||
_, spendable_proofs = await wallet3.swap_to_send(
|
||||
wallet3.proofs, 12, set_reserved=True
|
||||
) # type: ignore
|
||||
|
||||
assert wallet3.available_balance == 64 - 12
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
assert wallet3.available_balance == 64
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 50)
|
||||
assert wallet3.balance == 108
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 64
|
||||
112
tests/wallet/test_wallet_subscription.py
Normal file
112
tests/wallet/test_wallet_subscription.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Method, MintQuoteState, ProofState
|
||||
from cashu.core.json_rpc.base import JSONRPCNotficationParams
|
||||
from cashu.core.nuts.nuts import WEBSOCKETS_NUT
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
is_fake,
|
||||
pay_if_regtest,
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(mint):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet_subscriptions",
|
||||
name="wallet_subscriptions",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_wallet_subscription_mint(wallet: Wallet):
|
||||
if not wallet.mint_info.supports_nut(WEBSOCKETS_NUT):
|
||||
pytest.skip("No websocket support")
|
||||
|
||||
if not wallet.mint_info.supports_websocket_mint_quote(
|
||||
Method["bolt11"], wallet.unit
|
||||
):
|
||||
pytest.skip("No websocket support for bolt11_mint_quote")
|
||||
|
||||
triggered = False
|
||||
msg_stack: list[JSONRPCNotficationParams] = []
|
||||
|
||||
def callback(msg: JSONRPCNotficationParams):
|
||||
nonlocal triggered, msg_stack
|
||||
triggered = True
|
||||
msg_stack.append(msg)
|
||||
asyncio.run(wallet.mint(int(mint_quote.amount), quote_id=mint_quote.quote))
|
||||
|
||||
mint_quote, sub = await wallet.request_mint_with_callback(128, callback=callback)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
wait = settings.fakewallet_delay_incoming_payment or 2
|
||||
await asyncio.sleep(wait + 2)
|
||||
|
||||
assert triggered
|
||||
assert len(msg_stack) == 3
|
||||
|
||||
assert msg_stack[0].payload["state"] == MintQuoteState.unpaid.value
|
||||
|
||||
assert msg_stack[1].payload["state"] == MintQuoteState.paid.value
|
||||
|
||||
assert msg_stack[2].payload["state"] == MintQuoteState.issued.value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_subscription_swap(wallet: Wallet):
|
||||
if not wallet.mint_info.supports_nut(WEBSOCKETS_NUT):
|
||||
pytest.skip("No websocket support")
|
||||
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
triggered = False
|
||||
msg_stack: list[JSONRPCNotficationParams] = []
|
||||
|
||||
def callback(msg: JSONRPCNotficationParams):
|
||||
nonlocal triggered, msg_stack
|
||||
triggered = True
|
||||
msg_stack.append(msg)
|
||||
|
||||
n_subscriptions = len(wallet.proofs)
|
||||
state, sub = await wallet.check_proof_state_with_callback(
|
||||
wallet.proofs, callback=callback
|
||||
)
|
||||
|
||||
_ = await wallet.swap_to_send(wallet.proofs, 64)
|
||||
|
||||
wait = 1
|
||||
await asyncio.sleep(wait)
|
||||
assert triggered
|
||||
|
||||
# we receive 3 messages for each subscription:
|
||||
# initial state (UNSPENT), pending state (PENDING), spent state (SPENT)
|
||||
assert len(msg_stack) == n_subscriptions * 3
|
||||
|
||||
# the first one is the UNSPENT state
|
||||
pending_stack = msg_stack[:n_subscriptions]
|
||||
for msg in pending_stack:
|
||||
proof_state = ProofState.parse_obj(msg.payload)
|
||||
assert proof_state.unspent
|
||||
|
||||
# the second one is the PENDING state
|
||||
spent_stack = msg_stack[n_subscriptions : n_subscriptions * 2]
|
||||
for msg in spent_stack:
|
||||
proof_state = ProofState.parse_obj(msg.payload)
|
||||
assert proof_state.pending
|
||||
|
||||
# the third one is the SPENT state
|
||||
spent_stack = msg_stack[n_subscriptions * 2 :]
|
||||
for msg in spent_stack:
|
||||
proof_state = ProofState.parse_obj(msg.payload)
|
||||
assert proof_state.spent
|
||||
63
tests/wallet/test_wallet_utils.py
Normal file
63
tests/wallet/test_wallet_utils.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from typing import List, Union
|
||||
|
||||
from cashu.core.errors import CashuError
|
||||
from cashu.wallet.utils import sanitize_url
|
||||
|
||||
|
||||
async def assert_err(f, msg: Union[str, CashuError]):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
error_message: str = str(exc.args[0])
|
||||
if isinstance(msg, CashuError):
|
||||
if msg.detail not in error_message:
|
||||
raise Exception(
|
||||
f"CashuError. Expected error: {msg.detail}, got: {error_message}"
|
||||
)
|
||||
return
|
||||
if msg not in error_message:
|
||||
raise Exception(f"Expected error: {msg}, got: {error_message}")
|
||||
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")
|
||||
|
||||
|
||||
def test_sanitize_url():
|
||||
url = "https://localhost:3338"
|
||||
assert sanitize_url(url) == "https://localhost:3338"
|
||||
|
||||
url = "https://mint.com:3338"
|
||||
assert sanitize_url(url) == "https://mint.com:3338"
|
||||
|
||||
url = "https://Mint.com:3338"
|
||||
assert sanitize_url(url) == "https://mint.com:3338"
|
||||
|
||||
url = "https://mint.com:3338/"
|
||||
assert sanitize_url(url) == "https://mint.com:3338"
|
||||
|
||||
url = "https://mint.com:3338/abc"
|
||||
assert sanitize_url(url) == "https://mint.com:3338/abc"
|
||||
|
||||
url = "https://mint.com:3338/Abc"
|
||||
assert sanitize_url(url) == "https://mint.com:3338/Abc"
|
||||
|
||||
url = "https://mint.com:3338/abc/"
|
||||
assert sanitize_url(url) == "https://mint.com:3338/abc"
|
||||
|
||||
url = "https://mint.com:3338/Abc/"
|
||||
assert sanitize_url(url) == "https://mint.com:3338/Abc"
|
||||
|
||||
url = "https://Mint.com:3338/Abc/def"
|
||||
assert sanitize_url(url) == "https://mint.com:3338/Abc/def"
|
||||
Reference in New Issue
Block a user