fix: require shortfall text for generic 11000 melt errors

11000 is nutshell's generic, unregistered TransactionError covering many
unrelated failures, so trusting the code alone made any 11000 retryable.
Gate the generic code on the 'not enough inputs' detail text; keep trusting
the registered 11005 (TransactionUnbalanced) on the code alone. Behavior is
unchanged for every existing case; a bare non-shortfall 11000 now surfaces
immediately instead of burning the retry budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jeroen Ubbink
2026-06-23 09:38:04 +02:00
co-authored by Claude Opus 4.8
parent eb6dc5189c
commit e4b26dd27b
2 changed files with 22 additions and 8 deletions
+18 -8
View File
@@ -127,10 +127,12 @@ _MAX_SWAP_ATTEMPTS = 3
_MINT_ERROR_CODE_RE = re.compile(r"\(Code: (\d+)\)")
_MELT_SHORTFALL_RE = re.compile(r"Provided: (\d+), needed: (\d+)")
# Insufficient melt inputs across implementations: 11005 is the registered
# "Transaction is not balanced" code (used by cdk); 11000 is nutshell's
# generic TransactionError, which is unregistered but what nutshell sends.
_RETRYABLE_MELT_CODES = frozenset({"11000", "11005"})
# Insufficient-melt-inputs failures differ across mint implementations. 11005 is
# the registered "Transaction is not balanced" code (cdk), specific enough to
# trust on the code alone. 11000 is nutshell's generic, unregistered
# TransactionError covering many unrelated failures, so it only counts as a fee
# shortfall alongside the "not enough inputs" detail text. With no code suffix at
# all, that same text is the only signal.
def _net_minted_amount(amount_msat: int, token_unit: str, fees: int) -> int:
@@ -160,11 +162,19 @@ def _melt_insufficient_shortfall(error: Exception) -> int | None:
"""
message = str(error)
code_match = _MINT_ERROR_CODE_RE.search(message)
if code_match is not None:
if code_match.group(1) not in _RETRYABLE_MELT_CODES:
code = code_match.group(1) if code_match is not None else None
has_shortfall_text = "not enough inputs" in message.lower()
match code:
case "11005": # registered TransactionUnbalanced: trust the code
pass
case "11000" if has_shortfall_text: # generic nutshell error: needs the text
pass
case None if has_shortfall_text: # no code suffix: text is the only signal
pass
case _: # other codes, a bare 11000, or no signal: must not retry
return None
elif "not enough inputs" not in message.lower():
return None
amounts = _MELT_SHORTFALL_RE.search(message)
if amounts is not None:
provided, needed = int(amounts.group(1)), int(amounts.group(2))
+4
View File
@@ -573,6 +573,10 @@ async def test_net_minted_amount_unit_conversions(
"Provided: 180, needed: 179 (Code: 11000)",
1,
),
# a generic 11000 without the shortfall text is not a fee shortfall:
# 11000 is nutshell's catch-all TransactionError, so retrying (shrinking
# the invoice) would never help and only masks the real error
("Mint Error: Duplicate inputs provided. (Code: 11000)", None),
# spent proofs must never be retried: the funds are gone
("Mint Error: Token already spent. (Code: 11001)", None),
# Lightning failures must never be retried: a smaller invoice won't help