#!/usr/bin/env python3 """ Interactive PacaBotManager CLI Tool ================================== An interactive command-line tool for managing PACA stakes and vestings through the PacaBotManager contract on BSC mainnet. Usage: python python_scripts/interactive_bot_manager.py """ import os import sys from web3 import Web3 from eth_account import Account from dotenv import load_dotenv import re from web3.middleware import ExtraDataToPOAMiddleware # Load environment variables load_dotenv() class InteractivePacaBotManager: def __init__(self, chain_id=None): print("๐Ÿ Interactive PacaBotManager CLI") print("=" * 40) # Chain configurations self.chains = { "bsc": { "name": "BSC Mainnet", "rpc_url": "https://bsc-dataseed1.binance.org", "chain_id": 56, "currency": "BNB", "bot_manager": "0x4E5d3cD7743934b61041ba2ac3E9df39a0A26dcC", "paca_contract": "0x3fF44D639a4982A4436f6d737430141aBE68b4E1", "explorer": "https://bscscan.com" }, "base": { "name": "Base Mainnet", "rpc_url": "https://virtual.base.us-east.rpc.tenderly.co/0552c4f5-a0ca-4b15-860f-fc73a3cb7983", "chain_id": 8453, "currency": "ETH", "bot_manager": "0x811e82b299F58649f1e0AAD33d6ba49Fa87EA969", "paca_contract": "0xDf2027318D27c4eD1C047B4d6247A7a705bb407b", # Correct Base PACA proxy "explorer": "https://basescan.org" }, "sonic": { "name": "Sonic Network", "rpc_url": "https://rpc.soniclabs.com", "chain_id": 146, "currency": "SONIC", "bot_manager": "0x5a9A8bE051282dd5505222b9c539EB1898BB5C06", "paca_contract": "0xa26F8128Ecb2FF2FC5618498758cC82Cf1FDad5F", # Correct Sonic PACA proxy "explorer": "https://sonicscan.org" } } # If chain_id provided, use it directly, otherwise prompt user if chain_id: self.current_chain = chain_id else: self.current_chain = self.select_chain() if not self.current_chain: print("โŒ No chain selected. Exiting.") sys.exit(1) # Set up connection for selected chain self.setup_chain_connection() def select_chain(self): """Prompt user to select which blockchain to use""" print("\n๐ŸŒ Select Blockchain Network:") print("=" * 30) print("1. ๐ŸŸก BSC Mainnet (Binance Smart Chain)") print("2. ๐Ÿ”ต Base Mainnet") print("3. โšก Sonic Network") print("4. ๐Ÿšช Exit") print("-" * 30) while True: choice = input("Select network (1-4): ").strip() if choice == "1": return "bsc" elif choice == "2": return "base" elif choice == "3": return "sonic" elif choice == "4" or choice.lower() in ['exit', 'quit', 'q']: return None else: print("โŒ Invalid choice. Please select 1-4.") def setup_chain_connection(self): """Set up Web3 connection for the selected chain""" chain_config = self.chains[self.current_chain] print(f"\n๐Ÿ”— Connecting to {chain_config['name']}...") # Set up RPC connection self.rpc_url = chain_config['rpc_url'] self.w3 = Web3(Web3.HTTPProvider(self.rpc_url)) # Add middleware for BSC (POA networks) if self.current_chain == "bsc": self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) # Set contract addresses for current chain self.bot_manager_address = chain_config['bot_manager'] self.paca_contract_address = chain_config['paca_contract'] self.currency = chain_config['currency'] self.explorer_url = chain_config['explorer'] self.private_key = os.getenv('PRIVATE_KEY') if not self.private_key: print("โŒ Error: PRIVATE_KEY not found in environment variables") print("Please add PRIVATE_KEY=your_private_key_here to your .env file") sys.exit(1) # Set up account self.account = Account.from_key(self.private_key) chain_config = self.chains[self.current_chain] print(f"๐Ÿ”‘ Connected as: {self.account.address}") try: balance = self.w3.eth.get_balance(self.account.address) print(f"๐Ÿ’ฐ Balance: {self.w3.from_wei(balance, 'ether'):.4f} {self.currency}") except Exception as e: print(f"โŒ Connection failed: {e}") sys.exit(1) print(f"๐ŸŒ Network: {chain_config['name']}") print(f"๐Ÿค– BotManager: {self.bot_manager_address}") print(f"๐Ÿ”— PACA Contract: {self.paca_contract_address}") print() # Contract ABIs self.bot_manager_abi = [ { "inputs": [ {"internalType": "address", "name": "pacaContract", "type": "address"}, {"internalType": "address", "name": "user", "type": "address"} ], "name": "clearStakes", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ {"internalType": "address", "name": "pacaContract", "type": "address"}, {"internalType": "address", "name": "user", "type": "address"} ], "name": "clearVesting", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "components": [ {"internalType": "address", "name": "target", "type": "address"}, {"internalType": "bytes", "name": "callData", "type": "bytes"} ], "internalType": "struct PacaBotManager.Call[]", "name": "calls", "type": "tuple[]" } ], "name": "multiCallAtomic", "outputs": [ {"internalType": "bytes[]", "name": "returnData", "type": "bytes[]"} ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [ {"internalType": "address", "name": "", "type": "address"} ], "stateMutability": "view", "type": "function" } ] self.paca_abi = [ { "inputs": [ {"internalType": "address", "name": "user", "type": "address"} ], "name": "getStakes", "outputs": [ { "components": [ {"internalType": "uint256", "name": "amount", "type": "uint256"}, {"internalType": "uint256", "name": "lastClaimed", "type": "uint256"}, {"internalType": "uint256", "name": "dailyRewardRate", "type": "uint256"}, {"internalType": "uint256", "name": "unlockTime", "type": "uint256"}, {"internalType": "bool", "name": "complete", "type": "bool"} ], "internalType": "struct PacaFinanceWithBoostAndScheduleBsc.Stake[]", "name": "", "type": "tuple[]" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ {"internalType": "address", "name": "user", "type": "address"} ], "name": "getVestings", "outputs": [ { "components": [ {"internalType": "uint256", "name": "amount", "type": "uint256"}, {"internalType": "uint256", "name": "bonus", "type": "uint256"}, {"internalType": "uint256", "name": "lockedUntil", "type": "uint256"}, {"internalType": "uint256", "name": "claimedAmount", "type": "uint256"}, {"internalType": "uint256", "name": "claimedBonus", "type": "uint256"}, {"internalType": "uint256", "name": "lastClaimed", "type": "uint256"}, {"internalType": "uint256", "name": "createdAt", "type": "uint256"}, {"internalType": "address", "name": "token", "type": "address"}, {"internalType": "bool", "name": "complete", "type": "bool"}, {"internalType": "uint256", "name": "usdAmount", "type": "uint256"} ], "internalType": "struct PacaFinanceWithBoostAndScheduleBsc.Vesting[]", "name": "", "type": "tuple[]" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [ {"internalType": "address", "name": "", "type": "address"} ], "stateMutability": "view", "type": "function" } ] # Create contract instances self.bot_manager = self.w3.eth.contract( address=self.bot_manager_address, abi=self.bot_manager_abi ) self.paca_contract = self.w3.eth.contract( address=self.paca_contract_address, abi=self.paca_abi ) def validate_address(self, address): """Validate Ethereum address format""" if not address: return False # Remove 0x prefix if present if address.startswith('0x') or address.startswith('0X'): address = address[2:] # Check if it's 40 hex characters if len(address) != 40: return False # Check if all characters are hex return re.match(r'^[0-9a-fA-F]{40}$', address) is not None def get_user_address(self): """Prompt user for an Ethereum address""" while True: address = input("๐Ÿ“ Enter user address (0x...): ").strip() if address.lower() in ['quit', 'exit', 'q']: return None if self.validate_address(address): # Ensure proper format if not address.startswith('0x'): address = '0x' + address return Web3.to_checksum_address(address) else: print("โŒ Invalid address format. Please enter a valid Ethereum address.") print(" Example: 0x41970Ce76b656030A79E7C1FA76FC4EB93980255") print(" (or type 'quit' to exit)") def send_transaction(self, tx_dict): """Send a transaction and wait for confirmation""" try: # Estimate gas first print("โณ Estimating gas...") estimated_gas = 2_000_000 # self.w3.eth.estimate_gas(tx_dict) # Add 50% buffer for safety (especially important for large operations) gas_limit = int(estimated_gas * 1.5) print(f"โ›ฝ Estimated gas: {estimated_gas:,}") print(f"โ›ฝ Gas limit (with buffer): {gas_limit:,}") # Add gas and nonce tx_dict['gas'] = gas_limit gas_price = self.w3.eth.gas_price tx_dict['maxFeePerGas'] = int(gas_price * 1) # 20% buffer on gas price tx_dict['maxPriorityFeePerGas'] = int(gas_price) tx_dict['nonce'] = self.w3.eth.get_transaction_count(self.account.address) tx_dict['type'] = 2 # Calculate total cost max_cost = gas_limit * tx_dict['maxFeePerGas'] print(f"โ›ฝ Max Fee Per Gas: {self.w3.from_wei(tx_dict['maxFeePerGas'], 'gwei'):.2f} gwei") print(f"๐Ÿ’ฐ Max Transaction Cost: {self.w3.from_wei(max_cost, 'ether'):.6f} {self.currency}") # Ask for confirmation if this is an expensive transaction if max_cost > self.w3.to_wei(0.01, 'ether'): # If more than 0.01 BNB confirm = input(f"\nโš ๏ธ High gas cost transaction! Continue? (yes/no): ") if confirm.lower() not in ['yes', 'y']: print("โŒ Transaction cancelled") return None # Sign and send signed = self.w3.eth.account.sign_transaction(tx_dict, self.private_key) tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction) print(f"๐Ÿงพ Transaction sent: {tx_hash.hex()}") print("โณ Waiting for confirmation...") # Wait for confirmation receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300) if receipt.status == 1: print("โœ… Transaction successful!") print(f"โ›ฝ Gas used: {receipt.gasUsed:,}") print(f"๐Ÿ”— Explorer: {self.explorer_url}/tx/{tx_hash.hex()}") return receipt else: print("โŒ Transaction failed!") return None except Exception as e: print(f"โŒ Transaction failed: {e}") return None def get_stakes(self, user_address): """Get and display stakes for a user""" print(f"\n๐Ÿ“Š Getting stakes for: {user_address}") try: stakes = self.paca_contract.functions.getStakes(user_address).call() if len(stakes) == 0: print("๐Ÿ“ญ No stakes found for this user") return stakes print(f"๐Ÿ“ˆ Found {len(stakes)} stakes:") total_amount = 0 active_stakes = 0 for i, stake in enumerate(stakes): amount = stake[0] last_claimed = stake[1] daily_reward_rate = stake[2] unlock_time = stake[3] complete = stake[4] is_active = not complete and amount > 0 if is_active: active_stakes += 1 total_amount += amount print(f"\n ๐Ÿ“Œ Stake {i + 1}:") print(f" Amount: {self.w3.from_wei(amount, 'ether'):.6f} ETH") print(f" Daily Reward: {self.w3.from_wei(daily_reward_rate, 'ether'):.6f} ETH") print(f" Complete: {complete}") print(f" Status: {'๐ŸŸข ACTIVE' if is_active else '๐Ÿ”ด COMPLETED'}") if unlock_time > 0: import datetime unlock_date = datetime.datetime.fromtimestamp(unlock_time) print(f" Unlock: {unlock_date.strftime('%Y-%m-%d %H:%M:%S')}") print(f"\n๐Ÿ’Ž Summary:") print(f" Total Stakes: {len(stakes)}") print(f" Active Stakes: {active_stakes}") print(f" Total Active: {self.w3.from_wei(total_amount, 'ether'):.6f} ETH") return stakes except Exception as e: print(f"โŒ Error getting stakes: {e}") return [] def get_vestings(self, user_address): """Get and display vestings for a user""" print(f"\n๐Ÿ”ฎ Getting vestings for: {user_address}") try: vestings = self.paca_contract.functions.getVestings(user_address).call() if len(vestings) == 0: print("๐Ÿ“ญ No vestings found for this user") return vestings print(f"๐Ÿ“ˆ Found {len(vestings)} vestings:") total_amount = 0 active_vestings = 0 for i, vesting in enumerate(vestings): amount = vesting[0] # amount bonus = vesting[1] # bonus locked_until = vesting[2] # lockedUntil claimed_amount = vesting[3] # claimedAmount claimed_bonus = vesting[4] # claimedBonus last_claimed = vesting[5] # lastClaimed created_at = vesting[6] # createdAt token = vesting[7] # token complete = vesting[8] # complete usd_amount = vesting[9] # usdAmount is_active = not complete and amount > 0 if is_active: active_vestings += 1 total_amount += amount print(f"\n ๐Ÿ“Œ Vesting {i + 1}:") print(f" Amount: {self.w3.from_wei(amount, 'ether'):.6f} tokens") print(f" Bonus: {self.w3.from_wei(bonus, 'ether'):.6f} tokens") print(f" Claimed: {self.w3.from_wei(claimed_amount, 'ether'):.6f} tokens") print(f" USD Value: ${self.w3.from_wei(usd_amount, 'ether'):.2f}") print(f" Token: {token}") print(f" Complete: {complete}") print(f" Status: {'๐ŸŸข ACTIVE' if is_active else '๐Ÿ”ด COMPLETED'}") if locked_until > 0: import datetime unlock_date = datetime.datetime.fromtimestamp(locked_until) print(f" Locked Until: {unlock_date.strftime('%Y-%m-%d %H:%M:%S')}") if created_at > 0: import datetime created_date = datetime.datetime.fromtimestamp(created_at) print(f" Created: {created_date.strftime('%Y-%m-%d %H:%M:%S')}") print(f"\n๐Ÿ’Ž Summary:") print(f" Total Vestings: {len(vestings)}") print(f" Active Vestings: {active_vestings}") print(f" Total Active: {self.w3.from_wei(total_amount, 'ether'):.6f} tokens") return vestings except Exception as e: print(f"โŒ Error getting vestings: {e}") return [] def clear_stakes(self, user_address): """Clear all stakes for a user""" print(f"\n๐Ÿ”ฅ Preparing to clear stakes for: {user_address}") # First show what will be cleared stakes = self.get_stakes(user_address) if not stakes: return active_stakes = sum(1 for stake in stakes if not stake[4] and stake[0] > 0) if active_stakes == 0: print("โš ๏ธ No active stakes to clear!") return print(f"\nโš ๏ธ WARNING: This will clear {active_stakes} active stakes!") confirm = input("Type 'CONFIRM' to proceed: ") if confirm != 'CONFIRM': print("โŒ Operation cancelled") return print("๐Ÿ”ฅ Executing clearStakes...") tx_dict = self.bot_manager.functions.clearStakes( self.paca_contract_address, user_address ).build_transaction({'from': self.account.address}) return self.send_transaction(tx_dict) def clear_vesting(self, user_address): """Clear all vestings for a user""" print(f"\n๐Ÿ”ฎ Preparing to clear vestings for: {user_address}") # First show what will be cleared vestings = self.get_vestings(user_address) if not vestings: return active_vestings = sum(1 for vesting in vestings if not vesting[8] and vesting[0] > 0) if active_vestings == 0: print("โš ๏ธ No active vestings to clear!") return print(f"\nโš ๏ธ WARNING: This will clear {active_vestings} active vestings!") confirm = input("Type 'CONFIRM' to proceed: ") if confirm != 'CONFIRM': print("โŒ Operation cancelled") return print("๐Ÿ”ฎ Executing clearVesting...") tx_dict = self.bot_manager.functions.clearVesting( self.paca_contract_address, user_address ).build_transaction({'from': self.account.address, 'gas': 1000000}) return self.send_transaction(tx_dict) def clear_both(self, user_address): """Clear both stakes and vestings atomically""" print(f"\n๐Ÿ”ฅ๐Ÿ”ฎ Preparing to clear BOTH stakes AND vestings for: {user_address}") # Show both stakes and vestings stakes = self.get_stakes(user_address) vestings = self.get_vestings(user_address) active_stakes = sum(1 for stake in stakes if not stake[4] and stake[0] > 0) if stakes else 0 active_vestings = sum(1 for vesting in vestings if not vesting[8] and vesting[0] > 0) if vestings else 0 if active_stakes == 0 and active_vestings == 0: print("โš ๏ธ No active stakes or vestings to clear!") return print(f"\nโš ๏ธ WARNING: This will clear:") print(f" - {active_stakes} active stakes") print(f" - {active_vestings} active vestings") print(" โš ๏ธ This operation is ATOMIC - both will be cleared together!") confirm = input("Type 'CONFIRM' to proceed: ") if confirm != 'CONFIRM': print("โŒ Operation cancelled") return print("๐Ÿ”ฅ๐Ÿ”ฎ Executing atomic clear...") # Prepare multi-call calls = [ { 'target': self.paca_contract_address, 'callData': self.w3.keccak(text="clearStakes(address)")[:4] + self.w3.eth.codec.encode(['address'], [user_address]) }, { 'target': self.paca_contract_address, 'callData': self.w3.keccak(text="clearVesting(address)")[:4] + self.w3.eth.codec.encode(['address'], [user_address]) } ] tx_dict = self.bot_manager.functions.multiCallAtomic(calls).build_transaction({ 'from': self.account.address, 'gas': 1000000 }) return self.send_transaction(tx_dict) def show_menu(self): """Display the main menu""" chain_config = self.chains[self.current_chain] print("\n" + "="*60) print(f"๐Ÿค– PacaBotManager Operations - {chain_config['name']}") print("="*60) print("1. ๐Ÿ“Š View Stakes") print("2. ๐Ÿ”ฎ View Vestings") print("3. ๐Ÿ”ฅ Clear Stakes") print("4. ๐Ÿ”ฎ Clear Vestings") print("5. ๐Ÿ”ฅ๐Ÿ”ฎ Clear BOTH (Atomic)") print("6. ๐ŸŒ Switch Chain") print("7. ๐Ÿƒ Exit") print("-"*60) def run(self): """Main interactive loop""" print("๐Ÿš€ Interactive PacaBotManager Ready!") while True: # try: self.show_menu() choice = input("Select operation (1-7): ").strip() if choice == '7' or choice.lower() in ['exit', 'quit', 'q']: print("๐Ÿ‘‹ Goodbye!") break elif choice == '6': # Switch chain new_chain = self.select_chain() if new_chain and new_chain != self.current_chain: print(f"๐Ÿ”„ Switching from {self.chains[self.current_chain]['name']} to {self.chains[new_chain]['name']}...") self.current_chain = new_chain self.setup_chain_connection() print("โœ… Chain switched successfully!") elif new_chain == self.current_chain: print(f"โ„น๏ธ Already connected to {self.chains[self.current_chain]['name']}") continue elif choice in ['1', '2', '3', '4', '5']: user_address = self.get_user_address() if user_address is None: continue if choice == '1': self.get_stakes(user_address) elif choice == '2': self.get_vestings(user_address) elif choice == '3': self.clear_stakes(user_address) elif choice == '4': self.clear_vesting(user_address) elif choice == '5': self.clear_both(user_address) else: print("โŒ Invalid choice. Please select 1-7.") # except KeyboardInterrupt: # print("\n\n๐Ÿ‘‹ Interrupted by user. Goodbye!") # break # except Exception as e: # print(f"โŒ Error: {e}") # continue def main(): """Main entry point""" # try: manager = InteractivePacaBotManager() manager.run() # except Exception as e: # print(f"๐Ÿ’ฅ Fatal error: {e}") # sys.exit(1) if __name__ == "__main__": main()