cryptochief-crypto-processing-python 0.4.0__tar.gz → 0.6.0__tar.gz

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 (44) hide show
  1. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/PKG-INFO +44 -4
  2. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/README.md +43 -3
  3. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/pyproject.toml +1 -1
  4. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/__init__.py +4 -0
  5. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/_version.py +1 -1
  6. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/errors.py +18 -4
  7. cryptochief_crypto_processing_python-0.6.0/src/cryptochief/py.typed +0 -0
  8. cryptochief_crypto_processing_python-0.6.0/src/cryptochief/services/wallets.py +222 -0
  9. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/transport.py +27 -7
  10. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/webhook.py +65 -0
  11. cryptochief_crypto_processing_python-0.4.0/src/cryptochief/services/wallets.py +0 -86
  12. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/.gitignore +0 -0
  13. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/LICENSE +0 -0
  14. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/_models.py +0 -0
  15. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/amount.py +0 -0
  16. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/assets.py +0 -0
  17. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/chains.py +0 -0
  18. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/client.py +0 -0
  19. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/__init__.py +0 -0
  20. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/base58.py +0 -0
  21. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/borsh.py +0 -0
  22. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/evm_abi.py +0 -0
  23. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/keccak.py +0 -0
  24. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/tron_address.py +0 -0
  25. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/pagination.py +0 -0
  26. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/poll.py +0 -0
  27. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/rsa.py +0 -0
  28. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/sentinels.py +0 -0
  29. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/__init__.py +0 -0
  30. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/base.py +0 -0
  31. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/blockchain.py +0 -0
  32. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/credits.py +0 -0
  33. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/currencies.py +0 -0
  34. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/payins.py +0 -0
  35. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/payouts.py +0 -0
  36. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/static_deposits.py +0 -0
  37. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/sweeps.py +0 -0
  38. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/transactions.py +0 -0
  39. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/withdrawals.py +0 -0
  40. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/sign.py +0 -0
  41. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/ton/__init__.py +0 -0
  42. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/ton/address.py +0 -0
  43. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/ton/messages.py +0 -0
  44. {cryptochief_crypto_processing_python-0.4.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/ton/rpc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cryptochief-crypto-processing-python
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Official async Python SDK for the Crypto Chief crypto payment gateway and crypto processing API. Accept crypto payments, send single and mass crypto payouts, sign on-chain transactions and smart-contract calls, manage wallets, convert fiat to crypto, and verify webhooks across Ethereum, BNB Smart Chain, Polygon, Tron, TON, Solana, Bitcoin, XRP and 20+ blockchains. USDT and USDC stablecoin support with int-precise amounts and asyncio/httpx.
5
5
  Project-URL: Homepage, https://crypto-chief.com/processing/
6
6
  Project-URL: Documentation, https://docs-sdk.crypto-chief.com/processing/python
@@ -120,7 +120,7 @@ Both credentials come from the Dashboard -> Project.
120
120
  | Solana programs | `client.transactions` | `sign_anchor_call`, `sign_solana_call` |
121
121
  | TON contract calls (Jetton / NFT / text) | `client.transactions` | `jetton_transfer`, `nft_transfer`, `send_ton_comment`, `sign_ton_call` |
122
122
  | Accept incoming payments | `client.pay_ins` | `create`, `select_asset`, `reset_asset`, `cancel`, `info`, `history`, `wait_for` |
123
- | Wallet management + RSA decrypt | `client.wallets` | `generate`, `list`, `info`, `freeze`, `decrypt_private_key` |
123
+ | Wallet management + RSA decrypt | `client.wallets` | `generate`, `list`, `info`, `freeze`, `rebind_master`, `set_callback_url`, `set_label`, `decrypt_private_key` |
124
124
  | Treasury sweeps | `client.sweeps` | `force`, `history`, `wallet_history`, `settings`, `update_settings` |
125
125
  | Withdrawals (read-only) | `client.withdrawals` | `info`, `history` |
126
126
  | Static-deposit history | `client.static_deposits` | `info`, `history` |
@@ -200,6 +200,14 @@ decimal strings round-trip exactly. Discover an asset's decimals with
200
200
 
201
201
  ## Contract calls without hand-encoding
202
202
 
203
+ > **This snippet shows the encoder, not a complete swap.** Uniswap's router
204
+ > moves your input token with `transferFrom`, so it needs an ERC-20
205
+ > `approve(address,uint256)` on that token first, confirmed before the swap is
206
+ > signed — without it the swap reverts and burns the gas. And an `amountOutMin`
207
+ > of `0` accepts whatever the pool returns, which on a public mempool hands the
208
+ > trade to the first sandwich bot that sees it. The runnable version, with both,
209
+ > is in `examples/`.
210
+
203
211
  ```python
204
212
  from cryptochief import EvmCallRequest, Erc20TransferRequest, Chain, human_to_base
205
213
 
@@ -294,8 +302,11 @@ the sender IPs in `WEBHOOK_SENDER_IPS` at your edge for defense in depth.
294
302
  ## Errors
295
303
 
296
304
  Everything the SDK raises derives from `CryptoChiefError`. API failures are
297
- `APIError` with a stable `.code` (and `.http_status`); branch on `ErrorCode`
298
- rather than parsing messages. 5xx and network errors are retried automatically;
305
+ `APIError` with a stable `.code` (plus `.message`, `.http_status` and the
306
+ untouched `.raw` body); branch on `ErrorCode` rather than parsing messages. Both
307
+ envelope shapes the gateway sends - its own refusals, which carry the code in
308
+ `error`, and refusals relayed from upstream as `SERVICE_ERROR` with the code in
309
+ `msg` - resolve to `.code`. 5xx and network errors are retried automatically;
299
310
  4xx is raised immediately.
300
311
 
301
312
  ```python
@@ -363,6 +374,35 @@ priv = client.wallets.decrypt_private_key(wallet.private_key_encrypted)
363
374
  `completed_at` filled in. Earlier platform versions reported `completed` at
364
375
  broadcast, so a sweep could read as settled while its transaction was still
365
376
  unconfirmed.
377
+ - **My deposits are settling on the wrong master wallet.**
378
+ `client.wallets.rebind_master(address, master_wallet_address)` re-points a
379
+ transit or static wallet at another master of the project - the link is
380
+ otherwise decided at creation, falling back to the project's *oldest* master
381
+ of that chain family when none was named. It moves no money: it changes where
382
+ the **next** sweep settles, including sweeps already queued, and anything
383
+ already swept sits on the previous master and has to be sent from there as an
384
+ ordinary payout. It is idempotent, so re-running the same list is safe.
385
+ - **A static address is announcing deposits to the wrong URL.** Deposits go to
386
+ the callback the *address* carries, fixed when it was minted - so an address
387
+ you did not create through your own integration, or one minted before your
388
+ endpoint moved, keeps notifying somewhere else.
389
+ `client.wallets.set_callback_url(address, url)` corrects it, from the next
390
+ deposit on (one already announced is not re-announced). Pass `""` to clear it
391
+ and stop the announcements - the SDK sends the empty string rather than
392
+ dropping it the way it drops unset optional fields, and the wallet then reads
393
+ back `callback_url=None`. Static wallets only.
394
+ - **How do I name a wallet?** Pass `label` on
395
+ `client.wallets.generate(GenerateWalletRequest(..., label="EU shop"))`. It
396
+ applies to every wallet type, is up to 255 characters, and is yours alone -
397
+ nothing on chain and nothing in routing depends on it.
398
+ - **How do I rename a wallet I already have?**
399
+ `client.wallets.set_label(address, "EU shop")` - every wallet type, master
400
+ and transit included, unlike the deposit callback. Pass `""` to clear the
401
+ name: as with `set_callback_url`, the empty string is sent rather than
402
+ dropped, and the wallet then reads back `label=None`. The name comes back on
403
+ every response that describes a wallet - generation, `info`, `list`, and the
404
+ answers of `rebind_master` / `set_callback_url` / `set_label` itself - as
405
+ `wallet.label`, `None` when the wallet is unnamed.
366
406
  - **How do I keep test payments off real chains?** Set `environment` on
367
407
  `CreatePayInRequest` to `Environment.TESTNET` or `Environment.MAINNET`. It
368
408
  constrains the asset the platform picks when you have not named a concrete
@@ -81,7 +81,7 @@ Both credentials come from the Dashboard -> Project.
81
81
  | Solana programs | `client.transactions` | `sign_anchor_call`, `sign_solana_call` |
82
82
  | TON contract calls (Jetton / NFT / text) | `client.transactions` | `jetton_transfer`, `nft_transfer`, `send_ton_comment`, `sign_ton_call` |
83
83
  | Accept incoming payments | `client.pay_ins` | `create`, `select_asset`, `reset_asset`, `cancel`, `info`, `history`, `wait_for` |
84
- | Wallet management + RSA decrypt | `client.wallets` | `generate`, `list`, `info`, `freeze`, `decrypt_private_key` |
84
+ | Wallet management + RSA decrypt | `client.wallets` | `generate`, `list`, `info`, `freeze`, `rebind_master`, `set_callback_url`, `set_label`, `decrypt_private_key` |
85
85
  | Treasury sweeps | `client.sweeps` | `force`, `history`, `wallet_history`, `settings`, `update_settings` |
86
86
  | Withdrawals (read-only) | `client.withdrawals` | `info`, `history` |
87
87
  | Static-deposit history | `client.static_deposits` | `info`, `history` |
@@ -161,6 +161,14 @@ decimal strings round-trip exactly. Discover an asset's decimals with
161
161
 
162
162
  ## Contract calls without hand-encoding
163
163
 
164
+ > **This snippet shows the encoder, not a complete swap.** Uniswap's router
165
+ > moves your input token with `transferFrom`, so it needs an ERC-20
166
+ > `approve(address,uint256)` on that token first, confirmed before the swap is
167
+ > signed — without it the swap reverts and burns the gas. And an `amountOutMin`
168
+ > of `0` accepts whatever the pool returns, which on a public mempool hands the
169
+ > trade to the first sandwich bot that sees it. The runnable version, with both,
170
+ > is in `examples/`.
171
+
164
172
  ```python
165
173
  from cryptochief import EvmCallRequest, Erc20TransferRequest, Chain, human_to_base
166
174
 
@@ -255,8 +263,11 @@ the sender IPs in `WEBHOOK_SENDER_IPS` at your edge for defense in depth.
255
263
  ## Errors
256
264
 
257
265
  Everything the SDK raises derives from `CryptoChiefError`. API failures are
258
- `APIError` with a stable `.code` (and `.http_status`); branch on `ErrorCode`
259
- rather than parsing messages. 5xx and network errors are retried automatically;
266
+ `APIError` with a stable `.code` (plus `.message`, `.http_status` and the
267
+ untouched `.raw` body); branch on `ErrorCode` rather than parsing messages. Both
268
+ envelope shapes the gateway sends - its own refusals, which carry the code in
269
+ `error`, and refusals relayed from upstream as `SERVICE_ERROR` with the code in
270
+ `msg` - resolve to `.code`. 5xx and network errors are retried automatically;
260
271
  4xx is raised immediately.
261
272
 
262
273
  ```python
@@ -324,6 +335,35 @@ priv = client.wallets.decrypt_private_key(wallet.private_key_encrypted)
324
335
  `completed_at` filled in. Earlier platform versions reported `completed` at
325
336
  broadcast, so a sweep could read as settled while its transaction was still
326
337
  unconfirmed.
338
+ - **My deposits are settling on the wrong master wallet.**
339
+ `client.wallets.rebind_master(address, master_wallet_address)` re-points a
340
+ transit or static wallet at another master of the project - the link is
341
+ otherwise decided at creation, falling back to the project's *oldest* master
342
+ of that chain family when none was named. It moves no money: it changes where
343
+ the **next** sweep settles, including sweeps already queued, and anything
344
+ already swept sits on the previous master and has to be sent from there as an
345
+ ordinary payout. It is idempotent, so re-running the same list is safe.
346
+ - **A static address is announcing deposits to the wrong URL.** Deposits go to
347
+ the callback the *address* carries, fixed when it was minted - so an address
348
+ you did not create through your own integration, or one minted before your
349
+ endpoint moved, keeps notifying somewhere else.
350
+ `client.wallets.set_callback_url(address, url)` corrects it, from the next
351
+ deposit on (one already announced is not re-announced). Pass `""` to clear it
352
+ and stop the announcements - the SDK sends the empty string rather than
353
+ dropping it the way it drops unset optional fields, and the wallet then reads
354
+ back `callback_url=None`. Static wallets only.
355
+ - **How do I name a wallet?** Pass `label` on
356
+ `client.wallets.generate(GenerateWalletRequest(..., label="EU shop"))`. It
357
+ applies to every wallet type, is up to 255 characters, and is yours alone -
358
+ nothing on chain and nothing in routing depends on it.
359
+ - **How do I rename a wallet I already have?**
360
+ `client.wallets.set_label(address, "EU shop")` - every wallet type, master
361
+ and transit included, unlike the deposit callback. Pass `""` to clear the
362
+ name: as with `set_callback_url`, the empty string is sent rather than
363
+ dropped, and the wallet then reads back `label=None`. The name comes back on
364
+ every response that describes a wallet - generation, `info`, `list`, and the
365
+ answers of `rebind_master` / `set_callback_url` / `set_label` itself - as
366
+ `wallet.label`, `None` when the wallet is unnamed.
327
367
  - **How do I keep test payments off real chains?** Set `environment` on
328
368
  `CreatePayInRequest` to `Environment.TESTNET` or `Environment.MAINNET`. It
329
369
  constrains the asset the platform picks when you have not named a concrete
@@ -110,5 +110,5 @@ target-version = "py310"
110
110
 
111
111
  [tool.mypy]
112
112
  python_version = "3.10"
113
+ mypy_path = "src"
113
114
  warn_unused_ignores = true
114
- ignore_missing_imports = true
@@ -170,6 +170,8 @@ from .webhook import (
170
170
  PayInWebhookEvent,
171
171
  PayoutWebhookEvent,
172
172
  StaticDepositWebhookEvent,
173
+ SweepWebhookEvent,
174
+ SWEEP_EVENT_CONFIRMED,
173
175
  TransactionWebhookEvent,
174
176
  WebhookSignatureError,
175
177
  coerce_webhook_event,
@@ -230,6 +232,8 @@ __all__ = [
230
232
  "TransactionWebhookEvent",
231
233
  "PayInWebhookEvent",
232
234
  "StaticDepositWebhookEvent",
235
+ "SweepWebhookEvent",
236
+ "SWEEP_EVENT_CONFIRMED",
233
237
  # Services
234
238
  "PayoutsService",
235
239
  "TransactionsService",
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.6.0"
@@ -19,19 +19,27 @@ class CryptoChiefError(Exception):
19
19
  class APIError(CryptoChiefError):
20
20
  """A typed Crypto Chief error response.
21
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::
22
+ :attr:`code` is the machine-readable identifier to branch on, whichever
23
+ envelope shape the refusal arrived in: the gateway's own refusals carry the
24
+ code in ``error`` and an English sentence in ``msg``
25
+ (``{"error": "LABEL_TOO_LONG", "msg": "label is longer than 255 characters"}``),
26
+ while a relayed upstream refusal carries the generic ``SERVICE_ERROR``
27
+ marker in ``error`` and the code in ``msg``
28
+ (``{"error": "SERVICE_ERROR", "msg": "wallet_not_found"}``). Both resolve to
29
+ :attr:`code`::
26
30
 
27
31
  try:
28
32
  await client.payouts.execute(req)
29
33
  except APIError as e:
30
34
  if e.code == ErrorCode.INSUFFICIENT_FUNDS:
31
35
  ... # top up and retry
36
+
37
+ :attr:`message` is the human-readable half - the sentence when the gateway
38
+ sent one - and :attr:`raw` is the untouched response body.
32
39
  """
33
40
 
34
41
  code: str
42
+ message: str
35
43
  http_status: int
36
44
  raw: Optional[str]
37
45
 
@@ -46,6 +54,7 @@ class APIError(CryptoChiefError):
46
54
  # Normalize an ErrorCode member to its wire string ("NETWORK_ERROR"),
47
55
  # not its enum repr ("ErrorCode.NETWORK_ERROR").
48
56
  self.code = code.value if isinstance(code, Enum) else str(code)
57
+ self.message = message or ""
49
58
  self.http_status = http_status
50
59
  self.raw = raw
51
60
  super().__init__(self._format(http_status, self.code, message))
@@ -75,6 +84,11 @@ class ErrorCode(str, Enum):
75
84
  ORDER_NOT_LIVE = "ORDER_NOT_LIVE"
76
85
  ASSET_ALREADY_SELECTED = "ASSET_ALREADY_SELECTED"
77
86
  INVALID_PARAMS = "INVALID_PARAMS"
87
+ #: A wallet label over 255 characters.
88
+ LABEL_TOO_LONG = "LABEL_TOO_LONG"
89
+ #: The gateway's marker for a refusal relayed from an upstream service; the
90
+ #: machine code then travels in ``msg`` and is what :attr:`APIError.code`
91
+ #: reports, so this member is rarely what you compare against.
78
92
  SERVICE_ERROR = "SERVICE_ERROR"
79
93
  UNAUTHORIZED = "UNAUTHORIZED"
80
94
  URL_CALLBACK_REQUIRED = "URL_CALLBACK_REQUIRED"
@@ -0,0 +1,222 @@
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 ..errors import CryptoChiefError
11
+ from .base import BaseService
12
+
13
+
14
+ class WalletType(str, Enum):
15
+ MASTER = "master"
16
+ TRANSIT = "transit"
17
+ STATIC = "static"
18
+
19
+
20
+ @dataclass(kw_only=True)
21
+ class GenerateWalletRequest:
22
+ wallet_type: str
23
+ chain_family: str
24
+ master_wallet_address: Optional[str] = None # transit/static wallets only
25
+ callback_url: Optional[str] = None # static wallets only - per-deposit webhook URL
26
+ #: A name for the wallet, for people reading a list of them. Applies to
27
+ #: every wallet type - it names the wallet, it is not a property of its
28
+ #: role - and is yours alone: nothing on chain and nothing in routing
29
+ #: depends on it. Up to 255 characters, longer answers ``LABEL_TOO_LONG``.
30
+ #: Leave it ``None`` to omit it; the endpoint rejects unknown fields, and an
31
+ #: empty string is a name rather than the absence of one.
32
+ #: :meth:`WalletsService.set_label` renames the wallet afterwards.
33
+ label: Optional[str] = None
34
+
35
+
36
+ @dataclass(kw_only=True)
37
+ class WalletCoinBalance:
38
+ address: Optional[str] = None
39
+ chain: Optional[str] = None
40
+ coin: Optional[str] = None
41
+ contract: Optional[str] = None
42
+ decimals: int = 0
43
+ value: Optional[str] = None
44
+ human_value: Optional[str] = None
45
+ amount_usd: Optional[str] = None
46
+ timestamp: Optional[int] = None
47
+
48
+
49
+ @dataclass(kw_only=True)
50
+ class Wallet:
51
+ address: str = ""
52
+ chain_family: Optional[str] = None
53
+ type: Optional[str] = None
54
+ wallet_type: Optional[str] = None
55
+ frozen: Optional[bool] = None
56
+ #: The master this wallet sweeps into, ``None`` when it has none - a master
57
+ #: wallet has no master of its own. The API always sends the key and sends
58
+ #: ``null`` rather than an empty string, so ``None`` here means "no master",
59
+ #: not "not reported". :meth:`WalletsService.rebind_master` changes it.
60
+ master_wallet_address: Optional[str] = None
61
+ #: Where deposits to this address are announced, ``None`` when nowhere. Only
62
+ #: a static wallet has one: a master or transit always reads ``None``.
63
+ #: :meth:`WalletsService.set_callback_url` changes it.
64
+ callback_url: Optional[str] = None
65
+ #: The wallet's name, ``None`` when it has none. Every wallet type can carry
66
+ #: one, and every response that describes a wallet reports it. The API
67
+ #: always sends the key and sends ``null`` rather than an empty string, so
68
+ #: ``None`` here means "unnamed" - a cleared label reads back as ``None``,
69
+ #: never as ``""``. :meth:`WalletsService.set_label` changes it.
70
+ label: Optional[str] = None
71
+ #: Base64 RSA-OAEP/SHA-256 ciphertext - decrypt with ``decrypt_private_key``.
72
+ private_key_encrypted: Optional[str] = None
73
+ created_at: Optional[str] = None
74
+ coins: Optional[List[WalletCoinBalance]] = None
75
+ total_balance_usd: Optional[str] = None
76
+
77
+
78
+ @dataclass(kw_only=True)
79
+ class ListWalletsResponse:
80
+ items: Optional[List[Wallet]] = None
81
+
82
+
83
+ class WalletsService(BaseService):
84
+ async def generate(self, req: GenerateWalletRequest) -> Wallet:
85
+ """Provision a new wallet on the requested chain family."""
86
+ return from_dict(Wallet, await self._post("/v1/wallets/generate", req))
87
+
88
+ async def list(self) -> ListWalletsResponse:
89
+ """Every wallet on the project."""
90
+ return from_dict(ListWalletsResponse, await self._post("/v1/wallets/list", {}))
91
+
92
+ async def info(self, address: str) -> Wallet:
93
+ """Details and current balances of one wallet."""
94
+ return from_dict(Wallet, await self._post("/v1/wallets/info", {"address": address}))
95
+
96
+ async def freeze(self, address: str) -> Wallet:
97
+ """Toggle the frozen flag - the response's ``frozen`` field is the new state."""
98
+ return from_dict(Wallet, await self._post("/v1/wallets/freeze", {"address": address}))
99
+
100
+ async def rebind_master(self, address: str, master_wallet_address: str) -> Wallet:
101
+ """Re-point a transit or static wallet at another master of the project.
102
+
103
+ The master link is decided when the wallet is created - at the master
104
+ named on that request, or, when none was named, at the project's *oldest*
105
+ master of that chain family, which on a project with more than one master
106
+ is rarely the one you meant. This is the way back.
107
+
108
+ It moves no money. It changes where the *next* sweep settles, including
109
+ sweeps already queued, because the destination is resolved when the sweep
110
+ runs; anything already swept sits on the previous master and has to be
111
+ sent from there as an ordinary payout.
112
+
113
+ Idempotent - a wallet already bound to that master answers 200 unchanged,
114
+ so re-running the same list is safe. A master wallet cannot be
115
+ re-pointed at all (``only transit and static wallets have a master``);
116
+ naming something that is not a master as the TARGET is a different
117
+ refusal (``not_a_master_wallet``). The target master must be the same
118
+ chain family (``chain_family_mismatch``) and not frozen
119
+ (``master_wallet_frozen``), since sweeping into a frozen master would
120
+ strand the funds there.
121
+
122
+ The gateway relays every upstream refusal as
123
+ ``{"error": "SERVICE_ERROR", "msg": "<token>"}``, and the SDK reports
124
+ that token as ``APIError.code`` - so branch on the code, not on the
125
+ message text. These tokens are per-endpoint and are not
126
+ :class:`~cryptochief.ErrorCode` members. Memo/tag-based families share one deposit
127
+ account across orders and are excluded
128
+ (``shared_transit_cannot_be_rebound``). Both addresses resolve against
129
+ the authenticated project, so one that is not yours answers
130
+ ``wallet_not_found`` / ``master_wallet_not_found`` rather than revealing
131
+ that it exists elsewhere.
132
+
133
+ Returns the wallet as it now stands.
134
+ """
135
+ return from_dict(
136
+ Wallet,
137
+ await self._post(
138
+ "/v1/wallets/rebind-master",
139
+ {"address": address, "master_wallet_address": master_wallet_address},
140
+ ),
141
+ )
142
+
143
+ async def set_callback_url(self, address: str, callback_url: str) -> Wallet:
144
+ """Set or clear a static wallet's deposit webhook after creation.
145
+
146
+ Deposits are announced to the callback URL the *address* carries, which
147
+ is fixed when the address is minted - so an address you did not create
148
+ through your own integration, or one minted before your endpoint moved,
149
+ keeps announcing its deposits somewhere else, or nowhere. This corrects
150
+ it, from the next deposit on: one already announced is not re-announced
151
+ to the new URL.
152
+
153
+ Pass ``""`` to clear it and stop announcing deposits for the address.
154
+ That is a real instruction rather than a missing field, so the SDK sends
155
+ the empty string instead of dropping it the way it drops unset optional
156
+ fields; the wallet then reads back ``callback_url=None``. ``None`` is
157
+ not that instruction and is refused here rather than silently leaving
158
+ the field off the body.
159
+
160
+ Static wallets only - a master or transit has no per-deposit callback
161
+ and answers 400. The address resolves against the authenticated project,
162
+ so one that is not yours answers ``wallet_not_found``.
163
+
164
+ Returns the wallet as it now stands.
165
+ """
166
+ if callback_url is None:
167
+ raise CryptoChiefError(
168
+ 'cryptochief: set_callback_url: callback_url is required; pass "" to clear it'
169
+ )
170
+ return from_dict(
171
+ Wallet,
172
+ await self._post(
173
+ "/v1/wallets/callback-url",
174
+ {"address": address, "callback_url": callback_url},
175
+ ),
176
+ )
177
+
178
+ async def set_label(self, address: str, label: str) -> Wallet:
179
+ """Set or clear a wallet's label - the name it is read by.
180
+
181
+ A label is yours alone: nothing on chain and nothing in routing depends
182
+ on it. It is also the only thing telling one freshly minted address
183
+ apart from the next in a list, so a wallet created before the label was
184
+ supported, or minted somewhere other than your own integration, is worth
185
+ naming after the fact. This is how.
186
+
187
+ Every wallet type can be renamed - master, transit and static alike,
188
+ because a label names the wallet rather than describing its role. That
189
+ is unlike :meth:`set_callback_url`, which only a static wallet has.
190
+
191
+ Pass ``""`` to clear the name and leave the wallet unnamed. That is a
192
+ real instruction rather than a missing field, so the SDK sends the empty
193
+ string instead of dropping it the way it drops unset optional fields;
194
+ the wallet then reads back ``label=None``. ``None`` is not that
195
+ instruction and is refused here rather than silently leaving the field
196
+ off the body.
197
+
198
+ Up to 255 characters, longer answers ``LABEL_TOO_LONG``. The address
199
+ resolves against the authenticated project, so one that is not yours
200
+ answers ``wallet_not_found`` rather than revealing that it exists
201
+ elsewhere.
202
+
203
+ Returns the wallet as it now stands.
204
+ """
205
+ if label is None:
206
+ raise CryptoChiefError(
207
+ 'cryptochief: set_label: label is required; pass "" to clear it'
208
+ )
209
+ return from_dict(
210
+ Wallet,
211
+ await self._post("/v1/wallets/label", {"address": address, "label": label}),
212
+ )
213
+
214
+ def decrypt_private_key(self, encrypted: str) -> str:
215
+ """Decrypt a generated wallet's ``private_key_encrypted`` field locally.
216
+
217
+ Uses the RSA private key configured on the client (``rsa_private_key``
218
+ option) and returns the chain-native hex private key. Raises
219
+ :class:`RsaKeyNotConfiguredError` if no key was configured. Synchronous -
220
+ never touches the network.
221
+ """
222
+ return self._client.rsa_decrypt(encrypted)
@@ -8,11 +8,26 @@ import random
8
8
  from .errors import APIError, ErrorCode
9
9
 
10
10
 
11
+ def _field(env: dict, key: str) -> str:
12
+ """Read ``key`` from an error envelope as a trimmed string (``""`` if absent)."""
13
+ value = env.get(key)
14
+ return value.strip() if isinstance(value, str) else ""
15
+
16
+
11
17
  def parse_api_error(status: int, body: str) -> APIError:
12
18
  """Parse a non-2xx response body into an :class:`APIError` with a stable code.
13
19
 
14
- The code is ``msg or error or HTTP_<status>``, and the message prefers
15
- ``msg`` when it differs from ``error``.
20
+ Refusals arrive in two envelope shapes. When the gateway itself refuses, the
21
+ machine code is in ``error`` and ``msg`` holds an English sentence
22
+ (``{"error": "LABEL_TOO_LONG", "msg": "label is longer than 255 characters"}``).
23
+ When it relays an upstream refusal, ``error`` is the generic
24
+ ``SERVICE_ERROR`` marker and the machine code is in ``msg``
25
+ (``{"error": "SERVICE_ERROR", "msg": "wallet_not_found"}``).
26
+
27
+ So the code is ``error`` unless that is ``SERVICE_ERROR``, in which case it
28
+ is ``msg``; an empty result falls back to ``error`` and then
29
+ ``HTTP_<status>``. The human-readable message prefers ``msg`` and falls back
30
+ to ``error``.
16
31
  """
17
32
  env: dict = {}
18
33
  try:
@@ -21,11 +36,16 @@ def parse_api_error(status: int, body: str) -> APIError:
21
36
  env = parsed
22
37
  except ValueError:
23
38
  pass # non-JSON error body -> fall back to HTTP_<status>
24
- code = env.get("msg") or env.get("error") or f"HTTP_{status}"
25
- message = env.get("error") or ""
26
- if env.get("msg") and env.get("msg") != env.get("error"):
27
- message = env.get("msg")
28
- return APIError(code, http_status=status, message=message, raw=body)
39
+
40
+ error = _field(env, "error")
41
+ msg = _field(env, "msg")
42
+ code = error if error and error != ErrorCode.SERVICE_ERROR else (msg or error)
43
+ return APIError(
44
+ code or f"HTTP_{status}",
45
+ http_status=status,
46
+ message=msg or error,
47
+ raw=body,
48
+ )
29
49
 
30
50
 
31
51
  def backoff_delay(attempt: int, base_ms: float, max_ms: float) -> float:
@@ -175,11 +175,75 @@ class StaticDepositWebhookEvent:
175
175
  paid_at: Optional[str] = None
176
176
 
177
177
 
178
+ #: The only sweep event the platform emits. There is deliberately no
179
+ #: ``sweep.broadcasted``: "we sent it" is not something you can act on, and an
180
+ #: event that means "maybe" is one more thing to reconcile.
181
+ SWEEP_EVENT_CONFIRMED = "sweep.confirmed"
182
+
183
+
184
+ @dataclass(kw_only=True)
185
+ class SweepWebhookEvent:
186
+ """Funds swept off a deposit wallet, confirmed on chain.
187
+
188
+ A ``static_deposit.paid`` tells you a customer paid you. This tells you the
189
+ money has finished moving into your own custody - until it fires, the
190
+ balance still sits on the deposit address. Reconciliation, treasury
191
+ reporting and "funds available to pay out" all key off this event, not off
192
+ the deposit.
193
+
194
+ Sweeps run on static deposit wallets *and* on the transit wallets issued per
195
+ pay-in order; both deliver here, to the callback URL configured for the
196
+ wallet the funds left.
197
+ """
198
+
199
+ event: str = ""
200
+ #: The sweeper task. One sweep settles once - use it as your idempotency key.
201
+ task_id: str = ""
202
+ #: Always ``"completed"``. A sweep reaches you in no other state.
203
+ status: str = ""
204
+
205
+ #: The wallet the funds left - the address your customer paid into.
206
+ wallet_address: str = ""
207
+ #: The master wallet they landed on.
208
+ to_address: Optional[str] = None
209
+
210
+ network: str = ""
211
+ chain_family: Optional[str] = None
212
+ asset_symbol: str = ""
213
+ asset_contract: Optional[str] = None
214
+ #: ``"native"`` or ``"token"``.
215
+ asset_type: Optional[str] = None
216
+ amount_raw: Optional[str] = None
217
+ amount_human: Optional[str] = None
218
+
219
+ sweep_tx_hash: str = ""
220
+ #: Set when the platform had to fund gas on the wallet before it could sweep.
221
+ gas_pump_tx_hash: Optional[str] = None
222
+
223
+ #: What makes this event true rather than hopeful, and never zero. It
224
+ #: travels with the event rather than being implied by it: "confirmed" is
225
+ #: not the same number on every chain, so if you run your own finality
226
+ #: policy you need the count to apply it.
227
+ sweep_confirmations: int = 0
228
+
229
+ #: When the chain was observed to hold the sweep. NOT the task's completion
230
+ #: timestamp, which is stamped on every terminal outcome - failures
231
+ #: included - and so says nothing about settlement.
232
+ confirmed_at: Optional[str] = None
233
+
234
+ #: What triggered it: ``"momentum"``, ``"threshold"`` or ``"force"``.
235
+ type_work: Optional[str] = None
236
+ #: What the sweep cost: network fee plus any gas or energy the platform
237
+ #: fronted to make it possible.
238
+ total_fee_usd: Optional[str] = None
239
+
240
+
178
241
  WebhookEvent = Union[
179
242
  PayoutWebhookEvent,
180
243
  TransactionWebhookEvent,
181
244
  PayInWebhookEvent,
182
245
  StaticDepositWebhookEvent,
246
+ SweepWebhookEvent,
183
247
  Dict[str, Any],
184
248
  ]
185
249
 
@@ -188,4 +252,5 @@ _EVENT_BY_PREFIX = {
188
252
  "transaction": TransactionWebhookEvent,
189
253
  "invoice": PayInWebhookEvent,
190
254
  "static_deposit": StaticDepositWebhookEvent,
255
+ "sweep": SweepWebhookEvent,
191
256
  }
@@ -1,86 +0,0 @@
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)