altpay-py 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
altpay/__init__.py ADDED
@@ -0,0 +1,119 @@
1
+ """AltPay - the official Python SDK for the AltPay crypto-payments API.
2
+
3
+ Quick start (sync)::
4
+
5
+ from decimal import Decimal
6
+ from altpay import AltPay, Credentials
7
+
8
+ client = AltPay(Credentials(
9
+ merchant_id="YOUR_MERCHANT_ID",
10
+ api_key="vc_live_...",
11
+ api_secret="YOUR_API_SECRET",
12
+ ))
13
+ invoice = client.invoices.create(
14
+ uuid="order-42",
15
+ amount=Decimal("100.00"),
16
+ fiat_currency="USD",
17
+ url_callback="https://example.com/altpay/webhook",
18
+ )
19
+ print(invoice.url)
20
+
21
+ Quick start (async)::
22
+
23
+ from altpay import AsyncAltPay, Credentials
24
+
25
+ async with AsyncAltPay(creds) as client:
26
+ invoice = await client.invoices.create(uuid="order-42", amount="100.00", fiat_currency="USD")
27
+
28
+ Verifying a webhook::
29
+
30
+ from altpay import WebhookVerifier
31
+ verifier = WebhookVerifier(webhook_secret, target="/altpay/webhook")
32
+ event = verifier.parse(raw_body, request_headers) # raises on a bad signature
33
+
34
+ Documentation: https://docs.altpay.money/docs
35
+ """
36
+
37
+ from .__meta__ import __version__
38
+ from .client import AltPay, AsyncAltPay
39
+ from .credentials import Credentials
40
+ from .enums import FiatCurrency, MerchantStatus, PaymentMethod, PaymentStatus, WithdrawalStatus
41
+ from .errors import (
42
+ AltPayError,
43
+ AltPayTransportError,
44
+ APIError,
45
+ AuthenticationError,
46
+ ConflictError,
47
+ Forbidden,
48
+ NotFoundError,
49
+ PermissionError_,
50
+ RateLimitError,
51
+ ServerError,
52
+ ValidationError,
53
+ )
54
+ from .models import (
55
+ Account,
56
+ AssetBalance,
57
+ AssetBalanceItem,
58
+ Balance,
59
+ BalanceAsset,
60
+ Invoice,
61
+ InvoicePage,
62
+ MethodBalance,
63
+ MethodBalanceItem,
64
+ Service,
65
+ ServiceCommission,
66
+ Statistics,
67
+ Wallet,
68
+ WalletDeposit,
69
+ WebhookEvent,
70
+ Withdrawal,
71
+ WithdrawalFee,
72
+ )
73
+ from .webhook import WebhookVerifier
74
+
75
+ __all__ = [
76
+ "__version__",
77
+ # Clients
78
+ "AltPay",
79
+ "AsyncAltPay",
80
+ "Credentials",
81
+ # Webhooks
82
+ "WebhookVerifier",
83
+ # Enums
84
+ "PaymentStatus",
85
+ "PaymentMethod",
86
+ "FiatCurrency",
87
+ "MerchantStatus",
88
+ "WithdrawalStatus",
89
+ # Models
90
+ "Invoice",
91
+ "InvoicePage",
92
+ "Service",
93
+ "ServiceCommission",
94
+ "Balance",
95
+ "BalanceAsset",
96
+ "AssetBalance",
97
+ "AssetBalanceItem",
98
+ "Wallet",
99
+ "WalletDeposit",
100
+ "Account",
101
+ "MethodBalance",
102
+ "MethodBalanceItem",
103
+ "Statistics",
104
+ "Withdrawal",
105
+ "WithdrawalFee",
106
+ "WebhookEvent",
107
+ # Errors
108
+ "AltPayError",
109
+ "AltPayTransportError",
110
+ "APIError",
111
+ "AuthenticationError",
112
+ "Forbidden",
113
+ "PermissionError_",
114
+ "NotFoundError",
115
+ "ValidationError",
116
+ "ConflictError",
117
+ "RateLimitError",
118
+ "ServerError",
119
+ ]
altpay/__meta__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Package metadata, kept in one place so the version has a single source of truth."""
2
+
3
+ __version__ = "0.1.0"
4
+ __title__ = "altpay-py"
5
+ __user_agent__ = f"altpay-py/{__version__}"
@@ -0,0 +1,6 @@
1
+ """HTTP clients: the synchronous :class:`AltPay` and asynchronous :class:`AsyncAltPay`."""
2
+
3
+ from .async_client import AsyncAltPay
4
+ from .sync_client import AltPay
5
+
6
+ __all__ = ["AltPay", "AsyncAltPay"]
@@ -0,0 +1,114 @@
1
+ """The asynchronous client, :class:`AsyncAltPay`.
2
+
3
+ The async twin of :class:`~altpay.client.sync_client.AltPay`. Same API, same resources, same
4
+ request/response logic from :mod:`altpay.client.base`; only the I/O is awaited.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from typing import Any, TypeVar
11
+
12
+ import httpx
13
+
14
+ from ..credentials import Credentials
15
+ from ..errors import AltPayTransportError, RateLimitError, ServerError
16
+ from ..methods.account import AccountResource
17
+ from ..methods.base import APICall
18
+ from ..methods.invoices import Invoices
19
+ from ..methods.withdrawals import Withdrawals
20
+ from .base import parse_response, prepare
21
+ from .sync_client import DEFAULT_BASE_URL, _backoff, _retry_delay
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ class AsyncAltPay:
27
+ """Asynchronous AltPay API client.
28
+
29
+ Example::
30
+
31
+ from decimal import Decimal
32
+ from altpay import AsyncAltPay, Credentials
33
+
34
+ async with AsyncAltPay(Credentials(
35
+ merchant_id="...", api_key="vc_live_...", api_secret="...",
36
+ )) as client:
37
+ invoice = await client.invoices.create(
38
+ uuid="order-1", amount=Decimal("100.00"), fiat_currency="USD",
39
+ )
40
+ print(invoice.url)
41
+
42
+ Args mirror :class:`~altpay.client.sync_client.AltPay`; ``http_client`` here is an
43
+ :class:`httpx.AsyncClient`.
44
+
45
+ API reference: https://docs.altpay.money/docs
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ credentials: Credentials,
51
+ *,
52
+ base_url: str = DEFAULT_BASE_URL,
53
+ timeout: float = 30.0,
54
+ max_retries: int = 2,
55
+ http_client: httpx.AsyncClient | None = None,
56
+ ) -> None:
57
+ self._credentials = credentials
58
+ self._base_url = base_url.rstrip("/")
59
+ self._max_retries = max_retries
60
+ self._owns_client = http_client is None
61
+ self._http = http_client or httpx.AsyncClient(timeout=timeout)
62
+
63
+ #: Invoice and static-wallet operations.
64
+ self.invoices = Invoices(self.call)
65
+ #: Payout (withdrawal) operations.
66
+ self.withdrawals = Withdrawals(self.call)
67
+ #: Account identity, balance and statistics.
68
+ self.account = AccountResource(self.call)
69
+
70
+ async def __aenter__(self) -> "AsyncAltPay":
71
+ return self
72
+
73
+ async def __aexit__(self, *exc: object) -> None:
74
+ await self.aclose()
75
+
76
+ async def aclose(self) -> None:
77
+ """Close the underlying HTTP connection pool (unless you supplied your own client)."""
78
+ if self._owns_client:
79
+ await self._http.aclose()
80
+
81
+ async def call(self, call: APICall[T]) -> T:
82
+ """Execute a described :class:`~altpay.methods.base.APICall` and return its typed result."""
83
+ request = prepare(
84
+ credentials=self._credentials,
85
+ base_url=self._base_url,
86
+ path=call.path,
87
+ payload=call.payload,
88
+ )
89
+ attempt = 0
90
+ while True:
91
+ try:
92
+ response = await self._http.request(
93
+ request.method, request.url, headers=request.headers, content=request.content
94
+ )
95
+ except httpx.HTTPError as exc:
96
+ if attempt >= self._max_retries:
97
+ raise AltPayTransportError(str(exc)) from exc
98
+ await asyncio.sleep(_backoff(attempt))
99
+ attempt += 1
100
+ continue
101
+
102
+ try:
103
+ result = parse_response(
104
+ status_code=response.status_code,
105
+ body_text=response.text,
106
+ headers=response.headers,
107
+ )
108
+ except (RateLimitError, ServerError) as exc:
109
+ if attempt >= self._max_retries:
110
+ raise
111
+ await asyncio.sleep(_retry_delay(exc, attempt))
112
+ attempt += 1
113
+ continue
114
+ return call.parse(result)
altpay/client/base.py ADDED
@@ -0,0 +1,149 @@
1
+ """Transport-agnostic request building and response parsing.
2
+
3
+ The sync and async clients differ only in how they *send* bytes over the wire; everything
4
+ else - serializing the body, signing it, choosing headers, unwrapping ``result``, turning a
5
+ non-2xx status into a typed exception - is identical and lives here so there is exactly one
6
+ implementation to reason about and audit.
7
+
8
+ A :class:`PreparedRequest` is a pure value: given credentials and a call, it produces the
9
+ exact method/url/headers/body to send. :func:`parse_response` is a pure function from a raw
10
+ response to either the unwrapped ``result`` payload or a raised :class:`~altpay.errors.APIError`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from dataclasses import dataclass
17
+ from typing import Any
18
+
19
+ from ..__meta__ import __user_agent__
20
+ from ..credentials import Credentials
21
+ from ..errors import error_from_status
22
+ from ..signing import sign_request
23
+
24
+ # The public API is JSON-only and every endpoint is POST (including reads). Bodies are
25
+ # serialized compactly and deterministically; the exact bytes are what we hash and sign, so
26
+ # the body that goes on the wire must be the body we signed - never re-serialize downstream.
27
+ _JSON_SEPARATORS = (",", ":")
28
+
29
+
30
+ def serialize_body(payload: dict[str, Any] | None) -> bytes:
31
+ """Serialize a request payload to the canonical bytes that get signed and sent.
32
+
33
+ ``None`` and ``{}`` both serialize to ``b""`` - the body-less endpoints (``me.get``,
34
+ ``invoice/services``) take no body, and the server hashes an empty body for them.
35
+ """
36
+ if not payload:
37
+ return b""
38
+ return json.dumps(payload, separators=_JSON_SEPARATORS, ensure_ascii=False).encode("utf-8")
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class PreparedRequest:
43
+ """A fully-signed request ready to hand to httpx."""
44
+
45
+ method: str
46
+ url: str
47
+ headers: dict[str, str]
48
+ content: bytes
49
+
50
+
51
+ def prepare(
52
+ *,
53
+ credentials: Credentials,
54
+ base_url: str,
55
+ path: str,
56
+ payload: dict[str, Any] | None,
57
+ ) -> PreparedRequest:
58
+ """Build and sign a request for ``path`` with ``payload``.
59
+
60
+ Args:
61
+ credentials: The merchant credentials to sign with.
62
+ base_url: API root, e.g. ``"https://api.altpay.money"`` (no trailing ``/api/v2``).
63
+ path: The endpoint path, e.g. ``"/api/v2/invoice/create"``.
64
+ payload: The request body as a dict, or ``None`` for body-less endpoints.
65
+
66
+ Returns:
67
+ A :class:`PreparedRequest` whose ``headers`` already include the signature set.
68
+ """
69
+ body = serialize_body(payload)
70
+ headers = {
71
+ "Content-Type": "application/json",
72
+ "Accept": "application/json",
73
+ "User-Agent": __user_agent__,
74
+ }
75
+ headers.update(
76
+ sign_request(
77
+ api_secret=credentials.api_secret,
78
+ merchant_id=credentials.merchant_id,
79
+ api_key=credentials.api_key,
80
+ method="POST",
81
+ path=path,
82
+ body=body,
83
+ )
84
+ )
85
+ return PreparedRequest(method="POST", url=f"{base_url}{path}", headers=headers, content=body)
86
+
87
+
88
+ def parse_response(
89
+ *,
90
+ status_code: int,
91
+ body_text: str,
92
+ headers: Any,
93
+ ) -> Any:
94
+ """Turn a raw response into the unwrapped ``result`` payload, or raise a typed error.
95
+
96
+ On a 2xx response, returns the value of the JSON body's ``result`` field (or the whole
97
+ body if it is not wrapped). On any other status, raises the most specific
98
+ :class:`~altpay.errors.APIError` subclass, carrying the server's ``detail`` and the
99
+ ``X-Request-Id`` header (when present) for support correlation.
100
+ """
101
+ parsed = _safe_json(body_text)
102
+ request_id = _get_header(headers, "X-Request-Id")
103
+
104
+ if 200 <= status_code < 300:
105
+ if isinstance(parsed, dict) and "result" in parsed:
106
+ return parsed["result"]
107
+ return parsed
108
+
109
+ detail = parsed.get("detail") if isinstance(parsed, dict) else None
110
+ retry_after = _parse_retry_after(_get_header(headers, "Retry-After"))
111
+ raise error_from_status(
112
+ status_code,
113
+ detail=detail if isinstance(detail, str) else None,
114
+ response_body=parsed,
115
+ request_id=request_id,
116
+ retry_after=retry_after,
117
+ )
118
+
119
+
120
+ def _safe_json(text: str) -> Any:
121
+ """Parse JSON, falling back to the raw text so a non-JSON error page is still surfaced."""
122
+ if not text:
123
+ return None
124
+ try:
125
+ return json.loads(text)
126
+ except ValueError:
127
+ return text
128
+
129
+
130
+ def _get_header(headers: Any, name: str) -> str | None:
131
+ """Read a header from an httpx.Headers (case-insensitive) or a plain mapping."""
132
+ try:
133
+ return headers.get(name)
134
+ except AttributeError:
135
+ lowered = name.lower()
136
+ for key, value in dict(headers).items():
137
+ if str(key).lower() == lowered:
138
+ return value
139
+ return None
140
+
141
+
142
+ def _parse_retry_after(value: str | None) -> float | None:
143
+ """Parse a ``Retry-After`` header expressed in seconds; ignore HTTP-date form."""
144
+ if not value:
145
+ return None
146
+ try:
147
+ return float(value)
148
+ except ValueError:
149
+ return None
@@ -0,0 +1,149 @@
1
+ """The synchronous client, :class:`AltPay`.
2
+
3
+ Wraps an :class:`httpx.Client`. Resources (``invoices``, ``account``) describe calls; this
4
+ class signs, sends and parses them. It is the synchronous twin of
5
+ :class:`~altpay.client.async_client.AsyncAltPay` and shares all of its logic via
6
+ :mod:`altpay.client.base`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from typing import Any, TypeVar
13
+
14
+ import httpx
15
+
16
+ from ..credentials import Credentials
17
+ from ..errors import AltPayTransportError, RateLimitError, ServerError
18
+ from ..methods.account import AccountResource
19
+ from ..methods.base import APICall
20
+ from ..methods.invoices import Invoices
21
+ from ..methods.withdrawals import Withdrawals
22
+ from .base import parse_response, prepare
23
+
24
+ T = TypeVar("T")
25
+
26
+ DEFAULT_BASE_URL = "https://api.altpay.money"
27
+
28
+
29
+ class AltPay:
30
+ """Synchronous AltPay API client.
31
+
32
+ Example::
33
+
34
+ from decimal import Decimal
35
+ from altpay import AltPay, Credentials
36
+
37
+ client = AltPay(Credentials(
38
+ merchant_id="...", api_key="vc_live_...", api_secret="...",
39
+ ))
40
+ invoice = client.invoices.create(
41
+ uuid="order-1", amount=Decimal("100.00"), fiat_currency="USD",
42
+ url_callback="https://example.com/altpay/webhook",
43
+ )
44
+ print(invoice.url)
45
+ client.close()
46
+
47
+ Use it as a context manager (``with AltPay(...) as client:``) to close the underlying
48
+ HTTP connection pool automatically.
49
+
50
+ Args:
51
+ credentials: Your merchant credentials.
52
+ base_url: API root (default ``https://api.altpay.money``). Override for staging.
53
+ timeout: Per-request timeout in seconds (default 30).
54
+ max_retries: How many times to retry a request that failed transiently - a network
55
+ error, a 5xx, or a 429. Retries use exponential backoff and honor ``Retry-After``.
56
+ Set to 0 to disable. Only idempotent reads and create-with-your-own-uuid are
57
+ safe to retry, which is the whole public surface.
58
+ http_client: Bring your own configured :class:`httpx.Client` (proxies, custom TLS,
59
+ etc.). If given, ``base_url``/``timeout`` on the httpx client are used and this
60
+ class will not close it for you.
61
+
62
+ API reference: https://docs.altpay.money/docs
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ credentials: Credentials,
68
+ *,
69
+ base_url: str = DEFAULT_BASE_URL,
70
+ timeout: float = 30.0,
71
+ max_retries: int = 2,
72
+ http_client: httpx.Client | None = None,
73
+ ) -> None:
74
+ self._credentials = credentials
75
+ self._base_url = base_url.rstrip("/")
76
+ self._max_retries = max_retries
77
+ self._owns_client = http_client is None
78
+ self._http = http_client or httpx.Client(timeout=timeout)
79
+
80
+ #: Invoice and static-wallet operations.
81
+ self.invoices = Invoices(self.call)
82
+ #: Payout (withdrawal) operations.
83
+ self.withdrawals = Withdrawals(self.call)
84
+ #: Account identity, balance and statistics.
85
+ self.account = AccountResource(self.call)
86
+
87
+ def __enter__(self) -> "AltPay":
88
+ return self
89
+
90
+ def __exit__(self, *exc: object) -> None:
91
+ self.close()
92
+
93
+ def close(self) -> None:
94
+ """Close the underlying HTTP connection pool (unless you supplied your own client)."""
95
+ if self._owns_client:
96
+ self._http.close()
97
+
98
+ def call(self, call: APICall[T]) -> T:
99
+ """Execute a described :class:`~altpay.methods.base.APICall` and return its typed result.
100
+
101
+ You normally call resource methods (``client.invoices.create(...)``) which return an
102
+ ``APICall`` and pass it here implicitly; use :meth:`call` directly only for advanced
103
+ or custom calls.
104
+ """
105
+ request = prepare(
106
+ credentials=self._credentials,
107
+ base_url=self._base_url,
108
+ path=call.path,
109
+ payload=call.payload,
110
+ )
111
+ attempt = 0
112
+ while True:
113
+ try:
114
+ response = self._http.request(
115
+ request.method, request.url, headers=request.headers, content=request.content
116
+ )
117
+ except httpx.HTTPError as exc:
118
+ if attempt >= self._max_retries:
119
+ raise AltPayTransportError(str(exc)) from exc
120
+ time.sleep(_backoff(attempt))
121
+ attempt += 1
122
+ continue
123
+
124
+ try:
125
+ result = parse_response(
126
+ status_code=response.status_code,
127
+ body_text=response.text,
128
+ headers=response.headers,
129
+ )
130
+ except (RateLimitError, ServerError) as exc:
131
+ if attempt >= self._max_retries:
132
+ raise
133
+ time.sleep(_retry_delay(exc, attempt))
134
+ attempt += 1
135
+ continue
136
+ return call.parse(result)
137
+
138
+
139
+ def _backoff(attempt: int) -> float:
140
+ """Exponential backoff: 0.5s, 1s, 2s, ... capped at 8s."""
141
+ return min(0.5 * (2 ** attempt), 8.0)
142
+
143
+
144
+ def _retry_delay(exc: Any, attempt: int) -> float:
145
+ """Honor a 429's Retry-After when present, else fall back to exponential backoff."""
146
+ retry_after = getattr(exc, "retry_after", None)
147
+ if retry_after is not None:
148
+ return float(retry_after)
149
+ return _backoff(attempt)
altpay/credentials.py ADDED
@@ -0,0 +1,40 @@
1
+ """Merchant credentials.
2
+
3
+ A small immutable holder for the four secrets the SDK needs. Grouping them keeps the client
4
+ constructors clean and makes it obvious which value plays which role - a frequent source of
5
+ ``invalid_signature`` errors is swapping the public ``api_key`` with the secret ``api_secret``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class Credentials:
15
+ """The credentials issued for one API key in your dashboard.
16
+
17
+ Args:
18
+ merchant_id: Your merchant UUID. Sent in the clear as ``X-Merchant-Id``.
19
+ api_key: The public key identifier (``vc_live_...``). Sent in the clear as
20
+ ``X-Api-Key``; the server looks it up by hash.
21
+ api_secret: The signing secret. Never transmitted - it is the HMAC key for every
22
+ request signature. Keep it server-side only.
23
+ webhook_secret: The secret used to sign webhooks AltPay sends you. Optional here;
24
+ required only if you verify webhooks with this SDK. Distinct from ``api_secret``.
25
+
26
+ All four are shown once at key creation and cannot be retrieved later - store them in
27
+ your secret manager. See https://docs.altpay.money/docs/authentication
28
+ """
29
+
30
+ merchant_id: str
31
+ api_key: str
32
+ api_secret: str
33
+ webhook_secret: str | None = None
34
+
35
+ def __post_init__(self) -> None:
36
+ if not self.merchant_id or not self.api_key or not self.api_secret:
37
+ raise ValueError(
38
+ "merchant_id, api_key and api_secret are all required. "
39
+ "See https://docs.altpay.money/docs/authentication"
40
+ )
altpay/enums.py ADDED
@@ -0,0 +1,94 @@
1
+ """Enumerations used across the public API.
2
+
3
+ These mirror the server's vocabulary exactly. Each is a ``str`` enum, so a member compares
4
+ equal to its wire value (``PaymentStatus.PAYED == "PAYED"``) and serializes transparently -
5
+ you can pass either a member or the raw string anywhere the SDK accepts one.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+
12
+
13
+ class PaymentStatus(str, Enum):
14
+ """Lifecycle state of an invoice.
15
+
16
+ * ``CREATED`` - issued, awaiting payment.
17
+ * ``INCOMPLETE`` - underpaid (received less than due); still open.
18
+ * ``PAYED`` - fully paid and confirmed (final). The webhook fires on this transition.
19
+ * ``EXPIRED`` - the lifetime window elapsed before payment (final).
20
+ * ``FAILED`` - the payment failed or was rejected (final).
21
+
22
+ ``CREATED``/``INCOMPLETE`` are non-final; the other three are final
23
+ (see :attr:`is_final`).
24
+ """
25
+
26
+ CREATED = "CREATED"
27
+ PAYED = "PAYED"
28
+ INCOMPLETE = "INCOMPLETE"
29
+ EXPIRED = "EXPIRED"
30
+ FAILED = "FAILED"
31
+
32
+ @property
33
+ def is_final(self) -> bool:
34
+ """Whether no further status change is possible (PAYED, EXPIRED or FAILED)."""
35
+ return self in (PaymentStatus.PAYED, PaymentStatus.EXPIRED, PaymentStatus.FAILED)
36
+
37
+
38
+ class PaymentMethod(str, Enum):
39
+ """A payment method the payer can settle with.
40
+
41
+ On-chain methods deliver to a generated deposit address; ``LOLZTEAM`` and ``CRYPTOBOT``
42
+ are off-chain providers that redirect the payer to a hosted page. ``USDT_*`` names encode
43
+ the network the stablecoin settles on (e.g. ``USDT_TRC20`` is USDT on TRON).
44
+ """
45
+
46
+ TRX = "TRX"
47
+ BTC = "BTC"
48
+ ETH = "ETH"
49
+ LTC = "LTC"
50
+ BNB = "BNB"
51
+ TON = "TON"
52
+ SOLANA = "SOLANA"
53
+ USDT_TRC20 = "USDT_TRC20"
54
+ USDT_ETH = "USDT_ETH"
55
+ USDT_BSC = "USDT_BSC"
56
+ USDT_TON = "USDT_TON"
57
+ USDT_SOLANA = "USDT_SOLANA"
58
+ LOLZTEAM = "LOLZTEAM"
59
+ CRYPTOBOT = "CRYPTOBOT"
60
+
61
+
62
+ class FiatCurrency(str, Enum):
63
+ """A fiat currency an invoice can be denominated in."""
64
+
65
+ RUB = "RUB"
66
+ USD = "USD"
67
+ EUR = "EUR"
68
+
69
+
70
+ class MerchantStatus(str, Enum):
71
+ """Account status of a merchant.
72
+
73
+ Only ``APPROVED`` merchants can create invoices; the others block payment creation
74
+ (``PENDING_REVIEW`` until onboarding completes, ``SUSPENDED``/``ARCHIVED``/``REJECTED``
75
+ permanently or until support intervenes).
76
+ """
77
+
78
+ PENDING_REVIEW = "PENDING_REVIEW"
79
+ APPROVED = "APPROVED"
80
+ REJECTED = "REJECTED"
81
+ ARCHIVED = "ARCHIVED"
82
+ SUSPENDED = "SUSPENDED"
83
+
84
+
85
+ class WithdrawalStatus(str, Enum):
86
+ """Lifecycle state of a payout request.
87
+
88
+ A new request is ``PENDING`` review; an operator then ``APPROVED`` it (funds are paid
89
+ out) or ``REJECTED`` it (the reserved amount is credited back to your balance).
90
+ """
91
+
92
+ PENDING = "PENDING"
93
+ APPROVED = "APPROVED"
94
+ REJECTED = "REJECTED"