Files
universe/scripts/e2e-exchange.py
T
zeekayandHanzo AI 15131f6af4
CI/CD / render (push) Waiting to run
chain letters are lowercase
The primary-network chains answer to either case; one spelling beats two.
/v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate.

Co-authored-by: Hanzo AI <[email protected]>
2026-09-08 15:23:20 -07:00

741 lines
33 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
E2E Exchange Test: AMM liquidity setup + trading bot + explorer verification.
Creates WLUX/LETH and WLUX/LUSDC pairs on all funded mainnet chains,
adds initial liquidity, runs a trading loop, and verifies the explorer
reflects live data.
Usage:
# Run with deployer key (mnemonic from KMS)
LUX_MNEMONIC="$(kms secrets get LUX_MNEMONIC)" python e2e-exchange.py
# Or with explicit private key
LUX_PRIVATE_KEY=88e3c3... python e2e-exchange.py
# Run continuously as trading bot
LUX_PRIVATE_KEY=88e3c3... python e2e-exchange.py --loop --interval 30
# Skip liquidity setup (already done), just trade
LUX_PRIVATE_KEY=88e3c3... python e2e-exchange.py --trade-only
# Dry-run: report state without sending txs
python e2e-exchange.py --dry-run
"""
import argparse
import os
import sys
import time
import json
import urllib.request
from typing import Optional
try:
from web3 import Web3
from eth_account import Account
try:
from web3.middleware import ExtraDataToPOAMiddleware # web3 >= 6
_POA_MIDDLEWARE = ExtraDataToPOAMiddleware
except ImportError:
try:
from web3.middleware import geth_poa_middleware # web3 < 6
_POA_MIDDLEWARE = geth_poa_middleware
except ImportError:
_POA_MIDDLEWARE = None
except ImportError:
print("Install deps: pip install web3 eth-account")
sys.exit(1)
Account.enable_unaudited_hdwallet_features()
# ---------------------------------------------------------------------------
# Chain configuration (mainnet)
# ---------------------------------------------------------------------------
CHAINS = {
"C-Chain": {
"chain_id": 96369,
"rpc": "http://localhost:19630/v1/chain/c/rpc",
"explorer": "https://api-explore.lux.network",
# Addresses generated by deployer 0xEAbCC110... starting at nonce=5
# (deployer had 5 txns in RLP import history before these deployments)
"wlux": "0xf295Bd72A08CAd1EfDAC25c204832220AE6E26cE",
"leth": "0x2d1D573097A1106EeFD9f07B9eaA79BA55D09fc2",
"lbtc": "0xAB95c8b59f68ce922f2f334dFc8bb8f5b0525326",
"lusdc": "0xF85CF66Fd0189C435033056edeC5e525F39374a6",
"factory": "0x84CF0A13db1be8e1F0676405cfcBC8b09692FD1C",
"router": "0x2382F7A49FA48E1F91Bec466c32e1D7F13ec8206",
},
# App chains: all use shared addresses (deployer had nonce=0 on each fresh chain)
"Zoo": {
"chain_id": 200200,
"rpc": "http://localhost:19630/v1/chain/2Y625Kvdd4wZ5LKx9kLSf1jGALyARmXVN1YsTb4F3Hzv4bPDbp/rpc",
"explorer": "https://api-explore-zoo.lux.network",
"wlux": "0x548f54dfb32ea6ce4fa3515236696cf3d1b7d26a",
"leth": "0xe0f7e9a0cb1688cca453995fd6e19ae4fbd9cbfd",
"lbtc": "0x7d7cc8d05bb0f38d80b5ce44b4b069a6fb769468",
"lusdc": "0xc5e4a6f54be469551a342872c1ab83ab46f61b22",
"factory": "0x84cf0a13db1be8e1f0676405cfcbc8b09692fd1c",
"router": "0x2382f7a49fa48e1f91bec466c32e1d7f13ec8206",
},
"Hanzo": {
"chain_id": 36963,
# v5 blockchain ID (redeployed 2026-03-01)
"rpc": "http://localhost:19630/v1/chain/2GiQb73CeJESjc4omFv2YtQHZrRgJf25NXPzAr5J6UNHRcDV2F/rpc",
"explorer": "https://api-explore-hanzo.lux.network",
"wlux": "0x548f54dfb32ea6ce4fa3515236696cf3d1b7d26a",
"leth": "0xe0f7e9a0cb1688cca453995fd6e19ae4fbd9cbfd",
"lbtc": "0x7d7cc8d05bb0f38d80b5ce44b4b069a6fb769468",
"lusdc": "0xc5e4a6f54be469551a342872c1ab83ab46f61b22",
"factory": "0x84cf0a13db1be8e1f0676405cfcbc8b09692fd1c",
"router": "0x2382f7a49fa48e1f91bec466c32e1d7f13ec8206",
},
"SPC": {
"chain_id": 36911,
# v5 blockchain ID
"rpc": "http://localhost:19630/v1/chain/rtjwvtE1tEvrokmpeYdTq7b2zqZgmybKwR5MLjKMGAR1W78dQ/rpc",
"explorer": "https://api-explore-spc.lux.network",
"wlux": "0x548f54dfb32ea6ce4fa3515236696cf3d1b7d26a",
"leth": "0xe0f7e9a0cb1688cca453995fd6e19ae4fbd9cbfd",
"lbtc": "0x7d7cc8d05bb0f38d80b5ce44b4b069a6fb769468",
"lusdc": "0xc5e4a6f54be469551a342872c1ab83ab46f61b22",
"factory": "0x84cf0a13db1be8e1f0676405cfcbc8b09692fd1c",
"router": "0x2382f7a49fa48e1f91bec466c32e1d7f13ec8206",
},
"Pars": {
"chain_id": 494949, # v5 chain ID (was 7070 on old chains)
# v5 blockchain ID (redeployed 2026-03-01)
"rpc": "http://localhost:19630/v1/chain/2pUskxqaL5Bpx7uRUGG1fDjPckjxQ4UKX4sLKeaS1NdSVBJd3F/rpc",
"explorer": "https://api-explore-pars.lux.network",
"wlux": "0x548f54dfb32ea6ce4fa3515236696cf3d1b7d26a",
"leth": "0xe0f7e9a0cb1688cca453995fd6e19ae4fbd9cbfd",
"lbtc": "0x7d7cc8d05bb0f38d80b5ce44b4b069a6fb769468",
"lusdc": "0xc5e4a6f54be469551a342872c1ab83ab46f61b22",
"factory": "0x84cf0a13db1be8e1f0676405cfcbc8b09692fd1c",
"router": "0x2382f7a49fa48e1f91bec466c32e1d7f13ec8206",
},
}
# Minimum native LUX required to participate in exchange setup
MIN_NATIVE_LUX = 10 # LUX (app chains have huge balances; C-Chain deployer may be low)
# KNOWN LIMITATION: AMM swaps fail on app chain EVMs (Zoo, Hanzo, Pars, SPC) with
# "invalid opcode: MCOPY" because the AMM router was compiled with Solidity >=0.8.24
# which uses the Cancun EIP-5656 MCOPY opcode. App chain EVMs do not yet support Cancun.
# Workaround: Recompile AMM contracts with Solidity <=0.8.23 and redeploy.
# C-Chain swaps work since C-Chain EVM supports Cancun.
APPCHAIN_SWAP_BROKEN = True # swaps on Hanzo/Pars chains will revert
# ---------------------------------------------------------------------------
# ABIs (minimal)
# ---------------------------------------------------------------------------
ERC20_ABI = [
{"name": "balanceOf", "type": "function", "inputs": [{"name": "account", "type": "address"}], "outputs": [{"type": "uint256"}], "stateMutability": "view"},
{"name": "totalSupply", "type": "function", "inputs": [], "outputs": [{"type": "uint256"}], "stateMutability": "view"},
{"name": "approve", "type": "function", "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": [{"type": "bool"}], "stateMutability": "nonpayable"},
{"name": "allowance", "type": "function", "inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "outputs": [{"type": "uint256"}], "stateMutability": "view"},
{"name": "mint", "type": "function", "inputs": [{"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": [], "stateMutability": "nonpayable"},
{"name": "owner", "type": "function", "inputs": [], "outputs": [{"type": "address"}], "stateMutability": "view"},
]
WLUX_ABI = ERC20_ABI + [
{"name": "deposit", "type": "function", "inputs": [], "outputs": [], "stateMutability": "payable"},
{"name": "withdraw", "type": "function", "inputs": [{"name": "wad", "type": "uint256"}], "outputs": [], "stateMutability": "nonpayable"},
]
FACTORY_ABI = [
{"name": "getPair", "type": "function", "inputs": [{"name": "tokenA", "type": "address"}, {"name": "tokenB", "type": "address"}], "outputs": [{"type": "address"}], "stateMutability": "view"},
{"name": "createPair", "type": "function", "inputs": [{"name": "tokenA", "type": "address"}, {"name": "tokenB", "type": "address"}], "outputs": [{"type": "address"}], "stateMutability": "nonpayable"},
{"name": "allPairsLength", "type": "function", "inputs": [], "outputs": [{"type": "uint256"}], "stateMutability": "view"},
]
ROUTER_ABI = [
{"name": "addLiquidityETH", "type": "function",
"inputs": [{"name": "token", "type": "address"}, {"name": "amountTokenDesired", "type": "uint256"},
{"name": "amountTokenMin", "type": "uint256"}, {"name": "amountETHMin", "type": "uint256"},
{"name": "to", "type": "address"}, {"name": "deadline", "type": "uint256"}],
"outputs": [{"type": "uint256"}, {"type": "uint256"}, {"type": "uint256"}], "stateMutability": "payable"},
{"name": "addLiquidity", "type": "function",
"inputs": [{"name": "tokenA", "type": "address"}, {"name": "tokenB", "type": "address"},
{"name": "amountADesired", "type": "uint256"}, {"name": "amountBDesired", "type": "uint256"},
{"name": "amountAMin", "type": "uint256"}, {"name": "amountBMin", "type": "uint256"},
{"name": "to", "type": "address"}, {"name": "deadline", "type": "uint256"}],
"outputs": [{"type": "uint256"}, {"type": "uint256"}, {"type": "uint256"}], "stateMutability": "nonpayable"},
{"name": "swapExactTokensForTokens", "type": "function",
"inputs": [{"name": "amountIn", "type": "uint256"}, {"name": "amountOutMin", "type": "uint256"},
{"name": "path", "type": "address[]"}, {"name": "to", "type": "address"}, {"name": "deadline", "type": "uint256"}],
"outputs": [{"type": "uint256[]"}], "stateMutability": "nonpayable"},
{"name": "swapExactETHForTokens", "type": "function",
"inputs": [{"name": "amountOutMin", "type": "uint256"}, {"name": "path", "type": "address[]"},
{"name": "to", "type": "address"}, {"name": "deadline", "type": "uint256"}],
"outputs": [{"type": "uint256[]"}], "stateMutability": "payable"},
{"name": "getAmountsOut", "type": "function",
"inputs": [{"name": "amountIn", "type": "uint256"}, {"name": "path", "type": "address[]"}],
"outputs": [{"type": "uint256[]"}], "stateMutability": "view"},
{"name": "WETH", "type": "function", "inputs": [], "outputs": [{"type": "address"}], "stateMutability": "pure"},
]
PAIR_ABI = [
{"name": "getReserves", "type": "function", "inputs": [],
"outputs": [{"type": "uint112"}, {"type": "uint112"}, {"type": "uint32"}], "stateMutability": "view"},
{"name": "token0", "type": "function", "inputs": [], "outputs": [{"type": "address"}], "stateMutability": "view"},
{"name": "token1", "type": "function", "inputs": [], "outputs": [{"type": "address"}], "stateMutability": "view"},
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
def wei(n: float) -> int:
return int(n * 1e18)
def from_wei(n: int) -> float:
return n / 1e18
def send_tx(w3: Web3, account, tx: dict, label: str = "") -> Optional[str]:
"""Sign, send, and wait for a transaction. Returns tx hash or None."""
tx.setdefault("from", account.address)
tx.setdefault("nonce", w3.eth.get_transaction_count(account.address))
tx.setdefault("chainId", w3.eth.chain_id)
if "gasPrice" not in tx and "maxFeePerGas" not in tx:
tx["gasPrice"] = w3.eth.gas_price
if "gas" not in tx:
try:
tx["gas"] = int(w3.eth.estimate_gas(tx) * 1.3)
except Exception:
tx["gas"] = 500_000
signed = account.sign_transaction(tx)
raw = getattr(signed, "raw_transaction", None) or getattr(signed, "rawTransaction", None)
try:
h = w3.eth.send_raw_transaction(raw)
receipt = w3.eth.wait_for_transaction_receipt(h, timeout=60)
status = "✅" if receipt.status == 1 else "❌"
print(f" {status} {label}: {h.hex()[:16]}... (block {receipt.blockNumber})")
return h.hex() if receipt.status == 1 else None
except Exception as e:
print(f" ❌ {label}: {e}")
return None
def check_explorer(name: str, cfg: dict) -> dict:
"""Fetch stats from blockscout API. Returns dict with block/tx counts."""
url = f"{cfg['explorer']}/api/v2/stats"
try:
req = urllib.request.Request(url, headers={"Accept": "application/json"})
data = json.loads(urllib.request.urlopen(req, timeout=10).read())
return {
"total_blocks": data.get("total_blocks", "?"),
"total_txs": data.get("total_transactions", "?"),
"total_addresses": data.get("total_addresses", "?"),
}
except Exception as e:
return {"error": str(e)}
# ---------------------------------------------------------------------------
# Core operations
# ---------------------------------------------------------------------------
def setup_chain_liquidity(name: str, cfg: dict, account, dry_run: bool = False) -> dict:
"""
Wrap LUX → WLUX, try to mint LETH, create pairs, add liquidity.
Returns dict with pair addresses and status.
"""
print(f"\n{'='*60}")
print(f" {name} (chainId={cfg['chain_id']})")
print(f"{'='*60}")
w3 = Web3(Web3.HTTPProvider(cfg["rpc"], request_kwargs={"timeout": 30}))
if _POA_MIDDLEWARE:
w3.middleware_onion.inject(_POA_MIDDLEWARE, layer=0)
if not w3.is_connected():
print(f" ⚠️ Cannot connect to {cfg['rpc']}")
return {"status": "unreachable"}
addr = account.address
native_bal = from_wei(w3.eth.get_balance(addr))
print(f" Native LUX: {native_bal:,.4f}")
if native_bal < MIN_NATIVE_LUX:
print(f" ⚠️ Insufficient LUX (need {MIN_NATIVE_LUX}), skipping setup")
return {"status": "insufficient_funds"}
# Normalize all addresses to checksum form up-front
factory_addr = Web3.to_checksum_address(cfg["factory"])
router_addr = Web3.to_checksum_address(cfg["router"])
wlux_addr = Web3.to_checksum_address(cfg["wlux"])
leth_addr = Web3.to_checksum_address(cfg["leth"])
lusdc_addr = Web3.to_checksum_address(cfg["lusdc"])
factory = w3.eth.contract(address=factory_addr, abi=FACTORY_ABI)
router = w3.eth.contract(address=router_addr, abi=ROUTER_ABI)
wlux = w3.eth.contract(address=wlux_addr, abi=WLUX_ABI)
leth = w3.eth.contract(address=leth_addr, abi=ERC20_ABI)
lusdc = w3.eth.contract(address=lusdc_addr, abi=ERC20_ABI)
wlux_bal = from_wei(wlux.functions.balanceOf(addr).call())
leth_bal = from_wei(leth.functions.balanceOf(addr).call())
lusdc_bal = from_wei(lusdc.functions.balanceOf(addr).call())
print(f" WLUX bal: {wlux_bal:,.4f} | LETH bal: {leth_bal:,.4f} | LUSDC bal: {lusdc_bal:,.4f}")
# 1. Wrap LUX → WLUX (keep 50 LUX for gas)
wrap_amount = min(native_bal - 50, 10_000) # wrap up to 10K LUX
if wrap_amount > 0 and wlux_bal < 1_000:
print(f" → Wrapping {wrap_amount:.2f} LUX → WLUX")
if not dry_run:
tx = wlux.functions.deposit().build_transaction({
"value": wei(wrap_amount),
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 100_000,
})
send_tx(w3, account, tx, "wrap LUX→WLUX")
wlux_bal = from_wei(wlux.functions.balanceOf(addr).call())
print(f" WLUX bal after wrap: {wlux_bal:,.4f}")
# 2. Try to mint LETH if deployer is owner
leth_needed = 5_000
if leth_bal < leth_needed:
try:
owner = leth.functions.owner().call()
if owner.lower() == addr.lower():
print(f" → Minting {leth_needed} LETH (deployer is owner)")
if not dry_run:
tx = leth.functions.mint(addr, wei(leth_needed)).build_transaction({
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 200_000,
})
send_tx(w3, account, tx, "mint LETH")
leth_bal = from_wei(leth.functions.balanceOf(addr).call())
except Exception:
print(f" ℹ️ LETH not mintable by deployer (total supply = {from_wei(leth.functions.totalSupply().call()):.0f})")
# 3. Try to mint LUSDC if deployer is owner
lusdc_needed = 1_000_000
if lusdc_bal < lusdc_needed:
try:
owner_l = lusdc.functions.owner().call()
if owner_l.lower() == addr.lower():
print(f" → Minting {lusdc_needed:,} LUSDC (deployer is owner)")
if not dry_run:
tx = lusdc.functions.mint(addr, wei(lusdc_needed)).build_transaction({
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 200_000,
})
send_tx(w3, account, tx, "mint LUSDC")
lusdc_bal = from_wei(lusdc.functions.balanceOf(addr).call())
except Exception:
pass
# Refresh after mints
wlux_bal = from_wei(wlux.functions.balanceOf(addr).call())
leth_bal = from_wei(leth.functions.balanceOf(addr).call())
lusdc_bal = from_wei(lusdc.functions.balanceOf(addr).call())
pairs_created = {}
# 4. WLUX/LETH pair
if leth_bal >= 100 and wlux_bal >= 100:
pair_addr = factory.functions.getPair(
wlux_addr,
leth_addr
).call()
if pair_addr == ZERO_ADDRESS:
print(" → Creating WLUX/LETH pair")
if not dry_run:
tx = factory.functions.createPair(
wlux_addr,
leth_addr
).build_transaction({
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 3_000_000,
})
send_tx(w3, account, tx, "createPair WLUX/LETH")
pair_addr = factory.functions.getPair(
Web3.to_checksum_address(cfg["wlux"]),
Web3.to_checksum_address(cfg["leth"])
).call()
else:
print(f" ✅ WLUX/LETH pair exists: {pair_addr}")
# Add liquidity: 1000 WLUX + 1000 LETH (1:1 initial price)
if pair_addr != ZERO_ADDRESS and not dry_run:
liq_wlux = min(wlux_bal * 0.1, 1_000)
liq_leth = min(leth_bal * 0.1, 1_000)
if liq_wlux > 0 and liq_leth > 0:
# Approve both tokens
for (token_c, token_name, amount) in [
(wlux, "WLUX", liq_wlux), (leth, "LETH", liq_leth)
]:
allow = from_wei(token_c.functions.allowance(addr, router_addr).call())
if allow < amount:
tx = token_c.functions.approve(
router_addr, wei(amount * 2)
).build_transaction({
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 100_000,
})
send_tx(w3, account, tx, f"approve {token_name}→router")
deadline = int(time.time()) + 300
tx = router.functions.addLiquidity(
wlux_addr,
leth_addr,
wei(liq_wlux), wei(liq_leth),
wei(liq_wlux * 0.99), wei(liq_leth * 0.99),
addr, deadline
).build_transaction({
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 500_000,
})
send_tx(w3, account, tx, f"addLiquidity WLUX/LETH ({liq_wlux:.0f}+{liq_leth:.0f})")
pairs_created["WLUX/LETH"] = pair_addr
else:
print(f" ⚠️ Skipping WLUX/LETH pair: wlux={wlux_bal:.0f} leth={leth_bal:.0f}")
# 5. WLUX/LUSDC pair (use 1 WLUX = 0.01 LUSDC price roughly, or 1:1 for simplicity)
if lusdc_bal >= 10_000 and wlux_bal >= 100:
pair_lusdc = factory.functions.getPair(
wlux_addr,
lusdc_addr
).call()
if pair_lusdc == ZERO_ADDRESS:
print(" → Creating WLUX/LUSDC pair")
if not dry_run:
tx = factory.functions.createPair(
wlux_addr,
lusdc_addr
).build_transaction({
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 3_000_000,
})
send_tx(w3, account, tx, "createPair WLUX/LUSDC")
pair_lusdc = factory.functions.getPair(
Web3.to_checksum_address(cfg["wlux"]),
Web3.to_checksum_address(cfg["lusdc"])
).call()
else:
print(f" ✅ WLUX/LUSDC pair exists: {pair_lusdc}")
if pair_lusdc != ZERO_ADDRESS and not dry_run:
liq_wlux2 = min(wlux_bal * 0.05, 500)
liq_lusdc = min(lusdc_bal * 0.01, 50_000)
if liq_wlux2 > 0 and liq_lusdc > 0:
for (token_c, token_name, amount) in [
(wlux, "WLUX", liq_wlux2), (lusdc, "LUSDC", liq_lusdc)
]:
allow = from_wei(token_c.functions.allowance(addr, router_addr).call())
if allow < amount:
tx = token_c.functions.approve(
router_addr, wei(amount * 2)
).build_transaction({
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 100_000,
})
send_tx(w3, account, tx, f"approve {token_name}→router")
deadline = int(time.time()) + 300
tx = router.functions.addLiquidity(
wlux_addr,
lusdc_addr,
wei(liq_wlux2), wei(liq_lusdc),
wei(liq_wlux2 * 0.99), wei(liq_lusdc * 0.99),
addr, deadline
).build_transaction({
"from": addr,
"nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"],
"gasPrice": w3.eth.gas_price,
"gas": 500_000,
})
send_tx(w3, account, tx, f"addLiquidity WLUX/LUSDC ({liq_wlux2:.0f}+{liq_lusdc:.0f})")
pairs_created["WLUX/LUSDC"] = pair_lusdc
total_pairs = factory.functions.allPairsLength().call()
print(f" Total AMM pairs on {name}: {total_pairs}")
return {"status": "ok", "pairs": pairs_created, "total_pairs": total_pairs}
def trade_on_chain(name: str, cfg: dict, account, swap_amount_lux: float = 10.0) -> bool:
"""
Execute one round of swaps: WLUX→LETH then LETH→WLUX.
Returns True if any swap succeeded.
NOTE: App chain EVMs (Hanzo, Pars, Zoo, SPC) fail with MCOPY opcode.
Only C-Chain supports Cancun opcodes needed by the AMM router.
"""
# App chains use AMM contracts compiled with Solidity >=0.8.24 (MCOPY opcode).
# App chain EVMs don't support Cancun; skip swaps until contracts are recompiled.
if APPCHAIN_SWAP_BROKEN and name != "C-Chain":
print(f" [{name}] Skipping swap (MCOPY not supported on app chain EVM — recompile AMM with Solidity <=0.8.23)")
return False
w3 = Web3(Web3.HTTPProvider(cfg["rpc"], request_kwargs={"timeout": 30}))
if _POA_MIDDLEWARE:
w3.middleware_onion.inject(_POA_MIDDLEWARE, layer=0)
if not w3.is_connected():
return False
addr = account.address
router_addr = Web3.to_checksum_address(cfg["router"])
wlux_addr = Web3.to_checksum_address(cfg["wlux"])
leth_addr = Web3.to_checksum_address(cfg["leth"])
factory_addr = Web3.to_checksum_address(cfg["factory"])
router = w3.eth.contract(address=router_addr, abi=ROUTER_ABI)
wlux = w3.eth.contract(address=wlux_addr, abi=WLUX_ABI)
leth = w3.eth.contract(address=leth_addr, abi=ERC20_ABI)
factory = w3.eth.contract(address=factory_addr, abi=FACTORY_ABI)
# Check WLUX/LETH pair exists
pair_addr = factory.functions.getPair(wlux_addr, leth_addr).call()
if pair_addr == ZERO_ADDRESS:
print(f" [{name}] No WLUX/LETH pair, skipping trade")
return False
wlux_bal = from_wei(wlux.functions.balanceOf(addr).call())
leth_bal = from_wei(leth.functions.balanceOf(addr).call())
success = False
deadline = int(time.time()) + 120
# WLUX → LETH
if wlux_bal >= swap_amount_lux:
path = [wlux_addr, leth_addr]
try:
amounts_out = router.functions.getAmountsOut(wei(swap_amount_lux), path).call()
min_out = int(amounts_out[-1] * 0.99)
# Approve
allow = wlux.functions.allowance(addr, router_addr).call()
if allow < wei(swap_amount_lux):
tx = wlux.functions.approve(router_addr, wei(swap_amount_lux * 100)).build_transaction({
"from": addr, "nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"], "gasPrice": w3.eth.gas_price, "gas": 100_000,
})
send_tx(w3, account, tx, f"[{name}] approve WLUX")
tx = router.functions.swapExactTokensForTokens(
wei(swap_amount_lux), min_out, path, addr, deadline
).build_transaction({
"from": addr, "nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"], "gasPrice": w3.eth.gas_price, "gas": 300_000,
})
result = send_tx(w3, account, tx, f"[{name}] swap {swap_amount_lux} WLUX→LETH")
if result:
success = True
except Exception as e:
print(f" [{name}] WLUX→LETH failed: {e}")
# LETH → WLUX (swap back ~half)
leth_bal = from_wei(leth.functions.balanceOf(addr).call())
swap_back = leth_bal * 0.5
if leth_bal >= 1:
path2 = [leth_addr, wlux_addr]
try:
amounts_out2 = router.functions.getAmountsOut(wei(swap_back), path2).call()
min_out2 = int(amounts_out2[-1] * 0.99)
allow2 = leth.functions.allowance(addr, router_addr).call()
if allow2 < wei(swap_back):
tx = leth.functions.approve(router_addr, wei(swap_back * 100)).build_transaction({
"from": addr, "nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"], "gasPrice": w3.eth.gas_price, "gas": 100_000,
})
send_tx(w3, account, tx, f"[{name}] approve LETH")
tx = router.functions.swapExactTokensForTokens(
wei(swap_back), min_out2, path2, addr, deadline
).build_transaction({
"from": addr, "nonce": w3.eth.get_transaction_count(addr),
"chainId": cfg["chain_id"], "gasPrice": w3.eth.gas_price, "gas": 300_000,
})
result2 = send_tx(w3, account, tx, f"[{name}] swap {swap_back:.2f} LETH→WLUX")
if result2:
success = True
except Exception as e:
print(f" [{name}] LETH→WLUX failed: {e}")
return success
def verify_explorers() -> None:
"""Check all available explorer APIs and print stats."""
print("\n=== Explorer Verification ===")
all_chains = {
"Lux C-Chain": CHAINS["C-Chain"],
"Zoo": CHAINS["Zoo"],
"Hanzo": CHAINS["Hanzo"],
"SPC": CHAINS["SPC"],
"Pars": CHAINS["Pars"],
}
for name, cfg in all_chains.items():
stats = check_explorer(name, cfg)
if "error" in stats:
print(f" ❌ {name}: {stats['error']}")
else:
print(f" ✅ {name}: {stats['total_blocks']} blocks | "
f"{stats['total_txs']} txs | {stats['total_addresses']} addresses")
def print_pair_info(name: str, cfg: dict) -> None:
"""Print AMM pair reserves."""
w3 = Web3(Web3.HTTPProvider(cfg["rpc"], request_kwargs={"timeout": 10}))
if _POA_MIDDLEWARE:
w3.middleware_onion.inject(_POA_MIDDLEWARE, layer=0)
if not w3.is_connected():
return
factory = w3.eth.contract(address=Web3.to_checksum_address(cfg["factory"]), abi=FACTORY_ABI)
print(f"\n {name} AMM pairs:")
total = factory.functions.allPairsLength().call()
print(f" Total pairs: {total}")
for (tA, tB, label) in [
(cfg["wlux"], cfg["leth"], "WLUX/LETH"),
(cfg["wlux"], cfg["lusdc"], "WLUX/LUSDC"),
]:
pair_addr = factory.functions.getPair(
Web3.to_checksum_address(tA),
Web3.to_checksum_address(tB),
).call()
if pair_addr == ZERO_ADDRESS:
print(f" {label}: no pair")
else:
pair = w3.eth.contract(address=pair_addr, abi=PAIR_ABI)
r0, r1, _ = pair.functions.getReserves().call()
print(f" {label}: {from_wei(r0):.4f} / {from_wei(r1):.4f} (pair={pair_addr[:10]}...)")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def load_account() -> Account:
pk = os.environ.get("LUX_PRIVATE_KEY")
if pk:
return Account.from_key(pk if pk.startswith("0x") else "0x" + pk)
mnemonic = os.environ.get("LUX_MNEMONIC")
if mnemonic:
return Account.from_mnemonic(mnemonic, account_path="m/44'/60'/0'/0/1")
# Default: use the known deploy key
default_key = "~/.lux/keys/testnet-idx1/ec/private.key"
path = os.path.expanduser(default_key)
if os.path.exists(path):
with open(path) as f:
raw = f.read().strip()
return Account.from_key("0x" + raw)
print("Set LUX_PRIVATE_KEY or LUX_MNEMONIC env var")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Lux Exchange E2E Test")
parser.add_argument("--dry-run", action="store_true", help="Report state, no txs")
parser.add_argument("--trade-only", action="store_true", help="Skip setup, just trade")
parser.add_argument("--loop", action="store_true", help="Run trading loop continuously")
parser.add_argument("--interval", type=int, default=30, help="Seconds between trade rounds")
parser.add_argument("--swap-amount", type=float, default=10.0, help="WLUX per swap")
parser.add_argument("--chains", nargs="+", choices=list(CHAINS.keys()), help="Limit to specific chains")
args = parser.parse_args()
account = load_account()
print(f"\n🔑 Trader: {account.address}")
target_chains = {k: v for k, v in CHAINS.items() if not args.chains or k in args.chains}
if not args.trade_only and not args.dry_run:
print("\n=== Setting up AMM liquidity on all chains ===")
for name, cfg in target_chains.items():
setup_chain_liquidity(name, cfg, account, dry_run=False)
elif args.dry_run:
print("\n=== Dry run — reporting current state ===")
for name, cfg in target_chains.items():
native_bal = 0
wlux_bal = 0
try:
w3 = Web3(Web3.HTTPProvider(cfg["rpc"], request_kwargs={"timeout": 10}))
if _POA_MIDDLEWARE:
w3.middleware_onion.inject(_POA_MIDDLEWARE, layer=0)
if w3.is_connected():
native_bal = from_wei(w3.eth.get_balance(account.address))
wlux = w3.eth.contract(address=Web3.to_checksum_address(cfg["wlux"]), abi=WLUX_ABI)
wlux_bal = from_wei(wlux.functions.balanceOf(account.address).call())
except Exception:
pass
print(f" {name}: {native_bal:,.2f} LUX native, {wlux_bal:,.2f} WLUX")
print_pair_info(name, cfg)
verify_explorers()
if args.dry_run:
return
# Trading loop
round_num = 0
while True:
round_num += 1
print(f"\n{'─'*60}")
print(f"🔄 Trade round {round_num} [{time.strftime('%H:%M:%S')}]")
print(f"{'─'*60}")
any_success = False
for name, cfg in target_chains.items():
ok = trade_on_chain(name, cfg, account, swap_amount_lux=args.swap_amount)
if ok:
any_success = True
if not any_success:
print("⚠️ No swaps succeeded this round (pairs may not have liquidity yet)")
if not args.loop:
break
print(f"\n Sleeping {args.interval}s until next round...")
time.sleep(args.interval)
verify_explorers()
print("\n✅ Done")
if __name__ == "__main__":
main()