|
29 | 29 | from flask import Flask, g, jsonify, request, has_request_context |
30 | 30 | from flask_cors import CORS |
31 | 31 |
|
32 | | -APP_VERSION = "2.3.1-nowpayments-ipnfix" |
| 32 | +APP_VERSION = "2.3.3-nowpayments-ipnstreamfix" |
33 | 33 | TRANSFER_TOPIC = "0xddf252ad00000000000000000000000000000000000000000000000000000000" |
34 | 34 | ZERO_EVM = "0x0000000000000000000000000000000000000000" |
35 | 35 | BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" |
@@ -4848,19 +4848,72 @@ def nowpayments_order_status(order_id: str): |
4848 | 4848 | }) |
4849 | 4849 |
|
4850 | 4850 |
|
4851 | | -def parse_nowpayments_ipn_payload() -> Dict[str, Any]: |
| 4851 | +def parse_nowpayments_ipn_payload() -> Tuple[Dict[str, Any], Dict[str, Any]]: |
4852 | 4852 | payload: Dict[str, Any] = {} |
| 4853 | + debug: Dict[str, Any] = {} |
4853 | 4854 |
|
4854 | | - raw = request.get_data(cache=True, as_text=True) or "" |
4855 | | - if raw: |
| 4855 | + raw_bytes = b"" |
| 4856 | + try: |
| 4857 | + raw_bytes = request.get_data(cache=True, as_text=False, parse_form_data=False) or b"" |
| 4858 | + except TypeError: |
| 4859 | + raw_bytes = request.get_data(cache=True, as_text=False) or b"" |
| 4860 | + except Exception: |
| 4861 | + raw_bytes = b"" |
| 4862 | + |
| 4863 | + if not raw_bytes: |
4856 | 4864 | try: |
4857 | | - parsed = json.loads(raw) |
4858 | | - if isinstance(parsed, dict): |
4859 | | - payload.update(parsed) |
| 4865 | + content_length = int(request.environ.get("CONTENT_LENGTH") or 0) |
4860 | 4866 | except Exception: |
4861 | | - pass |
| 4867 | + content_length = 0 |
| 4868 | + if content_length > 0: |
| 4869 | + try: |
| 4870 | + stream = request.environ.get("wsgi.input") |
| 4871 | + if stream is not None: |
| 4872 | + raw_bytes = stream.read(content_length) or b"" |
| 4873 | + except Exception: |
| 4874 | + raw_bytes = b"" |
| 4875 | + |
| 4876 | + raw_text = "" |
| 4877 | + if raw_bytes: |
| 4878 | + try: |
| 4879 | + raw_text = raw_bytes.decode("utf-8-sig", errors="replace") |
| 4880 | + except Exception: |
| 4881 | + raw_text = str(raw_bytes) |
| 4882 | + |
| 4883 | + if raw_text: |
| 4884 | + parsed = None |
| 4885 | + try: |
| 4886 | + parsed = json.loads(raw_text) |
| 4887 | + except Exception: |
| 4888 | + parsed = None |
| 4889 | + if isinstance(parsed, str) and parsed.strip(): |
| 4890 | + try: |
| 4891 | + parsed = json.loads(parsed) |
| 4892 | + except Exception: |
| 4893 | + parsed = None |
| 4894 | + if isinstance(parsed, dict): |
| 4895 | + payload.update(parsed) |
| 4896 | + else: |
| 4897 | + try: |
| 4898 | + form_like = urllib.parse.parse_qs(raw_text, keep_blank_values=True) |
| 4899 | + for key, values in form_like.items(): |
| 4900 | + if not values: |
| 4901 | + continue |
| 4902 | + field_value = values[-1] |
| 4903 | + if key in {"payload", "body", "data"} and isinstance(field_value, str) and field_value.strip(): |
| 4904 | + try: |
| 4905 | + nested = json.loads(field_value) |
| 4906 | + if isinstance(nested, dict): |
| 4907 | + payload.update(nested) |
| 4908 | + continue |
| 4909 | + except Exception: |
| 4910 | + pass |
| 4911 | + if key not in payload: |
| 4912 | + payload[str(key)] = field_value |
| 4913 | + except Exception: |
| 4914 | + pass |
4862 | 4915 |
|
4863 | | - json_payload = request.get_json(silent=True) |
| 4916 | + json_payload = request.get_json(silent=True, force=False) |
4864 | 4917 | if isinstance(json_payload, dict): |
4865 | 4918 | payload.update(json_payload) |
4866 | 4919 |
|
@@ -4889,25 +4942,37 @@ def parse_nowpayments_ipn_payload() -> Dict[str, Any]: |
4889 | 4942 | except Exception: |
4890 | 4943 | pass |
4891 | 4944 |
|
4892 | | - return payload |
| 4945 | + debug.update({ |
| 4946 | + "content_type": str(request.content_type or ""), |
| 4947 | + "mimetype": str(request.mimetype or ""), |
| 4948 | + "content_length": int(request.content_length or 0), |
| 4949 | + "raw_len": len(raw_bytes), |
| 4950 | + "raw_preview": raw_text[:200], |
| 4951 | + "payload_keys": sorted([str(k) for k in payload.keys()]), |
| 4952 | + }) |
| 4953 | + |
| 4954 | + return payload, debug |
4893 | 4955 |
|
4894 | 4956 |
|
4895 | 4957 | @app.post("/api/payments/nowpayments/ipn") |
4896 | 4958 | def nowpayments_ipn_receive(): |
4897 | | - payload = parse_nowpayments_ipn_payload() |
| 4959 | + payload, payload_debug = parse_nowpayments_ipn_payload() |
4898 | 4960 | bypass_enabled = str(os.getenv("NOWPAYMENTS_IPN_TEST_BYPASS", "0")).strip().lower() in {"1", "true", "yes", "on"} |
4899 | 4961 | bypass_requested = str(request.args.get("test_bypass") or "").strip().lower() in {"1", "true", "yes", "on"} |
4900 | 4962 | has_identifier = any(normalize_text(payload.get(key), 120) for key in ("order_id", "invoice_id", "invoiceId", "payment_id", "id", "purchase_id")) |
4901 | 4963 | payment_status_value = normalize_text(payload.get("payment_status") or payload.get("status"), 80) |
4902 | 4964 | if not has_identifier or not payment_status_value: |
| 4965 | + details = { |
| 4966 | + "has_identifier": bool(has_identifier), |
| 4967 | + "has_payment_status": bool(payment_status_value), |
| 4968 | + } |
| 4969 | + if bypass_enabled and bypass_requested: |
| 4970 | + details["debug_payload"] = payload_debug |
4903 | 4971 | raise ApiError( |
4904 | 4972 | "NOWPayments IPN payload is missing required fields.", |
4905 | 4973 | 400, |
4906 | 4974 | code="NOWPAYMENTS_IPN_PAYLOAD_INVALID", |
4907 | | - details={ |
4908 | | - "has_identifier": bool(has_identifier), |
4909 | | - "has_payment_status": bool(payment_status_value), |
4910 | | - }, |
| 4975 | + details=details, |
4911 | 4976 | ) |
4912 | 4977 | if not (bypass_enabled and bypass_requested): |
4913 | 4978 | signature = str(request.headers.get("x-nowpayments-sig") or request.headers.get("X-NOWPAYMENTS-SIG") or "").strip() |
@@ -4951,6 +5016,8 @@ def nowpayments_ipn_receive(): |
4951 | 5016 | "invoice_id": normalize_text(payload.get("invoice_id") or payload.get("invoiceId"), 120), |
4952 | 5017 | "payment_status": normalize_text(payload.get("payment_status") or payload.get("status"), 80), |
4953 | 5018 | "payload_keys": sorted([str(k) for k in payload.keys()]), |
| 5019 | + "raw_len": payload_debug.get("raw_len", 0), |
| 5020 | + "content_type": payload_debug.get("content_type", ""), |
4954 | 5021 | "app_version": APP_VERSION, |
4955 | 5022 | } |
4956 | 5023 | return jsonify(response_payload) |
|
0 commit comments