xync-client 0.0.141__py3-none-any.whl → 0.0.155__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.
@@ -0,0 +1,636 @@
1
+ """
2
+ MEXC P2P OpenAPI v1.2 Async Client
3
+ """
4
+
5
+ import hmac
6
+ import hashlib
7
+ import time
8
+ from typing import Optional, List, Literal
9
+ from decimal import Decimal
10
+
11
+ import aiohttp
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ # ============ Enums ============
16
+
17
+
18
+ class Side(str):
19
+ BUY = "BUY"
20
+ SELL = "SELL"
21
+
22
+
23
+ class AdvStatus(str):
24
+ CLOSE = "CLOSE"
25
+ OPEN = "OPEN"
26
+ DELETE = "DELETE"
27
+ LOW_STOCK = "LOW_STOCK"
28
+
29
+
30
+ class OrderDealState(str):
31
+ NOT_PAID = "NOT_PAID"
32
+ PAID = "PAID"
33
+ WAIT_PROCESS = "WAIT_PROCESS"
34
+ PROCESSING = "PROCESSING"
35
+ DONE = "DONE"
36
+ CANCEL = "CANCEL"
37
+ INVALID = "INVALID"
38
+ REFUSE = "REFUSE"
39
+ TIMEOUT = "TIMEOUT"
40
+
41
+
42
+ class NotifyType(str):
43
+ SMS = "SMS"
44
+ MAIL = "MAIL"
45
+ GA = "GA"
46
+
47
+
48
+ # ============ Request Models ============
49
+
50
+
51
+ class CreateUpdateAdRequest(BaseModel):
52
+ advNo: Optional[str] = None
53
+ payTimeLimit: int
54
+ initQuantity: Decimal
55
+ supplyQuantity: Optional[Decimal] = None
56
+ price: Decimal
57
+ coinId: str
58
+ countryCode: Optional[str] = None
59
+ side: str
60
+ advStatus: Optional[str] = None
61
+ allowSys: Optional[bool] = None
62
+ fiatUnit: str
63
+ payMethod: str
64
+ autoReplyMsg: Optional[str] = None
65
+ tradeTerms: Optional[str] = None
66
+ minSingleTransAmount: Decimal
67
+ maxSingleTransAmount: Decimal
68
+ kycLevel: Optional[str] = None
69
+ requireMobile: Optional[bool] = None
70
+ userAllTradeCountMin: int
71
+ userAllTradeCountMax: int
72
+ exchangeCount: Optional[int] = None
73
+ maxPayLimit: Optional[int] = None
74
+ buyerRegDaysLimit: Optional[int] = None
75
+ creditAmount: Optional[Decimal] = None
76
+ blockTrade: Optional[bool] = None
77
+ deviceId: Optional[str] = None
78
+
79
+
80
+ class CreateOrderRequest(BaseModel):
81
+ advNo: str
82
+ amount: Optional[Decimal] = None
83
+ tradableQuantity: Optional[Decimal] = None
84
+ userConfirmPaymentId: int
85
+ userConfirmPayMethodId: Optional[int] = None
86
+ deviceId: Optional[str] = None
87
+
88
+
89
+ class ConfirmPaidRequest(BaseModel):
90
+ advOrderNo: str
91
+ payId: int
92
+
93
+
94
+ class ReleaseCoinRequest(BaseModel):
95
+ advOrderNo: str
96
+ notifyType: Optional[str] = None
97
+ notifyCode: Optional[str] = None
98
+
99
+
100
+ class ServiceSwitchRequest(BaseModel):
101
+ open: bool
102
+
103
+
104
+ # ============ Response Models ============
105
+
106
+
107
+ class BaseResponse(BaseModel):
108
+ code: int
109
+ msg: str
110
+
111
+
112
+ class PaymentInfo(BaseModel):
113
+ id: int
114
+ payMethod: int
115
+ bankName: str
116
+ account: str
117
+ bankAddress: str
118
+ payee: str
119
+ extend: str
120
+
121
+
122
+ class MerchantInfo(BaseModel):
123
+ nickName: str
124
+ imId: str
125
+ memberId: str
126
+ registry: int
127
+ vipLevel: int
128
+ greenDiamond: bool
129
+ emailAuthentication: bool
130
+ smsAuthentication: bool
131
+ identityVerification: bool
132
+ lastOnlineTime: int
133
+ badge: str
134
+ merchantType: str
135
+
136
+
137
+ class MerchantStatistics(BaseModel):
138
+ totalBuyCount: int
139
+ totalSellCount: int
140
+ doneLastMonthCount: int
141
+ avgBuyHandleTime: int
142
+ avgSellHandleTime: int
143
+ lastMonthCompleteRate: str
144
+ completeRate: str
145
+ avgHandleTime: int
146
+
147
+
148
+ class Advertisement(BaseModel):
149
+ advNo: str
150
+ payTimeLimit: int
151
+ quantity: int
152
+ price: Decimal
153
+ initAmount: Decimal
154
+ frozenQuantity: Decimal
155
+ availableQuantity: Decimal
156
+ coinId: str
157
+ coinName: str
158
+ countryCode: str
159
+ commissionRate: Decimal
160
+ advStatus: str
161
+ side: str
162
+ createTime: int
163
+ updateTime: int
164
+ fiatUnit: str
165
+ feeType: int
166
+ autoReplyMsg: str
167
+ tradeTerms: str
168
+ payMethod: str
169
+ paymentInfo: List[PaymentInfo]
170
+ minSingleTransAmount: Decimal
171
+ maxSingleTransAmount: Decimal
172
+ kycLevel: int
173
+ requireMobile: bool
174
+ userAllTradeCountMax: int
175
+ userAllTradeCountMin: int
176
+ exchangeCount: int
177
+ maxPayLimit: int
178
+ buyerRegDaysLimit: int
179
+ blockTrade: bool
180
+
181
+
182
+ class MarketAdvertisement(Advertisement):
183
+ merchant: MerchantInfo
184
+ merchantStatistics: MerchantStatistics
185
+ orderPayCount: int
186
+ tags: str
187
+
188
+
189
+ class PageInfo(BaseModel):
190
+ total: int
191
+ currPage: int
192
+ pageSize: int
193
+ totalPage: int
194
+
195
+
196
+ class UserInfo(BaseModel):
197
+ nickName: str
198
+
199
+
200
+ class Order(BaseModel):
201
+ advNo: str
202
+ advOrderNo: str
203
+ tradableQuantity: Decimal
204
+ price: Decimal
205
+ amount: Decimal
206
+ coinName: str
207
+ state: int
208
+ payTimeLimit: int
209
+ side: str
210
+ fiatUnit: str
211
+ createTime: int
212
+ updateTime: int
213
+ userInfo: UserInfo
214
+ complained: bool
215
+ blockUser: bool
216
+ unreadCount: int
217
+ complainId: Optional[str] = None
218
+
219
+
220
+ class OrderDetail(Order):
221
+ paymentInfo: List[PaymentInfo]
222
+ allowComplainTime: int
223
+ confirmPaymentInfo: PaymentInfo
224
+ userInfo: dict
225
+ userFiatStatistics: dict
226
+ spotCount: int
227
+
228
+
229
+ class CreateAdResponse(BaseResponse):
230
+ data: str # advNo
231
+
232
+
233
+ class AdListResponse(BaseResponse):
234
+ data: List[Advertisement]
235
+ page: PageInfo
236
+
237
+
238
+ class MarketAdListResponse(BaseResponse):
239
+ data: List[MarketAdvertisement]
240
+ page: PageInfo
241
+
242
+
243
+ class CreateOrderResponse(BaseResponse):
244
+ data: str # advOrderNo
245
+
246
+
247
+ class OrderListResponse(BaseResponse):
248
+ data: List[Order]
249
+ page: PageInfo
250
+
251
+
252
+ class OrderDetailResponse(BaseResponse):
253
+ data: OrderDetail
254
+
255
+
256
+ class ListenKeyResponse(BaseResponse):
257
+ listenKey: str
258
+
259
+
260
+ class ConversationResponse(BaseResponse):
261
+ data: dict
262
+
263
+
264
+ class ChatMessage(BaseModel):
265
+ id: int
266
+ content: Optional[str] = None
267
+ createTime: str
268
+ fromNickName: str
269
+ fromUserId: str
270
+ type: int
271
+ imageUrl: Optional[str] = None
272
+ imageThumbUrl: Optional[str] = None
273
+ videoUrl: Optional[str] = None
274
+ fileUrl: Optional[str] = None
275
+ self_: bool = Field(alias="self")
276
+ conversationId: int
277
+
278
+
279
+ class ChatMessagesResponse(BaseResponse):
280
+ data: dict
281
+
282
+
283
+ class UploadFileResponse(BaseResponse):
284
+ data: dict
285
+
286
+
287
+ # ============ Client ============
288
+
289
+
290
+ class MEXCP2PClient:
291
+ """Асинхронный клиент для MEXC P2P API v1.2"""
292
+
293
+ BASE_URL = "https://api.mexc.com"
294
+
295
+ def __init__(self, api_key: str, api_secret: str):
296
+ self.api_key = api_key
297
+ self.api_secret = api_secret
298
+ self.session: Optional[aiohttp.ClientSession] = None
299
+
300
+ async def __aenter__(self):
301
+ self.session = aiohttp.ClientSession()
302
+ return self
303
+
304
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
305
+ if self.session:
306
+ await self.session.close()
307
+
308
+ def _generate_signature(self, query_string: str) -> str:
309
+ """Генерация HMAC SHA256 подписи"""
310
+ return hmac.new(self.api_secret.encode("utf-8"), query_string.encode("utf-8"), hashlib.sha256).hexdigest()
311
+
312
+ def _get_timestamp(self) -> int:
313
+ """Получение текущего timestamp в миллисекундах"""
314
+ return int(time.time() * 1000)
315
+
316
+ async def _request(
317
+ self, method: str, endpoint: str, params: Optional[dict] = None, data: Optional[BaseModel] = None
318
+ ) -> dict:
319
+ """Базовый метод для HTTP запросов"""
320
+ if not self.session:
321
+ raise RuntimeError("Client not initialized. Use async context manager.")
322
+
323
+ timestamp = self._get_timestamp()
324
+
325
+ # Формирование query string для подписи
326
+ query_params = params.copy() if params else {}
327
+ query_params["timestamp"] = timestamp
328
+
329
+ query_string = "&".join(f"{k}={v}" for k, v in sorted(query_params.items()))
330
+ signature = self._generate_signature(query_string)
331
+
332
+ query_params["signature"] = signature
333
+
334
+ headers = {"X-MEXC-APIKEY": self.api_key, "Content-Type": "application/json"}
335
+
336
+ url = f"{self.BASE_URL}{endpoint}"
337
+
338
+ json_data = data.model_dump(exclude_none=True) if data else None
339
+
340
+ async with self.session.request(method, url, params=query_params, json=json_data, headers=headers) as response:
341
+ return await response.json()
342
+
343
+ # ============ Advertisement Methods ============
344
+
345
+ async def create_or_update_ad(self, request: CreateUpdateAdRequest) -> CreateAdResponse:
346
+ """Создание или обновление объявления"""
347
+ result = await self._request("POST", "/api/v3/fiat/merchant/ads/save_or_update", data=request)
348
+ return CreateAdResponse(**result)
349
+
350
+ async def get_my_ads(
351
+ self,
352
+ coin_id: Optional[str] = None,
353
+ adv_status: Optional[str] = None,
354
+ merchant_id: Optional[str] = None,
355
+ fiat_unit: Optional[str] = None,
356
+ side: Optional[str] = None,
357
+ kyc_level: Optional[str] = None,
358
+ start_time: Optional[int] = None,
359
+ end_time: Optional[int] = None,
360
+ page: int = 1,
361
+ limit: int = 10,
362
+ ) -> AdListResponse:
363
+ """Получение списка моих объявлений с пагинацией"""
364
+ params = {"page": page, "limit": limit}
365
+
366
+ if coin_id:
367
+ params["coinId"] = coin_id
368
+ if adv_status:
369
+ params["advStatus"] = adv_status
370
+ if merchant_id:
371
+ params["merchantId"] = merchant_id
372
+ if fiat_unit:
373
+ params["fiatUnit"] = fiat_unit
374
+ if side:
375
+ params["side"] = side
376
+ if kyc_level:
377
+ params["kycLevel"] = kyc_level
378
+ if start_time:
379
+ params["startTime"] = start_time
380
+ if end_time:
381
+ params["endTime"] = end_time
382
+
383
+ result = await self._request("GET", "/api/v3/fiat/merchant/ads/pagination", params=params)
384
+ return AdListResponse(**result)
385
+
386
+ async def get_market_ads(
387
+ self,
388
+ fiat_unit: str,
389
+ coin_id: str,
390
+ country_code: Optional[str] = None,
391
+ side: Optional[str] = None,
392
+ amount: Optional[Decimal] = None,
393
+ quantity: Optional[Decimal] = None,
394
+ pay_method: Optional[str] = None,
395
+ block_trade: Optional[bool] = None,
396
+ allow_trade: Optional[bool] = None,
397
+ have_trade: Optional[bool] = None,
398
+ follow: Optional[bool] = None,
399
+ page: int = 1,
400
+ ) -> MarketAdListResponse:
401
+ """Получение рыночных объявлений"""
402
+ params = {"fiatUnit": fiat_unit, "coinId": coin_id, "page": page}
403
+
404
+ if country_code:
405
+ params["countryCode"] = country_code
406
+ if side:
407
+ params["side"] = side
408
+ if amount:
409
+ params["amount"] = str(amount)
410
+ if quantity:
411
+ params["quantity"] = str(quantity)
412
+ if pay_method:
413
+ params["payMethod"] = pay_method
414
+ if block_trade is not None:
415
+ params["blockTrade"] = block_trade
416
+ if allow_trade is not None:
417
+ params["allowTrade"] = allow_trade
418
+ if have_trade is not None:
419
+ params["haveTrade"] = have_trade
420
+ if follow is not None:
421
+ params["follow"] = follow
422
+
423
+ result = await self._request("GET", "/api/v3/fiat/market/ads/pagination", params=params)
424
+ return MarketAdListResponse(**result)
425
+
426
+ # ============ Order Methods ============
427
+
428
+ async def create_order(self, request: CreateOrderRequest) -> CreateOrderResponse:
429
+ """Создание ордера (захват объявления)"""
430
+ result = await self._request("POST", "/api/v3/fiat/merchant/order/deal", data=request)
431
+ return CreateOrderResponse(**result)
432
+
433
+ async def get_my_orders(
434
+ self,
435
+ start_time: int,
436
+ end_time: int,
437
+ coin_id: Optional[str] = None,
438
+ adv_order_no: Optional[str] = None,
439
+ side: Optional[str] = None,
440
+ order_deal_state: Optional[str] = None,
441
+ page: int = 1,
442
+ limit: int = 10,
443
+ ) -> OrderListResponse:
444
+ """Получение моих ордеров (только как maker)"""
445
+ params = {"startTime": start_time, "endTime": end_time, "page": page, "limit": limit}
446
+
447
+ if coin_id:
448
+ params["coinId"] = coin_id
449
+ if adv_order_no:
450
+ params["advOrderNo"] = adv_order_no
451
+ if side:
452
+ params["side"] = side
453
+ if order_deal_state:
454
+ params["orderDealState"] = order_deal_state
455
+
456
+ result = await self._request("GET", "/api/v3/fiat/merchant/order/pagination", params=params)
457
+ return OrderListResponse(**result)
458
+
459
+ async def get_market_orders(
460
+ self,
461
+ coin_id: Optional[str] = None,
462
+ adv_order_no: Optional[str] = None,
463
+ side: Optional[str] = None,
464
+ order_deal_state: Optional[str] = None,
465
+ start_time: Optional[int] = None,
466
+ end_time: Optional[int] = None,
467
+ page: int = 1,
468
+ limit: int = 10,
469
+ ) -> OrderListResponse:
470
+ """Получение всех ордеров (как maker и taker)"""
471
+ params = {"page": page, "limit": limit}
472
+
473
+ if coin_id:
474
+ params["coinId"] = coin_id
475
+ if adv_order_no:
476
+ params["advOrderNo"] = adv_order_no
477
+ if side:
478
+ params["side"] = side
479
+ if order_deal_state:
480
+ params["orderDealState"] = order_deal_state
481
+ if start_time:
482
+ params["startTime"] = start_time
483
+ if end_time:
484
+ params["endTime"] = end_time
485
+
486
+ result = await self._request("GET", "/api/v3/fiat/market/order/pagination", params=params)
487
+ return OrderListResponse(**result)
488
+
489
+ async def confirm_paid(self, request: ConfirmPaidRequest) -> BaseResponse:
490
+ """Подтверждение оплаты"""
491
+ result = await self._request("POST", "/api/v3/fiat/confirm_paid", data=request)
492
+ return BaseResponse(**result)
493
+
494
+ async def release_coin(self, request: ReleaseCoinRequest) -> BaseResponse:
495
+ """Релиз криптовалюты"""
496
+ result = await self._request("POST", "/api/v3/fiat/release_coin", data=request)
497
+ return BaseResponse(**result)
498
+
499
+ async def get_order_detail(self, adv_order_no: str) -> OrderDetailResponse:
500
+ """Получение деталей ордера"""
501
+ params = {"advOrderNo": adv_order_no}
502
+
503
+ result = await self._request("GET", "/api/v3/fiat/order/detail", params=params)
504
+ return OrderDetailResponse(**result)
505
+
506
+ # ============ Service Methods ============
507
+
508
+ async def switch_service(self, request: ServiceSwitchRequest) -> BaseResponse:
509
+ """Открытие/закрытие торговли"""
510
+ result = await self._request("POST", "/api/v3/fiat/merchant/service/switch", data=request)
511
+ return BaseResponse(**result)
512
+
513
+ # ============ WebSocket Methods ============
514
+
515
+ async def generate_listen_key(self) -> ListenKeyResponse:
516
+ """Генерация listenKey для WebSocket"""
517
+ result = await self._request("POST", "/api/v3/userDataStream")
518
+ return ListenKeyResponse(**result)
519
+
520
+ async def get_listen_key(self) -> ListenKeyResponse:
521
+ """Получение существующего listenKey"""
522
+ result = await self._request("GET", "/api/v3/userDataStream")
523
+ return ListenKeyResponse(**result)
524
+
525
+ # ============ Chat Methods ============
526
+ async def get_chat_conversation(self, order_no: str) -> ConversationResponse:
527
+ """Получение ID чат-сессии для ордера"""
528
+ params = {"orderNo": order_no}
529
+
530
+ result = await self._request("GET", "/api/v3/fiat/retrieveChatConversation", params=params)
531
+ return ConversationResponse(**result)
532
+
533
+ async def get_chat_messages(
534
+ self,
535
+ conversation_id: int,
536
+ page: int = 1,
537
+ limit: int = 20,
538
+ chat_message_type: Optional[str] = None,
539
+ message_id: Optional[int] = None,
540
+ sort: Literal["DESC", "ASC"] = "DESC",
541
+ ) -> ChatMessagesResponse:
542
+ """Получение истории чата с пагинацией"""
543
+ params = {"conversationId": conversation_id, "page": page, "limit": limit, "sort": sort}
544
+
545
+ if chat_message_type:
546
+ params["chatMessageType"] = chat_message_type
547
+ if message_id:
548
+ params["id"] = message_id
549
+
550
+ result = await self._request("GET", "/api/v3/fiat/retrieveChatMessageWithPagination", params=params)
551
+ return ChatMessagesResponse(**result)
552
+
553
+ async def upload_file(self, file_data: bytes, filename: str) -> UploadFileResponse:
554
+ """Загрузка файла"""
555
+ if not self.session:
556
+ raise RuntimeError("Client not initialized.")
557
+
558
+ timestamp = self._get_timestamp()
559
+ query_string = f"timestamp={timestamp}"
560
+ signature = self._generate_signature(query_string)
561
+
562
+ url = f"{self.BASE_URL}/api/v3/fiat/uploadFile"
563
+ params = {"timestamp": timestamp, "signature": signature}
564
+
565
+ headers = {"X-MEXC-APIKEY": self.api_key}
566
+
567
+ form = aiohttp.FormData()
568
+ form.add_field("file", file_data, filename=filename)
569
+
570
+ async with self.session.post(url, params=params, data=form, headers=headers) as response:
571
+ result = await response.json()
572
+
573
+ return UploadFileResponse(**result)
574
+
575
+ async def download_file(self, file_id: str) -> dict:
576
+ """Скачивание файла"""
577
+ params = {"fileId": file_id}
578
+
579
+ result = await self._request("GET", "/api/v3/fiat/downloadFile", params=params)
580
+ return result
581
+
582
+
583
+ # ============ Usage Example ============
584
+ async def main():
585
+ """Пример использования клиента"""
586
+
587
+ api_key = "your_api_key"
588
+ api_secret = "your_api_secret"
589
+
590
+ async with MEXCP2PClient(api_key, api_secret) as client:
591
+ # Создание объявления
592
+ ad_request = CreateUpdateAdRequest(
593
+ payTimeLimit=15,
594
+ initQuantity=Decimal("100"),
595
+ price=Decimal("70000"),
596
+ coinId="5989b56ba96a43599dbeeca5bb053f43",
597
+ side=Side.BUY,
598
+ fiatUnit="USD",
599
+ payMethod="1",
600
+ minSingleTransAmount=Decimal("10"),
601
+ maxSingleTransAmount=Decimal("1000"),
602
+ userAllTradeCountMin=0,
603
+ userAllTradeCountMax=100,
604
+ )
605
+
606
+ ad_response = await client.create_or_update_ad(ad_request)
607
+ print(f"Created ad: {ad_response.data}")
608
+
609
+ # Получение рыночных объявлений
610
+ market_ads = await client.get_market_ads(
611
+ fiat_unit="USD", coin_id="5989b56ba96a43599dbeeca5bb053f43", side=Side.SELL, page=1
612
+ )
613
+
614
+ print(f"Found {len(market_ads.data)} ads")
615
+
616
+ # Создание ордера
617
+ if market_ads.data:
618
+ first_ad = market_ads.data[0]
619
+ order_request = CreateOrderRequest(advNo=first_ad.advNo, amount=Decimal("100"), userConfirmPaymentId=123)
620
+
621
+ order_response = await client.create_order(order_request)
622
+ print(f"Created order: {order_response.data}")
623
+
624
+ # Получение деталей ордера
625
+ order_detail = await client.get_order_detail("order_id_here")
626
+ print(f"Order state: {order_detail.data.state}")
627
+
628
+ # Генерация listenKey для WebSocket
629
+ listen_key = await client.generate_listen_key()
630
+ print(f"ListenKey: {listen_key.listenKey}")
631
+
632
+
633
+ if __name__ == "__main__":
634
+ import asyncio
635
+
636
+ asyncio.run(main())