cryptochief-crypto-processing-python 0.5.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.5.0 → cryptochief_crypto_processing_python-0.6.0}/PKG-INFO +36 -4
  2. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/README.md +35 -3
  3. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/_version.py +1 -1
  4. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/errors.py +18 -4
  5. cryptochief_crypto_processing_python-0.6.0/src/cryptochief/services/wallets.py +222 -0
  6. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/transport.py +27 -7
  7. cryptochief_crypto_processing_python-0.5.0/src/cryptochief/services/wallets.py +0 -86
  8. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/.gitignore +0 -0
  9. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/LICENSE +0 -0
  10. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/pyproject.toml +0 -0
  11. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/__init__.py +0 -0
  12. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/_models.py +0 -0
  13. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/amount.py +0 -0
  14. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/assets.py +0 -0
  15. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/chains.py +0 -0
  16. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/client.py +0 -0
  17. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/__init__.py +0 -0
  18. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/base58.py +0 -0
  19. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/borsh.py +0 -0
  20. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/evm_abi.py +0 -0
  21. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/keccak.py +0 -0
  22. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/contract/tron_address.py +0 -0
  23. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/pagination.py +0 -0
  24. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/poll.py +0 -0
  25. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/py.typed +0 -0
  26. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/rsa.py +0 -0
  27. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/sentinels.py +0 -0
  28. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/__init__.py +0 -0
  29. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/base.py +0 -0
  30. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/blockchain.py +0 -0
  31. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/credits.py +0 -0
  32. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/currencies.py +0 -0
  33. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/payins.py +0 -0
  34. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/payouts.py +0 -0
  35. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/static_deposits.py +0 -0
  36. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/sweeps.py +0 -0
  37. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/transactions.py +0 -0
  38. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/services/withdrawals.py +0 -0
  39. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/sign.py +0 -0
  40. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/ton/__init__.py +0 -0
  41. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/ton/address.py +0 -0
  42. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/ton/messages.py +0 -0
  43. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/ton/rpc.py +0 -0
  44. {cryptochief_crypto_processing_python-0.5.0 → cryptochief_crypto_processing_python-0.6.0}/src/cryptochief/webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cryptochief-crypto-processing-python
3
- Version: 0.5.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` |
@@ -302,8 +302,11 @@ the sender IPs in `WEBHOOK_SENDER_IPS` at your edge for defense in depth.
302
302
  ## Errors
303
303
 
304
304
  Everything the SDK raises derives from `CryptoChiefError`. API failures are
305
- `APIError` with a stable `.code` (and `.http_status`); branch on `ErrorCode`
306
- 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;
307
310
  4xx is raised immediately.
308
311
 
309
312
  ```python
@@ -371,6 +374,35 @@ priv = client.wallets.decrypt_private_key(wallet.private_key_encrypted)
371
374
  `completed_at` filled in. Earlier platform versions reported `completed` at
372
375
  broadcast, so a sweep could read as settled while its transaction was still
373
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.
374
406
  - **How do I keep test payments off real chains?** Set `environment` on
375
407
  `CreatePayInRequest` to `Environment.TESTNET` or `Environment.MAINNET`. It
376
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` |
@@ -263,8 +263,11 @@ the sender IPs in `WEBHOOK_SENDER_IPS` at your edge for defense in depth.
263
263
  ## Errors
264
264
 
265
265
  Everything the SDK raises derives from `CryptoChiefError`. API failures are
266
- `APIError` with a stable `.code` (and `.http_status`); branch on `ErrorCode`
267
- 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;
268
271
  4xx is raised immediately.
269
272
 
270
273
  ```python
@@ -332,6 +335,35 @@ priv = client.wallets.decrypt_private_key(wallet.private_key_encrypted)
332
335
  `completed_at` filled in. Earlier platform versions reported `completed` at
333
336
  broadcast, so a sweep could read as settled while its transaction was still
334
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.
335
367
  - **How do I keep test payments off real chains?** Set `environment` on
336
368
  `CreatePayInRequest` to `Environment.TESTNET` or `Environment.MAINNET`. It
337
369
  constrains the asset the platform picks when you have not named a concrete
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "0.5.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:
@@ -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)