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,321 @@
1
+ """Solidity ABI encoder - turns a function signature + argument values into
2
+ calldata, so callers never hand-encode the ``data`` field. Shared by EVM and
3
+ TRON (TRON uses the same ABI).
4
+
5
+ Supported types: ``uint<M>`` / ``int<M>`` (M in 8..256, step 8; bare ``uint`` /
6
+ ``int`` alias to 256), ``address`` (0x hex, 0x41 TRON hex, or ``T...`` base58),
7
+ ``bool``, ``bytes``, ``bytes<N>`` (N in 1..32), ``string``, and fixed / dynamic
8
+ arrays ``T[]`` / ``T[N]`` of any supported ``T``.
9
+
10
+ Argument value forms: integers accept ``int`` or a string (decimal / ``0x``
11
+ hex); ``bytes`` accept ``bytes`` / ``bytearray`` or a string (raw / ``0x`` hex);
12
+ ``address`` / ``string`` take strings; arrays take lists of the above.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Any, List, Optional
19
+
20
+ from ..errors import CryptoChiefError
21
+ from .keccak import keccak_256
22
+ from .tron_address import tron_to_hex
23
+
24
+
25
+ class EvmAbiError(CryptoChiefError):
26
+ def __init__(self, message: str) -> None:
27
+ super().__init__(f"cryptochief/evm: {message}")
28
+
29
+
30
+ @dataclass
31
+ class _AbiType:
32
+ # kind: uint | int | address | bool | bytes | string | bytesN | array
33
+ kind: str
34
+ size: int = 0 # bits for int/uint; byte length for bytesN; element count for fixed arrays (-1 dynamic)
35
+ element: Optional["_AbiType"] = None
36
+
37
+
38
+ # -- Signature parsing --------------------------------------------------------
39
+
40
+
41
+ def _expand_alias(t: str) -> str:
42
+ i = t.rfind("[")
43
+ if i > 0:
44
+ return _expand_alias(t[:i]) + t[i:]
45
+ if t == "uint":
46
+ return "uint256"
47
+ if t == "int":
48
+ return "int256"
49
+ if t == "byte":
50
+ return "bytes1"
51
+ return t
52
+
53
+
54
+ def _strip_param_name(p: str) -> str:
55
+ p = p.strip()
56
+ sp = p.find(" ")
57
+ if sp >= 0:
58
+ p = p[:sp].strip()
59
+ return _expand_alias(p)
60
+
61
+
62
+ def canonical_signature(sig: str) -> str:
63
+ """Canonical form keccak hashes against (no spaces, no parameter names)."""
64
+ open_i = sig.find("(")
65
+ close_i = sig.rfind(")")
66
+ if open_i < 0 or close_i < 0 or close_i < open_i:
67
+ return sig.replace(" ", "")
68
+ name = sig[:open_i].strip()
69
+ body = sig[open_i + 1 : close_i].strip()
70
+ if body == "":
71
+ return f"{name}()"
72
+ parts = [_strip_param_name(p) for p in body.split(",")]
73
+ return f"{name}({','.join(parts)})"
74
+
75
+
76
+ def _parse_signature(sig: str) -> tuple[str, List[str]]:
77
+ open_i = sig.find("(")
78
+ close_i = sig.rfind(")")
79
+ if open_i < 0 or close_i < 0 or close_i < open_i:
80
+ raise EvmAbiError(f"bad signature {sig!r}")
81
+ name = sig[:open_i].strip()
82
+ if name == "":
83
+ raise EvmAbiError("signature missing name")
84
+ body = sig[open_i + 1 : close_i].strip()
85
+ if body == "":
86
+ return name, []
87
+ return name, [_strip_param_name(p) for p in body.split(",")]
88
+
89
+
90
+ def _parse_int_bits(s: str, kind: str) -> int:
91
+ if s == "":
92
+ return 256
93
+ if not s.isdigit():
94
+ raise EvmAbiError(f"invalid {kind} width {s!r}")
95
+ bits = int(s)
96
+ if bits <= 0 or bits > 256 or bits % 8 != 0:
97
+ raise EvmAbiError(f"invalid {kind} width {s!r}")
98
+ return bits
99
+
100
+
101
+ def _parse_type(raw: str) -> _AbiType:
102
+ t = raw.strip()
103
+ if t == "":
104
+ raise EvmAbiError("empty type")
105
+ if t.endswith("]"):
106
+ open_i = t.rfind("[")
107
+ if open_i < 0:
108
+ raise EvmAbiError(f"malformed type {t!r}")
109
+ element = _parse_type(t[:open_i])
110
+ span = t[open_i + 1 : len(t) - 1]
111
+ size = -1
112
+ if span != "":
113
+ if not span.isdigit():
114
+ raise EvmAbiError(f"bad array size in {t!r}")
115
+ size = int(span)
116
+ return _AbiType(kind="array", size=size, element=element)
117
+ if t.startswith("uint"):
118
+ return _AbiType(kind="uint", size=_parse_int_bits(t[4:], "uint"))
119
+ if t.startswith("int"):
120
+ return _AbiType(kind="int", size=_parse_int_bits(t[3:], "int"))
121
+ if t == "address":
122
+ return _AbiType(kind="address")
123
+ if t == "bool":
124
+ return _AbiType(kind="bool")
125
+ if t == "string":
126
+ return _AbiType(kind="string")
127
+ if t == "bytes":
128
+ return _AbiType(kind="bytes")
129
+ if t.startswith("bytes"):
130
+ rest = t[5:]
131
+ if not rest.isdigit():
132
+ raise EvmAbiError(f"invalid fixed bytes type {t!r}")
133
+ n = int(rest)
134
+ if n < 1 or n > 32:
135
+ raise EvmAbiError(f"invalid fixed bytes type {t!r}")
136
+ return _AbiType(kind="bytesN", size=n)
137
+ raise EvmAbiError(f"unsupported type {t!r}")
138
+
139
+
140
+ def _is_dynamic(t: _AbiType) -> bool:
141
+ if t.kind in ("bytes", "string"):
142
+ return True
143
+ if t.kind == "array":
144
+ return t.size < 0 or _is_dynamic(t.element) # type: ignore[arg-type]
145
+ return False
146
+
147
+
148
+ # -- Value coercion -----------------------------------------------------------
149
+
150
+
151
+ def _to_int(v: Any) -> int:
152
+ if isinstance(v, bool):
153
+ raise EvmAbiError("integer: got bool")
154
+ if isinstance(v, int):
155
+ return v
156
+ if isinstance(v, str):
157
+ s = v.strip()
158
+ if s == "":
159
+ raise EvmAbiError("integer: empty string")
160
+ try:
161
+ return int(s, 16) if s[:2].lower() == "0x" else int(s, 10)
162
+ except ValueError as err:
163
+ raise EvmAbiError(f"invalid integer string {v!r}") from err
164
+ raise EvmAbiError(f"integer: unsupported type {type(v).__name__}")
165
+
166
+
167
+ def _to_big_uint(v: Any, bits: int) -> int:
168
+ n = _to_int(v)
169
+ if n < 0:
170
+ raise EvmAbiError(f"uint{bits}: negative value {n}")
171
+ if n >= (1 << bits):
172
+ raise EvmAbiError(f"uint{bits}: value {n} exceeds max")
173
+ return n
174
+
175
+
176
+ def _to_bytes(v: Any) -> bytes:
177
+ if isinstance(v, (bytes, bytearray)):
178
+ return bytes(v)
179
+ if isinstance(v, str):
180
+ s = v.strip()
181
+ if s[:2].lower() == "0x":
182
+ hexpart = s[2:]
183
+ try:
184
+ return bytes.fromhex(hexpart)
185
+ except ValueError as err:
186
+ raise EvmAbiError(f"bytes: bad hex {v!r}") from err
187
+ return s.encode("utf-8")
188
+ raise EvmAbiError(f"bytes: unsupported type {type(v).__name__}")
189
+
190
+
191
+ def _normalize_evm_address(value: Any) -> bytes:
192
+ """Accept 0x hex, 0x41 TRON hex, or ``T...`` base58; return the 20-byte address."""
193
+ if not isinstance(value, str):
194
+ raise EvmAbiError(f"address: want string, got {type(value).__name__}")
195
+ s = value.strip()
196
+ if s == "":
197
+ raise EvmAbiError("address: empty")
198
+ if len(s) >= 30 and s[0] in "Tt" and s[:2].lower() != "0x":
199
+ raw = bytes.fromhex(tron_to_hex(s)[2:])
200
+ if len(raw) == 21 and raw[0] == 0x41:
201
+ return raw[1:]
202
+ if len(raw) == 20:
203
+ return raw
204
+ raise EvmAbiError(f"address: unexpected TRON length {len(raw)}")
205
+ if s[:2].lower() == "0x":
206
+ s = s[2:]
207
+ if len(s) == 42 and s[:2] == "41": # 0x41-prefixed TRON hex
208
+ s = s[2:]
209
+ if len(s) != 40:
210
+ raise EvmAbiError(f"address: want 20 hex bytes, got {len(s)} chars")
211
+ try:
212
+ return bytes.fromhex(s)
213
+ except ValueError as err:
214
+ raise EvmAbiError("address: bad hex") from err
215
+
216
+
217
+ # -- Word packing -------------------------------------------------------------
218
+
219
+ _TWO_256 = 1 << 256
220
+
221
+
222
+ def _uint256_bytes(n: int) -> bytes:
223
+ return (n % _TWO_256).to_bytes(32, "big")
224
+
225
+
226
+ def _round_up_32(n: int) -> int:
227
+ r = n % 32
228
+ return n if r == 0 else n + 32 - r
229
+
230
+
231
+ def _encode_dyn_bytes(b: bytes) -> bytes:
232
+ return _uint256_bytes(len(b)) + b + b"\x00" * (_round_up_32(len(b)) - len(b))
233
+
234
+
235
+ def _encode_one(t: _AbiType, v: Any) -> bytes:
236
+ if t.kind == "uint":
237
+ return _uint256_bytes(_to_big_uint(v, t.size))
238
+ if t.kind == "int":
239
+ return _uint256_bytes(_to_int(v))
240
+ if t.kind == "address":
241
+ return b"\x00" * 12 + _normalize_evm_address(v)
242
+ if t.kind == "bool":
243
+ if not isinstance(v, bool):
244
+ raise EvmAbiError(f"bool: want bool, got {type(v).__name__}")
245
+ return b"\x00" * 31 + (b"\x01" if v else b"\x00")
246
+ if t.kind == "bytesN":
247
+ b = _to_bytes(v)
248
+ if len(b) != t.size:
249
+ raise EvmAbiError(f"bytes{t.size}: expected {t.size} bytes, got {len(b)}")
250
+ return b + b"\x00" * (32 - t.size)
251
+ if t.kind == "bytes":
252
+ return _encode_dyn_bytes(_to_bytes(v))
253
+ if t.kind == "string":
254
+ if not isinstance(v, str):
255
+ raise EvmAbiError(f"string: want string, got {type(v).__name__}")
256
+ return _encode_dyn_bytes(v.encode("utf-8"))
257
+ if t.kind == "array":
258
+ if not isinstance(v, (list, tuple)):
259
+ raise EvmAbiError(f"array: want list, got {type(v).__name__}")
260
+ if t.size >= 0 and len(v) != t.size:
261
+ raise EvmAbiError(f"fixed array T[{t.size}]: expected {t.size} items, got {len(v)}")
262
+ assert t.element is not None # arrays always carry an element type
263
+ inner = [t.element] * len(v)
264
+ body = _encode_components(inner, list(v))
265
+ if t.size < 0:
266
+ return _uint256_bytes(len(v)) + body
267
+ return body
268
+ raise EvmAbiError(f"cannot encode kind {t.kind}")
269
+
270
+
271
+ def _encode_components(types: List[_AbiType], args: List[Any]) -> bytes:
272
+ tails: List[bytes] = []
273
+ for i, t in enumerate(types):
274
+ try:
275
+ tails.append(_encode_one(t, args[i]))
276
+ except EvmAbiError as err:
277
+ raise EvmAbiError(f"arg {i}: {_strip_prefix(str(err))}") from err
278
+ head_size = 32 * len(types)
279
+ offsets: List[int] = [0] * len(types)
280
+ cursor = head_size
281
+ for i, t in enumerate(types):
282
+ if _is_dynamic(t):
283
+ offsets[i] = cursor
284
+ cursor += len(tails[i])
285
+ heads: List[bytes] = []
286
+ for i, t in enumerate(types):
287
+ heads.append(_uint256_bytes(offsets[i]) if _is_dynamic(t) else tails[i])
288
+ dynamic_tails = [tails[i] if _is_dynamic(t) else b"" for i, t in enumerate(types)]
289
+ return b"".join(heads) + b"".join(dynamic_tails)
290
+
291
+
292
+ def _strip_prefix(msg: str) -> str:
293
+ prefix = "cryptochief/evm: "
294
+ return msg[len(prefix) :] if msg.startswith(prefix) else msg
295
+
296
+
297
+ # -- Public API ---------------------------------------------------------------
298
+
299
+
300
+ def evm_selector(signature: str) -> bytes:
301
+ """The 4-byte function selector for a Solidity signature."""
302
+ return keccak_256(canonical_signature(signature).encode("utf-8"))[:4]
303
+
304
+
305
+ def encode_evm_call(signature: str, *args: Any) -> bytes:
306
+ """Build ABI calldata (selector + encoded args) as raw bytes."""
307
+ name, type_strs = _parse_signature(signature)
308
+ if len(type_strs) != len(args):
309
+ raise EvmAbiError(f"signature has {len(type_strs)} args, got {len(args)}")
310
+ parsed: List[_AbiType] = []
311
+ for i, s in enumerate(type_strs):
312
+ try:
313
+ parsed.append(_parse_type(s))
314
+ except EvmAbiError as err:
315
+ raise EvmAbiError(f"arg {i} ({s}): {_strip_prefix(str(err))}") from err
316
+ return evm_selector(signature) + _encode_components(parsed, list(args))
317
+
318
+
319
+ def encode_evm_call_hex(signature: str, *args: Any) -> str:
320
+ """Build ABI calldata as a ``0x...`` hex string (the form the ``data`` field expects)."""
321
+ return "0x" + encode_evm_call(signature, *args).hex()
@@ -0,0 +1,16 @@
1
+ """Keccak-256 (Ethereum's legacy hash, distinct from NIST SHA3-256).
2
+
3
+ Used only to derive EVM/TRON function selectors. Backed by pycryptodome's
4
+ well-tested implementation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from Cryptodome.Hash import keccak as _keccak
10
+
11
+
12
+ def keccak_256(data: bytes) -> bytes:
13
+ """Return the 32-byte Keccak-256 digest of ``data``."""
14
+ h = _keccak.new(digest_bits=256)
15
+ h.update(data)
16
+ return h.digest()
@@ -0,0 +1,60 @@
1
+ """TRON address conversion (Base58Check <-> 0x41 hex)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+
7
+ from ..errors import CryptoChiefError
8
+ from .base58 import base58_decode, base58_encode
9
+
10
+
11
+ def _sha256d(b: bytes) -> bytes:
12
+ return hashlib.sha256(hashlib.sha256(b).digest()).digest()
13
+
14
+
15
+ def tron_to_hex(base58_addr: str) -> str:
16
+ """Convert a TRON base58 address (``T...``) to its 0x41-prefixed 21-byte hex.
17
+
18
+ Validates the Base58Check (double-SHA-256) checksum.
19
+
20
+ >>> tron_to_hex("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")
21
+ '0x41a614f803b6fd780986a42c78ec9c7f77e6ded13c'
22
+ """
23
+ decoded = base58_decode(base58_addr.strip())
24
+ if len(decoded) != 25:
25
+ raise CryptoChiefError(f"cryptochief/tron: decoded length {len(decoded)}, want 25")
26
+ payload = decoded[:21]
27
+ checksum = decoded[21:]
28
+ if payload[0] != 0x41:
29
+ raise CryptoChiefError(f"cryptochief/tron: leading byte 0x{payload[0]:02x}, want 0x41")
30
+ if checksum != _sha256d(payload)[:4]:
31
+ raise CryptoChiefError("cryptochief/tron: checksum mismatch")
32
+ return "0x" + payload.hex()
33
+
34
+
35
+ def hex_to_tron(hex_addr: str) -> str:
36
+ """Convert a 20-byte EVM-style hex (or a 0x41-prefixed 21-byte TRON hex) to base58.
37
+
38
+ A 20-byte input is prefixed with ``0x41`` automatically.
39
+ """
40
+ s = hex_addr.strip()
41
+ if s[:2].lower() == "0x":
42
+ s = s[2:]
43
+ try:
44
+ raw = bytes.fromhex(s)
45
+ except ValueError as err:
46
+ raise CryptoChiefError(f"cryptochief/tron: bad hex {hex_addr!r}: {err}") from err
47
+ if len(raw) == 20:
48
+ payload = b"\x41" + raw
49
+ elif len(raw) == 21:
50
+ if raw[0] != 0x41:
51
+ raise CryptoChiefError(
52
+ f"cryptochief/tron: 21-byte input must start with 0x41, got 0x{raw[0]:02x}"
53
+ )
54
+ payload = raw
55
+ else:
56
+ raise CryptoChiefError(
57
+ f"cryptochief/tron: want 20- or 21-byte hex address, got {len(raw)} bytes"
58
+ )
59
+ checksum = _sha256d(payload)[:4]
60
+ return base58_encode(payload + checksum)
cryptochief/errors.py ADDED
@@ -0,0 +1,111 @@
1
+ """Error model for the SDK.
2
+
3
+ Everything the SDK raises derives from :class:`CryptoChiefError`, so a single
4
+ ``except CryptoChiefError`` covers the library. API-level failures arrive as
5
+ :class:`APIError` with a stable :attr:`APIError.code` string - branch on
6
+ :class:`ErrorCode` (or ``error.code``) rather than parsing messages.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum
12
+ from typing import Optional
13
+
14
+
15
+ class CryptoChiefError(Exception):
16
+ """Base class for every error raised by the SDK."""
17
+
18
+
19
+ class APIError(CryptoChiefError):
20
+ """A typed Crypto Chief error response.
21
+
22
+ The API returns either ``{"error": "SERVICE_ERROR", "msg": "<CODE>", ...}``
23
+ (then :attr:`code` is ``<CODE>``) or ``{"error": "<CODE>", ...}`` (then
24
+ :attr:`code` is that value). Either way :attr:`code` is the stable
25
+ identifier to branch on::
26
+
27
+ try:
28
+ await client.payouts.execute(req)
29
+ except APIError as e:
30
+ if e.code == ErrorCode.INSUFFICIENT_FUNDS:
31
+ ... # top up and retry
32
+ """
33
+
34
+ code: str
35
+ http_status: int
36
+ raw: Optional[str]
37
+
38
+ def __init__(
39
+ self,
40
+ code: str,
41
+ *,
42
+ http_status: int = 0,
43
+ message: Optional[str] = None,
44
+ raw: Optional[str] = None,
45
+ ) -> None:
46
+ # Normalize an ErrorCode member to its wire string ("NETWORK_ERROR"),
47
+ # not its enum repr ("ErrorCode.NETWORK_ERROR").
48
+ self.code = code.value if isinstance(code, Enum) else str(code)
49
+ self.http_status = http_status
50
+ self.raw = raw
51
+ super().__init__(self._format(http_status, self.code, message))
52
+
53
+ @staticmethod
54
+ def _format(status: int, code: str, message: Optional[str]) -> str:
55
+ if status == 0:
56
+ return f"cryptochief: {code}"
57
+ if message and message != code:
58
+ return f"cryptochief: {status} {code}: {message}"
59
+ return f"cryptochief: {status} {code}"
60
+
61
+
62
+ class ErrorCode(str, Enum):
63
+ """Stable error codes.
64
+
65
+ Not exhaustive - the API defines more per endpoint and may add new ones, so
66
+ treat an unknown :attr:`APIError.code` as opaque.
67
+ """
68
+
69
+ INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS"
70
+ INSUFFICIENT_CREDITS = "INSUFFICIENT_CREDITS"
71
+ DEBT_LIMIT_EXCEEDED = "DEBT_LIMIT_EXCEEDED"
72
+ ASSET_NOT_ENABLED = "ASSET_NOT_ENABLED"
73
+ ORDER_ALREADY_EXIST = "ORDER_ALREADY_EXIST"
74
+ ORDER_CANNOT_CANCEL = "ORDER_CANNOT_CANCEL"
75
+ ORDER_NOT_LIVE = "ORDER_NOT_LIVE"
76
+ ASSET_ALREADY_SELECTED = "ASSET_ALREADY_SELECTED"
77
+ INVALID_PARAMS = "INVALID_PARAMS"
78
+ SERVICE_ERROR = "SERVICE_ERROR"
79
+ UNAUTHORIZED = "UNAUTHORIZED"
80
+ URL_CALLBACK_REQUIRED = "URL_CALLBACK_REQUIRED"
81
+ BATCH_EMPTY = "BATCH_EMPTY"
82
+ BATCH_TOO_LARGE = "BATCH_TOO_LARGE"
83
+ BATCH_DUPLICATE_ORDER_ID = "BATCH_DUPLICATE_ORDER_ID"
84
+ FROM_WALLET_NOT_OWNED = "FROM_WALLET_NOT_OWNED"
85
+ SIGNATURE_EXPIRED = "SIGNATURE_EXPIRED"
86
+ ALREADY_EXECUTED = "ALREADY_EXECUTED"
87
+ PREFLIGHT_FAILED = "PREFLIGHT_FAILED"
88
+ BROADCAST_FAILED = "BROADCAST_FAILED"
89
+ SIGNED_TX_MISMATCH = "SIGNED_TX_MISMATCH"
90
+ CONTRACT_REQUIRED_FOR_TOKEN = "CONTRACT_REQUIRED_FOR_TOKEN"
91
+ TRANSFER_FIELDS_NOT_ALLOWED_FOR_CONTRACT = "TRANSFER_FIELDS_NOT_ALLOWED_FOR_CONTRACT"
92
+ CALLS_REQUIRED = "CALLS_REQUIRED"
93
+ CALLS_NOT_ALLOWED_FOR_TRANSFER = "CALLS_NOT_ALLOWED_FOR_TRANSFER"
94
+ CONTRACT_CALLS_UNSUPPORTED_ON_NETWORK = "CONTRACT_CALLS_UNSUPPORTED_ON_NETWORK"
95
+ NETWORK_ERROR = "NETWORK_ERROR"
96
+
97
+
98
+ def is_api_error(err: object, code: Optional[str] = None) -> bool:
99
+ """``True`` when ``err`` is an :class:`APIError` (optionally with ``code``)."""
100
+ return isinstance(err, APIError) and (code is None or err.code == code)
101
+
102
+
103
+ def is_retryable(err: object) -> bool:
104
+ """Report whether an error is plausibly transient and worth retrying.
105
+
106
+ Only 5xx responses and transport ``NETWORK_ERROR`` failures qualify; 4xx is
107
+ the caller's fault and is never retried.
108
+ """
109
+ if isinstance(err, APIError):
110
+ return err.http_status >= 500 or err.code == ErrorCode.NETWORK_ERROR
111
+ return False
@@ -0,0 +1,32 @@
1
+ """Shared pagination shapes for the history endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass(kw_only=True)
10
+ class HistoryQuery:
11
+ """Common filter for history endpoints with simple pagination.
12
+
13
+ Omitted (``None``) fields are not sent.
14
+ """
15
+
16
+ page: Optional[int] = None
17
+ page_size: Optional[int] = None
18
+ status: Optional[str] = None
19
+ coin: Optional[str] = None
20
+ network: Optional[str] = None
21
+ date_from: Optional[str] = None
22
+ date_to: Optional[str] = None
23
+
24
+
25
+ @dataclass(kw_only=True)
26
+ class HistoryMeta:
27
+ """Pagination envelope returned by every history endpoint."""
28
+
29
+ page: int = 0
30
+ page_size: int = 0
31
+ total: int = 0
32
+ total_pages: Optional[int] = None
cryptochief/poll.py ADDED
@@ -0,0 +1,56 @@
1
+ """Async polling helper used by the ``wait_for`` methods."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import Awaitable, Callable, Generic, Optional, TypeVar
8
+
9
+ from .errors import CryptoChiefError, is_retryable
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ class PollTimeoutError(CryptoChiefError, Generic[T]):
15
+ """Raised when a ``wait_for`` helper times out before reaching a terminal state."""
16
+
17
+ last_state: Optional[T]
18
+
19
+ def __init__(self, timeout: float, last_state: Optional[T] = None) -> None:
20
+ super().__init__(
21
+ f"cryptochief: poll did not reach a terminal state within {timeout}s"
22
+ )
23
+ self.last_state = last_state
24
+
25
+
26
+ async def wait_for_terminal(
27
+ fetch_one: Callable[[], Awaitable[T]],
28
+ is_terminal: Callable[[T], bool],
29
+ *,
30
+ interval: float = 5.0,
31
+ timeout: float = 600.0,
32
+ ) -> T:
33
+ """Poll ``fetch_one`` until ``is_terminal`` holds or ``timeout`` elapses.
34
+
35
+ Transient (retryable) fetch errors are tolerated and retried on the next
36
+ tick; other errors propagate immediately. On timeout a
37
+ :class:`PollTimeoutError` carrying the last observed state is raised.
38
+ """
39
+ interval = interval if interval and interval > 0 else 5.0
40
+ timeout = timeout if timeout and timeout > 0 else 600.0
41
+ deadline = time.monotonic() + timeout
42
+ last: Optional[T] = None
43
+
44
+ while True:
45
+ try:
46
+ value = await fetch_one()
47
+ last = value
48
+ if is_terminal(value):
49
+ return value
50
+ except Exception as err: # noqa: BLE001 - re-raised unless retryable
51
+ if not is_retryable(err):
52
+ raise
53
+ remaining = deadline - time.monotonic()
54
+ if remaining <= 0:
55
+ raise PollTimeoutError(timeout, last)
56
+ await asyncio.sleep(min(interval, remaining))
cryptochief/rsa.py ADDED
@@ -0,0 +1,70 @@
1
+ """Local RSA decryption of generated wallets' private keys.
2
+
3
+ When the API generates a wallet it returns the private key encrypted with the
4
+ RSA public key uploaded to your project (Project Settings -> RSA Key). The scheme
5
+ is RSA-OAEP / SHA-256 over base64-encoded ciphertext. Configure the matching
6
+ private key on the client to decrypt it locally.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ from typing import Union
13
+
14
+ from cryptography.hazmat.primitives import hashes, serialization
15
+ from cryptography.hazmat.primitives.asymmetric import padding
16
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
17
+
18
+ from .errors import CryptoChiefError
19
+
20
+
21
+ class RsaKeyNotConfiguredError(CryptoChiefError):
22
+ """Raised by ``client.wallets.decrypt_private_key`` when no RSA key was configured."""
23
+
24
+ def __init__(self) -> None:
25
+ super().__init__(
26
+ "cryptochief: RSA private key not configured - pass rsa_private_key to the client"
27
+ )
28
+
29
+
30
+ def load_rsa_private_key_pem(pem: Union[str, bytes, bytearray]) -> RSAPrivateKey:
31
+ """Parse a PEM-encoded RSA private key (PKCS#1 or PKCS#8) into a key object."""
32
+ data = pem.encode("utf-8") if isinstance(pem, str) else bytes(pem)
33
+ try:
34
+ key = serialization.load_pem_private_key(data, password=None)
35
+ except Exception as err: # noqa: BLE001 - normalize to our error type
36
+ raise CryptoChiefError(f"cryptochief: RSA key: {err}") from err
37
+ if not isinstance(key, RSAPrivateKey):
38
+ raise CryptoChiefError("cryptochief: RSA key: not an RSA private key")
39
+ return key
40
+
41
+
42
+ def load_rsa_private_key_file(path: str) -> RSAPrivateKey:
43
+ """Read and parse a PEM-encoded RSA private key from disk."""
44
+ try:
45
+ with open(path, "rb") as fh:
46
+ data = fh.read()
47
+ except OSError as err:
48
+ raise CryptoChiefError(f"cryptochief: read RSA key {path!r}: {err}") from err
49
+ return load_rsa_private_key_pem(data)
50
+
51
+
52
+ def decrypt_rsa_oaep(key: RSAPrivateKey, base64_ciphertext: str) -> str:
53
+ """Decrypt a single base64-encoded RSA-OAEP / SHA-256 payload.
54
+
55
+ The exact encoding the API uses for ``private_key_encrypted``. Returns the
56
+ wallet's raw private key in the chain's native hex form.
57
+ """
58
+ ciphertext = base64.b64decode(base64_ciphertext)
59
+ try:
60
+ plaintext = key.decrypt(
61
+ ciphertext,
62
+ padding.OAEP(
63
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
64
+ algorithm=hashes.SHA256(),
65
+ label=None,
66
+ ),
67
+ )
68
+ except Exception as err: # noqa: BLE001 - normalize to our error type
69
+ raise CryptoChiefError(f"cryptochief: RSA decrypt: {err}") from err
70
+ return plaintext.decode("utf-8")
@@ -0,0 +1 @@
1
+ """Domain services exposed as attributes on :class:`cryptochief.CryptoChiefClient`."""