Skip to content

Commit 3f10f36

Browse files
Update app.py
1 parent 6f1b95b commit 3f10f36

1 file changed

Lines changed: 82 additions & 15 deletions

File tree

app.py

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from flask import Flask, g, jsonify, request, has_request_context
3030
from flask_cors import CORS
3131

32-
APP_VERSION = "2.3.1-nowpayments-ipnfix"
32+
APP_VERSION = "2.3.3-nowpayments-ipnstreamfix"
3333
TRANSFER_TOPIC = "0xddf252ad00000000000000000000000000000000000000000000000000000000"
3434
ZERO_EVM = "0x0000000000000000000000000000000000000000"
3535
BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
@@ -4848,19 +4848,72 @@ def nowpayments_order_status(order_id: str):
48484848
})
48494849

48504850

4851-
def parse_nowpayments_ipn_payload() -> Dict[str, Any]:
4851+
def parse_nowpayments_ipn_payload() -> Tuple[Dict[str, Any], Dict[str, Any]]:
48524852
payload: Dict[str, Any] = {}
4853+
debug: Dict[str, Any] = {}
48534854

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:
48564864
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)
48604866
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
48624915

4863-
json_payload = request.get_json(silent=True)
4916+
json_payload = request.get_json(silent=True, force=False)
48644917
if isinstance(json_payload, dict):
48654918
payload.update(json_payload)
48664919

@@ -4889,25 +4942,37 @@ def parse_nowpayments_ipn_payload() -> Dict[str, Any]:
48894942
except Exception:
48904943
pass
48914944

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
48934955

48944956

48954957
@app.post("/api/payments/nowpayments/ipn")
48964958
def nowpayments_ipn_receive():
4897-
payload = parse_nowpayments_ipn_payload()
4959+
payload, payload_debug = parse_nowpayments_ipn_payload()
48984960
bypass_enabled = str(os.getenv("NOWPAYMENTS_IPN_TEST_BYPASS", "0")).strip().lower() in {"1", "true", "yes", "on"}
48994961
bypass_requested = str(request.args.get("test_bypass") or "").strip().lower() in {"1", "true", "yes", "on"}
49004962
has_identifier = any(normalize_text(payload.get(key), 120) for key in ("order_id", "invoice_id", "invoiceId", "payment_id", "id", "purchase_id"))
49014963
payment_status_value = normalize_text(payload.get("payment_status") or payload.get("status"), 80)
49024964
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
49034971
raise ApiError(
49044972
"NOWPayments IPN payload is missing required fields.",
49054973
400,
49064974
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,
49114976
)
49124977
if not (bypass_enabled and bypass_requested):
49134978
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():
49515016
"invoice_id": normalize_text(payload.get("invoice_id") or payload.get("invoiceId"), 120),
49525017
"payment_status": normalize_text(payload.get("payment_status") or payload.get("status"), 80),
49535018
"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", ""),
49545021
"app_version": APP_VERSION,
49555022
}
49565023
return jsonify(response_payload)

0 commit comments

Comments
 (0)