Fix crypto watcher stuck payments and improve cart deactivation
- Exclude already-swept payments from watcher queue to prevent endless processing - Mark swept payments as "swept" status to remove from future cycles - Fix cart deactivation by ensuring shop/user objects are attached to session - Add debug logging for cart deactivation to track behavior - Resolves issue where confirmed payments were processed every 20 seconds indefinitely 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4fbe9fedd4
commit
800b7965b5
10 changed files with 273 additions and 208 deletions
|
|
@ -7,44 +7,49 @@ import urllib.request
|
|||
import urllib.error
|
||||
import json
|
||||
|
||||
|
||||
def examine_auth_challenge():
|
||||
"""Look at the WWW-Authenticate header from the server"""
|
||||
print("=== Examining Authentication Challenge ===")
|
||||
|
||||
|
||||
rpc_url = "http://127.0.0.1:18083/json_rpc"
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
# Make request without auth to get the challenge
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
print("Unexpected: No auth required")
|
||||
return None
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 401:
|
||||
www_auth = e.headers.get('WWW-Authenticate', '')
|
||||
www_auth = e.headers.get("WWW-Authenticate", "")
|
||||
print(f"WWW-Authenticate header: {www_auth!r}")
|
||||
|
||||
|
||||
# Parse it
|
||||
if www_auth.startswith('Digest '):
|
||||
if www_auth.startswith("Digest "):
|
||||
challenge = www_auth[7:].strip()
|
||||
print(f"Challenge details: {challenge}")
|
||||
|
||||
|
||||
# Parse individual components
|
||||
import re
|
||||
|
||||
pattern = r'(\w+)=(?:"([^"]*)"|([^,\s]+))'
|
||||
matches = re.findall(pattern, challenge)
|
||||
|
||||
|
||||
print("Parsed components:")
|
||||
for key, quoted_val, unquoted_val in matches:
|
||||
val = quoted_val if quoted_val else unquoted_val
|
||||
print(f" {key} = {val!r}")
|
||||
|
||||
return dict((key, quoted_val if quoted_val else unquoted_val)
|
||||
for key, quoted_val, unquoted_val in matches)
|
||||
|
||||
return dict(
|
||||
(key, quoted_val if quoted_val else unquoted_val)
|
||||
for key, quoted_val, unquoted_val in matches
|
||||
)
|
||||
else:
|
||||
print(f"Not Digest auth: {www_auth}")
|
||||
return None
|
||||
|
|
@ -52,19 +57,20 @@ def examine_auth_challenge():
|
|||
print(f"Unexpected error: {e.code} {e.reason}")
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
challenge = examine_auth_challenge()
|
||||
|
||||
|
||||
if challenge:
|
||||
print()
|
||||
print("=== Analysis ===")
|
||||
print(f"Realm: {challenge.get('realm', 'NOT SET')}")
|
||||
print(f"Nonce: {challenge.get('nonce', 'NOT SET')}")
|
||||
print(f"Nonce: {challenge.get('nonce', 'NOT SET')}")
|
||||
print(f"QOP: {challenge.get('qop', 'NOT SET')}")
|
||||
print(f"Algorithm: {challenge.get('algorithm', 'NOT SET (defaults to MD5)')}")
|
||||
|
||||
|
||||
# Check for any unusual parameters
|
||||
standard_params = {'realm', 'nonce', 'qop', 'algorithm', 'opaque'}
|
||||
standard_params = {"realm", "nonce", "qop", "algorithm", "opaque"}
|
||||
extra_params = set(challenge.keys()) - standard_params
|
||||
if extra_params:
|
||||
print(f"Extra parameters: {extra_params}")
|
||||
print(f"Extra parameters: {extra_params}")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import json
|
|||
import hashlib
|
||||
import secrets
|
||||
|
||||
|
||||
def debug_digest_auth_process():
|
||||
"""
|
||||
Manually trace through the Digest authentication process to understand
|
||||
|
|
@ -17,23 +18,24 @@ def debug_digest_auth_process():
|
|||
"""
|
||||
print("=== Digest Authentication Debug Analysis ===")
|
||||
print()
|
||||
|
||||
|
||||
# Enable HTTP debugging to see what's being sent
|
||||
http.client.HTTPConnection.debuglevel = 1
|
||||
|
||||
|
||||
rpc_url = "http://127.0.0.1:18083/json_rpc"
|
||||
rpc_user = "mps_xmr_user_5a4c3592d2b4b52d"
|
||||
rpc_user = "mps_xmr_user_5a4c3592d2b4b52d"
|
||||
rpc_pass = "jLOnImOKWT1Vomeje3HvKBzsDjT6VPcxvf7FMtEpjvY4"
|
||||
|
||||
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
print("Step 1: Attempting initial request (should get 401 with WWW-Authenticate)")
|
||||
|
||||
|
||||
# Make initial request without auth to get the challenge
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
print("Unexpected: Got response without auth required")
|
||||
|
|
@ -41,12 +43,12 @@ def debug_digest_auth_process():
|
|||
except urllib.error.HTTPError as e:
|
||||
if e.code == 401:
|
||||
print(f"✓ Got 401 as expected: {e.reason}")
|
||||
auth_header = e.headers.get('WWW-Authenticate')
|
||||
auth_header = e.headers.get("WWW-Authenticate")
|
||||
if auth_header:
|
||||
print(f"WWW-Authenticate header: {auth_header}")
|
||||
|
||||
|
||||
# Parse the challenge
|
||||
if auth_header.startswith('Digest '):
|
||||
if auth_header.startswith("Digest "):
|
||||
print("✓ Server supports Digest authentication")
|
||||
challenge_parts = auth_header[7:] # Remove 'Digest '
|
||||
print(f"Challenge details: {challenge_parts}")
|
||||
|
|
@ -56,22 +58,23 @@ def debug_digest_auth_process():
|
|||
print("✗ No WWW-Authenticate header found")
|
||||
else:
|
||||
print(f"Unexpected HTTP error: {e.code} {e.reason}")
|
||||
|
||||
|
||||
print("\nStep 2: Now trying with Python's built-in Digest handler")
|
||||
|
||||
|
||||
# Reset debug level for cleaner output
|
||||
http.client.HTTPConnection.debuglevel = 0
|
||||
|
||||
|
||||
# Try with the urllib digest handler
|
||||
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_mgr.add_password(None, rpc_url, rpc_user, rpc_pass)
|
||||
|
||||
|
||||
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with opener.open(req) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -79,56 +82,57 @@ def debug_digest_auth_process():
|
|||
except urllib.error.HTTPError as e:
|
||||
print(f"✗ Digest auth FAILED: {e.code} {e.reason}")
|
||||
# Try to get more details
|
||||
if hasattr(e, 'read'):
|
||||
if hasattr(e, "read"):
|
||||
body = e.read().decode()
|
||||
print(f"Response body: {body}")
|
||||
|
||||
|
||||
def compare_with_requests_library():
|
||||
"""
|
||||
Compare urllib with the requests library to see if there's a difference.
|
||||
This helps identify if it's a urllib-specific issue.
|
||||
"""
|
||||
print("\n=== Comparison with requests library ===")
|
||||
|
||||
|
||||
try:
|
||||
import requests
|
||||
from requests.auth import HTTPDigestAuth
|
||||
|
||||
|
||||
rpc_url = "http://127.0.0.1:18083/json_rpc"
|
||||
rpc_user = "mps_xmr_user_5a4c3592d2b4b52d"
|
||||
rpc_pass = "jLOnImOKWT1Vomeje3HvKBzsDjT6VPcxvf7FMtEpjvY4"
|
||||
|
||||
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
|
||||
|
||||
print("Testing with requests library...")
|
||||
|
||||
|
||||
response = requests.post(
|
||||
rpc_url,
|
||||
json=payload,
|
||||
auth=HTTPDigestAuth(rpc_user, rpc_pass),
|
||||
timeout=15
|
||||
rpc_url, json=payload, auth=HTTPDigestAuth(rpc_user, rpc_pass), timeout=15
|
||||
)
|
||||
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✓ requests library SUCCESS:", result)
|
||||
else:
|
||||
print(f"✗ requests library FAILED: {response.status_code} {response.reason}")
|
||||
print(
|
||||
f"✗ requests library FAILED: {response.status_code} {response.reason}"
|
||||
)
|
||||
print(f"Response body: {response.text}")
|
||||
|
||||
|
||||
except ImportError:
|
||||
print("requests library not available for comparison")
|
||||
except Exception as e:
|
||||
print(f"requests library error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
debug_digest_auth_process()
|
||||
compare_with_requests_library()
|
||||
|
||||
|
||||
print("\n=== Summary ===")
|
||||
print("This debug script helps identify:")
|
||||
print("1. What WWW-Authenticate challenge the server sends")
|
||||
print("2. Whether urllib's Digest implementation works correctly")
|
||||
print("2. Whether urllib's Digest implementation works correctly")
|
||||
print("3. How different HTTP libraries handle the same credentials")
|
||||
print()
|
||||
print("Run this script on the production server where RPC auth is required.")
|
||||
print("Run this script on the production server where RPC auth is required.")
|
||||
|
|
|
|||
|
|
@ -9,15 +9,17 @@ import json
|
|||
import base64
|
||||
import sys
|
||||
|
||||
|
||||
def test_no_auth(rpc_url):
|
||||
"""Test without authentication"""
|
||||
print("=== Testing No Authentication ===")
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -25,7 +27,7 @@ def test_no_auth(rpc_url):
|
|||
return True
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"✗ No auth FAILED: HTTP {e.code} {e.reason}")
|
||||
if hasattr(e, 'read'):
|
||||
if hasattr(e, "read"):
|
||||
try:
|
||||
body = e.read().decode()
|
||||
print(f"Response body: {body}")
|
||||
|
|
@ -36,20 +38,21 @@ def test_no_auth(rpc_url):
|
|||
print(f"✗ No auth ERROR: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_basic_auth(rpc_url, rpc_user, rpc_pass):
|
||||
"""Test Basic authentication"""
|
||||
print("=== Testing Basic Authentication ===")
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
auth = f"{rpc_user}:{rpc_pass}".encode()
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic " + base64.b64encode(auth).decode()
|
||||
"Authorization": "Basic " + base64.b64encode(auth).decode(),
|
||||
}
|
||||
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data, headers=headers)
|
||||
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -57,7 +60,7 @@ def test_basic_auth(rpc_url, rpc_user, rpc_pass):
|
|||
return True
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"✗ Basic auth FAILED: HTTP {e.code} {e.reason}")
|
||||
if hasattr(e, 'read'):
|
||||
if hasattr(e, "read"):
|
||||
try:
|
||||
body = e.read().decode()
|
||||
print(f"Response body: {body}")
|
||||
|
|
@ -68,22 +71,24 @@ def test_basic_auth(rpc_url, rpc_user, rpc_pass):
|
|||
print(f"✗ Basic auth ERROR: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_digest_auth_simple(rpc_url, rpc_user, rpc_pass):
|
||||
"""Test Digest authentication - current implementation"""
|
||||
print("=== Testing Digest Authentication (Current Implementation) ===")
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
# Current implementation from crypto_clients.py
|
||||
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_mgr.add_password(None, rpc_url, rpc_user, rpc_pass)
|
||||
|
||||
|
||||
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with opener.open(req, timeout=15) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -91,7 +96,7 @@ def test_digest_auth_simple(rpc_url, rpc_user, rpc_pass):
|
|||
return True
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"✗ Digest auth FAILED: HTTP {e.code} {e.reason}")
|
||||
if hasattr(e, 'read'):
|
||||
if hasattr(e, "read"):
|
||||
try:
|
||||
body = e.read().decode()
|
||||
print(f"Response body: {body}")
|
||||
|
|
@ -102,24 +107,26 @@ def test_digest_auth_simple(rpc_url, rpc_user, rpc_pass):
|
|||
print(f"✗ Digest auth ERROR: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_digest_auth_with_realm(rpc_url, rpc_user, rpc_pass):
|
||||
"""Test Digest authentication with specific realm"""
|
||||
print("=== Testing Digest Authentication (with realm) ===")
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
# Try with specific realm
|
||||
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
# Try different realm possibilities
|
||||
for realm in [None, "monero-wallet-rpc", "RPC", ""]:
|
||||
password_mgr.add_password(realm, rpc_url, rpc_user, rpc_pass)
|
||||
|
||||
|
||||
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with opener.open(req, timeout=15) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -127,7 +134,7 @@ def test_digest_auth_with_realm(rpc_url, rpc_user, rpc_pass):
|
|||
return True
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"✗ Digest auth with realm FAILED: HTTP {e.code} {e.reason}")
|
||||
if hasattr(e, 'read'):
|
||||
if hasattr(e, "read"):
|
||||
try:
|
||||
body = e.read().decode()
|
||||
print(f"Response body: {body}")
|
||||
|
|
@ -138,20 +145,21 @@ def test_digest_auth_with_realm(rpc_url, rpc_user, rpc_pass):
|
|||
print(f"✗ Digest auth with realm ERROR: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test configuration - these would be the production values
|
||||
# (keeping them in the script for easy testing)
|
||||
rpc_url = "http://127.0.0.1:18083/json_rpc"
|
||||
rpc_user = "mps_xmr_user_5a4c3592d2b4b52d"
|
||||
rpc_pass = "jLOnImOKWT1Vomeje3HvKBzsDjT6VPcxvf7FMtEpjvY4"
|
||||
|
||||
|
||||
print("Monero RPC Authentication Debug")
|
||||
print("=" * 50)
|
||||
print(f"URL: {rpc_url}")
|
||||
print(f"User: {rpc_user}")
|
||||
print(f"Pass: {'*' * len(rpc_pass)}")
|
||||
print()
|
||||
|
||||
|
||||
# Test all methods
|
||||
test_no_auth(rpc_url)
|
||||
print()
|
||||
|
|
@ -159,4 +167,4 @@ if __name__ == "__main__":
|
|||
print()
|
||||
test_digest_auth_simple(rpc_url, rpc_user, rpc_pass)
|
||||
print()
|
||||
test_digest_auth_with_realm(rpc_url, rpc_user, rpc_pass)
|
||||
test_digest_auth_with_realm(rpc_url, rpc_user, rpc_pass)
|
||||
|
|
|
|||
|
|
@ -356,13 +356,26 @@ def finalize_invoice(env_request, crypto_payment: CryptoPayment):
|
|||
# Deactivate the user's current cart and create a new empty one
|
||||
# This mirrors the behavior in save_cart() for Stripe payments
|
||||
if invoice.user and invoice.shop:
|
||||
# Ensure objects are attached to the session
|
||||
env_request.dbsession.add(invoice.shop)
|
||||
env_request.dbsession.add(invoice.user)
|
||||
|
||||
# Get the user's current active cart for this shop
|
||||
current_active_cart = invoice.shop.get_active_cart_for_user(invoice.user)
|
||||
logger.info(
|
||||
f"Current active cart for user {invoice.user.id}: {current_active_cart.id if current_active_cart else 'None'}, "
|
||||
f"empty: {current_active_cart.is_empty if current_active_cart else 'N/A'}"
|
||||
)
|
||||
|
||||
if current_active_cart and not current_active_cart.is_empty:
|
||||
# Create a new empty cart and make it active
|
||||
invoice.shop.create_new_cart_for_user(invoice.user)
|
||||
new_cart = invoice.shop.create_new_cart_for_user(invoice.user)
|
||||
logger.info(
|
||||
f"Created new empty cart for user {invoice.user.id} after crypto payment confirmation"
|
||||
f"Created new empty cart {new_cart.id} for user {invoice.user.id} after crypto payment confirmation"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Skipping cart deactivation for user {invoice.user.id} - cart is empty or doesn't exist"
|
||||
)
|
||||
|
||||
# Emails (configurable)
|
||||
|
|
@ -506,10 +519,27 @@ def process_payment(
|
|||
crypto_payment.received_amount >= crypto_payment.expected_amount
|
||||
and min_confs >= int(crypto_payment.confirmations_required)
|
||||
):
|
||||
# Check if invoice needs finalization (products not unlocked yet)
|
||||
invoice_needs_finalization = False
|
||||
if crypto_payment.invoice and crypto_payment.invoice.line_items:
|
||||
# Check if any products are still locked for this user
|
||||
for line_item in crypto_payment.invoice.line_items:
|
||||
if not line_item.product.is_unlocked_for_user(
|
||||
crypto_payment.invoice.user
|
||||
):
|
||||
invoice_needs_finalization = True
|
||||
break
|
||||
|
||||
if crypto_payment.status != "confirmed":
|
||||
crypto_payment.status = "confirmed"
|
||||
# Finalize the invoice: unlock products, notify, and update inventory
|
||||
finalize_invoice(env_request, crypto_payment)
|
||||
elif invoice_needs_finalization:
|
||||
# Handle stuck confirmed payments that were never finalized
|
||||
logger.warning(
|
||||
f"Payment {crypto_payment.id} is confirmed but invoice not finalized - finalizing now"
|
||||
)
|
||||
finalize_invoice(env_request, crypto_payment)
|
||||
|
||||
# Check for overpayment refund AFTER confirming the order
|
||||
if (
|
||||
|
|
@ -606,7 +636,13 @@ def process_payment(
|
|||
logger.info(
|
||||
f"Funds unlocked ({unlocked_balance} atomic units) - attempting sweep for payment {crypto_payment.id}"
|
||||
)
|
||||
auto_sweep_payment(client, crypto_payment)
|
||||
sweep_success = auto_sweep_payment(client, crypto_payment)
|
||||
if sweep_success and crypto_payment.is_swept:
|
||||
# Mark as finalized so it's excluded from future processing
|
||||
crypto_payment.status = "swept"
|
||||
logger.info(
|
||||
f"Payment {crypto_payment.id} swept successfully - marked as finalized"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Payment {crypto_payment.id} confirmed but funds not yet unlocked"
|
||||
|
|
@ -628,9 +664,16 @@ def run_once(env, interval):
|
|||
with request.tm:
|
||||
db = request.dbsession
|
||||
|
||||
q = db.query(CryptoPayment).filter(
|
||||
CryptoPayment.status.in_(
|
||||
["pending", "received", "confirmed", "confirmed_overpaid"]
|
||||
q = (
|
||||
db.query(CryptoPayment)
|
||||
.filter(
|
||||
CryptoPayment.status.in_(
|
||||
["pending", "received", "confirmed", "confirmed_overpaid"]
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
# Exclude payments that are already swept (finalized and done)
|
||||
CryptoPayment.swept_tx_hash.is_(None)
|
||||
)
|
||||
)
|
||||
payments = q.all()
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||
|
||||
from make_post_sell.lib.crypto_clients import MoneroClient
|
||||
|
||||
|
||||
def test_no_auth():
|
||||
"""Test that requests fail without authentication"""
|
||||
print("=== Testing No Authentication (should fail) ===")
|
||||
|
||||
|
||||
client = MoneroClient("http://127.0.0.1:18083/json_rpc")
|
||||
|
||||
|
||||
try:
|
||||
height = client.get_height()
|
||||
print(f"✗ UNEXPECTED SUCCESS: {height}")
|
||||
|
|
@ -28,16 +29,13 @@ def test_no_auth():
|
|||
print(f"✓ EXPECTED FAILURE: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def test_wrong_credentials():
|
||||
"""Test that requests fail with wrong credentials"""
|
||||
print("=== Testing Wrong Credentials (should fail) ===")
|
||||
|
||||
client = MoneroClient(
|
||||
"http://127.0.0.1:18083/json_rpc",
|
||||
"wrong_user",
|
||||
"wrong_pass"
|
||||
)
|
||||
|
||||
|
||||
client = MoneroClient("http://127.0.0.1:18083/json_rpc", "wrong_user", "wrong_pass")
|
||||
|
||||
try:
|
||||
height = client.get_height()
|
||||
print(f"✗ UNEXPECTED SUCCESS: {height}")
|
||||
|
|
@ -46,16 +44,13 @@ def test_wrong_credentials():
|
|||
print(f"✓ EXPECTED FAILURE: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def test_correct_credentials():
|
||||
"""Test that requests succeed with correct credentials"""
|
||||
print("=== Testing Correct Credentials (should work) ===")
|
||||
|
||||
client = MoneroClient(
|
||||
"http://127.0.0.1:18083/json_rpc",
|
||||
"test_user",
|
||||
"test_pass"
|
||||
)
|
||||
|
||||
|
||||
client = MoneroClient("http://127.0.0.1:18083/json_rpc", "test_user", "test_pass")
|
||||
|
||||
try:
|
||||
height = client.get_height()
|
||||
print(f"✓ SUCCESS: Current height = {height}")
|
||||
|
|
@ -64,21 +59,28 @@ def test_correct_credentials():
|
|||
print(f"✗ FAILED: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_curl_comparison():
|
||||
"""Test the same credentials with curl to compare"""
|
||||
print("=== Testing with curl for comparison ===")
|
||||
|
||||
|
||||
import subprocess
|
||||
|
||||
|
||||
curl_cmd = [
|
||||
"curl", "-X", "POST",
|
||||
"curl",
|
||||
"-X",
|
||||
"POST",
|
||||
"http://127.0.0.1:18083/json_rpc",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-d", '{"jsonrpc":"2.0","id":1,"method":"get_height"}',
|
||||
"--digest", "-u", "test_user:test_pass",
|
||||
"--silent"
|
||||
"-H",
|
||||
"Content-Type: application/json",
|
||||
"-d",
|
||||
'{"jsonrpc":"2.0","id":1,"method":"get_height"}',
|
||||
"--digest",
|
||||
"-u",
|
||||
"test_user:test_pass",
|
||||
"--silent",
|
||||
]
|
||||
|
||||
|
||||
try:
|
||||
result = subprocess.run(curl_cmd, capture_output=True, text=True, timeout=10)
|
||||
if result.returncode == 0:
|
||||
|
|
@ -88,6 +90,7 @@ def test_curl_comparison():
|
|||
except Exception as e:
|
||||
print(f"✗ curl ERROR: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Local Digest Authentication Test")
|
||||
print("=" * 50)
|
||||
|
|
@ -95,18 +98,18 @@ if __name__ == "__main__":
|
|||
print("IMPORTANT: Start the authenticated wallet RPC first:")
|
||||
print(" make monero-wallet-remote-auth")
|
||||
print()
|
||||
|
||||
|
||||
# Run all tests
|
||||
test_no_auth()
|
||||
print()
|
||||
test_wrong_credentials()
|
||||
test_wrong_credentials()
|
||||
print()
|
||||
test_correct_credentials()
|
||||
print()
|
||||
test_curl_comparison()
|
||||
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("If authentication is working correctly:")
|
||||
print("- No auth and wrong credentials should fail")
|
||||
print("- Correct credentials should succeed")
|
||||
print("- curl and Python should behave the same")
|
||||
print("- curl and Python should behave the same")
|
||||
|
|
|
|||
|
|
@ -10,59 +10,58 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||
|
||||
from make_post_sell.lib.crypto_clients import DogecoinClient
|
||||
|
||||
|
||||
def test_dogecoin_mock():
|
||||
"""Test DogecoinClient with mock implementation (no real RPC needed)"""
|
||||
print("=== Testing DogecoinClient (Mock mode) ===")
|
||||
|
||||
|
||||
# This uses the MockDogecoinClient which doesn't need a real server
|
||||
from make_post_sell.lib.crypto_clients import MockDogecoinClient
|
||||
|
||||
|
||||
client = MockDogecoinClient()
|
||||
|
||||
|
||||
try:
|
||||
# Test basic operations
|
||||
address = client.getnewaddress("test")
|
||||
print(f"✓ Generated address: {address}")
|
||||
|
||||
|
||||
balance = client.getbalance()
|
||||
print(f"✓ Balance: {balance} DOGE")
|
||||
|
||||
|
||||
height = client.getblockcount()
|
||||
print(f"✓ Block height: {height}")
|
||||
|
||||
|
||||
print("✓ MockDogecoinClient working correctly")
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ MockDogecoinClient failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_dogecoin_real():
|
||||
"""Test DogecoinClient with real RPC (would need running dogecoind)"""
|
||||
print("\n=== Testing DogecoinClient (Real RPC - will likely fail) ===")
|
||||
|
||||
|
||||
# This would need a real Dogecoin RPC running
|
||||
client = DogecoinClient(
|
||||
"http://127.0.0.1:22555",
|
||||
"test_user",
|
||||
"test_pass"
|
||||
)
|
||||
|
||||
client = DogecoinClient("http://127.0.0.1:22555", "test_user", "test_pass")
|
||||
|
||||
try:
|
||||
info = client.getnetworkinfo()
|
||||
print(f"✓ Network info: {info}")
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ EXPECTED FAILURE (no Dogecoin RPC running): {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Dogecoin Client Test")
|
||||
print("=" * 40)
|
||||
|
||||
|
||||
test_dogecoin_mock()
|
||||
test_dogecoin_real()
|
||||
|
||||
|
||||
print("\nNote: Real RPC test expected to fail unless dogecoind is running")
|
||||
print("The important thing is that the mock client works correctly.")
|
||||
print("The important thing is that the mock client works correctly.")
|
||||
|
|
|
|||
|
|
@ -9,26 +9,23 @@ import json
|
|||
import base64
|
||||
import os
|
||||
|
||||
|
||||
def test_basic_auth(rpc_url, rpc_user, rpc_pass):
|
||||
"""Test Basic authentication (old method)"""
|
||||
print("Testing Basic Authentication...")
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "get_height"
|
||||
}
|
||||
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
# Basic auth header
|
||||
auth = f"{rpc_user}:{rpc_pass}".encode()
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic " + base64.b64encode(auth).decode()
|
||||
"Authorization": "Basic " + base64.b64encode(auth).decode(),
|
||||
}
|
||||
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data, headers=headers)
|
||||
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -41,26 +38,24 @@ def test_basic_auth(rpc_url, rpc_user, rpc_pass):
|
|||
print(f"✗ Basic auth ERROR: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_digest_auth(rpc_url, rpc_user, rpc_pass):
|
||||
"""Test Digest authentication (new method)"""
|
||||
print("Testing Digest Authentication...")
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "get_height"
|
||||
}
|
||||
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
# Digest auth setup
|
||||
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_mgr.add_password(None, rpc_url, rpc_user, rpc_pass)
|
||||
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with opener.open(req, timeout=15) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -73,20 +68,18 @@ def test_digest_auth(rpc_url, rpc_user, rpc_pass):
|
|||
print(f"✗ Digest auth ERROR: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_no_auth(rpc_url):
|
||||
"""Test no authentication"""
|
||||
print("Testing No Authentication...")
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "get_height"
|
||||
}
|
||||
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -99,30 +92,31 @@ def test_no_auth(rpc_url):
|
|||
print(f"✗ No auth ERROR: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Configuration from environment or defaults
|
||||
rpc_url = os.environ.get("MPS_MONERO_RPC_URL", "http://127.0.0.1:18083/json_rpc")
|
||||
rpc_user = os.environ.get("MPS_MONERO_RPC_USER", "")
|
||||
rpc_pass = os.environ.get("MPS_MONERO_RPC_PASS", "")
|
||||
|
||||
|
||||
print("=== Monero RPC Authentication Test ===")
|
||||
print(f"URL: {rpc_url}")
|
||||
print(f"User: {rpc_user!r}")
|
||||
print(f"Pass: {'*' * len(rpc_pass) if rpc_pass else '(empty)'}")
|
||||
print()
|
||||
|
||||
|
||||
if not rpc_user or not rpc_pass:
|
||||
print("⚠️ No credentials found in environment variables")
|
||||
print("Set MPS_MONERO_RPC_USER and MPS_MONERO_RPC_PASS")
|
||||
print()
|
||||
|
||||
|
||||
# Test all three methods
|
||||
test_no_auth(rpc_url)
|
||||
print()
|
||||
|
||||
|
||||
if rpc_user and rpc_pass:
|
||||
test_basic_auth(rpc_url, rpc_user, rpc_pass)
|
||||
print()
|
||||
test_digest_auth(rpc_url, rpc_user, rpc_pass)
|
||||
else:
|
||||
print("Skipping credential tests - no username/password provided")
|
||||
print("Skipping credential tests - no username/password provided")
|
||||
|
|
|
|||
|
|
@ -10,22 +10,23 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||
|
||||
from make_post_sell.lib.crypto_clients import MoneroClient
|
||||
|
||||
|
||||
def test_monero_client():
|
||||
"""Test the MoneroClient with the production credentials"""
|
||||
print("=== Testing MoneroClient ===")
|
||||
|
||||
|
||||
# Production credentials from the remote server
|
||||
rpc_url = "http://127.0.0.1:18083/json_rpc"
|
||||
rpc_user = "mps_xmr_user_5a4c3592d2b4b52d"
|
||||
rpc_user = "mps_xmr_user_5a4c3592d2b4b52d"
|
||||
rpc_pass = "jLOnImOKWT1Vomeje3HvKBzsDjT6VPcxvf7FMtEpjvY4"
|
||||
|
||||
|
||||
print(f"URL: {rpc_url}")
|
||||
print(f"User: {rpc_user}")
|
||||
print(f"Pass: {'*' * len(rpc_pass)}")
|
||||
print()
|
||||
|
||||
|
||||
client = MoneroClient(rpc_url, rpc_user, rpc_pass)
|
||||
|
||||
|
||||
# Test basic connection
|
||||
try:
|
||||
print("Testing get_height()...")
|
||||
|
|
@ -35,24 +36,25 @@ def test_monero_client():
|
|||
except Exception as e:
|
||||
print(f"✗ FAILED: {e}")
|
||||
print(f"Error type: {type(e).__name__}")
|
||||
|
||||
|
||||
# Try to get more detailed error info
|
||||
if hasattr(e, '__cause__') and e.__cause__:
|
||||
if hasattr(e, "__cause__") and e.__cause__:
|
||||
print(f"Caused by: {e.__cause__}")
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def test_monero_client_localhost():
|
||||
"""Test with localhost connection - might not work but useful for debugging"""
|
||||
print("=== Testing MoneroClient with localhost (fallback) ===")
|
||||
|
||||
|
||||
# This won't work since we're not running the RPC locally, but good for testing
|
||||
rpc_url = "http://127.0.0.1:18083/json_rpc"
|
||||
rpc_user = "test_user"
|
||||
rpc_pass = "test_pass"
|
||||
|
||||
|
||||
client = MoneroClient(rpc_url, rpc_user, rpc_pass)
|
||||
|
||||
|
||||
try:
|
||||
print("Testing get_height() on localhost...")
|
||||
height = client.get_height()
|
||||
|
|
@ -62,14 +64,15 @@ def test_monero_client_localhost():
|
|||
print(f"✗ EXPECTED FAILURE (no local RPC): {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Monero Client Test")
|
||||
print("=" * 40)
|
||||
print()
|
||||
|
||||
|
||||
# Test with production creds (won't work locally but shows the code path)
|
||||
test_monero_client()
|
||||
print()
|
||||
|
||||
|
||||
# Test with localhost (also won't work but different error pattern)
|
||||
test_monero_client_localhost()
|
||||
test_monero_client_localhost()
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ If requests works, we can see what it does differently.
|
|||
try:
|
||||
import requests
|
||||
from requests.auth import HTTPDigestAuth
|
||||
|
||||
|
||||
def test_with_requests():
|
||||
print("=== Testing with requests library ===")
|
||||
|
||||
|
||||
url = "http://127.0.0.1:18083/json_rpc"
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
auth = HTTPDigestAuth("test_user", "test_pass")
|
||||
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, auth=auth, timeout=15)
|
||||
if response.status_code == 200:
|
||||
|
|
@ -28,10 +28,10 @@ try:
|
|||
except Exception as e:
|
||||
print(f"✗ requests ERROR: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_with_requests()
|
||||
|
||||
|
||||
except ImportError:
|
||||
print("requests library not installed. Install with: pip install requests")
|
||||
print("This would help us compare urllib vs requests for Digest auth.")
|
||||
print("This would help us compare urllib vs requests for Digest auth.")
|
||||
|
|
|
|||
|
|
@ -7,29 +7,31 @@ import urllib.error
|
|||
import json
|
||||
import http.client
|
||||
|
||||
|
||||
def test_urllib_debug():
|
||||
"""Test urllib with full HTTP debugging enabled"""
|
||||
print("=== urllib with HTTP debugging ===")
|
||||
|
||||
|
||||
# Enable debugging to see what's being sent
|
||||
http.client.HTTPConnection.debuglevel = 1
|
||||
|
||||
|
||||
rpc_url = "http://127.0.0.1:18083/json_rpc"
|
||||
rpc_user = "test_user"
|
||||
rpc_pass = "test_pass"
|
||||
|
||||
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_mgr.add_password(None, rpc_url, rpc_user, rpc_pass)
|
||||
|
||||
|
||||
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
|
||||
req = urllib.request.Request(rpc_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with opener.open(req) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -39,29 +41,31 @@ def test_urllib_debug():
|
|||
finally:
|
||||
http.client.HTTPConnection.debuglevel = 0
|
||||
|
||||
|
||||
def test_urllib_different_url():
|
||||
"""Test if the issue is with the /json_rpc path"""
|
||||
print("\n=== Testing different URL formats ===")
|
||||
|
||||
|
||||
base_url = "http://127.0.0.1:18083"
|
||||
paths = ["/json_rpc", "/json_rpc/", ""]
|
||||
|
||||
|
||||
for path in paths:
|
||||
print(f"\nTesting with path: {path!r}")
|
||||
test_url = base_url + path if path else base_url
|
||||
|
||||
|
||||
payload = {"jsonrpc": "2.0", "id": 1, "method": "get_height"}
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
|
||||
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_mgr.add_password(None, test_url, "test_user", "test_pass")
|
||||
|
||||
|
||||
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
|
||||
req = urllib.request.Request(test_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
req = urllib.request.Request(
|
||||
test_url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with opener.open(req) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
|
@ -70,6 +74,7 @@ def test_urllib_different_url():
|
|||
except Exception as e:
|
||||
print(f" ✗ FAILED: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_urllib_debug()
|
||||
test_urllib_different_url()
|
||||
test_urllib_different_url()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue