chore: remove obsolete payment architecture and integration test documentation - Remove AITBC_PAYMENT_ARCHITECTURE.md (dual-currency system documentation) - Remove IMPLEMENTATION_COMPLETE_SUMMARY.md (integration test completion summary) - Remove INTEGRATION_TEST_FIXES.md (test fixes documentation) - Remove INTEGRATION_TEST_UPDATES.md (real features implementation notes) - Remove PAYMENT_INTEGRATION_COMPLETE.md (wallet-coordinator integration docs) - Remove WALLET_COORDINATOR_INTEGRATION.md (payment
327 lines
12 KiB
Python
327 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Test script for AITBC blockchain nodes
|
|
Tests both nodes for functionality and consistency
|
|
"""
|
|
|
|
import httpx
|
|
import json
|
|
import time
|
|
import sys
|
|
from typing import Dict, Any, Optional
|
|
|
|
# Configuration
|
|
NODES = {
|
|
"node1": {"url": "http://127.0.0.1:8082", "name": "Node 1"},
|
|
"node2": {"url": "http://127.0.0.1:8081", "name": "Node 2"},
|
|
}
|
|
|
|
# Test addresses
|
|
TEST_ADDRESSES = {
|
|
"alice": "aitbc1alice00000000000000000000000000000000000",
|
|
"bob": "aitbc1bob0000000000000000000000000000000000000",
|
|
"charlie": "aitbc1charl0000000000000000000000000000000000",
|
|
}
|
|
|
|
def print_header(message: str):
|
|
"""Print test header"""
|
|
print(f"\n{'='*60}")
|
|
print(f" {message}")
|
|
print(f"{'='*60}")
|
|
|
|
def print_step(message: str):
|
|
"""Print test step"""
|
|
print(f"\n→ {message}")
|
|
|
|
def print_success(message: str):
|
|
"""Print success message"""
|
|
print(f"✅ {message}")
|
|
|
|
def print_error(message: str):
|
|
"""Print error message"""
|
|
print(f"❌ {message}")
|
|
|
|
def print_warning(message: str):
|
|
"""Print warning message"""
|
|
print(f"⚠️ {message}")
|
|
|
|
def check_node_health(node_name: str, node_config: Dict[str, str]) -> bool:
|
|
"""Check if node is responsive"""
|
|
try:
|
|
response = httpx.get(f"{node_config['url']}/openapi.json", timeout=5)
|
|
if response.status_code == 200:
|
|
print_success(f"{node_config['name']} is responsive")
|
|
return True
|
|
else:
|
|
print_error(f"{node_config['name']} returned status {response.status_code}")
|
|
return False
|
|
except Exception as e:
|
|
print_error(f"{node_config['name']} is not responding: {e}")
|
|
return False
|
|
|
|
def get_chain_head(node_name: str, node_config: Dict[str, str]) -> Optional[Dict[str, Any]]:
|
|
"""Get current chain head from node"""
|
|
try:
|
|
response = httpx.get(f"{node_config['url']}/rpc/head", timeout=5)
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
print_error(f"Failed to get chain head from {node_config['name']}: {response.status_code}")
|
|
return None
|
|
except Exception as e:
|
|
print_error(f"Error getting chain head from {node_config['name']}: {e}")
|
|
return None
|
|
|
|
def get_balance(node_name: str, node_config: Dict[str, str], address: str) -> Optional[int]:
|
|
"""Get balance for an address"""
|
|
try:
|
|
response = httpx.get(f"{node_config['url']}/rpc/getBalance/{address}", timeout=5)
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return data.get("balance", 0)
|
|
else:
|
|
print_error(f"Failed to get balance from {node_config['name']}: {response.status_code}")
|
|
return None
|
|
except Exception as e:
|
|
print_error(f"Error getting balance from {node_config['name']}: {e}")
|
|
return None
|
|
|
|
def mint_faucet(node_name: str, node_config: Dict[str, str], address: str, amount: int) -> bool:
|
|
"""Mint tokens to an address (devnet only)"""
|
|
try:
|
|
response = httpx.post(
|
|
f"{node_config['url']}/rpc/admin/mintFaucet",
|
|
json={"address": address, "amount": amount},
|
|
timeout=5
|
|
)
|
|
if response.status_code == 200:
|
|
print_success(f"Minted {amount} tokens to {address} on {node_config['name']}")
|
|
return True
|
|
else:
|
|
print_error(f"Failed to mint on {node_config['name']}: {response.status_code}")
|
|
print(f"Response: {response.text}")
|
|
return False
|
|
except Exception as e:
|
|
print_error(f"Error minting on {node_config['name']}: {e}")
|
|
return False
|
|
|
|
def send_transaction(node_name: str, node_config: Dict[str, str], tx: Dict[str, Any]) -> Optional[str]:
|
|
"""Send a transaction"""
|
|
try:
|
|
response = httpx.post(
|
|
f"{node_config['url']}/rpc/sendTx",
|
|
json=tx,
|
|
timeout=5
|
|
)
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return data.get("tx_hash")
|
|
else:
|
|
print_error(f"Failed to send transaction on {node_config['name']}: {response.status_code}")
|
|
print(f"Response: {response.text}")
|
|
return None
|
|
except Exception as e:
|
|
print_error(f"Error sending transaction on {node_config['name']}: {e}")
|
|
return None
|
|
|
|
def wait_for_block(node_name: str, node_config: Dict[str, str], target_height: int, timeout: int = 30) -> bool:
|
|
"""Wait for node to reach a target block height"""
|
|
start_time = time.time()
|
|
while time.time() - start_time < timeout:
|
|
head = get_chain_head(node_name, node_config)
|
|
if head and head.get("height", 0) >= target_height:
|
|
return True
|
|
time.sleep(1)
|
|
return False
|
|
|
|
def test_node_connectivity():
|
|
"""Test if both nodes are running and responsive"""
|
|
print_header("Testing Node Connectivity")
|
|
|
|
all_healthy = True
|
|
for node_name, node_config in NODES.items():
|
|
if not check_node_health(node_name, node_config):
|
|
all_healthy = False
|
|
|
|
assert all_healthy, "Not all nodes are healthy"
|
|
|
|
def test_chain_consistency():
|
|
"""Test if both nodes have consistent chain heads"""
|
|
print_header("Testing Chain Consistency")
|
|
|
|
heads = {}
|
|
for node_name, node_config in NODES.items():
|
|
print_step(f"Getting chain head from {node_config['name']}")
|
|
head = get_chain_head(node_name, node_config)
|
|
if head:
|
|
heads[node_name] = head
|
|
print(f" Height: {head.get('height', 'unknown')}")
|
|
print(f" Hash: {head.get('hash', 'unknown')[:16]}...")
|
|
else:
|
|
print_error(f"Failed to get chain head from {node_config['name']}")
|
|
|
|
if len(heads) == len(NODES):
|
|
# Compare heights
|
|
heights = [head.get("height", 0) for head in heads.values()]
|
|
if len(set(heights)) == 1:
|
|
print_success("Both nodes have the same block height")
|
|
else:
|
|
print_error(f"Node heights differ: {heights}")
|
|
|
|
# Compare hashes
|
|
hashes = [head.get("hash", "") for head in heads.values()]
|
|
if len(set(hashes)) == 1:
|
|
print_success("Both nodes have the same chain hash")
|
|
else:
|
|
print_warning("Nodes have different chain hashes (may be syncing)")
|
|
|
|
assert len(heads) == len(NODES), "Failed to get chain heads from all nodes"
|
|
|
|
def test_faucet_and_balances():
|
|
"""Test faucet minting and balance queries"""
|
|
print_header("Testing Faucet and Balances")
|
|
|
|
# Test on node1
|
|
print_step("Testing faucet on Node 1")
|
|
if mint_faucet("node1", NODES["node1"], TEST_ADDRESSES["alice"], 1000):
|
|
time.sleep(2) # Wait for block
|
|
|
|
# Check balance on both nodes
|
|
for node_name, node_config in NODES.items():
|
|
balance = get_balance(node_name, node_config, TEST_ADDRESSES["alice"])
|
|
if balance is not None:
|
|
print(f" {node_config['name']} balance for alice: {balance}")
|
|
if balance >= 1000:
|
|
print_success(f"Balance correct on {node_config['name']}")
|
|
else:
|
|
print_error(f"Balance incorrect on {node_config['name']}")
|
|
else:
|
|
print_error(f"Failed to get balance from {node_config['name']}")
|
|
|
|
# Test on node2
|
|
print_step("Testing faucet on Node 2")
|
|
if mint_faucet("node2", NODES["node2"], TEST_ADDRESSES["bob"], 500):
|
|
time.sleep(2) # Wait for block
|
|
|
|
# Check balance on both nodes
|
|
for node_name, node_config in NODES.items():
|
|
balance = get_balance(node_name, node_config, TEST_ADDRESSES["bob"])
|
|
if balance is not None:
|
|
print(f" {node_config['name']} balance for bob: {balance}")
|
|
if balance >= 500:
|
|
print_success(f"Balance correct on {node_config['name']}")
|
|
else:
|
|
print_error(f"Balance incorrect on {node_config['name']}")
|
|
else:
|
|
print_error(f"Failed to get balance from {node_config['name']}")
|
|
|
|
def test_transaction_submission():
|
|
"""Test transaction submission between addresses"""
|
|
print_header("Testing Transaction Submission")
|
|
|
|
# First ensure alice has funds
|
|
print_step("Ensuring alice has funds")
|
|
mint_faucet("node1", NODES["node1"], TEST_ADDRESSES["alice"], 2000)
|
|
time.sleep(2)
|
|
|
|
# Create a transfer transaction (simplified - normally needs proper signing)
|
|
print_step("Submitting transfer transaction")
|
|
tx = {
|
|
"type": "TRANSFER",
|
|
"sender": TEST_ADDRESSES["alice"],
|
|
"nonce": 0,
|
|
"fee": 10,
|
|
"payload": {
|
|
"to": TEST_ADDRESSES["bob"],
|
|
"amount": 100
|
|
},
|
|
"sig": None # In devnet, signature might be optional
|
|
}
|
|
|
|
tx_hash = send_transaction("node1", NODES["node1"], tx)
|
|
if tx_hash:
|
|
print_success(f"Transaction submitted: {tx_hash[:16]}...")
|
|
time.sleep(3) # Wait for inclusion
|
|
|
|
# Check final balances
|
|
print_step("Checking final balances")
|
|
for node_name, node_config in NODES.items():
|
|
alice_balance = get_balance(node_name, node_config, TEST_ADDRESSES["alice"])
|
|
bob_balance = get_balance(node_name, node_config, TEST_ADDRESSES["bob"])
|
|
|
|
if alice_balance is not None and bob_balance is not None:
|
|
print(f" {node_config['name']}: alice={alice_balance}, bob={bob_balance}")
|
|
else:
|
|
print_error("Failed to submit transaction")
|
|
|
|
def test_block_production():
|
|
"""Test that nodes are producing blocks"""
|
|
print_header("Testing Block Production")
|
|
|
|
initial_heights = {}
|
|
for node_name, node_config in NODES.items():
|
|
head = get_chain_head(node_name, node_config)
|
|
if head:
|
|
initial_heights[node_name] = head.get("height", 0)
|
|
print(f" {node_config['name']} initial height: {initial_heights[node_name]}")
|
|
|
|
print_step("Waiting for new blocks...")
|
|
time.sleep(10) # Wait for block production (2s block time)
|
|
|
|
final_heights = {}
|
|
for node_name, node_config in NODES.items():
|
|
head = get_chain_head(node_name, node_config)
|
|
if head:
|
|
final_heights[node_name] = head.get("height", 0)
|
|
print(f" {node_config['name']} final height: {final_heights[node_name]}")
|
|
|
|
# Check if blocks were produced
|
|
for node_name in NODES:
|
|
if node_name in initial_heights and node_name in final_heights:
|
|
produced = final_heights[node_name] - initial_heights[node_name]
|
|
if produced > 0:
|
|
print_success(f"{NODES[node_name]['name']} produced {produced} block(s)")
|
|
else:
|
|
print_error(f"{NODES[node_name]['name']} produced no blocks")
|
|
|
|
def main():
|
|
"""Run all tests"""
|
|
print_header("AITBC Blockchain Node Test Suite")
|
|
|
|
tests = [
|
|
("Node Connectivity", test_node_connectivity),
|
|
("Chain Consistency", test_chain_consistency),
|
|
("Faucet and Balances", test_faucet_and_balances),
|
|
("Transaction Submission", test_transaction_submission),
|
|
("Block Production", test_block_production),
|
|
]
|
|
|
|
results = {}
|
|
for test_name, test_func in tests:
|
|
try:
|
|
results[test_name] = test_func()
|
|
except Exception as e:
|
|
print_error(f"Test '{test_name}' failed with exception: {e}")
|
|
results[test_name] = False
|
|
|
|
# Summary
|
|
print_header("Test Summary")
|
|
passed = sum(1 for result in results.values() if result)
|
|
total = len(results)
|
|
|
|
for test_name, result in results.items():
|
|
status = "✅ PASSED" if result else "❌ FAILED"
|
|
print(f"{test_name:.<40} {status}")
|
|
|
|
print(f"\nOverall: {passed}/{total} tests passed")
|
|
|
|
if passed == total:
|
|
print_success("All tests passed! 🎉")
|
|
return 0
|
|
else:
|
|
print_error("Some tests failed. Check the logs above.")
|
|
return 1
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|