Files
pacahh/python_scripts/interactive_bot_manager.py
2025-09-04 02:48:34 +02:00

651 lines
26 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
"""
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()