import os import time import json import re from web3 import Web3 from eth_account import Account from dotenv import load_dotenv # --- Helper Functions --- def clean_address(addr): return re.sub(r'[^0-9a-fA-FxX]', '', addr) def price_from_sqrt_price_x96(sqrt_price_x96, token0_decimals, token1_decimals): price = (sqrt_price_x96 / (2**96))**2 # Adjust for token decimals assuming price is Token1 per Token0 price = price * (10**(token0_decimals - token1_decimals)) return price def price_from_tick(tick, token0_decimals, token1_decimals): price = 1.0001**tick # Adjust for token decimals assuming price is Token1 per Token0 price = price * (10**(token0_decimals - token1_decimals)) return price def from_wei(amount, decimals): return amount / (10**decimals) # --- V3 Math Helpers --- def get_sqrt_ratio_at_tick(tick): # Returns sqrt(price) as a Q96 number return int((1.0001 ** (tick / 2)) * (2 ** 96)) def get_liquidity_for_amount0(sqrt_ratio_a, sqrt_ratio_b, amount0): # This function is not used directly in the current calculate_mint_amounts logic, # but is a common V3 helper if sqrt_ratio_a > sqrt_ratio_b: sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a # This formula is for a single-sided deposit when current price is outside the range return int(amount0 * sqrt_ratio_a * sqrt_ratio_b / (sqrt_ratio_b - sqrt_ratio_a)) def get_liquidity_for_amount1(sqrt_ratio_a, sqrt_ratio_b, amount1): # This function is not used directly in the current calculate_mint_amounts logic, # but is a common V3 helper if sqrt_ratio_a > sqrt_ratio_b: sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a # This formula is for a single-sided deposit when current price is outside the range return int(amount1 / (sqrt_ratio_b - sqrt_ratio_a)) def get_amounts_for_liquidity(sqrt_ratio_current, sqrt_ratio_a, sqrt_ratio_b, liquidity): # Calculates the required amount of token0 and token1 for a given liquidity and price range if sqrt_ratio_a > sqrt_ratio_b: sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a amount0 = 0 amount1 = 0 Q96 = 1 << 96 # 2^96 # Current price below the lower tick boundary if sqrt_ratio_current <= sqrt_ratio_a: amount0 = ((liquidity * Q96) // sqrt_ratio_a) - ((liquidity * Q96) // sqrt_ratio_b) amount1 = 0 # Current price within the range elif sqrt_ratio_current < sqrt_ratio_b: amount0 = ((liquidity * Q96) // sqrt_ratio_current) - ((liquidity * Q96) // sqrt_ratio_b) amount1 = (liquidity * (sqrt_ratio_current - sqrt_ratio_a)) // Q96 # Current price above the upper tick boundary else: amount1 = (liquidity * (sqrt_ratio_b - sqrt_ratio_a)) // Q96 amount0 = 0 return amount0, amount1 # --- Configuration --- # RPC URL and Private Key are loaded from .env RPC_URL = os.environ.get("MAINNET_RPC_URL") PRIVATE_KEY = os.environ.get("MAIN_WALLET_PRIVATE_KEY") or os.environ.get("PRIVATE_KEY") # Script behavior flags MONITOR_INTERVAL_SECONDS = 451 COLLECT_FEES_ENABLED = False # If True, will attempt to collect fees once and exit if no open auto position CLOSE_POSITION_ENABLED = True # If True, will attempt to close auto position when out of range CLOSE_IF_OUT_OF_RANGE_ONLY = True # If True, closes only if out of range; if False, closes immediately OPEN_POSITION_ENABLED = True # If True, will open a new position if no auto position exists REBALANCE_ON_CLOSE_BELOW_RANGE = False # If True, will sell 50% of WETH to USDC when closing below range # New Position Parameters TARGET_INVESTMENT_VALUE_TOKEN1 = 2000.0 # Target total investment value in Token1 terms (e.g. 350 USDC) RANGE_WIDTH_PCT = 0.01 # +/- 2% range for new positions # JSON File for tracking position state STATUS_FILE = "hedge_status.json" # --- JSON State Helpers --- def get_active_automatic_position(): """Reads hedge_status.json and returns the first OPEN AUTOMATIC position dict, or None.""" if not os.path.exists(STATUS_FILE): return None try: with open(STATUS_FILE, 'r') as f: data = json.load(f) for entry in data: if entry.get('type') == 'AUTOMATIC' and entry.get('status') == 'OPEN': return entry except Exception as e: print(f"ERROR reading status file: {e}") return None def get_all_open_positions(): """Reads hedge_status.json and returns a list of all OPEN positions (Manual and Automatic).""" if not os.path.exists(STATUS_FILE): return [] try: with open(STATUS_FILE, 'r') as f: data = json.load(f) return [entry for entry in data if entry.get('status') == 'OPEN'] except Exception as e: print(f"ERROR reading status file: {e}") return [] def update_hedge_status_file(action, position_data): """ Updates the hedge_status.json file. action: "OPEN" or "CLOSE" position_data: Dict containing details (token_id, entry_price, range, etc.) """ current_data = [] if os.path.exists(STATUS_FILE): try: with open(STATUS_FILE, 'r') as f: current_data = json.load(f) except: current_data = [] if action == "OPEN": # Format Timestamp open_ts = int(time.time()) opened_str = time.strftime('%H:%M %d/%m/%y', time.localtime(open_ts)) # Scale Amounts raw_amt0 = position_data.get('amount0_initial', 0) raw_amt1 = position_data.get('amount1_initial', 0) # Handle if they are already scaled (unlikely here, but safe) if raw_amt0 > 1000: fmt_amt0 = round(raw_amt0 / 10**18, 4) else: fmt_amt0 = round(raw_amt0, 4) if raw_amt1 > 1000: fmt_amt1 = round(raw_amt1 / 10**6, 2) else: fmt_amt1 = round(raw_amt1, 2) new_entry = { "type": "AUTOMATIC", "token_id": position_data['token_id'], "opened": opened_str, "status": "OPEN", "entry_price": round(position_data['entry_price'], 2), "target_value": round(position_data.get('target_value', 0.0), 2), "amount0_initial": fmt_amt0, "amount1_initial": fmt_amt1, "range_upper": round(position_data['range_upper'], 2), # Zones (if present in position_data, otherwise None/Skip) "zone_top_start_price": round(position_data['zone_top_start_price'], 2) if 'zone_top_start_price' in position_data else None, "zone_close_top_price": round(position_data['zone_close_end_price'], 2) if 'zone_close_end_price' in position_data else None, "zone_close_bottom_price": round(position_data['zone_close_start_price'], 2) if 'zone_close_start_price' in position_data else None, "zone_bottom_limit_price": round(position_data['zone_bottom_limit_price'], 2) if 'zone_bottom_limit_price' in position_data else None, "range_lower": round(position_data['range_lower'], 2), "static_long": 0.0, "timestamp_open": open_ts, "timestamp_close": None } # Remove None keys to keep it clean? Or keep structure? # User wants specific structure. current_data.append(new_entry) print(f"Recorded new AUTOMATIC position {position_data['token_id']} in {STATUS_FILE}") elif action == "CLOSE": found = False for entry in current_data: if ( entry.get('type') == "AUTOMATIC" and entry.get('status') == "OPEN" and entry.get('token_id') == position_data['token_id'] ): entry['status'] = "CLOSED" entry['timestamp_close'] = int(time.time()) found = True print(f"Marked position {entry['token_id']} as CLOSED in {STATUS_FILE}") break if not found: print(f"WARNING: Could not find open AUTOMATIC position {position_data['token_id']} to close.") with open(STATUS_FILE, 'w') as f: json.dump(current_data, f, indent=2) # --- ABIs --- # Simplified for length, usually loaded from huge string NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads(''' [ {"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"}, {"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}, {"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}, {"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"} ] ''') UNISWAP_V3_POOL_ABI = json.loads(''' [ {"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint8", "name": "feeProtocol", "type": "uint8"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"} ] ''') ERC20_ABI = json.loads(''' [ {"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"} ] ''') UNISWAP_V3_FACTORY_ABI = json.loads(''' [ {"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"} ] ''') SWAP_ROUTER_ABI = json.loads(''' [ {"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"} ] ''') WETH9_ABI = json.loads(''' [ {"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"}, {"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"} ] ''') NONFUNGIBLE_POSITION_MANAGER_ADDRESS = bytes.fromhex("C36442b4" + "a4522E87" + "1399CD71" + "7aBDD847" + "Ab11FE88") UNISWAP_V3_SWAP_ROUTER_ADDRESS = bytes.fromhex("E592427A0AEce92De3Edee1F18E0157C05861564") WETH_ADDRESS = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # Arbitrum WETH # --- Core Logic Functions --- def get_position_details(w3_instance, npm_c, factory_c, token_id): try: position_data = npm_c.functions.positions(token_id).call() (nonce, operator, token0_address, token1_address, fee, tickLower, tickUpper, liquidity, feeGrowthInside0, feeGrowthInside1, tokensOwed0, tokensOwed1) = position_data token0_contract = w3_instance.eth.contract(address=token0_address, abi=ERC20_ABI) token1_contract = w3_instance.eth.contract(address=token1_address, abi=ERC20_ABI) token0_symbol = token0_contract.functions.symbol().call() token1_symbol = token1_contract.functions.symbol().call() token0_decimals = token0_contract.functions.decimals().call() token1_decimals = token1_contract.functions.decimals().call() pool_address = factory_c.functions.getPool(token0_address, token1_address, fee).call() if pool_address == '0x0000000000000000000000000000000000000000': return None, None pool_contract = w3_instance.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI) return { "token0_address": token0_address, "token1_address": token1_address, "token0_symbol": token0_symbol, "token1_symbol": token1_symbol, "token0_decimals": token0_decimals, "token1_decimals": token1_decimals, "fee": fee, "tickLower": tickLower, "tickUpper": tickUpper, "liquidity": liquidity, "pool_address": pool_address }, pool_contract except Exception as e: print(f"ERROR fetching position details: {e}") return None, None def get_pool_dynamic_data(pool_c): try: slot0_data = pool_c.functions.slot0().call() return {"sqrtPriceX96": slot0_data[0], "tick": slot0_data[1]} except Exception as e: print(f"ERROR fetching pool dynamic data: {e}") return None def calculate_mint_amounts(current_tick, tick_lower, tick_upper, investment_value_token1, decimals0, decimals1, sqrt_price_current_x96): sqrt_price_current = get_sqrt_ratio_at_tick(current_tick) sqrt_price_lower = get_sqrt_ratio_at_tick(tick_lower) sqrt_price_upper = get_sqrt_ratio_at_tick(tick_upper) # 1. Get Price of Token0 in terms of Token1 price_of_token0_in_token1_units = price_from_sqrt_price_x96(sqrt_price_current_x96, decimals0, decimals1) # 2. Estimate Amounts L_test = 1 << 128 amt0_test, amt1_test = get_amounts_for_liquidity(sqrt_price_current, sqrt_price_lower, sqrt_price_upper, L_test) # 3. Adjust for decimals real_amt0_test = amt0_test / (10**decimals0) real_amt1_test = amt1_test / (10**decimals1) # 4. Calculate Total Value of Test Position in Token1 terms value_test = (real_amt0_test * price_of_token0_in_token1_units) + real_amt1_test if value_test == 0: return 0, 0 # 5. Scale scale = investment_value_token1 / value_test # 6. Final Amounts final_amt0 = int(amt0_test * scale) final_amt1 = int(amt1_test * scale) return final_amt0, final_amt1 def check_and_swap(w3_instance, router_contract, account, token0, token1, amount0_needed, amount1_needed): token0_contract = w3_instance.eth.contract(address=token0, abi=ERC20_ABI) token1_contract = w3_instance.eth.contract(address=token1, abi=ERC20_ABI) bal0 = token0_contract.functions.balanceOf(account.address).call() bal1 = token1_contract.functions.balanceOf(account.address).call() # Debug Balances s0 = token0_contract.functions.symbol().call() s1 = token1_contract.functions.symbol().call() d0 = token0_contract.functions.decimals().call() d1 = token1_contract.functions.decimals().call() print(f"\n--- WALLET CHECK ---") print(f"Required: {from_wei(amount0_needed, d0):.6f} {s0} | {from_wei(amount1_needed, d1):.2f} {s1}") print(f"Balance : {from_wei(bal0, d0):.6f} {s0} | {from_wei(bal1, d1):.2f} {s1}") deficit0 = max(0, amount0_needed - bal0) deficit1 = max(0, amount1_needed - bal1) if deficit0 > 0: print(f"Deficit {s0}: {from_wei(deficit0, d0):.6f}") if deficit1 > 0: print(f"Deficit {s1}: {from_wei(deficit1, d1):.2f}") # --- AUTO-WRAP ETH LOGIC --- weth_addr_lower = WETH_ADDRESS.lower() # Wrap for Token0 Deficit if (deficit0 > 0 or deficit1 > 0) and token0.lower() == weth_addr_lower: native_bal = w3_instance.eth.get_balance(account.address) gas_reserve = 5 * 10**15 # 0.005 ETH (Reduced for L2) available_native = max(0, native_bal - gas_reserve) amount_to_wrap = 0 if deficit0 > 0: amount_to_wrap = deficit0 if deficit1 > 0: amount_to_wrap = available_native amount_to_wrap = min(amount_to_wrap, available_native) if amount_to_wrap > 0: print(f"Auto-Wrapping {from_wei(amount_to_wrap, 18)} ETH to WETH...") weth_contract = w3_instance.eth.contract(address=token0, abi=WETH9_ABI) wrap_txn = weth_contract.functions.deposit().build_transaction({ 'from': account.address, 'value': amount_to_wrap, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed_wrap = w3_instance.eth.account.sign_transaction(wrap_txn, private_key=account.key) raw_wrap = signed_wrap.rawTransaction if hasattr(signed_wrap, 'rawTransaction') else signed_wrap.raw_transaction tx_hash = w3_instance.eth.send_raw_transaction(raw_wrap) print(f"Wrap Sent: {tx_hash.hex()}") w3_instance.eth.wait_for_transaction_receipt(tx_hash) bal0 = token0_contract.functions.balanceOf(account.address).call() deficit0 = max(0, amount0_needed - bal0) else: if deficit0 > 0: print(f"Insufficient Native ETH to wrap. Need: {from_wei(deficit0, 18)}, Available: {from_wei(available_native, 18)}") # Wrap for Token1 Deficit (if Token1 is WETH) if deficit1 > 0 and token1.lower() == weth_addr_lower: native_bal = w3_instance.eth.get_balance(account.address) gas_reserve = 5 * 10**15 # 0.005 ETH available_native = max(0, native_bal - gas_reserve) if available_native >= deficit1: print(f"Auto-Wrapping {from_wei(deficit1, 18)} ETH to WETH...") weth_contract = w3_instance.eth.contract(address=token1, abi=WETH9_ABI) wrap_txn = weth_contract.functions.deposit().build_transaction({ 'from': account.address, 'value': deficit1, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed_wrap = w3_instance.eth.account.sign_transaction(wrap_txn, private_key=account.key) raw_wrap = signed_wrap.rawTransaction if hasattr(signed_wrap, 'rawTransaction') else signed_wrap.raw_transaction tx_hash = w3_instance.eth.send_raw_transaction(raw_wrap) print(f"Wrap Sent: {tx_hash.hex()}") w3_instance.eth.wait_for_transaction_receipt(tx_hash) bal1 = token1_contract.functions.balanceOf(account.address).call() deficit1 = max(0, amount1_needed - bal1) if deficit0 == 0 and deficit1 == 0: return True if deficit0 > 0 and bal1 > amount1_needed: surplus1 = bal1 - amount1_needed print(f"Swapping surplus Token1 ({surplus1}) for Token0...") approve_txn = token1_contract.functions.approve(router_contract.address, surplus1).build_transaction({ 'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed = w3_instance.eth.account.sign_transaction(approve_txn, private_key=account.key) raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction w3_instance.eth.send_raw_transaction(raw) time.sleep(2) params = (token1, token0, 500, account.address, int(time.time()) + 120, surplus1, 0, 0) swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({ 'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 300000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed_swap = w3_instance.eth.account.sign_transaction(swap_txn, private_key=account.key) raw_swap = signed_swap.rawTransaction if hasattr(signed_swap, 'rawTransaction') else signed_swap.raw_transaction tx_hash = w3_instance.eth.send_raw_transaction(raw_swap) print(f"Swap Sent: {tx_hash.hex()}") w3_instance.eth.wait_for_transaction_receipt(tx_hash) # Verify Balance After Swap bal0 = token0_contract.functions.balanceOf(account.address).call() if bal0 < amount0_needed: print(f"❌ Swap insufficient. Have {bal0}, Need {amount0_needed}") return False return True elif deficit1 > 0 and bal0 > amount0_needed: surplus0 = bal0 - amount0_needed print(f"Swapping surplus Token0 ({surplus0}) for Token1...") approve_txn = token0_contract.functions.approve(router_contract.address, surplus0).build_transaction({ 'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed = w3_instance.eth.account.sign_transaction(approve_txn, private_key=account.key) raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction w3_instance.eth.send_raw_transaction(raw) time.sleep(2) params = (token0, token1, 500, account.address, int(time.time()) + 120, surplus0, 0, 0) swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({ 'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 300000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed_swap = w3_instance.eth.account.sign_transaction(swap_txn, private_key=account.key) raw_swap = signed_swap.rawTransaction if hasattr(signed_swap, 'rawTransaction') else signed_swap.raw_transaction tx_hash = w3_instance.eth.send_raw_transaction(raw_swap) print(f"Swap Sent: {tx_hash.hex()}") w3_instance.eth.wait_for_transaction_receipt(tx_hash) # Verify Balance After Swap bal1 = token1_contract.functions.balanceOf(account.address).call() if bal1 < amount1_needed: print(f"❌ Swap insufficient. Have {bal1}, Need {amount1_needed}") return False return True print("❌ Insufficient funds for required amounts.") return False def get_token_balances(w3_instance, account_address, token0_address, token1_address): try: token0_contract = w3_instance.eth.contract(address=token0, abi=ERC20_ABI) token1_contract = w3_instance.eth.contract(address=token1, abi=ERC20_ABI) b0 = token0_contract.functions.balanceOf(account_address).call() b1 = token1_contract.functions.balanceOf(account_address).call() return b0, b1 except: return 0, 0 def decrease_liquidity(w3_instance, npm_contract, account, position_id, liquidity_amount): try: txn = npm_contract.functions.decreaseLiquidity((position_id, liquidity_amount, 0, 0, int(time.time()) + 180)).build_transaction({ 'from': account.address, 'gas': 1000000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'chainId': w3_instance.eth.chain_id }) signed = w3_instance.eth.account.sign_transaction(txn, private_key=account.key) raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction tx_hash = w3_instance.eth.send_raw_transaction(raw) print(f"Decrease Sent: {tx_hash.hex()}") w3_instance.eth.wait_for_transaction_receipt(tx_hash) return True except Exception as e: print(f"Error decreasing: {e}") return False def mint_new_position(w3_instance, npm_contract, account, token0, token1, amount0, amount1, tick_lower, tick_upper): print(f"\n--- Attempting to Mint ---") try: token0_c = w3_instance.eth.contract(address=token0, abi=ERC20_ABI) token1_c = w3_instance.eth.contract(address=token1, abi=ERC20_ABI) # Approve 0 txn0 = token0_c.functions.approve(npm_contract.address, amount0).build_transaction({ 'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed0 = w3_instance.eth.account.sign_transaction(txn0, private_key=account.key) raw0 = signed0.rawTransaction if hasattr(signed0, 'rawTransaction') else signed0.raw_transaction w3_instance.eth.send_raw_transaction(raw0) time.sleep(2) # Approve 1 txn1 = token1_c.functions.approve(npm_contract.address, amount1).build_transaction({ 'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed1 = w3_instance.eth.account.sign_transaction(txn1, private_key=account.key) raw1 = signed1.rawTransaction if hasattr(signed1, 'rawTransaction') else signed1.raw_transaction w3_instance.eth.send_raw_transaction(raw1) time.sleep(2) # Mint params = (token0, token1, 500, tick_lower, tick_upper, amount0, amount1, 0, 0, account.address, int(time.time()) + 180) mint_txn = npm_contract.functions.mint(params).build_transaction({ 'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 800000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id }) signed_mint = w3_instance.eth.account.sign_transaction(mint_txn, private_key=account.key) raw_mint = signed_mint.rawTransaction if hasattr(signed_mint, 'rawTransaction') else signed_mint.raw_transaction tx_hash = w3_instance.eth.send_raw_transaction(raw_mint) print(f"Mint Sent: {tx_hash.hex()}") receipt = w3_instance.eth.wait_for_transaction_receipt(tx_hash) if receipt.status == 1: print("✅ Mint Successful!") result_data = {'token_id': None, 'liquidity': 0, 'amount0': 0, 'amount1': 0} # Web3.py Event Processing to capture ID and Amounts try: # 1. Capture Token ID from Transfer event transfer_events = npm_contract.events.Transfer().process_receipt(receipt) for event in transfer_events: if event['args']['from'] == "0x0000000000000000000000000000000000000000": result_data['token_id'] = event['args']['tokenId'] break # 2. Capture Amounts from IncreaseLiquidity event inc_liq_events = npm_contract.events.IncreaseLiquidity().process_receipt(receipt) for event in inc_liq_events: if result_data['token_id'] and event['args']['tokenId'] == result_data['token_id']: result_data['amount0'] = event['args']['amount0'] result_data['amount1'] = event['args']['amount1'] result_data['liquidity'] = event['args']['liquidity'] break except Exception as e: print(f"Event Processing Warning: {e}") if result_data['token_id']: print(f"Captured: ID {result_data['token_id']}, Amt0 {result_data['amount0']}, Amt1 {result_data['amount1']}") return result_data return None else: print("❌ Mint Failed!") return None except Exception as e: print(f"Mint Error: {e}") return None def collect_fees(w3_instance, npm_contract, account, position_id): try: txn = npm_contract.functions.collect((position_id, account.address, 2**128-1, 2**128-1)).build_transaction({ 'from': account.address, 'gas': 1000000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'chainId': w3_instance.eth.chain_id }) signed = w3_instance.eth.account.sign_transaction(txn, private_key=account.key) raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction tx_hash = w3_instance.eth.send_raw_transaction(raw) print(f"Collect Sent: {tx_hash.hex()}") w3_instance.eth.wait_for_transaction_receipt(tx_hash) return True except: return False def main(): print(f"CWD: {os.getcwd()}") # Load .env from current directory load_dotenv(override=True) rpc_url = os.environ.get("MAINNET_RPC_URL") private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY") or os.environ.get("PRIVATE_KEY") if not rpc_url or not private_key: print("Missing RPC or Private Key.") return w3 = Web3(Web3.HTTPProvider(rpc_url)) if not w3.is_connected(): print("RPC Connection Failed") return print(f"Connected to Chain ID: {w3.eth.chain_id}") account = Account.from_key(private_key) w3.eth.default_account = account.address print(f"Wallet: {account.address}") npm_contract = w3.eth.contract(address=NONFUNGIBLE_POSITION_MANAGER_ADDRESS, abi=NONFUNGIBLE_POSITION_MANAGER_ABI) factory_addr = npm_contract.functions.factory().call() factory_contract = w3.eth.contract(address=factory_addr, abi=UNISWAP_V3_FACTORY_ABI) router_contract = w3.eth.contract(address=UNISWAP_V3_SWAP_ROUTER_ADDRESS, abi=SWAP_ROUTER_ABI) print("\n--- STARTING LIFECYCLE MANAGER ---") while True: try: # 1. Get All Open Positions all_positions = get_all_open_positions() # Check if we have an active AUTOMATIC position active_automatic_position = next((p for p in all_positions if p['type'] == 'AUTOMATIC' and p['status'] == 'OPEN'), None) if all_positions: print("\n" + "="*60) print(f"Monitoring at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}") for position in all_positions: token_id = position['token_id'] pos_type = position['type'] # Fetch Details pos_details, pool_c = get_position_details(w3, npm_contract, factory_contract, token_id) if not pos_details: print(f"ERROR: Could not get details for Position {token_id}. Skipping.") continue pool_data = get_pool_dynamic_data(pool_c) current_tick = pool_data['tick'] # Calculate Fees (Simulation) unclaimed0 = 0 unclaimed1 = 0 try: fees_sim = npm_contract.functions.collect((token_id, "0x0000000000000000000000000000000000000000", 2**128-1, 2**128-1)).call() unclaimed0 = from_wei(fees_sim[0], pos_details['token0_decimals']) unclaimed1 = from_wei(fees_sim[1], pos_details['token1_decimals']) except: pass # Check Range is_out_of_range = False status_str = "IN RANGE" if current_tick < pos_details['tickLower']: is_out_of_range = True status_str = "OUT OF RANGE (BELOW)" elif current_tick >= pos_details['tickUpper']: is_out_of_range = True status_str = "OUT OF RANGE (ABOVE)" print(f"\nID: {token_id} | Type: {pos_type} | Status: {status_str}") print(f" Range: {position['range_lower']:.2f} - {position['range_upper']:.2f}") print(f" Fees: {unclaimed0:.4f} {pos_details['token0_symbol']} / {unclaimed1:.4f} {pos_details['token1_symbol']}") # --- AUTO CLOSE LOGIC (AUTOMATIC ONLY) --- if pos_type == 'AUTOMATIC' and CLOSE_POSITION_ENABLED and is_out_of_range: print(f"⚠️ Automatic Position {token_id} is OUT OF RANGE! Initiating Close...") liq = pos_details['liquidity'] if liq > 0: if decrease_liquidity(w3, npm_contract, account, token_id, liq): time.sleep(5) collect_fees(w3, npm_contract, account, token_id) update_hedge_status_file("CLOSE", {'token_id': token_id}) print("Position Closed & Status Updated.") # --- REBALANCE ON CLOSE (If Price Dropped) --- if REBALANCE_ON_CLOSE_BELOW_RANGE and status_str == "OUT OF RANGE (BELOW)": print("📉 Position closed BELOW range (100% ETH). Selling 50% of WETH inventory to USDC...") try: # Get WETH Balance token0_c = w3.eth.contract(address=pos_details['token0_address'], abi=ERC20_ABI) weth_bal = token0_c.functions.balanceOf(account.address).call() amount_in = weth_bal // 2 if amount_in > 0: # Approve Router approve_txn = token0_c.functions.approve(router_contract.address, amount_in).build_transaction({ 'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3.eth.gas_price * 2, 'maxPriorityFeePerGas': w3.eth.max_priority_fee, 'chainId': w3.eth.chain_id }) signed = w3.eth.account.sign_transaction(approve_txn, private_key=account.key) raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction w3.eth.send_raw_transaction(raw) time.sleep(2) # Swap WETH -> USDC params = (pos_details['token0_address'], pos_details['token1_address'], 500, account.address, int(time.time()) + 120, amount_in, 0, 0) swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({ 'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address), 'gas': 300000, 'maxFeePerGas': w3.eth.gas_price * 2, 'maxPriorityFeePerGas': w3.eth.max_priority_fee, 'chainId': w3.eth.chain_id }) signed_swap = w3.eth.account.sign_transaction(swap_txn, private_key=account.key) raw_swap = signed_swap.rawTransaction if hasattr(signed_swap, 'rawTransaction') else signed_swap.raw_transaction tx_hash = w3.eth.send_raw_transaction(raw_swap) print(f"⚖️ Rebalance Swap Sent: {tx_hash.hex()}") w3.eth.wait_for_transaction_receipt(tx_hash) print("✅ Rebalance Complete.") except Exception as e: print(f"Error during rebalance swap: {e}") else: print("Liquidity 0. Marking closed.") update_hedge_status_file("CLOSE", {'token_id': token_id}) # 2. Opening Logic (If no active automatic position) if not active_automatic_position and OPEN_POSITION_ENABLED: print("\n[OPENING] No active automatic position. Starting Open Sequence...") # Get Pool (WETH/USDC) token0 = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH token1 = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC pool_addr = factory_contract.functions.getPool(token0, token1, 500).call() pool_c = w3.eth.contract(address=pool_addr, abi=UNISWAP_V3_POOL_ABI) pool_data = get_pool_dynamic_data(pool_c) tick = pool_data['tick'] # Range +/- 2% import math tick_delta = int(math.log(1 + RANGE_WIDTH_PCT) / math.log(1.0001)) spacing = 10 lower = (tick - tick_delta) // spacing * spacing upper = (tick + tick_delta) // spacing * spacing # Amounts try: token0_c = w3.eth.contract(address=token0, abi=ERC20_ABI) token1_c = w3.eth.contract(address=token1, abi=ERC20_ABI) d0 = token0_c.functions.decimals().call() d1 = token1_c.functions.decimals().call() except Exception as e: print(f"Error fetching decimals: {e}") time.sleep(MONITOR_INTERVAL_SECONDS) continue amt0, amt1 = calculate_mint_amounts(tick, lower, upper, TARGET_INVESTMENT_VALUE_TOKEN1, d0, d1, pool_data['sqrtPriceX96']) amt0_buf, amt1_buf = int(amt0 * 1.02), int(amt1 * 1.02) if check_and_swap(w3, router_contract, account, token0, token1, amt0_buf, amt1_buf): mint_result = mint_new_position(w3, npm_contract, account, token0, token1, amt0, amt1, lower, upper) if mint_result: # Calculate Actual Value try: s0 = token0_c.functions.symbol().call() s1 = token1_c.functions.symbol().call() except: s0, s1 = "T0", "T1" real_amt0 = from_wei(mint_result['amount0'], d0) real_amt1 = from_wei(mint_result['amount1'], d1) entry_price = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1) actual_value = (real_amt0 * entry_price) + real_amt1 print(f"ACTUAL MINT VALUE: {actual_value:.2f} {s1}/{s0}") pos_data = { 'token_id': mint_result['token_id'], 'entry_price': entry_price, 'range_lower': price_from_tick(lower, d0, d1), 'range_upper': price_from_tick(upper, d0, d1), 'target_value': actual_value, 'amount0_initial': mint_result['amount0'], 'amount1_initial': mint_result['amount1'] } update_hedge_status_file("OPEN", pos_data) print("Cycle Complete. Monitoring.") elif not all_positions: print("No open positions (Manual or Automatic). Waiting...") time.sleep(MONITOR_INTERVAL_SECONDS) except KeyboardInterrupt: print("\nManager stopped.") break except Exception as e: print(f"Error in Main Loop: {e}") time.sleep(MONITOR_INTERVAL_SECONDS) if __name__ == "__main__": main()