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,470 @@
1
+ """Two-phase sign/execute for arbitrary merchant-owned transactions, plus
2
+ one-call helpers for EVM/TRON contracts, Solana Anchor programs, and TON
3
+ Jetton/NFT/comment transfers.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import base64
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import Any, List, Optional, Union
12
+
13
+ from .._models import from_dict
14
+ from ..contract.borsh import BorshValue, encode_anchor_instruction
15
+ from ..contract.evm_abi import encode_evm_call_hex
16
+ from ..errors import CryptoChiefError
17
+ from ..pagination import HistoryMeta, HistoryQuery
18
+ from ..poll import wait_for_terminal
19
+ from ..ton.messages import (
20
+ build_jetton_transfer_body,
21
+ build_nft_transfer_body,
22
+ build_text_comment_body,
23
+ build_text_comment_cell,
24
+ parse_ton_addr,
25
+ )
26
+ from .base import BaseService
27
+
28
+
29
+ class TxType(str, Enum):
30
+ """Transaction type discriminator the API uses to pick a signing path."""
31
+
32
+ NATIVE = "native" # native-asset transfer: to_address + value
33
+ TOKEN = "token" # ERC-20-style token transfer: to_address + value + contract
34
+ CONTRACT = "contract" # arbitrary contract call(s): calls[]
35
+
36
+
37
+ class TxStatus(str, Enum):
38
+ SIGNED = "signed"
39
+ BROADCASTING = "broadcasting"
40
+ BROADCASTED = "broadcasted"
41
+ CONFIRMED = "confirmed"
42
+ FAILED = "failed"
43
+ EXPIRED = "expired"
44
+
45
+
46
+ _TX_TERMINAL = frozenset({"confirmed", "failed", "expired"})
47
+
48
+
49
+ def is_transaction_terminal(status: str) -> bool:
50
+ """Whether a transaction status is final."""
51
+ return status in _TX_TERMINAL
52
+
53
+
54
+ # Default attached-gas budgets for TON transfers (nanoTON).
55
+ _JETTON_ATTACHED_EXISTING_WALLET = 70_000_000 # 0.07 TON
56
+ _JETTON_ATTACHED_NEW_WALLET = 150_000_000 # 0.15 TON
57
+ _NFT_ATTACHED_DEFAULT = 50_000_000 # 0.05 TON
58
+
59
+
60
+ def _b64(data: bytes) -> str:
61
+ return base64.b64encode(data).decode("ascii")
62
+
63
+
64
+ def _value_string(v: Union[str, int, None]) -> str:
65
+ if v is None or v == "":
66
+ return "0"
67
+ return str(v)
68
+
69
+
70
+ @dataclass(kw_only=True)
71
+ class SolanaAccount:
72
+ """Solana account meta."""
73
+
74
+ pubkey: str
75
+ is_signer: bool
76
+ is_writable: bool
77
+
78
+
79
+ @dataclass(kw_only=True)
80
+ class ContractCall:
81
+ """One instruction in a ``contract``-type request.
82
+
83
+ Per-family encoding:
84
+
85
+ * EVM/TRON - ``data`` is hex calldata (``0x...``), single call.
86
+ * TON - ``data`` is a base64 BoC body cell, single call, ``bounce`` defaults true.
87
+ * Solana - ``to`` is the program id, ``data`` base64 instruction data,
88
+ ``accounts`` lists the metas; multiple instructions allowed.
89
+ """
90
+
91
+ to: str
92
+ data: str
93
+ value: Optional[str] = None
94
+ accounts: Optional[List[SolanaAccount]] = None
95
+ bounce: Optional[bool] = None
96
+
97
+
98
+ @dataclass(kw_only=True)
99
+ class SignTransactionRequest:
100
+ network: str
101
+ from_address: str
102
+ type: str
103
+ to_address: Optional[str] = None # transfer-mode (native/token)
104
+ value: Optional[str] = None # transfer-mode value in BASE units (e.g. wei)
105
+ contract: Optional[str] = None # token contract for `token` type
106
+ calls: Optional[List[ContractCall]] = None # contract-mode instructions
107
+ url_callback: Optional[str] = None
108
+
109
+
110
+ @dataclass(kw_only=True)
111
+ class SignTransactionResponse:
112
+ uuid: str = ""
113
+ status: str = ""
114
+ signed_tx_hex: Optional[str] = None
115
+ tx_hash: Optional[str] = None
116
+ expires_at: Optional[str] = None
117
+ chain_family: Optional[str] = None
118
+ network: Optional[str] = None
119
+
120
+
121
+ @dataclass(kw_only=True)
122
+ class ExecuteTransactionRequest:
123
+ uuid: str
124
+ signed_tx_hex: Optional[str] = None # optional client-vs-server byte-match check
125
+
126
+
127
+ @dataclass(kw_only=True)
128
+ class TransactionInfo:
129
+ uuid: str = ""
130
+ status: str = ""
131
+ network: Optional[str] = None
132
+ chain_family: Optional[str] = None
133
+ from_address: Optional[str] = None
134
+ to_address: Optional[str] = None
135
+ type: Optional[str] = None
136
+ value: Optional[str] = None
137
+ coin: Optional[str] = None
138
+ contract: Optional[str] = None
139
+ tx_hash: Optional[str] = None
140
+ signed_tx_hex: Optional[str] = None
141
+ expires_at: Optional[str] = None
142
+ nonce: Optional[int] = None
143
+ actual_fee: Optional[str] = None
144
+ actual_fee_fiat: Optional[str] = None
145
+ created_at: Optional[str] = None
146
+ updated_at: Optional[str] = None
147
+ error: Optional[str] = None
148
+
149
+
150
+ @dataclass(kw_only=True)
151
+ class TransactionHistoryResponse:
152
+ items: Optional[List[TransactionInfo]] = None
153
+ meta: Optional[HistoryMeta] = None
154
+
155
+
156
+ # -- High-level contract-call request shapes ----------------------------------
157
+
158
+
159
+ @dataclass(kw_only=True)
160
+ class EvmCallRequest:
161
+ """EVM / TRON contract call by Solidity-style signature."""
162
+
163
+ network: str
164
+ from_address: str
165
+ contract: str
166
+ method: str # e.g. "transfer(address,uint256)"
167
+ args: List[Any] = field(default_factory=list)
168
+ value: Optional[str] = None
169
+ url_callback: Optional[str] = None
170
+
171
+
172
+ @dataclass(kw_only=True)
173
+ class Erc20TransferRequest:
174
+ network: str
175
+ from_address: str
176
+ token_contract: str
177
+ recipient: str
178
+ amount: Union[int, str] # token base units (use human_to_base with the decimals)
179
+ url_callback: Optional[str] = None
180
+
181
+
182
+ @dataclass(kw_only=True)
183
+ class AnchorCallRequest:
184
+ network: str
185
+ from_address: str
186
+ program: str
187
+ method: str
188
+ args: List[BorshValue] = field(default_factory=list)
189
+ accounts: List[SolanaAccount] = field(default_factory=list)
190
+ url_callback: Optional[str] = None
191
+
192
+
193
+ @dataclass(kw_only=True)
194
+ class SolanaCallRequest:
195
+ network: str
196
+ from_address: str
197
+ program: str
198
+ instruction_data: bytes
199
+ accounts: List[SolanaAccount] = field(default_factory=list)
200
+ url_callback: Optional[str] = None
201
+
202
+
203
+ @dataclass(kw_only=True)
204
+ class TonCallRequest:
205
+ network: str
206
+ from_address: str
207
+ contract: str
208
+ body_cell: bytes # raw BoC bytes; base64-encoded internally
209
+ value: Union[str, int, None] = None
210
+ bounce: Optional[bool] = None
211
+ url_callback: Optional[str] = None
212
+
213
+
214
+ @dataclass(kw_only=True)
215
+ class JettonTransferRequest:
216
+ network: str
217
+ from_address: str # sender's TON wallet (owns the Jetton wallet)
218
+ recipient: str # recipient's *main* TON wallet (not their Jetton wallet)
219
+ amount: int # Jetton amount in base units
220
+ jetton_master: Optional[str] = None # token id; needed if jetton_wallet_address omitted
221
+ jetton_wallet_address: Optional[str] = None # pre-resolved sender Jetton wallet
222
+ response_destination: Optional[str] = None # receives unused gas; defaults to from_address
223
+ attached_ton: Optional[int] = None # gas budget nanoTON; auto-picked when omitted
224
+ forward_ton_amount: Optional[int] = None # nanoTON; defaults to 1 when memo set, else 0
225
+ memo: Optional[str] = None # comment shown by wallets (encoded as forward payload)
226
+ query_id: Optional[int] = None
227
+ url_callback: Optional[str] = None
228
+
229
+
230
+ @dataclass(kw_only=True)
231
+ class NftTransferRequest:
232
+ network: str
233
+ from_address: str
234
+ nft_item: str
235
+ new_owner: str
236
+ response_destination: Optional[str] = None
237
+ attached_ton: Optional[int] = None
238
+ forward_ton_amount: Optional[int] = None
239
+ query_id: Optional[int] = None
240
+ url_callback: Optional[str] = None
241
+
242
+
243
+ @dataclass(kw_only=True)
244
+ class TonCommentRequest:
245
+ network: str
246
+ from_address: str
247
+ recipient: str
248
+ text: str
249
+ amount_ton: Optional[int] = None # amount to send in nanoTON
250
+ url_callback: Optional[str] = None
251
+
252
+
253
+ class TransactionsService(BaseService):
254
+ async def sign(self, req: SignTransactionRequest) -> SignTransactionResponse:
255
+ """Build and sign a transaction WITHOUT broadcasting.
256
+
257
+ The signature has a per-family TTL (EVM 10m, UTXO 15m, TRON 45s, Solana
258
+ 60s, XRP 90s, TON 300s) - call ``execute`` before it elapses.
259
+ """
260
+ return from_dict(SignTransactionResponse, await self._post("/v1/transaction/signature", req))
261
+
262
+ async def execute(self, req: ExecuteTransactionRequest) -> TransactionInfo:
263
+ """Broadcast a previously-signed transaction by uuid."""
264
+ return from_dict(TransactionInfo, await self._post("/v1/transaction/execute", req))
265
+
266
+ async def info(self, uuid: str) -> TransactionInfo:
267
+ """Fetch the current state of one transaction by uuid."""
268
+ return from_dict(TransactionInfo, await self._post("/v1/transaction/info", {"uuid": uuid}))
269
+
270
+ async def history(self, query: Optional[HistoryQuery] = None) -> TransactionHistoryResponse:
271
+ """Paged list of merchant-owned transactions."""
272
+ return from_dict(
273
+ TransactionHistoryResponse,
274
+ await self._post("/v1/transaction/history", query or HistoryQuery()),
275
+ )
276
+
277
+ async def wait_for(
278
+ self, uuid: str, *, interval: float = 5.0, timeout: float = 600.0
279
+ ) -> TransactionInfo:
280
+ """Poll ``info`` until the transaction reaches a terminal state (or timeout)."""
281
+
282
+ async def fetch() -> TransactionInfo:
283
+ return await self.info(uuid)
284
+
285
+ return await wait_for_terminal(
286
+ fetch, lambda t: is_transaction_terminal(t.status), interval=interval, timeout=timeout
287
+ )
288
+
289
+ # -- Contract-call helpers --------------------------------------------------
290
+
291
+ async def sign_evm_call(self, req: EvmCallRequest) -> SignTransactionResponse:
292
+ """Sign an EVM/TRON contract call, ABI-encoding ``data`` from the signature + args."""
293
+ try:
294
+ data = encode_evm_call_hex(req.method, *req.args)
295
+ except Exception as err: # noqa: BLE001 - add call context
296
+ raise CryptoChiefError(f"cryptochief: encode call {req.method!r}: {err}") from err
297
+ return await self.sign(
298
+ SignTransactionRequest(
299
+ network=req.network,
300
+ from_address=req.from_address,
301
+ type=TxType.CONTRACT.value,
302
+ url_callback=req.url_callback,
303
+ calls=[ContractCall(to=req.contract, value=_value_string(req.value), data=data)],
304
+ )
305
+ )
306
+
307
+ async def sign_tron_call(self, req: EvmCallRequest) -> SignTransactionResponse:
308
+ """Alias for :meth:`sign_evm_call` - TRON shares the EVM ABI encoding."""
309
+ return await self.sign_evm_call(req)
310
+
311
+ async def erc20_transfer(self, req: Erc20TransferRequest) -> SignTransactionResponse:
312
+ """One-liner for an ERC-20 / TRC-20 ``transfer(address,uint256)``."""
313
+ return await self.sign_evm_call(
314
+ EvmCallRequest(
315
+ network=req.network,
316
+ from_address=req.from_address,
317
+ contract=req.token_contract,
318
+ method="transfer(address,uint256)",
319
+ args=[req.recipient, req.amount],
320
+ url_callback=req.url_callback,
321
+ )
322
+ )
323
+
324
+ async def sign_anchor_call(self, req: AnchorCallRequest) -> SignTransactionResponse:
325
+ """Sign an Anchor program call (8-byte discriminator + Borsh-encoded args)."""
326
+ try:
327
+ data = encode_anchor_instruction(req.method, *req.args)
328
+ except Exception as err: # noqa: BLE001 - add call context
329
+ raise CryptoChiefError(
330
+ f"cryptochief: encode anchor instruction {req.method!r}: {err}"
331
+ ) from err
332
+ return await self.sign(
333
+ SignTransactionRequest(
334
+ network=req.network,
335
+ from_address=req.from_address,
336
+ type=TxType.CONTRACT.value,
337
+ url_callback=req.url_callback,
338
+ calls=[ContractCall(to=req.program, data=_b64(data), accounts=req.accounts)],
339
+ )
340
+ )
341
+
342
+ async def sign_solana_call(self, req: SolanaCallRequest) -> SignTransactionResponse:
343
+ """Sign a non-Anchor Solana program call with raw instruction bytes."""
344
+ return await self.sign(
345
+ SignTransactionRequest(
346
+ network=req.network,
347
+ from_address=req.from_address,
348
+ type=TxType.CONTRACT.value,
349
+ url_callback=req.url_callback,
350
+ calls=[
351
+ ContractCall(
352
+ to=req.program, data=_b64(req.instruction_data), accounts=req.accounts
353
+ )
354
+ ],
355
+ )
356
+ )
357
+
358
+ async def sign_ton_call(self, req: TonCallRequest) -> SignTransactionResponse:
359
+ """Sign a TON contract call from a pre-built BoC body cell."""
360
+ return await self.sign(
361
+ SignTransactionRequest(
362
+ network=req.network,
363
+ from_address=req.from_address,
364
+ type=TxType.CONTRACT.value,
365
+ url_callback=req.url_callback,
366
+ calls=[
367
+ ContractCall(
368
+ to=req.contract,
369
+ value=_value_string(req.value),
370
+ data=_b64(req.body_cell),
371
+ bounce=req.bounce,
372
+ )
373
+ ],
374
+ )
375
+ )
376
+
377
+ async def jetton_transfer(self, req: JettonTransferRequest) -> SignTransactionResponse:
378
+ """Transfer Jetton tokens.
379
+
380
+ Builds the TEP-74 transfer body, resolves the sender's Jetton wallet (via
381
+ RPC if not supplied), and picks a sensible gas budget automatically.
382
+ """
383
+ if not req.recipient:
384
+ raise CryptoChiefError("cryptochief: jetton_transfer: recipient required")
385
+ if not req.jetton_master and not req.jetton_wallet_address:
386
+ raise CryptoChiefError(
387
+ "cryptochief: jetton_transfer: jetton_master or jetton_wallet_address required"
388
+ )
389
+ rpc = self._client.ton_rpc()
390
+
391
+ if req.jetton_wallet_address:
392
+ jetton_wallet = req.jetton_wallet_address
393
+ else:
394
+ assert req.jetton_master # guaranteed by the check above
395
+ jetton_wallet = await rpc.lookup_jetton_wallet(req.jetton_master, req.from_address)
396
+
397
+ destination = parse_ton_addr(req.recipient)
398
+ response_dest = parse_ton_addr(req.response_destination or req.from_address)
399
+ forward_payload = build_text_comment_cell(req.memo) if req.memo else None
400
+ forward_ton = req.forward_ton_amount
401
+ if forward_ton is None:
402
+ forward_ton = 1 if req.memo else 0
403
+
404
+ body_cell = build_jetton_transfer_body(
405
+ query_id=req.query_id or 0,
406
+ amount=req.amount,
407
+ destination=destination,
408
+ response_dest=response_dest,
409
+ forward_ton=forward_ton,
410
+ forward_payload=forward_payload,
411
+ )
412
+
413
+ attached = req.attached_ton
414
+ if attached is None:
415
+ attached = _JETTON_ATTACHED_NEW_WALLET
416
+ if req.jetton_master and await rpc.has_jetton_wallet(req.jetton_master, req.recipient):
417
+ attached = _JETTON_ATTACHED_EXISTING_WALLET
418
+
419
+ return await self.sign_ton_call(
420
+ TonCallRequest(
421
+ network=req.network,
422
+ from_address=req.from_address,
423
+ contract=jetton_wallet,
424
+ body_cell=body_cell,
425
+ value=attached,
426
+ bounce=True,
427
+ url_callback=req.url_callback,
428
+ )
429
+ )
430
+
431
+ async def nft_transfer(self, req: NftTransferRequest) -> SignTransactionResponse:
432
+ """Transfer ownership of an NFT item (TEP-62 transfer body)."""
433
+ if not req.nft_item or not req.new_owner:
434
+ raise CryptoChiefError("cryptochief: nft_transfer: nft_item and new_owner required")
435
+ new_owner = parse_ton_addr(req.new_owner)
436
+ response_dest = parse_ton_addr(req.response_destination or req.from_address)
437
+ body_cell = build_nft_transfer_body(
438
+ query_id=req.query_id or 0,
439
+ new_owner=new_owner,
440
+ response_dest=response_dest,
441
+ forward_ton=req.forward_ton_amount or 0,
442
+ )
443
+ return await self.sign_ton_call(
444
+ TonCallRequest(
445
+ network=req.network,
446
+ from_address=req.from_address,
447
+ contract=req.nft_item,
448
+ body_cell=body_cell,
449
+ value=req.attached_ton or _NFT_ATTACHED_DEFAULT,
450
+ bounce=True,
451
+ url_callback=req.url_callback,
452
+ )
453
+ )
454
+
455
+ async def send_ton_comment(self, req: TonCommentRequest) -> SignTransactionResponse:
456
+ """Send TON with a text comment (the note every wallet displays)."""
457
+ if not req.recipient:
458
+ raise CryptoChiefError("cryptochief: send_ton_comment: recipient required")
459
+ body_cell = build_text_comment_body(req.text)
460
+ return await self.sign_ton_call(
461
+ TonCallRequest(
462
+ network=req.network,
463
+ from_address=req.from_address,
464
+ contract=req.recipient,
465
+ body_cell=body_cell,
466
+ value=req.amount_ton or 0,
467
+ bounce=False,
468
+ url_callback=req.url_callback,
469
+ )
470
+ )
@@ -0,0 +1,86 @@
1
+ """Wallet management + local RSA private-key decryption."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import List, Optional
8
+
9
+ from .._models import from_dict
10
+ from .base import BaseService
11
+
12
+
13
+ class WalletType(str, Enum):
14
+ MASTER = "master"
15
+ TRANSIT = "transit"
16
+ STATIC = "static"
17
+
18
+
19
+ @dataclass(kw_only=True)
20
+ class GenerateWalletRequest:
21
+ wallet_type: str
22
+ chain_family: str
23
+ master_wallet_address: Optional[str] = None # transit/static wallets only
24
+ callback_url: Optional[str] = None # static wallets only - per-deposit webhook URL
25
+
26
+
27
+ @dataclass(kw_only=True)
28
+ class WalletCoinBalance:
29
+ address: Optional[str] = None
30
+ chain: Optional[str] = None
31
+ coin: Optional[str] = None
32
+ contract: Optional[str] = None
33
+ decimals: int = 0
34
+ value: Optional[str] = None
35
+ human_value: Optional[str] = None
36
+ amount_usd: Optional[str] = None
37
+ timestamp: Optional[int] = None
38
+
39
+
40
+ @dataclass(kw_only=True)
41
+ class Wallet:
42
+ address: str = ""
43
+ chain_family: Optional[str] = None
44
+ type: Optional[str] = None
45
+ wallet_type: Optional[str] = None
46
+ frozen: Optional[bool] = None
47
+ master_wallet_address: Optional[str] = None
48
+ callback_url: Optional[str] = None
49
+ #: Base64 RSA-OAEP/SHA-256 ciphertext - decrypt with ``decrypt_private_key``.
50
+ private_key_encrypted: Optional[str] = None
51
+ created_at: Optional[str] = None
52
+ coins: Optional[List[WalletCoinBalance]] = None
53
+ total_balance_usd: Optional[str] = None
54
+
55
+
56
+ @dataclass(kw_only=True)
57
+ class ListWalletsResponse:
58
+ items: Optional[List[Wallet]] = None
59
+
60
+
61
+ class WalletsService(BaseService):
62
+ async def generate(self, req: GenerateWalletRequest) -> Wallet:
63
+ """Provision a new wallet on the requested chain family."""
64
+ return from_dict(Wallet, await self._post("/v1/wallets/generate", req))
65
+
66
+ async def list(self) -> ListWalletsResponse:
67
+ """Every wallet on the project."""
68
+ return from_dict(ListWalletsResponse, await self._post("/v1/wallets/list", {}))
69
+
70
+ async def info(self, address: str) -> Wallet:
71
+ """Details and current balances of one wallet."""
72
+ return from_dict(Wallet, await self._post("/v1/wallets/info", {"address": address}))
73
+
74
+ async def freeze(self, address: str) -> Wallet:
75
+ """Toggle the frozen flag - the response's ``frozen`` field is the new state."""
76
+ return from_dict(Wallet, await self._post("/v1/wallets/freeze", {"address": address}))
77
+
78
+ def decrypt_private_key(self, encrypted: str) -> str:
79
+ """Decrypt a generated wallet's ``private_key_encrypted`` field locally.
80
+
81
+ Uses the RSA private key configured on the client (``rsa_private_key``
82
+ option) and returns the chain-native hex private key. Raises
83
+ :class:`RsaKeyNotConfiguredError` if no key was configured. Synchronous -
84
+ never touches the network.
85
+ """
86
+ return self._client.rsa_decrypt(encrypted)
@@ -0,0 +1,51 @@
1
+ """Read-only withdrawal endpoints.
2
+
3
+ The public API does not create withdrawals directly - they are produced by the
4
+ sweep/treasury system.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import List, Optional
11
+
12
+ from .._models import from_dict
13
+ from ..pagination import HistoryMeta, HistoryQuery
14
+ from .base import BaseService
15
+
16
+
17
+ @dataclass(kw_only=True)
18
+ class Withdrawal:
19
+ uuid: str = ""
20
+ status: str = ""
21
+ network: Optional[str] = None
22
+ coin: Optional[str] = None
23
+ contract: Optional[str] = None
24
+ amount: Optional[str] = None
25
+ amount_fiat: Optional[str] = None
26
+ from_address: Optional[str] = None
27
+ to_address: Optional[str] = None
28
+ tx_hash: Optional[str] = None
29
+ created_at: Optional[str] = None
30
+ updated_at: Optional[str] = None
31
+ confirmed_at: Optional[str] = None
32
+ error: Optional[str] = None
33
+
34
+
35
+ @dataclass(kw_only=True)
36
+ class WithdrawalHistoryResponse:
37
+ items: Optional[List[Withdrawal]] = None
38
+ meta: Optional[HistoryMeta] = None
39
+
40
+
41
+ class WithdrawalsService(BaseService):
42
+ async def info(self, uuid: str) -> Withdrawal:
43
+ """Fetch one withdrawal by uuid."""
44
+ return from_dict(Withdrawal, await self._post("/v1/withdrawal/info", {"uuid": uuid}))
45
+
46
+ async def history(self, query: Optional[HistoryQuery] = None) -> WithdrawalHistoryResponse:
47
+ """Paged list of withdrawals."""
48
+ return from_dict(
49
+ WithdrawalHistoryResponse,
50
+ await self._post("/v1/withdrawal/history", query or HistoryQuery()),
51
+ )