mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
refactor: discover trusted mint swap units
This commit is contained in:
@@ -52,6 +52,7 @@ ROUTSTR_SECRET_KEY=
|
|||||||
# MINT_MAX_CONCURRENCY=4
|
# MINT_MAX_CONCURRENCY=4
|
||||||
# MINT_RETRY_MAX_ATTEMPTS=3
|
# MINT_RETRY_MAX_ATTEMPTS=3
|
||||||
# Foreign top-up tokens are swapped into the first CASHU_MINTS entry over Lightning.
|
# Foreign top-up tokens are swapped into the first CASHU_MINTS entry over Lightning.
|
||||||
|
# Its unit is discovered from active keysets and enabled NUT-04/NUT-05 Bolt11 methods.
|
||||||
# FOREIGN_MINT_OPERATION_TIMEOUT_SECONDS=5
|
# FOREIGN_MINT_OPERATION_TIMEOUT_SECONDS=5
|
||||||
# FOREIGN_MINT_MAX_CONCURRENCY=4
|
# FOREIGN_MINT_MAX_CONCURRENCY=4
|
||||||
# SWAP_RECONCILE_INTERVAL_SECONDS=60
|
# SWAP_RECONCILE_INTERVAL_SECONDS=60
|
||||||
|
|||||||
@@ -169,7 +169,11 @@ A fresh node ships with two mints preconfigured:
|
|||||||
|
|
||||||
Setting `CASHU_MINTS` (env) or editing the list in the dashboard replaces this
|
Setting `CASHU_MINTS` (env) or editing the list in the dashboard replaces this
|
||||||
default entirely. List order is significant: automatic foreign-mint swaps use
|
default entirely. List order is significant: automatic foreign-mint swaps use
|
||||||
the first configured trusted mint. With an empty list, foreign top-ups are
|
the first configured trusted mint. Core discovers that mint's active units from
|
||||||
|
its keysets and, when advertised, filters them through its enabled NUT-04/NUT-05
|
||||||
|
Bolt11 methods. For an existing key, its liability unit must remain supported;
|
||||||
|
for a new key, Core prefers the foreign token's unit, then `sat`, then `msat`.
|
||||||
|
With an empty trusted-mint list or no compatible unit, foreign top-ups are
|
||||||
rejected before any token proofs are spent.
|
rejected before any token proofs are spent.
|
||||||
|
|
||||||
#### Tokens from other mints
|
#### Tokens from other mints
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ async def topup_wallet_endpoint(
|
|||||||
extra={
|
extra={
|
||||||
"event": "cashu_topup_started",
|
"event": "cashu_topup_started",
|
||||||
"source_mint": source_mint,
|
"source_mint": source_mint,
|
||||||
"primary_mint": settings.primary_mint,
|
|
||||||
"trusted_mints": settings.cashu_mints,
|
"trusted_mints": settings.cashu_mints,
|
||||||
"key_hash": billing_key.hashed_key[:8],
|
"key_hash": billing_key.hashed_key[:8],
|
||||||
},
|
},
|
||||||
@@ -201,7 +200,6 @@ async def topup_wallet_endpoint(
|
|||||||
extra={
|
extra={
|
||||||
"event": "cashu_topup_failed",
|
"event": "cashu_topup_failed",
|
||||||
"source_mint": source_mint,
|
"source_mint": source_mint,
|
||||||
"primary_mint": settings.primary_mint,
|
|
||||||
"trusted_mints": settings.cashu_mints,
|
"trusted_mints": settings.cashu_mints,
|
||||||
"error_chain": _error_chain(e),
|
"error_chain": _error_chain(e),
|
||||||
},
|
},
|
||||||
@@ -213,7 +211,6 @@ async def topup_wallet_endpoint(
|
|||||||
extra={
|
extra={
|
||||||
"event": "cashu_topup_failed",
|
"event": "cashu_topup_failed",
|
||||||
"source_mint": source_mint,
|
"source_mint": source_mint,
|
||||||
"primary_mint": settings.primary_mint,
|
|
||||||
"trusted_mints": settings.cashu_mints,
|
"trusted_mints": settings.cashu_mints,
|
||||||
"status_code": status_code,
|
"status_code": status_code,
|
||||||
"error_type": error_type,
|
"error_type": error_type,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ from .wallet import (
|
|||||||
_execute_bolt11_payment,
|
_execute_bolt11_payment,
|
||||||
_wallet_operation_depth,
|
_wallet_operation_depth,
|
||||||
get_proofs_per_mint_and_unit,
|
get_proofs_per_mint_and_unit,
|
||||||
|
get_supported_mint_units,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
preferred_trusted_mint,
|
preferred_trusted_mint,
|
||||||
resolve_trusted_source_mint,
|
resolve_trusted_source_mint,
|
||||||
@@ -339,6 +340,35 @@ def _trusted_swap_destination() -> str:
|
|||||||
) from error
|
) from error
|
||||||
|
|
||||||
|
|
||||||
|
async def _trusted_mint_unit(
|
||||||
|
mint_url: str,
|
||||||
|
*,
|
||||||
|
liability_unit: str | None,
|
||||||
|
source_unit: str,
|
||||||
|
bolt11_operation: str,
|
||||||
|
) -> str:
|
||||||
|
supported = [
|
||||||
|
unit
|
||||||
|
for unit in await get_supported_mint_units(
|
||||||
|
mint_url, bolt11_operation=bolt11_operation
|
||||||
|
)
|
||||||
|
if unit in _UNITS
|
||||||
|
]
|
||||||
|
if liability_unit is not None:
|
||||||
|
if liability_unit not in supported:
|
||||||
|
raise ValueError(
|
||||||
|
"Trusted mint does not support the API key liability unit: "
|
||||||
|
f"{liability_unit}"
|
||||||
|
)
|
||||||
|
return liability_unit
|
||||||
|
for candidate in (source_unit, "sat", "msat"):
|
||||||
|
if candidate in supported:
|
||||||
|
return candidate
|
||||||
|
raise ForeignMintSwapError(
|
||||||
|
"Trusted destination mint has no supported Bolt11 sat or msat unit"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def swap_in_and_credit(
|
async def swap_in_and_credit(
|
||||||
cashu_token: str, key: ApiKey, session: AsyncSession
|
cashu_token: str, key: ApiKey, session: AsyncSession
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -357,20 +387,20 @@ async def swap_in_and_credit(
|
|||||||
if resolve_trusted_source_mint(source_mint) is not None:
|
if resolve_trusted_source_mint(source_mint) is not None:
|
||||||
raise ValueError("Token is from a trusted mint; redeem it directly")
|
raise ValueError("Token is from a trusted mint; redeem it directly")
|
||||||
source_unit = str(token_obj.unit)
|
source_unit = str(token_obj.unit)
|
||||||
dest_unit = settings.primary_mint_unit
|
if source_unit not in _UNITS:
|
||||||
dest_mint = _trusted_swap_destination()
|
|
||||||
if source_unit not in _UNITS or dest_unit not in _UNITS:
|
|
||||||
raise ForeignMintSwapError("Unsupported token unit for swap")
|
raise ForeignMintSwapError("Unsupported token unit for swap")
|
||||||
if key.refund_currency is not None and key.refund_currency != dest_unit:
|
|
||||||
raise ValueError(
|
|
||||||
"Cashu token unit does not match the API key liability unit: "
|
|
||||||
f"expected {key.refund_currency}, got {dest_unit}"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
await assert_public_https_origin(source_mint)
|
await assert_public_https_origin(source_mint)
|
||||||
except BlockedDestinationError as error:
|
except BlockedDestinationError as error:
|
||||||
raise ForeignMintSwapError(str(error)) from error
|
raise ForeignMintSwapError(str(error)) from error
|
||||||
|
|
||||||
|
dest_mint = _trusted_swap_destination()
|
||||||
|
dest_unit = await _trusted_mint_unit(
|
||||||
|
dest_mint,
|
||||||
|
liability_unit=key.refund_currency,
|
||||||
|
source_unit=source_unit,
|
||||||
|
bolt11_operation="mint",
|
||||||
|
)
|
||||||
token_hash = hashlib.sha256(cashu_token.encode()).hexdigest()
|
token_hash = hashlib.sha256(cashu_token.encode()).hexdigest()
|
||||||
prior = await _prior_swap_for_token(token_hash)
|
prior = await _prior_swap_for_token(token_hash)
|
||||||
if prior is not None:
|
if prior is not None:
|
||||||
@@ -707,6 +737,12 @@ async def swap_out_for_refund(
|
|||||||
unit = refund.unit
|
unit = refund.unit
|
||||||
amount = refund.amount_msats // 1000 if unit == "sat" else refund.amount_msats
|
amount = refund.amount_msats // 1000 if unit == "sat" else refund.amount_msats
|
||||||
source_mint = _trusted_swap_destination()
|
source_mint = _trusted_swap_destination()
|
||||||
|
await _trusted_mint_unit(
|
||||||
|
source_mint,
|
||||||
|
liability_unit=unit,
|
||||||
|
source_unit=unit,
|
||||||
|
bolt11_operation="melt",
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await assert_public_https_origin(destination_mint)
|
await assert_public_https_origin(destination_mint)
|
||||||
except BlockedDestinationError as error:
|
except BlockedDestinationError as error:
|
||||||
|
|||||||
+65
-14
@@ -1402,22 +1402,62 @@ _BALANCE_FETCH_RETRY_SECONDS = 60.0
|
|||||||
_MINT_UNITS_CACHE_SECONDS = 300.0
|
_MINT_UNITS_CACHE_SECONDS = 300.0
|
||||||
_balance_fetch_failures: dict[tuple[str, str], tuple[float, str, str]] = {}
|
_balance_fetch_failures: dict[tuple[str, str], tuple[float, str, str]] = {}
|
||||||
_balance_fetch_locks: dict[str, asyncio.Lock] = {}
|
_balance_fetch_locks: dict[str, asyncio.Lock] = {}
|
||||||
_mint_supported_units: dict[str, tuple[float, list[str]]] = {}
|
_mint_supported_units: dict[tuple[str, str | None], tuple[float, list[str]]] = {}
|
||||||
|
|
||||||
|
|
||||||
async def _get_supported_mint_units(mint_url: str) -> list[str]:
|
def _bolt11_units(wallet: Wallet, nut_number: int) -> set[str] | None:
|
||||||
|
mint_info = wallet.mint_info
|
||||||
|
nuts = mint_info.nuts if mint_info is not None else None
|
||||||
|
nut = (nuts.get(nut_number) or nuts.get(str(nut_number))) if nuts else None
|
||||||
|
if not isinstance(nut, dict) or "methods" not in nut:
|
||||||
|
return None
|
||||||
|
if nut.get("disabled") is True:
|
||||||
|
return set()
|
||||||
|
methods = nut.get("methods")
|
||||||
|
if not isinstance(methods, list):
|
||||||
|
return set()
|
||||||
|
return {
|
||||||
|
str(method.get("unit"))
|
||||||
|
for method in methods
|
||||||
|
if isinstance(method, dict)
|
||||||
|
and method.get("method") == "bolt11"
|
||||||
|
and method.get("unit")
|
||||||
|
and method.get("disabled") is not True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_supported_mint_units(
|
||||||
|
mint_url: str, *, bolt11_operation: str | None = None
|
||||||
|
) -> list[str]:
|
||||||
|
"""Discover active units without activating a unit-specific wallet."""
|
||||||
|
if bolt11_operation not in (None, "mint", "melt"):
|
||||||
|
raise ValueError(f"Unsupported Bolt11 operation: {bolt11_operation}")
|
||||||
|
cache_key = (mint_url, bolt11_operation)
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
cached = _mint_supported_units.get(mint_url)
|
cached = _mint_supported_units.get(cache_key)
|
||||||
if cached is not None and now < cached[0]:
|
if cached is not None and now < cached[0]:
|
||||||
return cached[1]
|
return cached[1]
|
||||||
|
|
||||||
# A metadata load populates Cashu's shared keyset cache for all units.
|
# Wallet construction requires a unit, but keyset discovery does not. Avoid
|
||||||
wallet = await get_wallet(
|
# load_mint(), which activates that bootstrap unit before we know the mint's
|
||||||
mint_url,
|
# supported units.
|
||||||
settings.primary_mint_unit,
|
wallet = await get_wallet(mint_url, "sat", load=False)
|
||||||
|
lock = _mint_metadata_load_locks.setdefault(mint_url, asyncio.Lock())
|
||||||
|
async with lock:
|
||||||
|
await run_mint_operation(
|
||||||
|
wallet.load_mint_keysets,
|
||||||
|
op_name="discover_mint_keysets",
|
||||||
|
mint_url=mint_url,
|
||||||
retry_on_rate_limit=False,
|
retry_on_rate_limit=False,
|
||||||
load_proofs=False,
|
|
||||||
)
|
)
|
||||||
|
if bolt11_operation is not None:
|
||||||
|
await run_mint_operation(
|
||||||
|
lambda: wallet.load_mint_info(reload=True),
|
||||||
|
op_name="discover_mint_info",
|
||||||
|
mint_url=mint_url,
|
||||||
|
retry_on_rate_limit=False,
|
||||||
|
)
|
||||||
|
|
||||||
keysets = await get_cashu_keysets(mint_url=wallet.url, db=wallet.db)
|
keysets = await get_cashu_keysets(mint_url=wallet.url, db=wallet.db)
|
||||||
units: list[str] = []
|
units: list[str] = []
|
||||||
for keyset in keysets:
|
for keyset in keysets:
|
||||||
@@ -1426,19 +1466,30 @@ async def _get_supported_mint_units(mint_url: str) -> list[str]:
|
|||||||
unit = keyset.unit if isinstance(keyset.unit, str) else keyset.unit.name
|
unit = keyset.unit if isinstance(keyset.unit, str) else keyset.unit.name
|
||||||
if unit and unit not in units:
|
if unit and unit not in units:
|
||||||
units.append(unit)
|
units.append(unit)
|
||||||
if not units:
|
|
||||||
units = [settings.primary_mint_unit]
|
|
||||||
elif settings.primary_mint_unit in units:
|
|
||||||
units.remove(settings.primary_mint_unit)
|
|
||||||
units.insert(0, settings.primary_mint_unit)
|
|
||||||
|
|
||||||
_mint_supported_units[mint_url] = (
|
if bolt11_operation is not None:
|
||||||
|
nut_number = 4 if bolt11_operation == "mint" else 5
|
||||||
|
bolt11_units = _bolt11_units(wallet, nut_number)
|
||||||
|
if bolt11_units is not None:
|
||||||
|
units = [unit for unit in units if unit in bolt11_units]
|
||||||
|
|
||||||
|
_mint_supported_units[cache_key] = (
|
||||||
time.monotonic() + _MINT_UNITS_CACHE_SECONDS,
|
time.monotonic() + _MINT_UNITS_CACHE_SECONDS,
|
||||||
units,
|
units,
|
||||||
)
|
)
|
||||||
return units
|
return units
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_supported_mint_units(mint_url: str) -> list[str]:
|
||||||
|
units = await get_supported_mint_units(mint_url)
|
||||||
|
if not units:
|
||||||
|
return [settings.primary_mint_unit]
|
||||||
|
if settings.primary_mint_unit in units:
|
||||||
|
units = [settings.primary_mint_unit, *units]
|
||||||
|
units = list(dict.fromkeys(units))
|
||||||
|
return units
|
||||||
|
|
||||||
|
|
||||||
def _balance_error(
|
def _balance_error(
|
||||||
mint_url: str,
|
mint_url: str,
|
||||||
unit: str,
|
unit: str,
|
||||||
|
|||||||
@@ -137,11 +137,13 @@ async def test_supported_mint_units_come_from_active_keysets() -> None:
|
|||||||
usd = MagicMock(active=True)
|
usd = MagicMock(active=True)
|
||||||
usd.unit.name = "usd"
|
usd.unit.name = "usd"
|
||||||
wallet = MagicMock(url="http://mint:3338", db=MagicMock())
|
wallet = MagicMock(url="http://mint:3338", db=MagicMock())
|
||||||
|
wallet.load_mint_keysets = AsyncMock()
|
||||||
|
get_wallet = AsyncMock(return_value=wallet)
|
||||||
get_keysets = AsyncMock(return_value=[usd, msat, sat])
|
get_keysets = AsyncMock(return_value=[usd, msat, sat])
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch.object(settings, "primary_mint_unit", "sat"),
|
patch.object(settings, "primary_mint_unit", "sat"),
|
||||||
patch("routstr.wallet.get_wallet", AsyncMock(return_value=wallet)),
|
patch("routstr.wallet.get_wallet", get_wallet),
|
||||||
patch("routstr.wallet.get_cashu_keysets", get_keysets),
|
patch("routstr.wallet.get_cashu_keysets", get_keysets),
|
||||||
):
|
):
|
||||||
units = await _get_supported_mint_units("http://mint:3338")
|
units = await _get_supported_mint_units("http://mint:3338")
|
||||||
@@ -149,9 +151,51 @@ async def test_supported_mint_units_come_from_active_keysets() -> None:
|
|||||||
|
|
||||||
assert units == ["sat", "usd"]
|
assert units == ["sat", "usd"]
|
||||||
assert cached_units == units
|
assert cached_units == units
|
||||||
|
get_wallet.assert_awaited_once_with("http://mint:3338", "sat", load=False)
|
||||||
|
wallet.load_mint_keysets.assert_awaited_once()
|
||||||
get_keysets.assert_awaited_once_with(mint_url=wallet.url, db=wallet.db)
|
get_keysets.assert_awaited_once_with(mint_url=wallet.url, db=wallet.db)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_supported_mint_units_filter_bolt11_mint_methods() -> None:
|
||||||
|
from routstr.wallet import get_supported_mint_units
|
||||||
|
|
||||||
|
sat = MagicMock(active=True, unit="sat")
|
||||||
|
msat = MagicMock(active=True, unit="msat")
|
||||||
|
wallet = MagicMock(url="http://mint:3338", db=MagicMock())
|
||||||
|
wallet.load_mint_keysets = AsyncMock()
|
||||||
|
wallet.load_mint_info = AsyncMock()
|
||||||
|
wallet.mint_info.nuts = {
|
||||||
|
4: {
|
||||||
|
"methods": [
|
||||||
|
{"method": "bolt11", "unit": "sat"},
|
||||||
|
{"method": "bolt11", "unit": "msat", "disabled": True},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
5: {"methods": [{"method": "bolt11", "unit": "msat"}]},
|
||||||
|
}
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("routstr.wallet.get_wallet", AsyncMock(return_value=wallet)),
|
||||||
|
patch(
|
||||||
|
"routstr.wallet.get_cashu_keysets",
|
||||||
|
AsyncMock(return_value=[sat, msat]),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
mint_units = await get_supported_mint_units(
|
||||||
|
"http://mint:3338", bolt11_operation="mint"
|
||||||
|
)
|
||||||
|
melt_units = await get_supported_mint_units(
|
||||||
|
"http://mint:3338", bolt11_operation="melt"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mint_units == ["sat"]
|
||||||
|
assert melt_units == ["msat"]
|
||||||
|
assert wallet.load_mint_keysets.await_count == 2
|
||||||
|
assert wallet.load_mint_info.await_count == 2
|
||||||
|
wallet.load_mint_info.assert_awaited_with(reload=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_fetch_all_balances_backs_off_after_connection_failure() -> None:
|
async def test_fetch_all_balances_backs_off_after_connection_failure() -> None:
|
||||||
from routstr.core.settings import settings
|
from routstr.core.settings import settings
|
||||||
|
|||||||
@@ -93,9 +93,11 @@ def _proof(amount: int) -> SimpleNamespace:
|
|||||||
return SimpleNamespace(amount=amount, reserved=False, secret=f"s{amount}", id="k")
|
return SimpleNamespace(amount=amount, reserved=False, secret=f"s{amount}", id="k")
|
||||||
|
|
||||||
|
|
||||||
def _token(amount: int = 1000, mint: str = FOREIGN) -> SimpleNamespace:
|
def _token(
|
||||||
|
amount: int = 1000, mint: str = FOREIGN, unit: str = "sat"
|
||||||
|
) -> SimpleNamespace:
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
mint=mint, unit="sat", amount=amount, keysets=["k"], proofs=[_proof(amount)]
|
mint=mint, unit=unit, amount=amount, keysets=["k"], proofs=[_proof(amount)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -183,6 +185,7 @@ async def _swap_env(
|
|||||||
foreign: _ForeignWallet,
|
foreign: _ForeignWallet,
|
||||||
primary: _PrimaryWallet,
|
primary: _PrimaryWallet,
|
||||||
token: SimpleNamespace,
|
token: SimpleNamespace,
|
||||||
|
supported_units: list[str] | None = None,
|
||||||
) -> AsyncGenerator[None, None]:
|
) -> AsyncGenerator[None, None]:
|
||||||
wallets = {FOREIGN: foreign, PRIMARY: primary}
|
wallets = {FOREIGN: foreign, PRIMARY: primary}
|
||||||
|
|
||||||
@@ -196,6 +199,11 @@ async def _swap_env(
|
|||||||
patch.object(fms, "deserialize_token_from_string", return_value=token),
|
patch.object(fms, "deserialize_token_from_string", return_value=token),
|
||||||
patch.object(fms, "assert_public_https_origin", AsyncMock()),
|
patch.object(fms, "assert_public_https_origin", AsyncMock()),
|
||||||
patch.object(fms, "get_wallet", get_wallet),
|
patch.object(fms, "get_wallet", get_wallet),
|
||||||
|
patch.object(
|
||||||
|
fms,
|
||||||
|
"get_supported_mint_units",
|
||||||
|
AsyncMock(return_value=supported_units or ["sat"]),
|
||||||
|
),
|
||||||
patch.object(fms, "run_mint_operation", run_mint_operation),
|
patch.object(fms, "run_mint_operation", run_mint_operation),
|
||||||
patch.object(
|
patch.object(
|
||||||
fms,
|
fms,
|
||||||
@@ -335,6 +343,42 @@ async def test_swap_in_rejects_non_https_mint_before_any_contact(
|
|||||||
assert await _swap_rows(session) == []
|
assert await _swap_rows(session) == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trusted_mint_unit_prefers_existing_liability_then_source() -> None:
|
||||||
|
supported = AsyncMock(return_value=["sat", "msat"])
|
||||||
|
with patch.object(fms, "get_supported_mint_units", supported):
|
||||||
|
existing = await fms._trusted_mint_unit(
|
||||||
|
PRIMARY,
|
||||||
|
liability_unit="msat",
|
||||||
|
source_unit="sat",
|
||||||
|
bolt11_operation="mint",
|
||||||
|
)
|
||||||
|
new_key = await fms._trusted_mint_unit(
|
||||||
|
PRIMARY,
|
||||||
|
liability_unit=None,
|
||||||
|
source_unit="sat",
|
||||||
|
bolt11_operation="mint",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert existing == "msat"
|
||||||
|
assert new_key == "sat"
|
||||||
|
supported.assert_awaited_with(PRIMARY, bolt11_operation="mint")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trusted_mint_unit_rejects_unsupported_liability() -> None:
|
||||||
|
with patch.object(
|
||||||
|
fms, "get_supported_mint_units", AsyncMock(return_value=["sat"])
|
||||||
|
):
|
||||||
|
with pytest.raises(ValueError, match="liability unit"):
|
||||||
|
await fms._trusted_mint_unit(
|
||||||
|
PRIMARY,
|
||||||
|
liability_unit="msat",
|
||||||
|
source_unit="sat",
|
||||||
|
bolt11_operation="melt",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_swap_in_requires_a_configured_trusted_destination(
|
async def test_swap_in_requires_a_configured_trusted_destination(
|
||||||
engine: AsyncEngine, session: AsyncSession
|
engine: AsyncEngine, session: AsyncSession
|
||||||
|
|||||||
Reference in New Issue
Block a user