Restructured hedger modules: moved CLP hedger and auto hedger into separate folders, updated data fetchers and main app, removed deprecated files
This commit is contained in:
347
clp_auto_hedger/collect_fees_simple.py
Normal file
347
clp_auto_hedger/collect_fees_simple.py
Normal file
@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fee Collection & Position Recovery Script
|
||||
Collects all accumulated fees and handles stuck positions
|
||||
|
||||
Features:
|
||||
- Collects fees from all positions (OPEN, CLOSING, etc.)
|
||||
- Recovers stuck positions with timeout transactions
|
||||
- Handles zero liquidity positions
|
||||
- Enhanced gas settings for reliability
|
||||
- Detailed logging and status reporting
|
||||
|
||||
Usage:
|
||||
python collect_fees.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# Required libraries
|
||||
try:
|
||||
from web3 import Web3
|
||||
from eth_account import Account
|
||||
except ImportError as e:
|
||||
print(f"[ERROR] Missing required library: {e}")
|
||||
print("Please install with: pip install web3 eth-account python-dotenv")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
except ImportError:
|
||||
print("[WARNING] python-dotenv not found, using environment variables directly")
|
||||
def load_dotenv(override=True):
|
||||
pass
|
||||
|
||||
def setup_logging():
|
||||
"""Setup logging for fee collection"""
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('collect_fees.log', encoding='utf-8')
|
||||
]
|
||||
)
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
# --- Contract ABIs ---
|
||||
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
|
||||
[
|
||||
{"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"}
|
||||
]
|
||||
''')
|
||||
|
||||
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"}
|
||||
]
|
||||
''')
|
||||
|
||||
# --- Contract Addresses ---
|
||||
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = Web3.to_checksum_address("0xC36442b4a4522E871399CD71a7BDD847Ab11FE88")
|
||||
|
||||
def load_status_file():
|
||||
"""Load hedge status file"""
|
||||
status_file = "hedge_status.json"
|
||||
if not os.path.exists(status_file):
|
||||
logger.error(f"Status file {status_file} not found")
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(status_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading status file: {e}")
|
||||
return []
|
||||
|
||||
def update_position_status(token_id, new_status):
|
||||
"""Update position status in status file"""
|
||||
try:
|
||||
current_data = load_status_file()
|
||||
|
||||
for position in current_data:
|
||||
if position.get('token_id') == token_id:
|
||||
old_status = position.get('status', 'UNKNOWN')
|
||||
position['status'] = new_status
|
||||
position['timestamp_close'] = int(time.time()) if new_status == 'CLOSED' else None
|
||||
|
||||
with open('hedge_status.json', 'w') as f:
|
||||
json.dump(current_data, f, indent=2)
|
||||
|
||||
logger.info(f"Updated Position {token_id}: {old_status} -> {new_status}")
|
||||
return True
|
||||
|
||||
logger.warning(f"Position {token_id} not found in status file")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating position status: {e}")
|
||||
return False
|
||||
|
||||
def from_wei(amount, decimals):
|
||||
"""Convert wei to human readable amount"""
|
||||
if amount is None:
|
||||
return 0
|
||||
return amount / (10**decimals)
|
||||
|
||||
def get_position_details(w3, npm_contract, token_id):
|
||||
"""Get detailed position information"""
|
||||
try:
|
||||
position_data = npm_contract.functions.positions(token_id).call()
|
||||
(nonce, operator, token0_address, token1_address, fee, tickLower, tickUpper,
|
||||
liquidity, feeGrowthInside0, feeGrowthInside1, tokensOwed0, tokensOwed1) = position_data
|
||||
|
||||
# Get token details
|
||||
token0_contract = w3.eth.contract(address=token0_address, abi=ERC20_ABI)
|
||||
token1_contract = w3.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()
|
||||
|
||||
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,
|
||||
"tokensOwed0": tokensOwed0,
|
||||
"tokensOwed1": tokensOwed1
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting position {token_id} details: {e}")
|
||||
return None
|
||||
|
||||
def simulate_fees(w3, npm_contract, token_id):
|
||||
"""Simulate fee collection to get amounts without executing"""
|
||||
try:
|
||||
result = npm_contract.functions.collect(
|
||||
(token_id, "0x0000000000000000000000000000000000000000000", 2**128-1, 2**128-1)
|
||||
).call()
|
||||
return result[0], result[1] # amount0, amount1
|
||||
except Exception as e:
|
||||
logger.error(f"Error simulating fees for position {token_id}: {e}")
|
||||
return 0, 0
|
||||
|
||||
def collect_fees_simple(w3, npm_contract, account, token_id):
|
||||
"""Simple fee collection without complex retry logic"""
|
||||
try:
|
||||
logger.info(f"Collecting fees from position {token_id}")
|
||||
|
||||
# Simulate first to see what we'll get
|
||||
sim_amount0, sim_amount1 = simulate_fees(w3, npm_contract, token_id)
|
||||
|
||||
if sim_amount0 == 0 and sim_amount1 == 0:
|
||||
logger.info(f"Position {token_id} has no fees to collect")
|
||||
return True, "no_fees"
|
||||
|
||||
logger.info(f"Expected fees: {sim_amount0} token0, {sim_amount1} token1")
|
||||
|
||||
# Build collect transaction with higher gas
|
||||
txn = npm_contract.functions.collect(
|
||||
(token_id, account.address, 2**128-1, 2**128-1)
|
||||
).build_transaction({
|
||||
'from': account.address,
|
||||
'nonce': w3.eth.get_transaction_count(account.address),
|
||||
'gas': 300000, # Higher gas limit
|
||||
'maxFeePerGas': w3.eth.gas_price * 4, # 4x gas price
|
||||
'maxPriorityFeePerGas': w3.eth.max_priority_fee * 3,
|
||||
'chainId': w3.eth.chain_id
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_txn = w3.eth.account.sign_transaction(txn, private_key=account.key)
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)
|
||||
|
||||
logger.info(f"Collect fees sent: {tx_hash.hex()}")
|
||||
logger.info(f"Arbiscan: https://arbiscan.io/tx/{tx_hash.hex()}")
|
||||
|
||||
# Wait with longer timeout
|
||||
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=600)
|
||||
|
||||
if receipt.status == 1:
|
||||
logger.info(f"[SUCCESS] Fees collected from position {token_id}")
|
||||
return True, tx_hash.hex()
|
||||
else:
|
||||
logger.error(f"[ERROR] Fee collection failed for position {token_id}. Status: {receipt.status}")
|
||||
return False, tx_hash.hex()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ERROR] Fee collection failed for position {token_id}: {e}")
|
||||
return False, None
|
||||
|
||||
def process_all_positions(w3, npm_contract, account):
|
||||
"""Process all positions for fee collection"""
|
||||
positions = load_status_file()
|
||||
if not positions:
|
||||
logger.info("No positions found in status file")
|
||||
return
|
||||
|
||||
logger.info(f"Processing {len(positions)} positions for fee collection...")
|
||||
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
no_fees_count = 0
|
||||
|
||||
for position in positions:
|
||||
token_id = position.get('token_id')
|
||||
status = position.get('status', 'UNKNOWN')
|
||||
|
||||
try:
|
||||
# Get on-chain position details
|
||||
onchain_details = get_position_details(w3, npm_contract, token_id)
|
||||
|
||||
if not onchain_details:
|
||||
logger.warning(f"Could not get details for position {token_id}, skipping...")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
logger.info(f"\n--- Processing Position {token_id} ({status}) ---")
|
||||
logger.info(f"Token Pair: {onchain_details['token0_symbol']}/{onchain_details['token1_symbol']}")
|
||||
logger.info(f"On-chain Liquidity: {onchain_details['liquidity']}")
|
||||
|
||||
# Always try to collect fees
|
||||
success, tx_hash = collect_fees_simple(w3, npm_contract, account, token_id)
|
||||
|
||||
if success == True and tx_hash == "no_fees":
|
||||
no_fees_count += 1
|
||||
logger.info(f"Position {token_id}: No fees available")
|
||||
elif success == True:
|
||||
success_count += 1
|
||||
logger.info(f"Position {token_id}: Fees collected successfully")
|
||||
else:
|
||||
failed_count += 1
|
||||
logger.error(f"Position {token_id}: Fee collection failed")
|
||||
|
||||
time.sleep(2) # Brief pause between positions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing position {token_id}: {e}")
|
||||
failed_count += 1
|
||||
|
||||
# Report final results
|
||||
logger.info(f"\n=== Fee Collection Summary ===")
|
||||
logger.info(f"Total Positions: {len(positions)}")
|
||||
logger.info(f"Successful: {success_count}")
|
||||
logger.info(f"Failed: {failed_count}")
|
||||
logger.info(f"No Fees: {no_fees_count}")
|
||||
|
||||
if success_count > 0:
|
||||
logger.info(f"[SUCCESS] Fee collection completed for {success_count} positions!")
|
||||
|
||||
if failed_count > 0:
|
||||
logger.warning(f"[WARNING] {failed_count} positions failed. Check collect_fees.log for details.")
|
||||
|
||||
def main():
|
||||
logger.info("=== Fee Collection & Position Recovery Script ===")
|
||||
logger.info("This script will collect all accumulated fees")
|
||||
|
||||
# Load environment
|
||||
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:
|
||||
logger.error("[ERROR] Missing RPC URL or Private Key")
|
||||
logger.error("Please ensure MAINNET_RPC_URL and PRIVATE_KEY are set in your .env file")
|
||||
return
|
||||
|
||||
# Connect to Arbitrum
|
||||
try:
|
||||
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
||||
if not w3.is_connected():
|
||||
logger.error("[ERROR] Failed to connect to Arbitrum RPC")
|
||||
return
|
||||
logger.info(f"[SUCCESS] Connected to Chain ID: {w3.eth.chain_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"[ERROR] Connection error: {e}")
|
||||
return
|
||||
|
||||
# Setup account and contracts
|
||||
try:
|
||||
account = Account.from_key(private_key)
|
||||
w3.eth.default_account = account.address
|
||||
logger.info(f"Wallet: {account.address}")
|
||||
|
||||
npm_contract = w3.eth.contract(address=NONFUNGIBLE_POSITION_MANAGER_ADDRESS, abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ERROR] Account/Contract setup error: {e}")
|
||||
return
|
||||
|
||||
# Show current wallet balances
|
||||
try:
|
||||
eth_balance = w3.eth.get_balance(account.address)
|
||||
logger.info(f"ETH Balance: {eth_balance / 10**18:.6f} ETH")
|
||||
|
||||
# Check WETH balance if we have the address
|
||||
weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
|
||||
try:
|
||||
weth_contract = w3.eth.contract(address=weth_address, abi=ERC20_ABI)
|
||||
weth_balance = weth_contract.functions.balanceOf(account.address).call()
|
||||
logger.info(f"WETH Balance: {weth_balance / 10**18:.6f} WETH")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check USDC balance
|
||||
usdc_address = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
|
||||
try:
|
||||
usdc_contract = w3.eth.contract(address=usdc_address, abi=ERC20_ABI)
|
||||
usdc_balance = usdc_contract.functions.balanceOf(account.address).call()
|
||||
logger.info(f"USDC Balance: {usdc_balance / 10**6:.2f} USDC")
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch balances: {e}")
|
||||
|
||||
# Confirm before proceeding
|
||||
confirm = input("\nProceed with fee collection from all positions? (y/N): ").strip().lower()
|
||||
if confirm != 'y':
|
||||
logger.info("Operation cancelled by user")
|
||||
return
|
||||
|
||||
# Process all positions
|
||||
process_all_positions(w3, npm_contract, account)
|
||||
|
||||
logger.info("=== Fee Collection Script Complete ===")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user