cryptochief-crypto-processing-python 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.
Files changed (39) hide show
  1. cryptochief/__init__.py +336 -0
  2. cryptochief/_models.py +93 -0
  3. cryptochief/_version.py +3 -0
  4. cryptochief/amount.py +97 -0
  5. cryptochief/assets.py +32 -0
  6. cryptochief/chains.py +115 -0
  7. cryptochief/client.py +189 -0
  8. cryptochief/contract/__init__.py +69 -0
  9. cryptochief/contract/base58.py +40 -0
  10. cryptochief/contract/borsh.py +141 -0
  11. cryptochief/contract/evm_abi.py +321 -0
  12. cryptochief/contract/keccak.py +16 -0
  13. cryptochief/contract/tron_address.py +60 -0
  14. cryptochief/errors.py +111 -0
  15. cryptochief/pagination.py +32 -0
  16. cryptochief/poll.py +56 -0
  17. cryptochief/rsa.py +70 -0
  18. cryptochief/services/__init__.py +1 -0
  19. cryptochief/services/base.py +24 -0
  20. cryptochief/services/blockchain.py +76 -0
  21. cryptochief/services/currencies.py +55 -0
  22. cryptochief/services/payins.py +145 -0
  23. cryptochief/services/payouts.py +175 -0
  24. cryptochief/services/static_deposits.py +77 -0
  25. cryptochief/services/sweeps.py +85 -0
  26. cryptochief/services/transactions.py +470 -0
  27. cryptochief/services/wallets.py +86 -0
  28. cryptochief/services/withdrawals.py +51 -0
  29. cryptochief/sign.py +112 -0
  30. cryptochief/ton/__init__.py +19 -0
  31. cryptochief/ton/address.py +109 -0
  32. cryptochief/ton/messages.py +105 -0
  33. cryptochief/ton/rpc.py +159 -0
  34. cryptochief/transport.py +48 -0
  35. cryptochief/webhook.py +191 -0
  36. cryptochief_crypto_processing_python-0.1.0.dist-info/METADATA +346 -0
  37. cryptochief_crypto_processing_python-0.1.0.dist-info/RECORD +39 -0
  38. cryptochief_crypto_processing_python-0.1.0.dist-info/WHEEL +4 -0
  39. cryptochief_crypto_processing_python-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,336 @@
1
+ """Crypto Chief Python SDK - the official async client for the
2
+ `Crypto Chief <https://crypto-chief.com/processing/>`_ crypto processing API.
3
+
4
+ Accept crypto payments, send single and mass payouts, sign on-chain
5
+ transactions and smart-contract calls, manage wallets, convert fiat to crypto,
6
+ and verify webhooks across Ethereum, Tron, TON, Solana, Bitcoin, XRP and 20+
7
+ more blockchains.
8
+
9
+ >>> import asyncio
10
+ >>> from cryptochief import CryptoChiefClient, Chain
11
+ >>> from cryptochief import EstimatePayoutRequest
12
+ >>>
13
+ >>> async def main():
14
+ ... async with CryptoChiefClient(merchant_id="M", api_key="K") as client:
15
+ ... est = await client.payouts.estimate(EstimatePayoutRequest(
16
+ ... network=Chain.ETH_SEPOLIA, coin="ETH", amount="0.0001",
17
+ ... to_address="0x...",
18
+ ... ))
19
+ ... print(est.amount_to_receive)
20
+ >>> asyncio.run(main()) # doctest: +SKIP
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from ._version import __version__
26
+ from .amount import InvalidAmountError, base_to_human, human_to_base, nano_ton
27
+ from .assets import Asset, AssetsPolicy
28
+ from .chains import Chain, ChainFamily, chain_family, supports_contract_calls
29
+ from .client import DEFAULT_BASE_URL, VERSION, CryptoChiefClient
30
+ from .contract import (
31
+ BorshValue,
32
+ anchor_discriminator,
33
+ base58_decode,
34
+ base58_encode,
35
+ borsh_bool,
36
+ borsh_bytes,
37
+ borsh_fixed_bytes,
38
+ borsh_i8,
39
+ borsh_i16,
40
+ borsh_i32,
41
+ borsh_i64,
42
+ borsh_option,
43
+ borsh_pubkey,
44
+ borsh_string,
45
+ borsh_struct,
46
+ borsh_u8,
47
+ borsh_u16,
48
+ borsh_u32,
49
+ borsh_u64,
50
+ borsh_u128,
51
+ borsh_vec,
52
+ canonical_signature,
53
+ decode_solana_pubkey,
54
+ encode_anchor_instruction,
55
+ encode_evm_call,
56
+ encode_evm_call_hex,
57
+ evm_selector,
58
+ hex_to_tron,
59
+ keccak_256,
60
+ tron_to_hex,
61
+ )
62
+ from .errors import APIError, CryptoChiefError, ErrorCode, is_api_error, is_retryable
63
+ from .pagination import HistoryMeta, HistoryQuery
64
+ from .poll import PollTimeoutError, wait_for_terminal
65
+ from .rsa import (
66
+ RsaKeyNotConfiguredError,
67
+ decrypt_rsa_oaep,
68
+ load_rsa_private_key_file,
69
+ load_rsa_private_key_pem,
70
+ )
71
+ from .services.blockchain import (
72
+ AvailableContract,
73
+ AvailableContractsResponse,
74
+ BlockchainService,
75
+ TxStatusRow,
76
+ WalletBalanceRow,
77
+ )
78
+ from .services.currencies import ConvertRequest, ConvertResponse, CurrenciesService
79
+ from .services.payins import (
80
+ CoinOption,
81
+ CreatePayInRequest,
82
+ PayIn,
83
+ PayInHistoryResponse,
84
+ PayInMode,
85
+ PayInsService,
86
+ PayInStatus,
87
+ SelectAssetRequest,
88
+ is_payin_terminal,
89
+ )
90
+ from .services.payouts import (
91
+ BatchItemResult,
92
+ BatchPayoutRequest,
93
+ BatchPayoutResponse,
94
+ EstimatePayoutRequest,
95
+ EstimatePayoutResponse,
96
+ ExecutePayoutRequest,
97
+ PayoutFeeInfo,
98
+ PayoutHistoryResponse,
99
+ PayoutInfo,
100
+ PayoutsService,
101
+ PayoutSource,
102
+ PayoutStatus,
103
+ is_payout_terminal,
104
+ )
105
+ from .services.static_deposits import (
106
+ StaticDeposit,
107
+ StaticDepositHistoryQuery,
108
+ StaticDepositHistoryResponse,
109
+ StaticDepositsService,
110
+ StaticDepositStatus,
111
+ )
112
+ from .services.sweeps import (
113
+ ForceSweepResponse,
114
+ Sweep,
115
+ SweepHistoryQuery,
116
+ SweepHistoryResponse,
117
+ SweepMode,
118
+ SweepsService,
119
+ )
120
+ from .services.transactions import (
121
+ AnchorCallRequest,
122
+ ContractCall,
123
+ Erc20TransferRequest,
124
+ EvmCallRequest,
125
+ ExecuteTransactionRequest,
126
+ JettonTransferRequest,
127
+ NftTransferRequest,
128
+ SignTransactionRequest,
129
+ SignTransactionResponse,
130
+ SolanaAccount,
131
+ SolanaCallRequest,
132
+ TonCallRequest,
133
+ TonCommentRequest,
134
+ TransactionHistoryResponse,
135
+ TransactionInfo,
136
+ TransactionsService,
137
+ TxStatus,
138
+ TxType,
139
+ is_transaction_terminal,
140
+ )
141
+ from .services.wallets import (
142
+ GenerateWalletRequest,
143
+ ListWalletsResponse,
144
+ Wallet,
145
+ WalletCoinBalance,
146
+ WalletsService,
147
+ WalletType,
148
+ )
149
+ from .services.withdrawals import Withdrawal, WithdrawalHistoryResponse, WithdrawalsService
150
+ from .sign import canonical_json, sign, sign_value
151
+ from .ton import (
152
+ TonAddress,
153
+ crc16_xmodem,
154
+ parse_ton_address,
155
+ ton_address_to_raw,
156
+ ton_address_to_string,
157
+ )
158
+ from .webhook import (
159
+ WEBHOOK_HEADER,
160
+ WEBHOOK_SENDER_IPS,
161
+ PayInWebhookEvent,
162
+ PayoutWebhookEvent,
163
+ StaticDepositWebhookEvent,
164
+ TransactionWebhookEvent,
165
+ WebhookSignatureError,
166
+ coerce_webhook_event,
167
+ parse_webhook_event,
168
+ verify_webhook_signature,
169
+ )
170
+
171
+ __all__ = [
172
+ "__version__",
173
+ # Client
174
+ "CryptoChiefClient",
175
+ "VERSION",
176
+ "DEFAULT_BASE_URL",
177
+ # Errors
178
+ "CryptoChiefError",
179
+ "APIError",
180
+ "ErrorCode",
181
+ "is_api_error",
182
+ "is_retryable",
183
+ # Signing
184
+ "canonical_json",
185
+ "sign",
186
+ "sign_value",
187
+ # Amounts
188
+ "human_to_base",
189
+ "base_to_human",
190
+ "nano_ton",
191
+ "InvalidAmountError",
192
+ # Chains / assets / pagination
193
+ "Chain",
194
+ "ChainFamily",
195
+ "chain_family",
196
+ "supports_contract_calls",
197
+ "Asset",
198
+ "AssetsPolicy",
199
+ "HistoryQuery",
200
+ "HistoryMeta",
201
+ # Polling
202
+ "wait_for_terminal",
203
+ "PollTimeoutError",
204
+ # RSA
205
+ "load_rsa_private_key_pem",
206
+ "load_rsa_private_key_file",
207
+ "decrypt_rsa_oaep",
208
+ "RsaKeyNotConfiguredError",
209
+ # Webhooks
210
+ "verify_webhook_signature",
211
+ "parse_webhook_event",
212
+ "coerce_webhook_event",
213
+ "WebhookSignatureError",
214
+ "WEBHOOK_HEADER",
215
+ "WEBHOOK_SENDER_IPS",
216
+ "PayoutWebhookEvent",
217
+ "TransactionWebhookEvent",
218
+ "PayInWebhookEvent",
219
+ "StaticDepositWebhookEvent",
220
+ # Services
221
+ "PayoutsService",
222
+ "TransactionsService",
223
+ "PayInsService",
224
+ "WalletsService",
225
+ "SweepsService",
226
+ "WithdrawalsService",
227
+ "StaticDepositsService",
228
+ "BlockchainService",
229
+ "CurrenciesService",
230
+ # Payout types
231
+ "EstimatePayoutRequest",
232
+ "ExecutePayoutRequest",
233
+ "EstimatePayoutResponse",
234
+ "PayoutInfo",
235
+ "PayoutFeeInfo",
236
+ "PayoutSource",
237
+ "PayoutHistoryResponse",
238
+ "BatchPayoutRequest",
239
+ "BatchPayoutResponse",
240
+ "BatchItemResult",
241
+ "PayoutStatus",
242
+ "is_payout_terminal",
243
+ # Transaction types
244
+ "SignTransactionRequest",
245
+ "SignTransactionResponse",
246
+ "ExecuteTransactionRequest",
247
+ "TransactionInfo",
248
+ "TransactionHistoryResponse",
249
+ "ContractCall",
250
+ "SolanaAccount",
251
+ "EvmCallRequest",
252
+ "Erc20TransferRequest",
253
+ "AnchorCallRequest",
254
+ "SolanaCallRequest",
255
+ "TonCallRequest",
256
+ "JettonTransferRequest",
257
+ "NftTransferRequest",
258
+ "TonCommentRequest",
259
+ "TxType",
260
+ "TxStatus",
261
+ "is_transaction_terminal",
262
+ # Pay-in types
263
+ "CreatePayInRequest",
264
+ "SelectAssetRequest",
265
+ "PayIn",
266
+ "CoinOption",
267
+ "PayInHistoryResponse",
268
+ "PayInMode",
269
+ "PayInStatus",
270
+ "is_payin_terminal",
271
+ # Wallet types
272
+ "GenerateWalletRequest",
273
+ "Wallet",
274
+ "WalletCoinBalance",
275
+ "ListWalletsResponse",
276
+ "WalletType",
277
+ # Sweep types
278
+ "Sweep",
279
+ "SweepHistoryQuery",
280
+ "SweepHistoryResponse",
281
+ "ForceSweepResponse",
282
+ "SweepMode",
283
+ # Withdrawal types
284
+ "Withdrawal",
285
+ "WithdrawalHistoryResponse",
286
+ # Static deposit types
287
+ "StaticDeposit",
288
+ "StaticDepositHistoryQuery",
289
+ "StaticDepositHistoryResponse",
290
+ "StaticDepositStatus",
291
+ # Blockchain types
292
+ "AvailableContract",
293
+ "AvailableContractsResponse",
294
+ "WalletBalanceRow",
295
+ "TxStatusRow",
296
+ # Currency types
297
+ "ConvertRequest",
298
+ "ConvertResponse",
299
+ # Contract encoders
300
+ "encode_evm_call",
301
+ "encode_evm_call_hex",
302
+ "evm_selector",
303
+ "canonical_signature",
304
+ "keccak_256",
305
+ "BorshValue",
306
+ "borsh_u8",
307
+ "borsh_u16",
308
+ "borsh_u32",
309
+ "borsh_u64",
310
+ "borsh_i8",
311
+ "borsh_i16",
312
+ "borsh_i32",
313
+ "borsh_i64",
314
+ "borsh_u128",
315
+ "borsh_bool",
316
+ "borsh_string",
317
+ "borsh_bytes",
318
+ "borsh_fixed_bytes",
319
+ "borsh_pubkey",
320
+ "borsh_option",
321
+ "borsh_vec",
322
+ "borsh_struct",
323
+ "anchor_discriminator",
324
+ "encode_anchor_instruction",
325
+ "decode_solana_pubkey",
326
+ "tron_to_hex",
327
+ "hex_to_tron",
328
+ "base58_encode",
329
+ "base58_decode",
330
+ # TON address utilities (offline)
331
+ "parse_ton_address",
332
+ "ton_address_to_string",
333
+ "ton_address_to_raw",
334
+ "crc16_xmodem",
335
+ "TonAddress",
336
+ ]
cryptochief/_models.py ADDED
@@ -0,0 +1,93 @@
1
+ """Dataclass <-> wire helpers.
2
+
3
+ The public API is modeled with dataclasses whose field names already match the
4
+ snake_case wire format, so there is no case conversion to do - requests serialize
5
+ straight to the body and responses parse straight back.
6
+
7
+ :func:`to_payload` turns a request (dataclass / dict / list / enum / scalar) into
8
+ a JSON-ready value, dropping ``None`` fields. :func:`from_dict` builds a typed
9
+ dataclass from a response dict, recursing into nested dataclass and list fields
10
+ and tolerating unknown keys (forward-compatible with new server fields).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import dataclasses
16
+ import enum
17
+ from typing import Any, Mapping, Optional, Type, TypeVar, Union, get_args, get_origin, get_type_hints
18
+
19
+ T = TypeVar("T")
20
+
21
+ _hints_cache: dict[type, dict[str, Any]] = {}
22
+
23
+
24
+ def _type_hints(cls: type) -> dict[str, Any]:
25
+ cached = _hints_cache.get(cls)
26
+ if cached is None:
27
+ cached = get_type_hints(cls)
28
+ _hints_cache[cls] = cached
29
+ return cached
30
+
31
+
32
+ def to_payload(value: Any) -> Any:
33
+ """Recursively convert a request value to a JSON-ready form, dropping ``None``."""
34
+ if value is None:
35
+ return None
36
+ if isinstance(value, bool):
37
+ return value
38
+ if isinstance(value, enum.Enum):
39
+ return value.value
40
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
41
+ out: dict[str, Any] = {}
42
+ for f in dataclasses.fields(value):
43
+ v = getattr(value, f.name)
44
+ if v is None:
45
+ continue
46
+ out[f.name] = to_payload(v)
47
+ return out
48
+ if isinstance(value, dict):
49
+ return {k: to_payload(v) for k, v in value.items() if v is not None}
50
+ if isinstance(value, (list, tuple)):
51
+ return [to_payload(v) for v in value]
52
+ return value
53
+
54
+
55
+ def _unwrap_optional(tp: Any) -> Any:
56
+ if get_origin(tp) is Union:
57
+ args = [a for a in get_args(tp) if a is not type(None)]
58
+ if len(args) == 1:
59
+ return args[0]
60
+ return args[0] if args else Any
61
+ return tp
62
+
63
+
64
+ def _coerce(tp: Any, value: Any) -> Any:
65
+ if value is None:
66
+ return None
67
+ tp = _unwrap_optional(tp)
68
+ origin = get_origin(tp)
69
+ if origin in (list, tuple):
70
+ args = get_args(tp)
71
+ elem = args[0] if args else Any
72
+ return [_coerce(elem, v) for v in value]
73
+ if isinstance(tp, type) and dataclasses.is_dataclass(tp):
74
+ return from_dict(tp, value)
75
+ return value
76
+
77
+
78
+ def from_dict(cls: Type[T], data: Optional[Mapping[str, Any]]) -> T:
79
+ """Build a dataclass of type ``cls`` from a response mapping.
80
+
81
+ Recurses into nested dataclass / list fields and ignores keys the dataclass
82
+ does not declare (forward-compatible with new server fields). A ``None`` or
83
+ empty body yields an all-defaults instance - every response model is
84
+ constructible with no arguments.
85
+ """
86
+ if not isinstance(data, Mapping):
87
+ data = {}
88
+ hints = _type_hints(cls)
89
+ kwargs: dict[str, Any] = {}
90
+ for f in dataclasses.fields(cls): # type: ignore[arg-type] # cls is a dataclass type
91
+ if f.name in data:
92
+ kwargs[f.name] = _coerce(hints.get(f.name, Any), data[f.name])
93
+ return cls(**kwargs)
@@ -0,0 +1,3 @@
1
+ """Single source of truth for the package version."""
2
+
3
+ __version__ = "0.1.0"
cryptochief/amount.py ADDED
@@ -0,0 +1,97 @@
1
+ """Amount conversion helpers backed by Python's arbitrary-precision ``int``.
2
+
3
+ **Never use ``float`` for crypto amounts**: ``0.1 + 0.2 != 0.3`` and large token
4
+ values lose precision. These helpers parse decimal strings exactly and return
5
+ ``int`` base units.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .errors import CryptoChiefError
11
+
12
+
13
+ class InvalidAmountError(CryptoChiefError):
14
+ """Raised by :func:`human_to_base` when its input is not a plain decimal."""
15
+
16
+ def __init__(self, message: str) -> None:
17
+ super().__init__(f"cryptochief: invalid amount: {message}")
18
+
19
+
20
+ def _is_ascii_digits(s: str) -> bool:
21
+ return len(s) > 0 and all("0" <= c <= "9" for c in s)
22
+
23
+
24
+ def human_to_base(human: str, decimals: int) -> int:
25
+ """Convert a decimal human amount (e.g. ``"0.0001"``) to its base-unit ``int``.
26
+
27
+ Precise to the last digit. Negative amounts and scientific notation are
28
+ rejected. Sub-base-unit precision is truncated, since it is meaningless
29
+ on-chain.
30
+
31
+ >>> human_to_base("1.5", 18)
32
+ 1500000000000000000
33
+ >>> human_to_base("0.0001", 8)
34
+ 10000
35
+ """
36
+ s = human.strip()
37
+ if s == "":
38
+ raise InvalidAmountError("empty")
39
+ if not isinstance(decimals, int) or isinstance(decimals, bool) or decimals < 0:
40
+ raise InvalidAmountError(f"negative or non-integer decimals {decimals!r}")
41
+ if "e" in s or "E" in s:
42
+ raise InvalidAmountError(f"scientific notation not allowed: {human!r}")
43
+ if s.startswith("-"):
44
+ raise InvalidAmountError(f"negative not allowed: {human!r}")
45
+
46
+ dot = s.find(".")
47
+ if dot < 0:
48
+ if not _is_ascii_digits(s):
49
+ raise InvalidAmountError(repr(human))
50
+ int_part, frac_part = s, ""
51
+ else:
52
+ int_part = s[:dot] or "0"
53
+ frac_part = s[dot + 1 :]
54
+ if frac_part == "":
55
+ raise InvalidAmountError(repr(human))
56
+ if not _is_ascii_digits(int_part) or not _is_ascii_digits(frac_part):
57
+ raise InvalidAmountError(repr(human))
58
+
59
+ if len(frac_part) > decimals:
60
+ frac_part = frac_part[:decimals]
61
+ else:
62
+ frac_part = frac_part.ljust(decimals, "0")
63
+
64
+ combined = (int_part + frac_part).lstrip("0") or "0"
65
+ return int(combined)
66
+
67
+
68
+ def base_to_human(base: int, decimals: int) -> str:
69
+ """Inverse of :func:`human_to_base`: a base-unit ``int`` to a decimal string.
70
+
71
+ Trailing zeroes are trimmed.
72
+
73
+ >>> base_to_human(1500000000000000000, 18)
74
+ '1.5'
75
+ >>> base_to_human(0, 18)
76
+ '0'
77
+ """
78
+ if decimals < 0:
79
+ decimals = 0
80
+ abs_s = str(-base if base < 0 else base)
81
+ if decimals == 0:
82
+ return abs_s
83
+ if len(abs_s) <= decimals:
84
+ abs_s = "0" * (decimals - len(abs_s) + 1) + abs_s
85
+ cut = len(abs_s) - decimals
86
+ int_part = abs_s[:cut]
87
+ frac_part = abs_s[cut:].rstrip("0")
88
+ return int_part if frac_part == "" else f"{int_part}.{frac_part}"
89
+
90
+
91
+ def nano_ton(human: str) -> int:
92
+ """Convert a human TON amount (``"0.05"``) into base-unit nanoTON (``50000000``).
93
+
94
+ Equivalent to ``human_to_base(human, 9)`` - the form the TON helpers'
95
+ ``attached_ton`` / ``forward_ton_amount`` fields expect.
96
+ """
97
+ return human_to_base(human, 9)
cryptochief/assets.py ADDED
@@ -0,0 +1,32 @@
1
+ """Asset selection policies used by payouts and FIAT-mode pay-ins."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import List, Optional
7
+
8
+
9
+ @dataclass(kw_only=True)
10
+ class Asset:
11
+ """A specific coin on a specific network.
12
+
13
+ ``network`` takes a chain code (e.g. ``Chain.ETH_MAINNET``) or the wildcard
14
+ ``"ANY"``; ``coin`` is the symbol (e.g. ``"USDT"``). Either field may be
15
+ omitted to mean "any".
16
+ """
17
+
18
+ network: Optional[str] = None
19
+ coin: Optional[str] = None
20
+
21
+
22
+ @dataclass(kw_only=True)
23
+ class AssetsPolicy:
24
+ """An allow / exclude filter over :class:`Asset` entries.
25
+
26
+ Omitting both lists means "no restriction". Used for payout auto-convert
27
+ source selection and to restrict which coins a FIAT-mode pay-in customer may
28
+ pick.
29
+ """
30
+
31
+ allow: Optional[List[Asset]] = None
32
+ exclude: Optional[List[Asset]] = None
cryptochief/chains.py ADDED
@@ -0,0 +1,115 @@
1
+ """Chain codes and protocol families.
2
+
3
+ :class:`Chain` is the value of the ``network`` / ``chain`` / ``network_code``
4
+ fields across the API; :class:`ChainFamily` (the ``chain_family`` field) groups
5
+ chains by underlying protocol and drives capability checks such as "does this
6
+ chain accept contract calls?".
7
+
8
+ Both are ``str`` enums: a member compares equal to its wire string, and any
9
+ plain string is accepted wherever a chain is expected, so new chains work before
10
+ this SDK is updated.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from enum import Enum
16
+ from typing import Optional
17
+
18
+
19
+ class Chain(str, Enum):
20
+ """Chain codes the API currently supports."""
21
+
22
+ ETH_MAINNET = "ETH_MAINNET"
23
+ ETH_SEPOLIA = "ETH_SEPOLIA"
24
+ BSC_MAINNET = "BSC_MAINNET"
25
+ BSC_TESTNET = "BSC_TESTNET"
26
+ POLYGON_MAINNET = "POLYGON_MAINNET"
27
+ POLYGON_AMOY = "POLYGON_AMOY"
28
+ ARBITRUM_ONE = "ARBITRUM_ONE"
29
+ ARBITRUM_SEPOLIA = "ARBITRUM_SEPOLIA"
30
+ OPTIMISM_MAINNET = "OPTIMISM_MAINNET"
31
+ OPTIMISM_SEPOLIA = "OPTIMISM_SEPOLIA"
32
+ AVAX_MAINNET = "AVAX_MAINNET"
33
+ AVAX_TESTNET = "AVAX_TESTNET"
34
+
35
+ BTC_MAINNET = "BTC_MAINNET"
36
+ BTC_TESTNET_4 = "BTC_TESTNET_4"
37
+ LITECOIN_MAINNET = "LITECOIN_MAINNET"
38
+ BITCOIN_CASH_MAINNET = "BITCOIN_CASH_MAINNET"
39
+ DOGECOIN_MAINNET = "DOGECOIN_MAINNET"
40
+
41
+ TRON_MAINNET = "TRON_MAINNET"
42
+ TRON_NILE = "TRON_NILE"
43
+
44
+ SOLANA_MAINNET = "SOLANA_MAINNET"
45
+ SOLANA_DEVNET = "SOLANA_DEVNET"
46
+
47
+ TON_MAINNET = "TON_MAINNET"
48
+ TON_TESTNET = "TON_TESTNET"
49
+
50
+ XRP_MAINNET = "XRP_MAINNET"
51
+ XRP_TESTNET = "XRP_TESTNET"
52
+
53
+
54
+ class ChainFamily(str, Enum):
55
+ """Protocol families (the ``chain_family`` field in API responses)."""
56
+
57
+ EVM = "EVM"
58
+ TRON = "TRON"
59
+ SOLANA = "SOLANA"
60
+ XRP_LEDGER = "XRP_LEDGER"
61
+ TON = "TON"
62
+ BTC_UTXO = "BTC_UTXO"
63
+ BTC_UTXO_TESTNET = "BTC_UTXO_TESTNET"
64
+ DOGECOIN_UTXO = "DOGECOIN_UTXO"
65
+ BTC_CASH_UTXO = "BTC_CASH_UTXO"
66
+ LITECOIN_UTXO = "LITECOIN_UTXO"
67
+
68
+
69
+ _CHAIN_TO_FAMILY = {
70
+ Chain.ETH_MAINNET: ChainFamily.EVM,
71
+ Chain.ETH_SEPOLIA: ChainFamily.EVM,
72
+ Chain.BSC_MAINNET: ChainFamily.EVM,
73
+ Chain.BSC_TESTNET: ChainFamily.EVM,
74
+ Chain.POLYGON_MAINNET: ChainFamily.EVM,
75
+ Chain.POLYGON_AMOY: ChainFamily.EVM,
76
+ Chain.ARBITRUM_ONE: ChainFamily.EVM,
77
+ Chain.ARBITRUM_SEPOLIA: ChainFamily.EVM,
78
+ Chain.OPTIMISM_MAINNET: ChainFamily.EVM,
79
+ Chain.OPTIMISM_SEPOLIA: ChainFamily.EVM,
80
+ Chain.AVAX_MAINNET: ChainFamily.EVM,
81
+ Chain.AVAX_TESTNET: ChainFamily.EVM,
82
+ Chain.BTC_MAINNET: ChainFamily.BTC_UTXO,
83
+ Chain.BTC_TESTNET_4: ChainFamily.BTC_UTXO_TESTNET,
84
+ Chain.LITECOIN_MAINNET: ChainFamily.LITECOIN_UTXO,
85
+ Chain.BITCOIN_CASH_MAINNET: ChainFamily.BTC_CASH_UTXO,
86
+ Chain.DOGECOIN_MAINNET: ChainFamily.DOGECOIN_UTXO,
87
+ Chain.TRON_MAINNET: ChainFamily.TRON,
88
+ Chain.TRON_NILE: ChainFamily.TRON,
89
+ Chain.SOLANA_MAINNET: ChainFamily.SOLANA,
90
+ Chain.SOLANA_DEVNET: ChainFamily.SOLANA,
91
+ Chain.TON_MAINNET: ChainFamily.TON,
92
+ Chain.TON_TESTNET: ChainFamily.TON,
93
+ Chain.XRP_MAINNET: ChainFamily.XRP_LEDGER,
94
+ Chain.XRP_TESTNET: ChainFamily.XRP_LEDGER,
95
+ }
96
+ # Allow plain-string lookups too (e.g. the value off a response).
97
+ _CHAIN_TO_FAMILY_STR = {k.value: v for k, v in _CHAIN_TO_FAMILY.items()}
98
+
99
+
100
+ def chain_family(chain: str) -> Optional[ChainFamily]:
101
+ """Return the protocol family for a chain, or ``None`` if unrecognized."""
102
+ return _CHAIN_TO_FAMILY_STR.get(str(chain))
103
+
104
+
105
+ def supports_contract_calls(family: str) -> bool:
106
+ """Whether a chain family accepts the ``contract`` transaction type.
107
+
108
+ Only EVM, TRON, Solana, and TON do.
109
+ """
110
+ return family in (
111
+ ChainFamily.EVM,
112
+ ChainFamily.TRON,
113
+ ChainFamily.SOLANA,
114
+ ChainFamily.TON,
115
+ )