django-cfg 1.2.27__py3-none-any.whl → 1.2.31__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/payments/admin/__init__.py +3 -2
- django_cfg/apps/payments/admin/balance_admin.py +18 -18
- django_cfg/apps/payments/admin/currencies_admin.py +319 -131
- django_cfg/apps/payments/admin/payments_admin.py +15 -4
- django_cfg/apps/payments/config/module.py +2 -2
- django_cfg/apps/payments/config/utils.py +2 -2
- django_cfg/apps/payments/decorators.py +2 -2
- django_cfg/apps/payments/management/commands/README.md +95 -127
- django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
- django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
- django_cfg/apps/payments/managers/__init__.py +3 -2
- django_cfg/apps/payments/managers/balance_manager.py +2 -2
- django_cfg/apps/payments/managers/currency_manager.py +272 -49
- django_cfg/apps/payments/managers/payment_manager.py +161 -13
- django_cfg/apps/payments/middleware/api_access.py +2 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
- django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
- django_cfg/apps/payments/models/__init__.py +3 -2
- django_cfg/apps/payments/models/currencies.py +187 -71
- django_cfg/apps/payments/models/payments.py +3 -2
- django_cfg/apps/payments/serializers/__init__.py +3 -2
- django_cfg/apps/payments/serializers/currencies.py +20 -12
- django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
- django_cfg/apps/payments/services/core/balance_service.py +2 -2
- django_cfg/apps/payments/services/core/fallback_service.py +2 -2
- django_cfg/apps/payments/services/core/payment_service.py +3 -6
- django_cfg/apps/payments/services/core/subscription_service.py +4 -7
- django_cfg/apps/payments/services/internal_types.py +171 -7
- django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
- django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
- django_cfg/apps/payments/services/providers/base.py +144 -43
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
- django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
- django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
- django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
- django_cfg/apps/payments/services/providers/registry.py +294 -11
- django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
- django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
- django_cfg/apps/payments/services/security/error_handler.py +6 -8
- django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
- django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/payment_signals.py +11 -5
- django_cfg/apps/payments/signals/subscription_signals.py +2 -2
- django_cfg/apps/payments/static/payments/css/payments.css +340 -0
- django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
- django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
- django_cfg/apps/payments/static/payments/js/theme.js +86 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
- django_cfg/apps/payments/templates/payments/base.html +182 -0
- django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +43 -0
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
- django_cfg/apps/payments/templates/payments/components/status_badge.html +34 -0
- django_cfg/apps/payments/templates/payments/components/status_overview.html +148 -0
- django_cfg/apps/payments/templates/payments/dashboard.html +258 -0
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
- django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
- django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
- django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
- django_cfg/apps/payments/templates/payments/stats.html +261 -0
- django_cfg/apps/payments/templates/payments/test.html +213 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -0
- django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
- django_cfg/apps/payments/urls.py +3 -1
- django_cfg/apps/payments/urls_admin.py +58 -0
- django_cfg/apps/payments/utils/__init__.py +1 -3
- django_cfg/apps/payments/utils/billing_utils.py +2 -2
- django_cfg/apps/payments/utils/config_utils.py +2 -8
- django_cfg/apps/payments/utils/validation_utils.py +2 -2
- django_cfg/apps/payments/views/__init__.py +3 -2
- django_cfg/apps/payments/views/currency_views.py +31 -20
- django_cfg/apps/payments/views/payment_views.py +2 -2
- django_cfg/apps/payments/views/templates/__init__.py +25 -0
- django_cfg/apps/payments/views/templates/ajax.py +451 -0
- django_cfg/apps/payments/views/templates/base.py +212 -0
- django_cfg/apps/payments/views/templates/dashboard.py +60 -0
- django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
- django_cfg/apps/payments/views/templates/payment_management.py +158 -0
- django_cfg/apps/payments/views/templates/qr_code.py +174 -0
- django_cfg/apps/payments/views/templates/stats.py +244 -0
- django_cfg/apps/payments/views/templates/utils.py +181 -0
- django_cfg/apps/payments/views/webhook_views.py +2 -2
- django_cfg/apps/payments/viewsets.py +3 -2
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +6 -3
- django_cfg/core/config.py +35 -0
- django_cfg/models/payments.py +2 -8
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_unfold/dashboard.py +7 -2
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/.gitignore +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/METADATA +13 -18
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/RECORD +130 -83
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -310
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,429 @@
|
|
1
|
+
"""
|
2
|
+
Cryptomus payment provider implementation.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django_cfg.modules.django_logger import get_logger
|
6
|
+
import hashlib
|
7
|
+
import json
|
8
|
+
import base64
|
9
|
+
from datetime import datetime
|
10
|
+
from decimal import Decimal
|
11
|
+
from typing import Dict, Any, Optional, List
|
12
|
+
from dataclasses import dataclass
|
13
|
+
|
14
|
+
import requests
|
15
|
+
from ..base import PaymentProvider
|
16
|
+
from ...internal_types import ProviderResponse, PaymentAmountEstimate, WebhookData
|
17
|
+
from .models import CryptomusConfig
|
18
|
+
|
19
|
+
logger = get_logger("cryptomus_old")
|
20
|
+
|
21
|
+
|
22
|
+
class CryptomusProvider(PaymentProvider):
|
23
|
+
"""Cryptomus payment provider with universal field mapping."""
|
24
|
+
|
25
|
+
def __init__(self, config: CryptomusConfig):
|
26
|
+
super().__init__(config)
|
27
|
+
self.config = config
|
28
|
+
self.merchant_id = config.merchant_id
|
29
|
+
self.api_key = config.api_key
|
30
|
+
self.test_mode = config.test_mode
|
31
|
+
self.base_url = self._get_base_url()
|
32
|
+
|
33
|
+
def _get_base_url(self) -> str:
|
34
|
+
"""Get base URL for API requests."""
|
35
|
+
if self.config.test_mode:
|
36
|
+
return "https://api.cryptomus.com/v1/test" # Test mode URL for identification
|
37
|
+
return "https://api.cryptomus.com/v1"
|
38
|
+
|
39
|
+
def _make_request(self, method: str, endpoint: str, data: Optional[dict] = None) -> Optional[dict]:
|
40
|
+
"""Make HTTP request to Cryptomus API with error handling."""
|
41
|
+
try:
|
42
|
+
url = f"{self.base_url}/{endpoint}" if endpoint else self.base_url
|
43
|
+
|
44
|
+
# Generate headers with signature for POST requests
|
45
|
+
headers = self._generate_headers(data or {})
|
46
|
+
|
47
|
+
response = requests.request(
|
48
|
+
method=method,
|
49
|
+
url=url,
|
50
|
+
headers=headers,
|
51
|
+
json=data,
|
52
|
+
timeout=30
|
53
|
+
)
|
54
|
+
|
55
|
+
response.raise_for_status()
|
56
|
+
return response.json()
|
57
|
+
|
58
|
+
except requests.exceptions.RequestException as e:
|
59
|
+
logger.error(f"Cryptomus API request failed: {e}")
|
60
|
+
return None
|
61
|
+
except Exception as e:
|
62
|
+
logger.error(f"Unexpected error in Cryptomus request: {e}")
|
63
|
+
return None
|
64
|
+
|
65
|
+
def check_payment_status(self, payment_id: str) -> ProviderResponse:
|
66
|
+
"""Check payment status via Cryptomus API."""
|
67
|
+
try:
|
68
|
+
endpoint = f"payment/{payment_id}"
|
69
|
+
response = self._make_request('POST', endpoint, {})
|
70
|
+
|
71
|
+
if response and 'state' in response and response['state'] == 0:
|
72
|
+
result = response.get('result', {})
|
73
|
+
return ProviderResponse(
|
74
|
+
success=True,
|
75
|
+
provider_payment_id=result.get('uuid', payment_id),
|
76
|
+
status=result.get('status', 'pending'),
|
77
|
+
pay_address=result.get('address'),
|
78
|
+
amount=Decimal(str(result.get('amount', 0))),
|
79
|
+
currency=result.get('currency', 'unknown'),
|
80
|
+
data=result
|
81
|
+
)
|
82
|
+
else:
|
83
|
+
return ProviderResponse(
|
84
|
+
success=False,
|
85
|
+
error_message=response.get('message', 'Failed to check payment status')
|
86
|
+
)
|
87
|
+
|
88
|
+
except Exception as e:
|
89
|
+
logger.error(f"Cryptomus check_payment_status error: {e}")
|
90
|
+
return ProviderResponse(
|
91
|
+
success=False,
|
92
|
+
error_message=str(e)
|
93
|
+
)
|
94
|
+
|
95
|
+
def create_payment(self, payment_data: dict) -> ProviderResponse:
|
96
|
+
"""
|
97
|
+
Create payment using Cryptomus API.
|
98
|
+
Maps to universal payment fields.
|
99
|
+
"""
|
100
|
+
try:
|
101
|
+
# Extract required data
|
102
|
+
order_id = payment_data.get('order_id')
|
103
|
+
amount = payment_data.get('amount')
|
104
|
+
currency = payment_data.get('currency', 'USD')
|
105
|
+
callback_url = payment_data.get('callback_url', self.config.callback_url)
|
106
|
+
|
107
|
+
if not all([order_id, amount]):
|
108
|
+
return ProviderResponse(
|
109
|
+
success=False,
|
110
|
+
error_message="Missing required fields: order_id, amount"
|
111
|
+
)
|
112
|
+
|
113
|
+
# Prepare Cryptomus API request
|
114
|
+
payload = {
|
115
|
+
"amount": str(amount),
|
116
|
+
"currency": currency,
|
117
|
+
"order_id": order_id,
|
118
|
+
"url_callback": callback_url,
|
119
|
+
"url_return": payment_data.get('return_url'),
|
120
|
+
"url_success": payment_data.get('success_url', self.config.success_url),
|
121
|
+
"url_cancel": payment_data.get('cancel_url', self.config.cancel_url),
|
122
|
+
"is_payment_multiple": False,
|
123
|
+
"lifetime": 3600, # 1 hour
|
124
|
+
"to_currency": payment_data.get('crypto_currency', 'BTC')
|
125
|
+
}
|
126
|
+
|
127
|
+
# Make API request using centralized method
|
128
|
+
result = self._make_request('POST', 'payment', payload)
|
129
|
+
|
130
|
+
if result and result.get('state') == 0: # Success
|
131
|
+
payment_info = result.get('result', {})
|
132
|
+
|
133
|
+
return ProviderResponse(
|
134
|
+
success=True,
|
135
|
+
provider_payment_id=payment_info.get('uuid'),
|
136
|
+
data={
|
137
|
+
# Universal field mapping
|
138
|
+
'provider_payment_id': payment_info.get('uuid'),
|
139
|
+
'receiver_address': payment_info.get('address'),
|
140
|
+
'crypto_amount': float(payment_info.get('amount', 0)),
|
141
|
+
'provider_callback_url': callback_url,
|
142
|
+
'payment_url': payment_info.get('url'),
|
143
|
+
'qr_code': payment_info.get('static_qr'),
|
144
|
+
|
145
|
+
# Cryptomus specific fields
|
146
|
+
'cryptomus_order_id': payment_info.get('order_id'),
|
147
|
+
'cryptomus_currency': payment_info.get('currency'),
|
148
|
+
'cryptomus_network': payment_info.get('network'),
|
149
|
+
'cryptomus_status': payment_info.get('status'),
|
150
|
+
'expires_at': payment_info.get('expired_at')
|
151
|
+
}
|
152
|
+
)
|
153
|
+
else:
|
154
|
+
error_msg = result.get('message', 'Unknown Cryptomus error') if result else 'No response from API'
|
155
|
+
return ProviderResponse(
|
156
|
+
success=False,
|
157
|
+
error_message=f"Cryptomus API error: {error_msg}"
|
158
|
+
)
|
159
|
+
|
160
|
+
except requests.RequestException as e:
|
161
|
+
logger.error(f"Cryptomus API request failed: {e}")
|
162
|
+
return ProviderResponse(
|
163
|
+
success=False,
|
164
|
+
error_message=f"Network error: {str(e)}"
|
165
|
+
)
|
166
|
+
except Exception as e:
|
167
|
+
logger.error(f"Cryptomus payment creation failed: {e}")
|
168
|
+
return ProviderResponse(
|
169
|
+
success=False,
|
170
|
+
error_message=f"Unexpected error: {str(e)}"
|
171
|
+
)
|
172
|
+
|
173
|
+
def validate_webhook(self, webhook_data: Dict[str, Any],
|
174
|
+
request_headers: Optional[Dict[str, str]] = None,
|
175
|
+
raw_body: Optional[bytes] = None) -> bool:
|
176
|
+
"""
|
177
|
+
Validate Cryptomus webhook with strict requirements.
|
178
|
+
"""
|
179
|
+
try:
|
180
|
+
# Strict required field validation
|
181
|
+
required_fields = ['uuid', 'order_id', 'amount', 'currency', 'status']
|
182
|
+
for field in required_fields:
|
183
|
+
if field not in webhook_data or not webhook_data[field]:
|
184
|
+
logger.warning(f"Missing or empty required field: {field}")
|
185
|
+
return False
|
186
|
+
|
187
|
+
# Validate signature (required for security)
|
188
|
+
sign = None
|
189
|
+
if request_headers:
|
190
|
+
sign = request_headers.get('sign')
|
191
|
+
if not sign:
|
192
|
+
sign = webhook_data.get('sign')
|
193
|
+
|
194
|
+
if not sign:
|
195
|
+
logger.error("No signature found in Cryptomus webhook")
|
196
|
+
return False # Require signature for security
|
197
|
+
|
198
|
+
# Verify signature
|
199
|
+
data_for_sign = {k: v for k, v in webhook_data.items() if k != 'sign'}
|
200
|
+
expected_sign = self._generate_webhook_signature(data_for_sign)
|
201
|
+
if sign != expected_sign:
|
202
|
+
logger.error("Cryptomus webhook signature validation failed")
|
203
|
+
return False
|
204
|
+
|
205
|
+
return True
|
206
|
+
|
207
|
+
except Exception as e:
|
208
|
+
logger.error(f"Cryptomus webhook validation failed: {e}")
|
209
|
+
return False
|
210
|
+
|
211
|
+
def process_webhook(self, webhook_data: Dict[str, Any]) -> WebhookData:
|
212
|
+
"""
|
213
|
+
Process Cryptomus webhook and map to universal fields.
|
214
|
+
"""
|
215
|
+
try:
|
216
|
+
# Map Cryptomus status to universal status
|
217
|
+
status = self._map_status(webhook_data.get('status', 'unknown'))
|
218
|
+
|
219
|
+
# Extract payment amount
|
220
|
+
amount = webhook_data.get('amount')
|
221
|
+
pay_amount = Decimal(str(amount)) if amount else Decimal('0')
|
222
|
+
|
223
|
+
# Ensure provider_payment_id is not None
|
224
|
+
provider_payment_id = webhook_data.get('uuid') or webhook_data.get('order_id') or 'unknown'
|
225
|
+
|
226
|
+
return WebhookData(
|
227
|
+
provider_payment_id=provider_payment_id,
|
228
|
+
status=status,
|
229
|
+
pay_amount=pay_amount,
|
230
|
+
pay_currency=webhook_data.get('currency', 'unknown'),
|
231
|
+
actually_paid=pay_amount, # For Cryptomus, same as pay_amount
|
232
|
+
order_id=webhook_data.get('order_id'),
|
233
|
+
signature=webhook_data.get('txid') or webhook_data.get('sign') # Use transaction ID or signature
|
234
|
+
)
|
235
|
+
|
236
|
+
except Exception as e:
|
237
|
+
logger.error(f"Cryptomus webhook processing failed: {e}")
|
238
|
+
raise
|
239
|
+
|
240
|
+
def estimate_payment_amount(self, amount: Decimal, currency_code: str) -> Optional['PaymentAmountEstimate']:
|
241
|
+
"""Estimate payment amount using Cryptomus exchange rates."""
|
242
|
+
try:
|
243
|
+
# Cryptomus exchange rate endpoint
|
244
|
+
response = self._make_request('POST', 'exchange-rate/list', {
|
245
|
+
'currency_from': 'USD',
|
246
|
+
'currency_to': currency_code.upper(),
|
247
|
+
'amount': float(amount)
|
248
|
+
})
|
249
|
+
|
250
|
+
if response and response.get('state') == 0:
|
251
|
+
result = response.get('result', {})
|
252
|
+
if result:
|
253
|
+
return PaymentAmountEstimate(
|
254
|
+
currency_from='usd',
|
255
|
+
currency_to=currency_code.lower(),
|
256
|
+
amount_from=amount,
|
257
|
+
estimated_amount=Decimal(str(result.get('amount', 0))),
|
258
|
+
exchange_rate=Decimal(str(result.get('course', 1))),
|
259
|
+
provider_name=self.name,
|
260
|
+
estimated_at=datetime.now()
|
261
|
+
)
|
262
|
+
|
263
|
+
return None
|
264
|
+
|
265
|
+
except Exception as e:
|
266
|
+
logger.error(f"Cryptomus estimate_payment_amount error: {e}")
|
267
|
+
return None
|
268
|
+
|
269
|
+
def get_payment_status(self, payment_id: str) -> ProviderResponse:
|
270
|
+
"""Get payment status from Cryptomus."""
|
271
|
+
try:
|
272
|
+
payload = {"uuid": payment_id}
|
273
|
+
headers = self._generate_headers(payload)
|
274
|
+
|
275
|
+
response = requests.post(
|
276
|
+
f"{self.base_url}/payment/info",
|
277
|
+
json=payload,
|
278
|
+
headers=headers,
|
279
|
+
timeout=30
|
280
|
+
)
|
281
|
+
|
282
|
+
if response.status_code == 200:
|
283
|
+
result = response.json()
|
284
|
+
if result.get('state') == 0:
|
285
|
+
payment_info = result.get('result', {})
|
286
|
+
|
287
|
+
return ProviderResponse(
|
288
|
+
success=True,
|
289
|
+
data={
|
290
|
+
'status': self._map_status(payment_info.get('status')),
|
291
|
+
'provider_payment_id': payment_info.get('uuid'),
|
292
|
+
'transaction_hash': payment_info.get('txid'),
|
293
|
+
'crypto_amount': float(payment_info.get('amount', 0)),
|
294
|
+
'confirmations_count': int(payment_info.get('confirmations', 0))
|
295
|
+
}
|
296
|
+
)
|
297
|
+
|
298
|
+
return ProviderResponse(
|
299
|
+
success=False,
|
300
|
+
error_message="Failed to get payment status"
|
301
|
+
)
|
302
|
+
|
303
|
+
except Exception as e:
|
304
|
+
logger.error(f"Cryptomus status check failed: {e}")
|
305
|
+
return ProviderResponse(
|
306
|
+
success=False,
|
307
|
+
error_message=f"Status check error: {str(e)}"
|
308
|
+
)
|
309
|
+
|
310
|
+
def _generate_headers(self, payload: dict) -> dict:
|
311
|
+
"""Generate authentication headers for Cryptomus API."""
|
312
|
+
# Convert data to JSON string with sorted keys for consistency
|
313
|
+
data_json = json.dumps(payload, sort_keys=True, separators=(',', ':'))
|
314
|
+
|
315
|
+
# Create signature: md5(base64(data) + api_key)
|
316
|
+
data_b64 = base64.b64encode(data_json.encode('utf-8')).decode('utf-8')
|
317
|
+
signature_string = data_b64 + self.config.api_key
|
318
|
+
signature = hashlib.md5(signature_string.encode('utf-8')).hexdigest()
|
319
|
+
|
320
|
+
return {
|
321
|
+
'Content-Type': 'application/json',
|
322
|
+
'merchant': self.config.merchant_id, # CRITICAL: merchant header
|
323
|
+
'sign': signature
|
324
|
+
}
|
325
|
+
|
326
|
+
def _generate_webhook_signature(self, webhook_data: dict) -> str:
|
327
|
+
"""Generate expected webhook signature for validation."""
|
328
|
+
# Cryptomus webhook signature generation
|
329
|
+
data_string = base64.b64encode(json.dumps(webhook_data, sort_keys=True).encode()).decode()
|
330
|
+
return hashlib.md5(f"{data_string}{self.api_key}".encode()).hexdigest()
|
331
|
+
|
332
|
+
def _map_status(self, cryptomus_status: str) -> str:
|
333
|
+
"""Map Cryptomus status to universal status."""
|
334
|
+
status_mapping = {
|
335
|
+
'check': 'pending',
|
336
|
+
'process': 'pending',
|
337
|
+
'confirm_check': 'pending',
|
338
|
+
'paid': 'completed', # Add missing 'paid' status
|
339
|
+
'confirmed': 'completed',
|
340
|
+
'fail': 'failed',
|
341
|
+
'cancel': 'cancelled',
|
342
|
+
'system_fail': 'failed',
|
343
|
+
'refund_process': 'refunding',
|
344
|
+
'refund_fail': 'failed',
|
345
|
+
'refund_paid': 'refunded'
|
346
|
+
}
|
347
|
+
return status_mapping.get(cryptomus_status, 'pending')
|
348
|
+
|
349
|
+
def get_supported_currencies(self) -> ProviderResponse:
|
350
|
+
"""Get supported currencies from Cryptomus."""
|
351
|
+
try:
|
352
|
+
result = self._make_request('POST', 'exchange-rate/list', {})
|
353
|
+
|
354
|
+
if result and result.get('state') == 0:
|
355
|
+
currencies = result.get('result', [])
|
356
|
+
return ProviderResponse(
|
357
|
+
success=True,
|
358
|
+
data={'currencies': currencies}
|
359
|
+
)
|
360
|
+
|
361
|
+
return ProviderResponse(
|
362
|
+
success=False,
|
363
|
+
error_message="Failed to get supported currencies"
|
364
|
+
)
|
365
|
+
|
366
|
+
except Exception as e:
|
367
|
+
logger.error(f"Cryptomus currencies request failed: {e}")
|
368
|
+
return ProviderResponse(
|
369
|
+
success=False,
|
370
|
+
error_message=f"Currencies request error: {str(e)}"
|
371
|
+
)
|
372
|
+
|
373
|
+
def get_supported_networks(self, currency_code: str = None) -> ProviderResponse:
|
374
|
+
"""Get supported networks from Cryptomus."""
|
375
|
+
try:
|
376
|
+
headers = self._generate_headers({})
|
377
|
+
|
378
|
+
# Cryptomus might have a specific API endpoint for networks
|
379
|
+
# For now, we'll extract from currencies data
|
380
|
+
currencies_response = self.get_supported_currencies()
|
381
|
+
if not currencies_response.success:
|
382
|
+
return currencies_response
|
383
|
+
|
384
|
+
networks = {}
|
385
|
+
currencies = currencies_response.data.get('currencies', [])
|
386
|
+
|
387
|
+
for currency in currencies:
|
388
|
+
currency_symbol = currency.get('currency_code', '').upper()
|
389
|
+
if currency_code and currency_symbol != currency_code.upper():
|
390
|
+
continue
|
391
|
+
|
392
|
+
# Extract network info from currency data
|
393
|
+
network_info = {
|
394
|
+
'code': currency.get('network', 'mainnet'),
|
395
|
+
'name': currency.get('network_name', 'Mainnet'),
|
396
|
+
'min_amount': currency.get('min_amount', 0),
|
397
|
+
'max_amount': currency.get('max_amount', 0),
|
398
|
+
'commission_percent': currency.get('commission_percent', 0)
|
399
|
+
}
|
400
|
+
|
401
|
+
if currency_symbol not in networks:
|
402
|
+
networks[currency_symbol] = []
|
403
|
+
networks[currency_symbol].append(network_info)
|
404
|
+
|
405
|
+
return ProviderResponse(
|
406
|
+
success=True,
|
407
|
+
data={'networks': networks}
|
408
|
+
)
|
409
|
+
|
410
|
+
except Exception as e:
|
411
|
+
logger.error(f"Cryptomus networks request failed: {e}")
|
412
|
+
return ProviderResponse(
|
413
|
+
success=False,
|
414
|
+
error_message=f"Networks request error: {str(e)}"
|
415
|
+
)
|
416
|
+
|
417
|
+
def get_currency_network_mapping(self) -> Dict[str, List[str]]:
|
418
|
+
"""Get mapping of currencies to their supported networks."""
|
419
|
+
networks_response = self.get_supported_networks()
|
420
|
+
if not networks_response.success:
|
421
|
+
return {}
|
422
|
+
|
423
|
+
mapping = {}
|
424
|
+
networks_data = networks_response.data.get('networks', {})
|
425
|
+
|
426
|
+
for currency_code, networks in networks_data.items():
|
427
|
+
mapping[currency_code] = [network['code'] for network in networks]
|
428
|
+
|
429
|
+
return mapping
|