Skip to content

feat(wallet): typed swap refusals from the desk instead of a 500 - #217

Merged
zaxovaiko merged 2 commits into
devfrom
feat/BF-0/swap-safety
Sep 24, 2026
Merged

zaxovaiko merged 2 commits into
devfrom
feat/BF-0/swap-safety

Conversation

@zaxovaiko

@zaxovaiko zaxovaiko commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

A swap adapter can now refuse a quote or a fill with a typed error that the wallet router maps to a 4xx carrying data.reason. Until now every such refusal reached the player as a 500. BF-0.

Why

SwapAdapter.getQuote and execute had no port-level error of their own. Anything an adapter threw fell through the mapErrors map on the swap.quote and swap.execute handlers, so a replayed quote, an expired quote or an over-limit swap all returned 500. The client could not tell them apart and could not offer a requote.

The port now exports two classes:

  • SwapRefusedError maps to CONFLICT. Reasons: quote_missing, quote_invalid, quote_expired, quote_spent, insufficient_inventory, no_rate. quote_invalid also covers a quote issued to another player, so the caller cannot learn which case it hit.
  • SwapLimitExceededError maps to BAD_REQUEST. Reasons: over_swap_limit, over_daily_limit.

SwapFillAmountMissingError stays unmapped: the desk filled but did not say how much, which is a fault on our side of the seam. If execute throws, the held funds are returned as before.

Alternatives considered

  • One refusal class with a reason-to-code table. Rejected: mapErrors keys a code off the class, and one class per code keeps it that way, the same as RgLimitExceededError.
  • Let adapters throw ORPCError directly. Rejected: that puts transport concerns inside an adapter and breaks the typed-errors-at-the-edge rule.

Risks

  • A client that treated a swap 500 as "try again" now gets a 409 or 400. The reason field says which case it is.
  • The changeset follows in its own PR.

A swap adapter had no way to refuse a quote or a fill that the router
understood, so a replayed quote or an over-limit swap reached the player
as a 500. The port now carries SwapRefusedError (CONFLICT) and
SwapLimitExceededError (BAD_REQUEST), each with a machine-readable
data.reason, mapped on both swap.quote and swap.execute.
SwapFillAmountMissingError stays unmapped.

Claude-Session: https://claude.ai/code/session_0178TH3L8uHsQUGnf8bsfmFF
@zaxovaiko zaxovaiko self-assigned this Sep 24, 2026
@zaxovaiko
zaxovaiko merged commit a4a76fa into dev Sep 24, 2026
2 checks passed
@zaxovaiko
zaxovaiko deleted the feat/BF-0/swap-safety branch September 24, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant