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.
- cryptochief/__init__.py +336 -0
- cryptochief/_models.py +93 -0
- cryptochief/_version.py +3 -0
- cryptochief/amount.py +97 -0
- cryptochief/assets.py +32 -0
- cryptochief/chains.py +115 -0
- cryptochief/client.py +189 -0
- cryptochief/contract/__init__.py +69 -0
- cryptochief/contract/base58.py +40 -0
- cryptochief/contract/borsh.py +141 -0
- cryptochief/contract/evm_abi.py +321 -0
- cryptochief/contract/keccak.py +16 -0
- cryptochief/contract/tron_address.py +60 -0
- cryptochief/errors.py +111 -0
- cryptochief/pagination.py +32 -0
- cryptochief/poll.py +56 -0
- cryptochief/rsa.py +70 -0
- cryptochief/services/__init__.py +1 -0
- cryptochief/services/base.py +24 -0
- cryptochief/services/blockchain.py +76 -0
- cryptochief/services/currencies.py +55 -0
- cryptochief/services/payins.py +145 -0
- cryptochief/services/payouts.py +175 -0
- cryptochief/services/static_deposits.py +77 -0
- cryptochief/services/sweeps.py +85 -0
- cryptochief/services/transactions.py +470 -0
- cryptochief/services/wallets.py +86 -0
- cryptochief/services/withdrawals.py +51 -0
- cryptochief/sign.py +112 -0
- cryptochief/ton/__init__.py +19 -0
- cryptochief/ton/address.py +109 -0
- cryptochief/ton/messages.py +105 -0
- cryptochief/ton/rpc.py +159 -0
- cryptochief/transport.py +48 -0
- cryptochief/webhook.py +191 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/METADATA +346 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/RECORD +39 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/WHEEL +4 -0
- 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`."""
|