cryptochief-crypto-processing-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cryptochief/__init__.py +336 -0
- cryptochief/_models.py +93 -0
- cryptochief/_version.py +3 -0
- cryptochief/amount.py +97 -0
- cryptochief/assets.py +32 -0
- cryptochief/chains.py +115 -0
- cryptochief/client.py +189 -0
- cryptochief/contract/__init__.py +69 -0
- cryptochief/contract/base58.py +40 -0
- cryptochief/contract/borsh.py +141 -0
- cryptochief/contract/evm_abi.py +321 -0
- cryptochief/contract/keccak.py +16 -0
- cryptochief/contract/tron_address.py +60 -0
- cryptochief/errors.py +111 -0
- cryptochief/pagination.py +32 -0
- cryptochief/poll.py +56 -0
- cryptochief/rsa.py +70 -0
- cryptochief/services/__init__.py +1 -0
- cryptochief/services/base.py +24 -0
- cryptochief/services/blockchain.py +76 -0
- cryptochief/services/currencies.py +55 -0
- cryptochief/services/payins.py +145 -0
- cryptochief/services/payouts.py +175 -0
- cryptochief/services/static_deposits.py +77 -0
- cryptochief/services/sweeps.py +85 -0
- cryptochief/services/transactions.py +470 -0
- cryptochief/services/wallets.py +86 -0
- cryptochief/services/withdrawals.py +51 -0
- cryptochief/sign.py +112 -0
- cryptochief/ton/__init__.py +19 -0
- cryptochief/ton/address.py +109 -0
- cryptochief/ton/messages.py +105 -0
- cryptochief/ton/rpc.py +159 -0
- cryptochief/transport.py +48 -0
- cryptochief/webhook.py +191 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/METADATA +346 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/RECORD +39 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/WHEEL +4 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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
|
+
)
|