mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
clean up
This commit is contained in:
+12
-24
@@ -1,12 +1,7 @@
|
||||
"""Compatibility shims for incoming Cashu token proofs.
|
||||
"""Compatibility shims applied to incoming token proofs before redemption.
|
||||
|
||||
Ecosystem migrations (NUT spec revisions, wallet-specific quirks) periodically
|
||||
change the shape of tokens we receive. Each such fix is a
|
||||
:class:`ProofCompatShim`; :func:`normalize_token_proofs` is the single entry
|
||||
point the redeem/swap paths call before touching a token's proofs.
|
||||
|
||||
To add a new shim: subclass :class:`ProofCompatShim` and append an instance to
|
||||
:data:`PROOF_SHIMS`.
|
||||
Add a shim by subclassing ProofCompatShim and appending an instance to
|
||||
PROOF_SHIMS.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -21,11 +16,8 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def is_short_v2_keyset_id(keyset_id: object) -> bool:
|
||||
"""True for a 16-char NUT-02 v2 short keyset id (version byte ``01``).
|
||||
|
||||
Short ids exist only inside tokens; mint keysets are keyed by the full
|
||||
66-char id, so a short id can never be activated or looked up directly.
|
||||
"""
|
||||
"""16-char NUT-02 v2 short id (version byte ``01``). Mint keysets are
|
||||
keyed by the full 66-char id, so short ids can't be looked up directly."""
|
||||
return (
|
||||
isinstance(keyset_id, str)
|
||||
and keyset_id.startswith("01")
|
||||
@@ -34,20 +26,17 @@ def is_short_v2_keyset_id(keyset_id: object) -> bool:
|
||||
|
||||
|
||||
class ProofCompatShim(ABC):
|
||||
"""One token-format compatibility fix applied to proofs in place."""
|
||||
|
||||
@abstractmethod
|
||||
def applies(self, proofs: list[Proof]) -> bool:
|
||||
"""Cheap check whether this shim is needed for these proofs."""
|
||||
def applies(self, proofs: list[Proof]) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
async def apply(self, wallet: "_CashuWallet", proofs: list[Proof]) -> None:
|
||||
"""Mutate ``proofs`` in place; may talk to the mint via ``wallet``."""
|
||||
"""Fix ``proofs`` in place; may talk to the mint via ``wallet``."""
|
||||
|
||||
|
||||
class ShortKeysetIdExpansion(ProofCompatShim):
|
||||
"""Expand short NUT-02 v2 keyset ids (e.g. minibits tokens) to full
|
||||
66-char ids in place; the mint won't accept a short id on melt/swap."""
|
||||
"""Expand short NUT-02 v2 keyset ids (e.g. minibits tokens) to the full
|
||||
66-char id; the mint rejects short ids on melt/swap."""
|
||||
|
||||
def applies(self, proofs: list[Proof]) -> bool:
|
||||
return any(
|
||||
@@ -89,11 +78,10 @@ PROOF_SHIMS: tuple[ProofCompatShim, ...] = (ShortKeysetIdExpansion(),)
|
||||
async def normalize_token_proofs(
|
||||
wallet: "_CashuWallet", token_obj: Token
|
||||
) -> list[Proof]:
|
||||
"""Capture a token's proofs once and run every applicable compat shim.
|
||||
"""Run every applicable shim and return the proof list to use from here on.
|
||||
|
||||
``token_obj.proofs`` rebuilds fresh Proof objects on every access, so
|
||||
callers must use the returned list — re-reading ``token_obj.proofs`` after
|
||||
this call silently discards the shims' in-place mutations.
|
||||
token_obj.proofs rebuilds fresh Proof objects on every access — callers
|
||||
must use the returned list or the in-place fixes are lost.
|
||||
"""
|
||||
proofs = token_obj.proofs
|
||||
for shim in PROOF_SHIMS:
|
||||
|
||||
+12
-5
@@ -133,6 +133,16 @@ class Wallet(_CashuWallet):
|
||||
)
|
||||
_CashuWallet.raise_on_error_request(resp)
|
||||
|
||||
async def load_mint(
|
||||
self, keyset_id: str = "", force_old_keysets: bool = False
|
||||
) -> None:
|
||||
"""Upstream load_mint minus its blanket ``except Exception: pass`` —
|
||||
the swallow hides 429/transport failures and leaves the wallet
|
||||
keyset-less, so redemption later dies with "No active keyset"."""
|
||||
await self.load_mint_keysets(force_old_keysets)
|
||||
await self.activate_keyset(keyset_id)
|
||||
await self.load_mint_info(reload=True)
|
||||
|
||||
|
||||
class MintConnectionError(Exception):
|
||||
"""The mint could not be reached (network transport failure).
|
||||
@@ -311,11 +321,8 @@ async def _redeem_same_mint(
|
||||
that, not the face value, or routstr over-credits the user and its wallet
|
||||
drifts insolvent.
|
||||
"""
|
||||
# A short NUT-02 v2 keyset id (e.g. minibits tokens) can't be activated:
|
||||
# mint keysets are keyed by full ids, activate_keyset silently fails inside
|
||||
# load_mint, and split() later dies with "No active keyset". Let load_mint
|
||||
# pick an active keyset for the wallet's unit instead; the proofs' short
|
||||
# ids are expanded via normalize_token_proofs below.
|
||||
# A short v2 id can't be activated (keysets are keyed by full ids); let
|
||||
# load_mint pick an active keyset and expand the proofs' ids below.
|
||||
load_keyset_id = token_obj.keysets[0]
|
||||
if is_short_v2_keyset_id(load_keyset_id):
|
||||
load_keyset_id = ""
|
||||
|
||||
@@ -24,7 +24,7 @@ from routstr.token_compat import ShortKeysetIdExpansion
|
||||
|
||||
|
||||
async def _expand_short_keysets(wallet, proofs) -> None: # type: ignore[no-untyped-def]
|
||||
"""Run the shim the way normalize_token_proofs does: applies() then apply()."""
|
||||
# Run the shim the way normalize_token_proofs does.
|
||||
shim = ShortKeysetIdExpansion()
|
||||
if shim.applies(proofs):
|
||||
await shim.apply(wallet, proofs)
|
||||
@@ -254,10 +254,7 @@ async def test_redeem_same_mint_expands_keysets_before_split() -> None:
|
||||
split_proofs = wallet.split.call_args.kwargs["proofs"]
|
||||
assert split_proofs[0].id == FULL_V2_ID
|
||||
assert token.proofs_access_count == 1
|
||||
# A short id can't be activated (mint keysets are keyed by full ids and
|
||||
# load_mint swallows the KeysetNotFoundError, leaving keyset_id unset →
|
||||
# split dies with "No active keyset"). load_mint must be asked for an
|
||||
# active keyset of the wallet's unit instead.
|
||||
# Short ids can't be activated; load_mint must pick an active keyset.
|
||||
assert wallet.load_mint.call_args.kwargs["keyset_id"] == ""
|
||||
|
||||
|
||||
|
||||
@@ -2927,3 +2927,40 @@ async def test_payout_reloads_wallet_snapshot_under_guard() -> None:
|
||||
mock_get_wallet.assert_awaited_once_with(
|
||||
"https://mint.example.com", "sat", force_reload=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_mint_propagates_rate_limit() -> None:
|
||||
"""Unlike upstream, our load_mint must not swallow a 429 — that leaves the
|
||||
wallet keyset-less and split() dies with "No active keyset"."""
|
||||
from routstr.mint import MintRateLimitedError
|
||||
from routstr.wallet import Wallet
|
||||
|
||||
wallet = Wallet.__new__(Wallet)
|
||||
error = MintRateLimitedError(
|
||||
"Cashu mint rate limited",
|
||||
request=httpx.Request("GET", "https://mint.example/v1/keysets"),
|
||||
response=httpx.Response(429),
|
||||
)
|
||||
with (
|
||||
patch.object(wallet, "load_mint_keysets", new=AsyncMock(side_effect=error)),
|
||||
pytest.raises(MintRateLimitedError),
|
||||
):
|
||||
await wallet.load_mint()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_mint_runs_keysets_activation_and_info() -> None:
|
||||
from routstr.wallet import Wallet
|
||||
|
||||
wallet = Wallet.__new__(Wallet)
|
||||
with (
|
||||
patch.object(wallet, "load_mint_keysets", new=AsyncMock()) as load_keysets,
|
||||
patch.object(wallet, "activate_keyset", new=AsyncMock()) as activate,
|
||||
patch.object(wallet, "load_mint_info", new=AsyncMock()) as load_info,
|
||||
):
|
||||
await wallet.load_mint(keyset_id="abc")
|
||||
|
||||
load_keysets.assert_awaited_once_with(False)
|
||||
activate.assert_awaited_once_with("abc")
|
||||
load_info.assert_awaited_once_with(reload=True)
|
||||
|
||||
Reference in New Issue
Block a user