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.
Files changed (138) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +3 -2
  3. django_cfg/apps/payments/admin/balance_admin.py +18 -18
  4. django_cfg/apps/payments/admin/currencies_admin.py +319 -131
  5. django_cfg/apps/payments/admin/payments_admin.py +15 -4
  6. django_cfg/apps/payments/config/module.py +2 -2
  7. django_cfg/apps/payments/config/utils.py +2 -2
  8. django_cfg/apps/payments/decorators.py +2 -2
  9. django_cfg/apps/payments/management/commands/README.md +95 -127
  10. django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
  11. django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
  12. django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
  13. django_cfg/apps/payments/managers/__init__.py +3 -2
  14. django_cfg/apps/payments/managers/balance_manager.py +2 -2
  15. django_cfg/apps/payments/managers/currency_manager.py +272 -49
  16. django_cfg/apps/payments/managers/payment_manager.py +161 -13
  17. django_cfg/apps/payments/middleware/api_access.py +2 -2
  18. django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
  19. django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
  20. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
  21. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
  22. django_cfg/apps/payments/models/__init__.py +3 -2
  23. django_cfg/apps/payments/models/currencies.py +187 -71
  24. django_cfg/apps/payments/models/payments.py +3 -2
  25. django_cfg/apps/payments/serializers/__init__.py +3 -2
  26. django_cfg/apps/payments/serializers/currencies.py +20 -12
  27. django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
  28. django_cfg/apps/payments/services/core/balance_service.py +2 -2
  29. django_cfg/apps/payments/services/core/fallback_service.py +2 -2
  30. django_cfg/apps/payments/services/core/payment_service.py +3 -6
  31. django_cfg/apps/payments/services/core/subscription_service.py +4 -7
  32. django_cfg/apps/payments/services/internal_types.py +171 -7
  33. django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
  34. django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
  35. django_cfg/apps/payments/services/providers/base.py +144 -43
  36. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
  37. django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
  38. django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
  39. django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
  40. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
  41. django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
  42. django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
  43. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
  44. django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
  45. django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
  46. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
  47. django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
  48. django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
  49. django_cfg/apps/payments/services/providers/registry.py +294 -11
  50. django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
  51. django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
  52. django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
  53. django_cfg/apps/payments/services/security/error_handler.py +6 -8
  54. django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
  55. django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
  56. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  57. django_cfg/apps/payments/signals/payment_signals.py +11 -5
  58. django_cfg/apps/payments/signals/subscription_signals.py +2 -2
  59. django_cfg/apps/payments/static/payments/css/payments.css +340 -0
  60. django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
  61. django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
  62. django_cfg/apps/payments/static/payments/js/theme.js +86 -0
  63. django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
  64. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  65. django_cfg/apps/payments/templates/payments/base.html +182 -0
  66. django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
  67. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
  68. django_cfg/apps/payments/templates/payments/components/progress_bar.html +43 -0
  69. django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
  70. django_cfg/apps/payments/templates/payments/components/status_badge.html +34 -0
  71. django_cfg/apps/payments/templates/payments/components/status_overview.html +148 -0
  72. django_cfg/apps/payments/templates/payments/dashboard.html +258 -0
  73. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  74. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  75. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  76. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  77. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  78. django_cfg/apps/payments/templates/payments/test.html +213 -0
  79. django_cfg/apps/payments/templatetags/__init__.py +1 -0
  80. django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
  81. django_cfg/apps/payments/urls.py +3 -1
  82. django_cfg/apps/payments/urls_admin.py +58 -0
  83. django_cfg/apps/payments/utils/__init__.py +1 -3
  84. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  85. django_cfg/apps/payments/utils/config_utils.py +2 -8
  86. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  87. django_cfg/apps/payments/views/__init__.py +3 -2
  88. django_cfg/apps/payments/views/currency_views.py +31 -20
  89. django_cfg/apps/payments/views/payment_views.py +2 -2
  90. django_cfg/apps/payments/views/templates/__init__.py +25 -0
  91. django_cfg/apps/payments/views/templates/ajax.py +451 -0
  92. django_cfg/apps/payments/views/templates/base.py +212 -0
  93. django_cfg/apps/payments/views/templates/dashboard.py +60 -0
  94. django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
  95. django_cfg/apps/payments/views/templates/payment_management.py +158 -0
  96. django_cfg/apps/payments/views/templates/qr_code.py +174 -0
  97. django_cfg/apps/payments/views/templates/stats.py +244 -0
  98. django_cfg/apps/payments/views/templates/utils.py +181 -0
  99. django_cfg/apps/payments/views/webhook_views.py +2 -2
  100. django_cfg/apps/payments/viewsets.py +3 -2
  101. django_cfg/apps/tasks/urls.py +0 -2
  102. django_cfg/apps/tasks/urls_admin.py +14 -0
  103. django_cfg/apps/urls.py +6 -3
  104. django_cfg/core/config.py +35 -0
  105. django_cfg/models/payments.py +2 -8
  106. django_cfg/modules/django_currency/__init__.py +16 -11
  107. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  108. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  109. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  110. django_cfg/modules/django_currency/core/__init__.py +1 -7
  111. django_cfg/modules/django_currency/core/converter.py +18 -23
  112. django_cfg/modules/django_currency/core/models.py +122 -11
  113. django_cfg/modules/django_currency/database/__init__.py +4 -4
  114. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  115. django_cfg/modules/django_unfold/dashboard.py +7 -2
  116. django_cfg/registry/core.py +1 -0
  117. django_cfg/template_archive/.gitignore +1 -0
  118. django_cfg/template_archive/django_sample.zip +0 -0
  119. django_cfg/templates/admin/components/action_grid.html +9 -9
  120. django_cfg/templates/admin/components/metric_card.html +5 -5
  121. django_cfg/templates/admin/components/status_badge.html +2 -2
  122. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  123. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  124. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  125. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  126. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/METADATA +13 -18
  127. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/RECORD +130 -83
  128. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  129. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  130. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  131. django_cfg/apps/payments/services/providers/cryptomus.py +0 -310
  132. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  133. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  134. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  135. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  136. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  137. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  138. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,439 @@
1
+ """
2
+ CryptAPI provider implementation.
3
+
4
+ Crypto payment provider using CryptAPI service.
5
+ """
6
+
7
+ import requests
8
+ import secrets
9
+ import string
10
+ import base64
11
+ from typing import Optional, List, Dict, Any
12
+ from decimal import Decimal
13
+ from cryptography.hazmat.primitives import hashes, serialization
14
+ from cryptography.hazmat.primitives.asymmetric import padding
15
+ from cryptography.hazmat.backends import default_backend
16
+ from ..base import PaymentProvider
17
+ from ...internal_types import ProviderResponse, WebhookData, PaymentAmountEstimate
18
+ from .models import CryptAPIConfig, CryptAPICallback
19
+ from django_cfg.modules.django_logger import get_logger
20
+
21
+ logger = get_logger("cryptapi")
22
+
23
+
24
+ class CryptAPIException(Exception):
25
+ """CryptAPI specific exception."""
26
+ pass
27
+
28
+
29
+ class CryptAPIProvider(PaymentProvider):
30
+ """CryptAPI cryptocurrency payment provider."""
31
+
32
+ CRYPTAPI_URL = 'https://api.cryptapi.io/'
33
+
34
+ def __init__(self, config: CryptAPIConfig):
35
+ """Initialize CryptAPI provider."""
36
+ super().__init__(config)
37
+ self.config = config
38
+ self.own_address = config.own_address
39
+ self.callback_url = config.callback_url
40
+ self.convert_payments = config.convert_payments
41
+ self.multi_token = config.multi_token
42
+ self.priority = config.priority
43
+
44
+ def _make_request(self, coin: str, endpoint: str, params: Optional[dict] = None) -> Optional[dict]:
45
+ """Make HTTP request to CryptAPI."""
46
+ try:
47
+ if coin:
48
+ coin = coin.replace('/', '_')
49
+ url = f"{self.CRYPTAPI_URL}{coin}/{endpoint}/"
50
+ else:
51
+ url = f"{self.CRYPTAPI_URL}{endpoint}/"
52
+
53
+ response = requests.get(url, params=params or {}, timeout=30)
54
+ response.raise_for_status()
55
+
56
+ result = response.json()
57
+
58
+ # Check for API errors
59
+ if 'error' in result:
60
+ logger.error(f"CryptAPI error: {result['error']}")
61
+ return None
62
+
63
+ return result
64
+
65
+ except requests.exceptions.RequestException as e:
66
+ logger.error(f"CryptAPI request failed: {e}")
67
+ return None
68
+ except Exception as e:
69
+ logger.error(f"Unexpected CryptAPI error: {e}")
70
+ return None
71
+
72
+ def create_payment(self, payment_data: dict) -> ProviderResponse:
73
+ """Create payment address via CryptAPI with full parameter support according to documentation."""
74
+ try:
75
+ # Required parameters
76
+ amount = Decimal(str(payment_data['amount']))
77
+ currency = payment_data['currency'].lower()
78
+ order_id = payment_data.get('order_id', f'payment_{int(amount * 100)}')
79
+
80
+ # Build callback URL with custom parameters
81
+ callback_url = payment_data.get('callback_url', self.config.callback_url)
82
+
83
+ # Add custom parameters to callback URL for tracking
84
+ callback_params = []
85
+ if order_id:
86
+ callback_params.append(f'order_id={order_id}')
87
+ if 'user_id' in payment_data:
88
+ callback_params.append(f'user_id={payment_data["user_id"]}')
89
+
90
+ # Generate nonce for security
91
+ security_nonce = self._generate_nonce()
92
+ callback_params.append(f'nonce={security_nonce}')
93
+
94
+ # Build full callback URL
95
+ if callback_params:
96
+ separator = '&' if '?' in callback_url else '?'
97
+ callback_url = f"{callback_url}{separator}{'&'.join(callback_params)}"
98
+
99
+ # Prepare API parameters according to documentation
100
+ params = {
101
+ 'address': payment_data.get('address', self.config.own_address),
102
+ 'callback': callback_url,
103
+ }
104
+
105
+ # Optional parameters from documentation
106
+ if payment_data.get('pending', False) or self.config.convert_payments:
107
+ params['pending'] = 1
108
+
109
+ if 'confirmations' in payment_data:
110
+ params['confirmations'] = int(payment_data['confirmations'])
111
+
112
+ if payment_data.get('post', False):
113
+ params['post'] = 1
114
+
115
+ if payment_data.get('json', True):
116
+ params['json'] = 1
117
+
118
+ if self.config.priority and self.config.priority != 'default':
119
+ params['priority'] = self.config.priority
120
+
121
+ if self.config.multi_token:
122
+ params['multi_token'] = 1
123
+
124
+ if self.config.convert_payments:
125
+ params['convert'] = 1
126
+
127
+ # Handle multi-address splitting if provided
128
+ if 'addresses' in payment_data and isinstance(payment_data['addresses'], list):
129
+ # Format: percentage@address|percentage@address
130
+ address_parts = []
131
+ for addr_info in payment_data['addresses']:
132
+ if isinstance(addr_info, dict) and 'address' in addr_info and 'percentage' in addr_info:
133
+ address_parts.append(f"{addr_info['percentage']}@{addr_info['address']}")
134
+ if address_parts:
135
+ params['address'] = '|'.join(address_parts)
136
+
137
+ # Make API request using ticker/create endpoint
138
+ response = self._make_request(currency, 'create', params)
139
+
140
+ if response and 'address_in' in response:
141
+ return ProviderResponse(
142
+ success=True,
143
+ provider_payment_id=response['address_in'],
144
+ payment_url=None, # CryptAPI doesn't provide hosted payment pages
145
+ pay_address=response['address_in'],
146
+ amount=amount,
147
+ currency=currency.upper(),
148
+ status='pending',
149
+ data={
150
+ 'callback_url': response.get('callback_url'),
151
+ 'minimum_transaction_coin': response.get('minimum_transaction_coin'),
152
+ 'priority': response.get('priority'),
153
+ 'nonce': security_nonce
154
+ }
155
+ )
156
+ else:
157
+ # Standardized error message format
158
+ if response and 'error' in response:
159
+ error_msg = f"CryptAPI error: {response['error']}"
160
+ elif response:
161
+ error_msg = "CryptAPI error: Invalid response format"
162
+ else:
163
+ error_msg = "CryptAPI error: No response from API"
164
+
165
+ return ProviderResponse(
166
+ success=False,
167
+ error_message=error_msg
168
+ )
169
+
170
+ except Exception as e:
171
+ logger.error(f"CryptAPI create_payment error: {e}")
172
+ return ProviderResponse(
173
+ success=False,
174
+ error_message=str(e)
175
+ )
176
+
177
+ def check_payment_status(self, payment_id: str) -> ProviderResponse:
178
+ """Check payment status via CryptAPI."""
179
+ try:
180
+ # For CryptAPI, payment_id is the address
181
+ # We need to check logs to see if payment was received
182
+ # This is a limitation of CryptAPI - no direct status check by address
183
+
184
+ # Return pending status as CryptAPI uses callbacks for status updates
185
+ return ProviderResponse(
186
+ success=True,
187
+ provider_payment_id=payment_id,
188
+ status='pending',
189
+ pay_address=payment_id,
190
+ amount=Decimal('0'), # Unknown without logs
191
+ currency='unknown'
192
+ )
193
+
194
+ except Exception as e:
195
+ logger.error(f"CryptAPI check_payment_status error: {e}")
196
+ return ProviderResponse(
197
+ success=False,
198
+ error_message=str(e)
199
+ )
200
+
201
+ def process_webhook(self, payload: dict) -> WebhookData:
202
+ """Process CryptAPI webhook according to official documentation."""
203
+ try:
204
+ # Parse and validate webhook using Pydantic model
205
+ webhook = CryptAPICallback(**payload)
206
+
207
+ # Determine status based on pending flag
208
+ pending = webhook.pending if webhook.pending is not None else 0
209
+ if pending == 1:
210
+ status = 'pending'
211
+ elif pending == 0:
212
+ # Confirmed webhook - check confirmations if available
213
+ if webhook.confirmations is not None and webhook.confirmations >= 1:
214
+ status = 'completed'
215
+ else:
216
+ status = 'processing'
217
+ else:
218
+ status = 'unknown'
219
+
220
+ # Use value_coin for confirmed webhooks, or estimate for pending
221
+ pay_amount = webhook.value_coin if webhook.value_coin is not None else Decimal('0')
222
+
223
+ # Use UUID if available, otherwise fall back to address_in
224
+ payment_id = webhook.uuid if webhook.uuid else webhook.address_in
225
+
226
+ return WebhookData(
227
+ provider_payment_id=payment_id,
228
+ status=status,
229
+ pay_amount=pay_amount,
230
+ pay_currency=webhook.coin.lower(),
231
+ actually_paid=pay_amount,
232
+ order_id=payload.get('order_id'), # Custom parameter from callback URL
233
+ signature=webhook.txid_in # Transaction hash as signature
234
+ )
235
+
236
+ except Exception as e:
237
+ logger.error(f"CryptAPI webhook processing error: {e}")
238
+ raise
239
+
240
+ def get_logs(self, callback_url: str) -> Optional[dict]:
241
+ """Get payment logs for a specific callback URL."""
242
+ try:
243
+ response = self._make_request('GET', 'logs', {
244
+ 'callback': callback_url
245
+ })
246
+
247
+ if response:
248
+ return {
249
+ 'success': True,
250
+ 'logs': response,
251
+ 'callback_url': callback_url
252
+ }
253
+
254
+ return None
255
+
256
+ except Exception as e:
257
+ logger.error(f"CryptAPI get_logs error: {e}")
258
+ return None
259
+
260
+ def generate_qr_code(self, address: str, amount: Optional[Decimal] = None,
261
+ size: int = 512) -> Optional[dict]:
262
+ """Generate QR code for payment address."""
263
+ try:
264
+ params = {
265
+ 'address': address,
266
+ 'size': size
267
+ }
268
+
269
+ if amount:
270
+ params['value'] = float(amount)
271
+
272
+ response = self._make_request('GET', 'qrcode', params)
273
+
274
+ if response and 'qr_code' in response:
275
+ return {
276
+ 'success': True,
277
+ 'qr_code_url': response['qr_code'],
278
+ 'address': address,
279
+ 'amount': amount,
280
+ 'size': size
281
+ }
282
+
283
+ return None
284
+
285
+ except Exception as e:
286
+ logger.error(f"CryptAPI generate_qr_code error: {e}")
287
+ return None
288
+
289
+ def get_supported_currencies(self) -> ProviderResponse:
290
+ """Get list of supported currencies."""
291
+ try:
292
+ response = self._make_request('', 'info')
293
+
294
+ if response and isinstance(response, dict):
295
+ # CryptAPI returns a dict with coin info
296
+ currencies = list(response.keys())
297
+ return ProviderResponse(
298
+ success=True,
299
+ data={'currencies': [{'currency_code': c, 'name': c.upper()} for c in currencies]}
300
+ )
301
+ else:
302
+ return ProviderResponse(
303
+ success=False,
304
+ error_message="Invalid response from CryptAPI info endpoint"
305
+ )
306
+
307
+ except Exception as e:
308
+ logger.error(f"Error getting supported currencies: {e}")
309
+ return ProviderResponse(
310
+ success=False,
311
+ error_message=f"Failed to get currencies from CryptAPI: {str(e)}"
312
+ )
313
+
314
+ def get_minimum_payment_amount(self, currency_from: str, currency_to: str = 'usd') -> Optional[Decimal]:
315
+ """Get minimum payment amount for currency."""
316
+ try:
317
+ response = self._make_request(currency_from.lower(), 'info')
318
+
319
+ if response and 'minimum_transaction' in response:
320
+ return Decimal(str(response['minimum_transaction']))
321
+
322
+ return None
323
+
324
+ except Exception as e:
325
+ logger.error(f"Error getting minimum amount: {e}")
326
+ return None
327
+
328
+ def estimate_payment_amount(self, amount: Decimal, currency_code: str) -> Optional[dict]:
329
+ """Estimate payment amount - CryptAPI doesn't provide this."""
330
+ # CryptAPI doesn't have a direct estimation API
331
+ # Would need to use external price APIs
332
+ return None
333
+
334
+ def validate_webhook(self, payload: dict, headers: Optional[dict] = None, raw_body: Optional[bytes] = None) -> bool:
335
+ """Validate CryptAPI webhook with RSA SHA256 signature verification."""
336
+ try:
337
+ # Skip signature verification if disabled
338
+ if not self.config.verify_signatures:
339
+ logger.warning("CryptAPI signature verification is disabled - using basic validation only")
340
+ return self._basic_webhook_validation(payload)
341
+
342
+ # Check for signature header
343
+ if not headers or 'x-ca-signature' not in headers:
344
+ logger.error("Missing x-ca-signature header in CryptAPI webhook")
345
+ return False
346
+
347
+ signature_b64 = headers['x-ca-signature']
348
+ if not signature_b64:
349
+ logger.error("Empty x-ca-signature header")
350
+ return False
351
+
352
+ # Verify the signature
353
+ if not self._verify_cryptapi_signature(payload, signature_b64, raw_body):
354
+ logger.error("CryptAPI webhook signature verification failed")
355
+ return False
356
+
357
+ # Signature verified - do basic validation
358
+ return self._basic_webhook_validation(payload)
359
+
360
+ except Exception as e:
361
+ logger.error(f"CryptAPI webhook validation error: {e}")
362
+ return False
363
+
364
+ def _basic_webhook_validation(self, payload: dict) -> bool:
365
+ """Basic webhook payload validation."""
366
+ # Core required fields (UUID is optional for compatibility)
367
+ required_fields = ['address_in', 'address_out', 'txid_in', 'coin']
368
+
369
+ for field in required_fields:
370
+ if field not in payload:
371
+ logger.warning(f"Missing required field in CryptAPI webhook: {field}")
372
+ return False
373
+
374
+ # Validate coin format if present
375
+ if 'coin' in payload and not payload['coin']:
376
+ logger.warning("Empty coin field in CryptAPI webhook")
377
+ return False
378
+
379
+ return True
380
+
381
+ def _verify_cryptapi_signature(self, payload: dict, signature_b64: str, raw_body: Optional[bytes] = None) -> bool:
382
+ """Verify CryptAPI RSA SHA256 signature according to documentation."""
383
+ try:
384
+ # Load public key
385
+ public_key = serialization.load_pem_public_key(
386
+ self.config.public_key.encode('utf-8'),
387
+ backend=default_backend()
388
+ )
389
+
390
+ # Decode signature
391
+ signature = base64.b64decode(signature_b64)
392
+
393
+ # Determine what data to verify
394
+ if raw_body:
395
+ # For POST requests - verify raw body
396
+ data_to_verify = raw_body
397
+ else:
398
+ # For GET requests - construct URL from payload
399
+ # This is a fallback if raw_body is not provided
400
+ params = "&".join([f"{k}={v}" for k, v in payload.items()])
401
+ data_to_verify = params.encode('utf-8')
402
+
403
+ # Verify signature using RSA SHA256
404
+ public_key.verify(
405
+ signature,
406
+ data_to_verify,
407
+ padding.PKCS1v15(),
408
+ hashes.SHA256()
409
+ )
410
+
411
+ return True
412
+
413
+ except Exception as e:
414
+ logger.error(f"CryptAPI signature verification error: {e}")
415
+ return False
416
+
417
+ def check_api_status(self) -> bool:
418
+ """Check if CryptAPI is available."""
419
+ try:
420
+ response = self._make_request('', 'info')
421
+ return response is not None
422
+ except:
423
+ return False
424
+
425
+ def get_logs(self, callback_url: str) -> Optional[dict]:
426
+ """Get payment logs for a callback URL."""
427
+ try:
428
+ params = {'callback': callback_url}
429
+ # Note: This would need a specific coin, but we don't know which one
430
+ # This is a limitation of the current implementation
431
+ return None
432
+ except Exception as e:
433
+ logger.error(f"Error getting logs: {e}")
434
+ return None
435
+
436
+ def _generate_nonce(self, length: int = 32) -> str:
437
+ """Generate cryptographically secure nonce for replay attack protection."""
438
+ sequence = string.ascii_letters + string.digits
439
+ return ''.join([secrets.choice(sequence) for _ in range(length)])
@@ -0,0 +1,4 @@
1
+ from .provider import CryptomusProvider
2
+ from .models import CryptomusConfig, CryptomusCurrency, CryptomusNetwork
3
+
4
+ __all__ = ['CryptomusProvider', 'CryptomusConfig', 'CryptomusCurrency', 'CryptomusNetwork']
@@ -0,0 +1,176 @@
1
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
2
+ from typing import Optional, List, Dict, Any
3
+ from decimal import Decimal
4
+
5
+ from ...internal_types import ProviderConfig
6
+
7
+
8
+ class CryptomusConfig(ProviderConfig):
9
+ """Cryptomus provider configuration with Pydantic v2."""
10
+
11
+ merchant_id: str = Field(..., description="Cryptomus merchant ID")
12
+ test_mode: bool = Field(default=False, description="Enable test mode")
13
+ callback_url: Optional[str] = Field(None, description="Default callback URL")
14
+ success_url: Optional[str] = Field(None, description="Success redirect URL")
15
+ cancel_url: Optional[str] = Field(None, description="Cancel redirect URL")
16
+
17
+ @field_validator('merchant_id')
18
+ @classmethod
19
+ def validate_merchant_id(cls, v: str) -> str:
20
+ if not v or not v.strip():
21
+ raise ValueError("Merchant ID is required")
22
+ return v.strip()
23
+
24
+
25
+ class CryptomusCurrency(BaseModel):
26
+ """Cryptomus specific currency model."""
27
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
28
+
29
+ currency_code: str = Field(..., description="Currency symbol (e.g., BTC, ETH)")
30
+ name: str = Field(..., description="Full currency name")
31
+ network: Optional[str] = Field(None, description="Network code")
32
+ network_name: Optional[str] = Field(None, description="Network display name")
33
+ min_amount: Optional[Decimal] = Field(None, description="Minimum transaction amount")
34
+ max_amount: Optional[Decimal] = Field(None, description="Maximum transaction amount")
35
+ commission_percent: Optional[Decimal] = Field(None, description="Commission percentage")
36
+ is_available: bool = Field(True, description="Currency availability")
37
+
38
+
39
+ class CryptomusNetwork(BaseModel):
40
+ """Cryptomus specific network model."""
41
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
42
+
43
+ code: str = Field(..., description="Network code")
44
+ name: str = Field(..., description="Network display name")
45
+ currency: str = Field(..., description="Currency this network belongs to")
46
+ min_amount: Optional[Decimal] = Field(None, description="Minimum amount for this network")
47
+ max_amount: Optional[Decimal] = Field(None, description="Maximum amount for this network")
48
+ commission_percent: Optional[Decimal] = Field(None, description="Commission percentage")
49
+ confirmations: int = Field(1, description="Required confirmations")
50
+
51
+
52
+ class CryptomusPaymentRequest(BaseModel):
53
+ """Cryptomus payment creation request."""
54
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
55
+
56
+ amount: str = Field(..., description="Payment amount")
57
+ currency: str = Field(..., description="Payment currency")
58
+ order_id: str = Field(..., description="Unique order identifier")
59
+ url_callback: Optional[str] = Field(None, description="Callback URL")
60
+ url_return: Optional[str] = Field(None, description="Return URL")
61
+ url_success: Optional[str] = Field(None, description="Success URL")
62
+ is_payment_multiple: bool = Field(False, description="Allow multiple payments")
63
+ lifetime: int = Field(3600, description="Payment lifetime in seconds")
64
+ to_currency: Optional[str] = Field(None, description="Target cryptocurrency")
65
+
66
+
67
+ class CryptomusPaymentResponse(BaseModel):
68
+ """Cryptomus payment creation response."""
69
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
70
+
71
+ state: int = Field(..., description="Response state (0 = success)")
72
+ message: Optional[str] = Field(None, description="Response message")
73
+ result: Optional[Dict[str, Any]] = Field(None, description="Payment result data")
74
+
75
+
76
+ class CryptomusPaymentInfo(BaseModel):
77
+ """Cryptomus payment information."""
78
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
79
+
80
+ uuid: str = Field(..., description="Payment UUID")
81
+ order_id: str = Field(..., description="Order ID")
82
+ amount: str = Field(..., description="Payment amount")
83
+ currency: str = Field(..., description="Payment currency")
84
+ address: Optional[str] = Field(None, description="Payment address")
85
+ url: Optional[str] = Field(None, description="Payment URL")
86
+ static_qr: Optional[str] = Field(None, description="QR code data")
87
+ network: Optional[str] = Field(None, description="Network")
88
+ status: str = Field(..., description="Payment status")
89
+ expired_at: Optional[str] = Field(None, description="Expiration timestamp")
90
+
91
+
92
+ class CryptomusWebhook(BaseModel):
93
+ """Cryptomus webhook data according to official documentation."""
94
+ model_config = ConfigDict(validate_assignment=True, extra="allow") # Allow extra fields
95
+
96
+ # Required fields from documentation
97
+ type: str = Field(..., description="Webhook type (payment/payout)")
98
+ uuid: str = Field(..., description="Payment/payout UUID")
99
+ order_id: str = Field(..., description="Order ID from your system")
100
+ amount: str = Field(..., description="Amount")
101
+ payment_amount: Optional[str] = Field(None, description="Payment amount")
102
+ payment_amount_usd: Optional[str] = Field(None, description="Payment amount in USD")
103
+ merchant_amount: Optional[str] = Field(None, description="Merchant amount after fees")
104
+ commission: Optional[str] = Field(None, description="Commission amount")
105
+ is_final: bool = Field(..., description="Is payment final")
106
+ status: str = Field(..., description="Payment status")
107
+ from_: Optional[str] = Field(None, alias="from", description="Sender address")
108
+ wallet_address_uuid: Optional[str] = Field(None, description="Wallet address UUID")
109
+ network: Optional[str] = Field(None, description="Blockchain network")
110
+ currency: str = Field(..., description="Currency")
111
+ payer_currency: Optional[str] = Field(None, description="Payer currency")
112
+ additional_data: Optional[str] = Field(None, description="Additional data")
113
+ txid: Optional[str] = Field(None, description="Transaction hash")
114
+ sign: str = Field(..., description="Webhook signature")
115
+
116
+ @property
117
+ def from_address(self) -> Optional[str]:
118
+ """Get sender address from 'from' field."""
119
+ return self.from_
120
+
121
+ @field_validator('status')
122
+ @classmethod
123
+ def validate_status(cls, v):
124
+ """Validate status is one of expected values."""
125
+ valid_statuses = ['check', 'paid', 'paid_over', 'fail', 'wrong_amount', 'cancel',
126
+ 'system_fail', 'refund_process', 'refund_fail', 'refund_paid']
127
+ if v not in valid_statuses:
128
+ # Don't import logger here to avoid issues, just pass
129
+ pass
130
+ return v
131
+
132
+
133
+ class CryptomusStatusResponse(BaseModel):
134
+ """Cryptomus payment status response."""
135
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
136
+
137
+ state: int = Field(..., description="Response state")
138
+ result: Optional[CryptomusPaymentInfo] = Field(None, description="Payment info")
139
+ message: Optional[str] = Field(None, description="Response message")
140
+
141
+
142
+ class CryptomusCurrenciesResponse(BaseModel):
143
+ """Cryptomus supported currencies response."""
144
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
145
+
146
+ state: int = Field(..., description="Response state")
147
+ result: List[CryptomusCurrency] = Field(default_factory=list, description="List of currencies")
148
+ message: Optional[str] = Field(None, description="Response message")
149
+
150
+
151
+ class CryptomusNetworksResponse(BaseModel):
152
+ """Cryptomus supported networks response."""
153
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
154
+
155
+ state: int = Field(..., description="Response state")
156
+ result: Dict[str, List[CryptomusNetwork]] = Field(default_factory=dict, description="Networks by currency")
157
+ message: Optional[str] = Field(None, description="Response message")
158
+
159
+
160
+ # =============================================================================
161
+ # MONITORING & HEALTH CHECK MODELS
162
+ # =============================================================================
163
+
164
+ class CryptomusErrorResponse(BaseModel):
165
+ """Cryptomus API error response schema for health checks."""
166
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
167
+
168
+ error: str = Field(..., description="Error message")
169
+
170
+ @field_validator('error')
171
+ @classmethod
172
+ def validate_not_found_error(cls, v):
173
+ """Validate this is a not found error (meaning API is responding)."""
174
+ if v.lower() not in ['not found', 'unauthorized', 'forbidden']:
175
+ raise ValueError(f"Unexpected error: {v}")
176
+ return v