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,380 @@
1
+ """
2
+ NowPayments provider implementation.
3
+
4
+ Enhanced crypto payment provider with minimal typing.
5
+ """
6
+
7
+ import requests
8
+ import hashlib
9
+ import hmac
10
+ from typing import Optional, List
11
+ from decimal import Decimal
12
+ from datetime import datetime
13
+ from ..base import PaymentProvider
14
+ from ...internal_types import ProviderResponse, WebhookData, PaymentAmountEstimate, UniversalCurrency, UniversalCurrenciesResponse
15
+ from .models import NowPaymentsConfig
16
+ from django_cfg.modules.django_logger import get_logger
17
+
18
+ logger = get_logger("nowpayments")
19
+
20
+
21
+ class NowPaymentsProvider(PaymentProvider):
22
+ """NowPayments cryptocurrency payment provider."""
23
+
24
+ # Map NowPayments status to universal status
25
+ STATUS_MAPPING = {
26
+ 'waiting': 'pending',
27
+ 'confirming': 'processing',
28
+ 'confirmed': 'completed',
29
+ 'sending': 'processing',
30
+ 'partially_paid': 'pending',
31
+ 'finished': 'completed',
32
+ 'failed': 'failed',
33
+ 'refunded': 'refunded',
34
+ 'expired': 'expired'
35
+ }
36
+
37
+ def __init__(self, config: NowPaymentsConfig):
38
+ """Initialize NowPayments provider."""
39
+ super().__init__(config)
40
+ self.config = config
41
+ self.api_key = config.api_key
42
+ # TEMP: Disable sandbox since sandbox registration site is down
43
+ # self.sandbox = config.sandbox
44
+ self.sandbox = False # Force production URL
45
+ self.ipn_secret = config.ipn_secret or ''
46
+ self.base_url = self._get_base_url()
47
+
48
+ # Configurable URLs
49
+ self.callback_url = config.callback_url
50
+ self.success_url = config.success_url
51
+ self.cancel_url = config.cancel_url
52
+
53
+ self.headers = {
54
+ 'x-api-key': self.api_key.get_secret_value() if hasattr(self.api_key, 'get_secret_value') else str(self.api_key),
55
+ 'Content-Type': 'application/json'
56
+ }
57
+
58
+ def _get_dynamic_callback_url(self) -> Optional[str]:
59
+ """Get dynamic callback URL with ngrok support."""
60
+ try:
61
+ from api.config import config
62
+
63
+ # Try ngrok first (development)
64
+ ngrok_url = getattr(config, 'get_ngrok_url', lambda x: None)('/cfg/admin/django_cfg_payments/webhooks/nowpayments/')
65
+ if ngrok_url:
66
+ logger.info(f"Using ngrok webhook URL: {ngrok_url}")
67
+ return ngrok_url
68
+
69
+ # Fallback to configured callback URL
70
+ if self.callback_url:
71
+ logger.info(f"Using configured webhook URL: {self.callback_url}")
72
+ return self.callback_url
73
+
74
+ # Fallback to site URL
75
+ site_webhook = f"{config.site_url}/cfg/admin/django_cfg_payments/webhooks/nowpayments/"
76
+ logger.info(f"Using site webhook URL: {site_webhook}")
77
+ return site_webhook
78
+
79
+ except Exception as e:
80
+ logger.warning(f"Failed to get dynamic callback URL: {e}")
81
+ return self.callback_url
82
+
83
+ def _get_base_url(self) -> str:
84
+ """Get base URL based on sandbox mode."""
85
+ if self.sandbox:
86
+ return 'https://api-sandbox.nowpayments.io/v1'
87
+ return 'https://api.nowpayments.io/v1'
88
+
89
+ def _make_request(self, method: str, endpoint: str, data: Optional[dict] = None) -> Optional[dict]:
90
+ """Make HTTP request to NowPayments API with error handling."""
91
+ try:
92
+ url = f"{self.base_url}/{endpoint}"
93
+
94
+ response = requests.request(
95
+ method=method,
96
+ url=url,
97
+ headers=self.headers,
98
+ json=data,
99
+ timeout=30
100
+ )
101
+
102
+ response.raise_for_status()
103
+ return response.json()
104
+
105
+ except requests.exceptions.RequestException as e:
106
+ logger.error(f"NowPayments API request failed: {e}")
107
+ return None
108
+ except Exception as e:
109
+ logger.error(f"Unexpected error in NowPayments request: {e}")
110
+ return None
111
+
112
+ def create_payment(self, payment_data: dict) -> ProviderResponse:
113
+ """Create payment via NowPayments API."""
114
+ try:
115
+ amount = Decimal(str(payment_data['amount']))
116
+ currency = payment_data['currency']
117
+ order_id = payment_data.get('order_id', f'payment_{int(amount * 100)}_{currency}')
118
+
119
+ payment_request = {
120
+ 'price_amount': float(amount),
121
+ 'price_currency': 'usd', # Base currency
122
+ 'pay_currency': currency,
123
+ 'order_id': order_id,
124
+ 'order_description': payment_data.get('description', f'Payment {order_id}'),
125
+ }
126
+
127
+ # Add optional URLs
128
+ if self.success_url:
129
+ payment_request['success_url'] = self.success_url
130
+ if self.cancel_url:
131
+ payment_request['cancel_url'] = self.cancel_url
132
+
133
+ # Get dynamic callback URL with ngrok support
134
+ dynamic_callback_url = self._get_dynamic_callback_url()
135
+ if dynamic_callback_url:
136
+ payment_request['ipn_callback_url'] = dynamic_callback_url
137
+
138
+ response = self._make_request('POST', 'payment', payment_request)
139
+
140
+ if response:
141
+ return ProviderResponse(
142
+ success=True,
143
+ provider_payment_id=response.get('payment_id'),
144
+ payment_url=response.get('invoice_url'),
145
+ pay_address=response.get('pay_address'),
146
+ amount=Decimal(str(response.get('pay_amount', 0))),
147
+ currency=response.get('pay_currency'),
148
+ status='pending'
149
+ )
150
+ else:
151
+ return ProviderResponse(
152
+ success=False,
153
+ error_message='Failed to create payment'
154
+ )
155
+
156
+ except Exception as e:
157
+ logger.error(f"NowPayments create_payment error: {e}")
158
+ return ProviderResponse(
159
+ success=False,
160
+ error_message=str(e)
161
+ )
162
+
163
+ def check_payment_status(self, payment_id: str) -> ProviderResponse:
164
+ """Check payment status via NowPayments API."""
165
+ try:
166
+ response = self._make_request('GET', f'payment/{payment_id}')
167
+
168
+ if response:
169
+ provider_status = response.get('payment_status', 'unknown')
170
+ universal_status = self.STATUS_MAPPING.get(provider_status, 'unknown')
171
+
172
+ return ProviderResponse(
173
+ success=True,
174
+ provider_payment_id=response.get('payment_id'),
175
+ status=universal_status,
176
+ pay_address=response.get('pay_address'),
177
+ amount=Decimal(str(response.get('pay_amount', 0))),
178
+ currency=response.get('pay_currency')
179
+ )
180
+ else:
181
+ return ProviderResponse(
182
+ success=False,
183
+ error_message='Payment not found'
184
+ )
185
+
186
+ except Exception as e:
187
+ logger.error(f"NowPayments check_payment_status error: {e}")
188
+ return ProviderResponse(
189
+ success=False,
190
+ error_message=str(e)
191
+ )
192
+
193
+ def process_webhook(self, payload: dict) -> WebhookData:
194
+ """Process NowPayments webhook."""
195
+ try:
196
+ provider_status = payload.get('payment_status', 'unknown')
197
+ universal_status = self.STATUS_MAPPING.get(provider_status, 'unknown')
198
+
199
+ return WebhookData(
200
+ provider_payment_id=str(payload.get('payment_id', '')),
201
+ status=universal_status,
202
+ pay_amount=Decimal(str(payload.get('pay_amount', 0))),
203
+ actually_paid=Decimal(str(payload.get('actually_paid', 0))),
204
+ order_id=payload.get('order_id'),
205
+ signature=payload.get('signature')
206
+ )
207
+
208
+ except Exception as e:
209
+ logger.error(f"NowPayments webhook processing error: {e}")
210
+ raise
211
+
212
+
213
+ def get_parsed_currencies(self) -> UniversalCurrenciesResponse:
214
+ """Get parsed and normalized currencies from NowPayments."""
215
+ try:
216
+ # Use full-currencies endpoint to get detailed currency info
217
+ response = self._make_request('GET', 'full-currencies')
218
+
219
+ if not response or 'currencies' not in response:
220
+ return UniversalCurrenciesResponse(currencies=[])
221
+
222
+ universal_currencies = []
223
+
224
+ for currency_data in response['currencies']:
225
+ if not currency_data.get('enable', True):
226
+ continue # Skip disabled currencies
227
+
228
+ provider_code = currency_data.get('code', '').upper()
229
+ if not provider_code:
230
+ continue
231
+
232
+ # Parse provider code into base currency + network using API data
233
+ currency_name = currency_data.get('name', '')
234
+ api_network = currency_data.get('network')
235
+ ticker = currency_data.get('ticker', '')
236
+ base_currency_code, network_code = self._parse_currency_code(provider_code, currency_name, api_network, ticker)
237
+
238
+ # Determine currency type
239
+ currency_type = 'fiat' if network_code is None else 'crypto'
240
+
241
+ universal_currency = UniversalCurrency(
242
+ provider_currency_code=provider_code,
243
+ base_currency_code=base_currency_code,
244
+ network_code=network_code,
245
+ name=currency_data.get('name', base_currency_code),
246
+ currency_type=currency_type,
247
+ is_enabled=currency_data.get('enable', True),
248
+ is_popular=currency_data.get('is_popular', False),
249
+ is_stable=currency_data.get('is_stable', False),
250
+ priority=currency_data.get('priority', 0),
251
+ logo_url=currency_data.get('logo_url', ''),
252
+ available_for_payment=currency_data.get('available_for_payment', True),
253
+ available_for_payout=currency_data.get('available_for_payout', True),
254
+ raw_data=currency_data
255
+ )
256
+
257
+ universal_currencies.append(universal_currency)
258
+
259
+ return UniversalCurrenciesResponse(currencies=universal_currencies)
260
+
261
+ except Exception as e:
262
+ logger.error(f"Error parsing currencies: {e}")
263
+ return UniversalCurrenciesResponse(currencies=[])
264
+
265
+ def _parse_currency_code(self, provider_code: str, currency_name: str, network_code: Optional[str] = None, ticker: str = '') -> tuple[str, Optional[str]]:
266
+ """
267
+ Smart parsing using API data, prioritizing ticker field.
268
+
269
+ Uses ticker as primary source for base currency, then falls back to name parsing.
270
+
271
+ Examples:
272
+ - "1INCHBSC", "1Inch Network (BSC)", "bsc", "1inch" → ("1INCH", "bsc")
273
+ - "USDTERC20", "Tether USD (ERC-20)", "eth", "usdt" → ("USDT", "eth")
274
+ - "BTC", "Bitcoin", "btc", "btc" → ("BTC", "btc")
275
+ """
276
+ # Priority 1: Use ticker if available and meaningful
277
+ if ticker and len(ticker.strip()) > 0:
278
+ base_currency = ticker.upper().strip()
279
+ return base_currency, network_code
280
+
281
+ # Priority 2: Extract from name using patterns
282
+ base_currency = self._extract_base_currency_from_name(currency_name, provider_code)
283
+ return base_currency, network_code
284
+
285
+ def _extract_base_currency_from_name(self, currency_name: str, fallback_code: str) -> str:
286
+ """Extract base currency from human-readable name using real API patterns."""
287
+ if not currency_name:
288
+ return fallback_code
289
+
290
+ name_lower = currency_name.lower()
291
+
292
+ # Precise patterns from real NowPayments API data
293
+ precise_patterns = {
294
+ # Stablecoins - most common
295
+ 'tether usd': 'USDT',
296
+ 'tether (': 'USDT', # "Tether (Arbitrum One)"
297
+ 'usd coin': 'USDC', # "USD Coin (Ethereum)"
298
+ 'usd coin bridged': 'USDC', # "USD Coin Bridged (Polygon)"
299
+ 'trueusd': 'TUSD', # "TrueUSD (Tron)"
300
+
301
+ # Major cryptocurrencies
302
+ 'bitcoin': 'BTC',
303
+ 'ethereum': 'ETH',
304
+ 'cardano': 'ADA',
305
+ 'dogecoin': 'DOGE',
306
+ 'litecoin': 'LTC',
307
+
308
+ # Exchange tokens
309
+ 'binance coin': 'BNB',
310
+ 'bnb': 'BNB',
311
+
312
+ # Layer 1/2 tokens
313
+ 'polygon': 'MATIC',
314
+ 'avalanche': 'AVAX',
315
+ 'solana': 'SOL',
316
+ 'chainlink': 'LINK',
317
+
318
+ # Other stablecoins
319
+ 'dai stablecoin': 'DAI',
320
+ 'frax': 'FRAX'
321
+ }
322
+
323
+ # Check precise patterns first (most reliable)
324
+ for pattern, base in precise_patterns.items():
325
+ if pattern in name_lower:
326
+ return base
327
+
328
+ # Fallback patterns for edge cases
329
+ fallback_patterns = {
330
+ 'usdt': 'USDT',
331
+ 'usdc': 'USDC',
332
+ 'tusd': 'TUSD',
333
+ 'btc': 'BTC',
334
+ 'eth ': 'ETH',
335
+ 'ada': 'ADA',
336
+ 'doge': 'DOGE',
337
+ 'matic': 'MATIC'
338
+ }
339
+
340
+ for pattern, base in fallback_patterns.items():
341
+ if pattern in name_lower:
342
+ return base
343
+
344
+ # Last resort: use the provider code as-is
345
+ return fallback_code
346
+
347
+
348
+ def validate_webhook(self, payload: dict, headers: Optional[dict] = None) -> bool:
349
+ """Validate NowPayments webhook signature."""
350
+ try:
351
+ if not self.ipn_secret:
352
+ logger.warning("IPN secret not configured, skipping webhook validation")
353
+ return True
354
+
355
+ if not headers:
356
+ logger.warning("No headers provided for webhook validation")
357
+ return False
358
+
359
+ # Get signature from headers
360
+ signature = headers.get('x-nowpayments-sig')
361
+ if not signature:
362
+ logger.warning("No signature found in webhook headers")
363
+ return False
364
+
365
+ # TODO: Implement proper HMAC signature validation
366
+ # This requires the raw payload body for proper validation
367
+ logger.info("Webhook signature validation placeholder")
368
+ return True
369
+
370
+ except Exception as e:
371
+ logger.error(f"Webhook validation error: {e}")
372
+ return False
373
+
374
+ def check_api_status(self) -> bool:
375
+ """Check if NowPayments API is available."""
376
+ try:
377
+ response = self._make_request('GET', 'status')
378
+ return response is not None and response.get('message') == 'OK'
379
+ except:
380
+ return False