Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/shade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
RateLimitError,
ShadeError,
)
from .models import SwapPayment, SwapStatus

__version__ = "0.1.0"

Expand All @@ -34,6 +35,8 @@
"ShadeClient",
"ShadeError",
"SyncHTTPClient",
"SwapPayment",
"SwapStatus",
"config",
"api_base",
"environment",
Expand Down
10 changes: 10 additions & 0 deletions src/shade/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Shade data models.
"""

from .swap import SwapPayment, SwapStatus

__all__ = [
"SwapPayment",
"SwapStatus",
]
188 changes: 188 additions & 0 deletions src/shade/models/swap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""
SwapPayment model for Shade's cross-asset payment routing.

Represents a payment where the payer pays in one token and the
merchant settles in a different token, routed through Shade's
liquidity pools on the Stellar network.
"""

from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import Any, Dict, List, Optional


class SwapStatus(str, Enum):
"""Lifecycle states of a swap-routed payment."""

PENDING = "pending"
SWAPPING = "swapping"
COMPLETED = "completed"
FAILED = "failed"
SLIPPAGE_EXCEEDED = "slippage_exceeded"


class SwapPayment:
"""Represents a swap-routed payment on the Shade network.

The payer sends ``pay_in_token`` and the merchant receives
``settle_out_token``. Shade routes the conversion through one
or more liquidity pools, expressed as ``routing_path``.

Parameters
----------
id : str
Unique identifier for this swap payment.
pay_in_token : str
Asset code (or full Stellar asset string) the payer sends.
settle_out_token : str
Asset code the merchant receives after the swap.
amount_in : Decimal
Amount of ``pay_in_token`` the payer is sending.
amount_out : Decimal or None
Expected / actual amount of ``settle_out_token`` received.
``None`` until the swap is executed.
routing_path : list[str]
Ordered list of asset codes describing the swap route,
e.g. ``["USDC", "XLM", "BTC"]``.
slippage_tolerance : float
Maximum acceptable price slippage expressed as a fraction in
the open interval (0, 1). For example ``0.005`` means 0.5 %.
status : SwapStatus
Current lifecycle state of the payment.
stellar_tx_hash : str or None
Stellar transaction hash once the swap is submitted on-chain.
created_at : datetime or None
UTC timestamp when the swap payment was created.

Raises
------
ValueError
If ``slippage_tolerance`` is not in the open interval (0, 1).
"""

def __init__(
self,
id: str,
pay_in_token: str,
settle_out_token: str,
amount_in: Decimal,
routing_path: List[str],
slippage_tolerance: float,
status: SwapStatus,
amount_out: Optional[Decimal] = None,
stellar_tx_hash: Optional[str] = None,
created_at: Optional[datetime] = None,
) -> None:
if not (0 < slippage_tolerance < 1):
raise ValueError(
f"slippage_tolerance must be in the open interval (0, 1), "
f"got {slippage_tolerance!r}"
)

self.id = id
self.pay_in_token = pay_in_token
self.settle_out_token = settle_out_token
self.amount_in = amount_in
self.amount_out = amount_out
self.routing_path: List[str] = list(routing_path)
self.slippage_tolerance = slippage_tolerance
self.status = status
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
self.stellar_tx_hash = stellar_tx_hash
self.created_at = created_at

# ------------------------------------------------------------------
# Construction helpers
# ------------------------------------------------------------------

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SwapPayment":
"""Construct a :class:`SwapPayment` from a raw dictionary.

This is the canonical way to deserialise an API response or a
stored record into a ``SwapPayment`` object.

Parameters
----------
data : dict
Must contain at minimum: ``id``, ``pay_in_token``,
``settle_out_token``, ``amount_in``, ``routing_path``,
``slippage_tolerance``, and ``status``.

Returns
-------
SwapPayment
A fully populated instance.

Raises
------
ValueError
If ``slippage_tolerance`` is outside (0, 1).
KeyError
If any required field is absent from *data*.
"""
raw_created_at = data.get("created_at")
if isinstance(raw_created_at, str):
created_at: Optional[datetime] = datetime.fromisoformat(raw_created_at)
elif isinstance(raw_created_at, datetime):
created_at = raw_created_at
else:
created_at = None

raw_amount_out = data.get("amount_out")
amount_out: Optional[Decimal] = (
Decimal(str(raw_amount_out)) if raw_amount_out is not None else None
)

return cls(
id=data["id"],
pay_in_token=data["pay_in_token"],
settle_out_token=data["settle_out_token"],
amount_in=Decimal(str(data["amount_in"])),
amount_out=amount_out,
routing_path=list(data["routing_path"]),
slippage_tolerance=float(data["slippage_tolerance"]),
status=SwapStatus(data["status"]),
stellar_tx_hash=data.get("stellar_tx_hash"),
created_at=created_at,
)

# ------------------------------------------------------------------
# Serialisation
# ------------------------------------------------------------------

def to_dict(self) -> Dict[str, Any]:
"""Serialise this instance to a plain dictionary."""
return {
"id": self.id,
"pay_in_token": self.pay_in_token,
"settle_out_token": self.settle_out_token,
"amount_in": str(self.amount_in),
"amount_out": str(self.amount_out) if self.amount_out is not None else None,
"routing_path": self.routing_path,
"slippage_tolerance": self.slippage_tolerance,
"status": self.status.value,
"stellar_tx_hash": self.stellar_tx_hash,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ------------------------------------------------------------------
# Dunder helpers
# ------------------------------------------------------------------

def __repr__(self) -> str: # pragma: no cover
return (
f"SwapPayment("
f"id={self.id!r}, "
f"pay_in_token={self.pay_in_token!r}, "
f"settle_out_token={self.settle_out_token!r}, "
f"amount_in={self.amount_in}, "
f"status={self.status!r})"
)

def __eq__(self, other: object) -> bool:
if not isinstance(other, SwapPayment):
return NotImplemented
return self.id == other.id
Loading