"""Crypto offramp module. Converts coins received by SHKeeper into USD at RelayFi bank via Coinbase Prime — sole provider per plan. See /home/justin/.claude/plans/swirling-napping-sonnet.md. Exports: Quote, ExecutionResult, ProviderStatus — dataclasses Rail — literal type alias OfframpError, InsufficientBalanceError, KycHoldError — exception hierarchy """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from decimal import Decimal from typing import Literal, Optional Rail = Literal["ach", "wire", "rtp"] # Supported coins (XMR intentionally excluded — Coinbase Prime doesn't offramp it). SUPPORTED_COINS: set[str] = { "BTC", "ETH", "MATIC", "LTC", "TRX", "BNB", "DOGE", "USDC", } @dataclass class Quote: """Pre-execute price quote from the offramp provider.""" from_coin: str to_currency: str = "USD" amount_coin: Decimal = Decimal("0") estimated_usd_cents: int = 0 fx_rate_usd: Decimal = Decimal("0") # 1 coin = N USD fee_usd_cents: int = 0 # provider fee estimate valid_until: Optional[datetime] = None quote_id: Optional[str] = None # provider-side quote handle, if any @dataclass class ExecutionResult: """Outcome of calling `execute()` — the sell + wire request was accepted.""" provider_ref: str # Prime order id / transfer id accepted_usd_cents: int # what the provider promised to settle fill_price_usd: Decimal # actual coin→USD price achieved state: Literal["submitted", "partial", "completed", "failed"] rail: Rail memo: Optional[str] = None # e.g., "PW-ORDER-FO-2026-0042" raw: dict = field(default_factory=dict) # provider-native response for debugging @dataclass class ProviderStatus: """Current state of a previously-executed offramp.""" provider_ref: str state: Literal[ "submitted", "selling", "selling_complete", "wiring", "completed", "failed", "held", ] settled_usd_cents: int = 0 fill_price_usd: Optional[Decimal] = None wire_tx_id: Optional[str] = None error_message: Optional[str] = None raw: dict = field(default_factory=dict) # ── Exception hierarchy ───────────────────────────────────────────────── class OfframpError(Exception): """Base for all offramp errors.""" class InsufficientBalanceError(OfframpError): """Exchange side doesn't have enough of the coin credited yet.""" class KycHoldError(OfframpError): """Exchange paused our withdrawal for AML/KYC review — admin must resolve.""" class SlippageExceededError(OfframpError): """Realized fill price was outside MAX_SLIPPAGE_BPS of quote.""" class ProviderDegradedError(OfframpError): """Repeated 5xx/429 — circuit breaker tripped.""" __all__ = [ "Rail", "SUPPORTED_COINS", "Quote", "ExecutionResult", "ProviderStatus", "OfframpError", "InsufficientBalanceError", "KycHoldError", "SlippageExceededError", "ProviderDegradedError", ]