From 74a4e698b5f3bc2bfe37e2a093874c4a2e2f7ae1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 19 Jan 2025 02:11:46 +0000 Subject: [PATCH 1/2] starter guide --- docs/index.md | 4 + docs/whale_watcher.md | 188 ++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 + 3 files changed, 194 insertions(+) create mode 100644 docs/whale_watcher.md diff --git a/docs/index.md b/docs/index.md index 925533ac..e9f2e5f4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,7 @@ {% include-markdown "../README.md" %} + +## Guides + +- [Creating a Whale Watcher Agent](whale_watcher.md) - Learn how to build an agent that monitors blockchain transactions for whale movements diff --git a/docs/whale_watcher.md b/docs/whale_watcher.md new file mode 100644 index 00000000..9206114d --- /dev/null +++ b/docs/whale_watcher.md @@ -0,0 +1,188 @@ +# Creating a Whale Watcher Agent + +This guide walks you through creating a whale watcher agent that monitors blockchain transactions for large transfers (whale movements). + +## Prerequisites + +- Ensure you have `auto_dev` installed and set up +- Access to an Ethereum RPC endpoint (you'll need to set this in your environment variables) +- Basic understanding of blockchain concepts and ERC20 tokens + +## Step-by-Step Guide + +### 1. Scaffold the Repository + +First, create a new repository structure: + +```bash +adev scaffold repo some_agent +``` + +### 2. Create the Agent + +Create a new agent based on the base template: + +```bash +adev create your_name/some_agent -t eightballer/base --no-clean-up +``` + +### 3. Navigate to Agent Directory + +```bash +cd some_agent +``` + +### 4. Eject the Metrics Skill + +```bash +aea -s eject skill eightballer/metrics +``` + +### 5. Create the FSM Configuration + +Create a file named `your_fsm.yaml` with the following content: + +```yaml +alphabet_in: + - BLOCK_RECEIVED # A new Ethereum block is detected + - TX_OVER_THRESHOLD # A transaction exceeds the whale threshold + - TX_UNDER_THRESHOLD # A transaction is under the whale threshold + - DONE # All transactions for this block are processed + - TIMEOUT # A timeout or other error occurs + +default_start_state: IdleRound + +final_states: + - ErrorRound + - DoneRound + +label: WhaleWatcherAbciApp + +start_states: + - IdleRound + +states: + - IdleRound + - BlockReceivedRound + - AlertRound + - DoneRound + - ErrorRound + +transition_func: + # 1. From Idle -> BlockReceived when a new block arrives + (IdleRound, BLOCK_RECEIVED): BlockReceivedRound + + # 2. In BlockReceived, if a transaction is over threshold -> Alert + (BlockReceivedRound, TX_OVER_THRESHOLD): AlertRound + + # 3. In BlockReceived, if a transaction is under threshold -> stay in BlockReceived + (BlockReceivedRound, TX_UNDER_THRESHOLD): BlockReceivedRound + + # 4. After all transactions are processed in BlockReceived -> DoneState + (BlockReceivedRound, DONE): DoneRound + + # 5. From Alert, once done handling that whale transaction -> DoneState + (AlertRound, DONE): DoneRound + + # 6. Any TIMEOUT event -> ErrorState + (BlockReceivedRound, TIMEOUT): ErrorRound + (AlertRound, TIMEOUT): ErrorRound +``` + +### 6. Generate Behaviour Code + +Generate the behaviour code from your FSM configuration: + +```bash +adev scaffold behaviour --behaviour-type simple_fsm your_fsm.yaml > skills/metrics/behaviours.py +``` + +### 7. Update Skill Configuration + +Modify the `skill.yaml` file to include your behaviour: + +```yaml +behaviours: + metrics_handler: + args: {} + class_name: WhalewatcherabciappFsmBehaviour +``` + +### 8. Fingerprint the Skill + +```bash +aea fingerprint skill your_name/metrics:0.1.0 +``` + +### 9. Publish Locally + +```bash +aea publish --local --push-missing +``` + +### 10. Run the Agent + +```bash +cd .. && adev run your_name/some_agent +``` + +## Environment Variables + +Create a `.env` file in your agent's directory with: + +```env +RPC_URL=your_ethereum_rpc_endpoint +WHALE_THRESHOLD=1000 # Threshold in ETH to consider an address a whale +``` + +## Customization + +The whale watcher agent can be customized by: + +1. Adjusting the `WHALE_THRESHOLD` in your environment variables +2. Modifying the alert format in the `AlertRound` class +3. Adding additional monitoring parameters in the `BlockReceivedRound` class +4. Implementing different alert delivery mechanisms in the `AlertRound` class + +## Monitoring Features + +The agent monitors: +- ERC20 token transfers +- Whale addresses (addresses with balance > threshold) +- Transaction details and balances +- Block-by-block updates + +## Alert Format + +Alerts include: +- Transaction hash and link +- Token contract address +- Sender and receiver addresses +- Wallet balances +- Blockchain explorer links + +## Error Handling + +The agent includes comprehensive error handling: +- Timeout management +- RPC connection issues +- Transaction decoding errors +- Recovery mechanisms + +## Best Practices + +1. Always use environment variables for sensitive data +2. Implement proper error handling +3. Use checksum addresses +4. Cache known whale addresses +5. Implement rate limiting for RPC calls +6. Monitor gas usage and costs +7. Keep logs for debugging and monitoring + +## Troubleshooting + +Common issues and solutions: +1. RPC Connection Issues: Check your RPC endpoint and internet connection +2. Missing Environment Variables: Ensure all required environment variables are set +3. Decoding Errors: Verify contract ABI and event signatures +4. Performance Issues: Adjust batch sizes and polling intervals \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 54d6b0c4..265688ef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,8 @@ nav: - Overview: usage.md - FSM: fsm.md - OpenAPI: openapi.md + - Guides: + - Whale Watcher Agent: whale_watcher.md - API Reference: - Overview: api/index.md - auto_dev: api/auto_dev.md From 3a496a03638f189f783c033e0c95f5a607e29a8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 19 Jan 2025 09:28:23 +0000 Subject: [PATCH 2/2] update guide --- docs/whale_watcher.md | 610 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 606 insertions(+), 4 deletions(-) diff --git a/docs/whale_watcher.md b/docs/whale_watcher.md index 9206114d..f7c3c7eb 100644 --- a/docs/whale_watcher.md +++ b/docs/whale_watcher.md @@ -108,6 +108,610 @@ behaviours: class_name: WhalewatcherabciappFsmBehaviour ``` +### 8. Create the Whale watcher logic + +Modify skills/metrics/behaviors.py to the following + +```python + # -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 +# Copyright 2023 valory-xyz +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a behaviour that autogenerated from the protocol ``.""" + +import sys +from abc import ABC +from typing import Optional, Any +from aea.skills.behaviours import FSMBehaviour, State +from enum import Enum +import os +from web3 import Web3 +from dotenv import load_dotenv +import time +from aea.protocols.base import Message + +load_dotenv() + +# Define states + +class IdleRound(State): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._is_done = False + self._event = None + self.context.logger.info("Initializing IdleRound with Web3...") + load_dotenv() + try: + # Initialize Web3 connection + rpc_url = os.getenv('RPC_URL') + self.w3 = Web3(Web3.HTTPProvider(rpc_url)) + if not self.w3.is_connected(): + raise Exception("Failed to connect to Web3") + self.context.logger.info("Successfully connected to Web3") + + # Store Web3 instance in shared state + self.context.shared_state["w3"] = self.w3 + + self.threshold = float(os.getenv('WHALE_THRESHOLD', '1000')) # Default 1000 ETH + self.context.logger.info(f"Whale threshold set to: {self.threshold} ETH") + self.last_block_number = None + + # Initialize ERC20 Transfer event signature + self.transfer_event_signature = self.w3.keccak(text="Transfer(address,address,uint256)").hex() + self.context.logger.info(f"ERC20 Transfer event signature: {self.transfer_event_signature}") + + except Exception as e: + self.context.logger.error(f"Initialization error: {str(e)}") + raise + + def act(self) -> None: + """Wait for new blocks and get block data via RPC.""" + self.context.logger.info("IdleRound.act() called") + try: + # Get latest block number + current_block_number = self.w3.eth.block_number + self.context.logger.info(f"Current block number: {current_block_number}") + + # Check if this is a new block + if self.last_block_number is None or current_block_number > self.last_block_number: + self.context.logger.info(f"New block detected! Processing block: {current_block_number}") + self.last_block_number = current_block_number + + # Get block with logs + latest_block = self.w3.eth.get_block(current_block_number, full_transactions=True) + + # Get logs for ERC20 Transfer events in this block + transfer_logs = self.w3.eth.get_logs({ + 'fromBlock': current_block_number, + 'toBlock': current_block_number, + 'topics': [self.transfer_event_signature] + }) + + # Store block and transfer data in shared state + self.context.shared_state["current_block"] = { + "number": latest_block.number, + "timestamp": latest_block.timestamp, + "transfer_logs": transfer_logs + } + self.context.logger.info(f"Processed {len(transfer_logs)} transfer logs") + self.context.shared_state["new_block_available"] = True + self._event = WhalewatcherabciappEvents.BLOCK_RECEIVED.value + self._is_done = True + self.context.logger.info("Block processing complete, transitioning...") + else: + self.context.logger.info(f"No new block (last: {self.last_block_number}, current: {current_block_number})") + time.sleep(1) + self._is_done = False + + except Exception as e: + self.context.logger.error(f"RPC error in IdleRound: {str(e)}") + self._event = WhalewatcherabciappEvents.TIMEOUT.value + self._is_done = True + time.sleep(5) + + def is_done(self) -> bool: + """Check if the round is done.""" + return self._is_done + + @property + def event(self) -> Optional[str]: + """Get the event for the round.""" + return self._event + + +class BlockReceivedRound(State): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._is_done = False + self._event = None + load_dotenv() + self.threshold = float(os.getenv('WHALE_THRESHOLD', '1000')) # Default 1000 ETH + self.whale_addresses = set() # Track known whale addresses + self.w3 = None + self.base_scan_url = "https://basescan.org" # Base network explorer URL + + def to_checksum_address(self, address: str) -> str: + """Convert address to checksum format.""" + try: + if not self.w3: + self.w3 = self.context.shared_state.get("w3") + return self.w3.to_checksum_address(address) + except Exception as e: + self.context.logger.error(f"Error converting to checksum address: {str(e)}") + return address + + def decode_transfer_log(self, log): + """Decode ERC20 transfer event log.""" + try: + # Transfer(address indexed from, address indexed to, uint256 value) + from_address = "0x" + log["topics"][1].hex()[-40:] # second topic is from + to_address = "0x" + log["topics"][2].hex()[-40:] # third topic is to + + # Properly decode the value from the data field + value = 0 + if log["data"] and log["data"] != "0x" and len(log["data"]) >= 2: + value_hex = log["data"].hex() + if value_hex.startswith("0x"): + value_hex = value_hex[2:] + if value_hex: # Check if there's actual data after removing "0x" + value = int(value_hex, 16) + + # Convert addresses to checksum format + from_address = self.to_checksum_address(from_address) + to_address = self.to_checksum_address(to_address) + token_contract = self.to_checksum_address(log["address"]) + + transfer_data = { + "from": from_address, + "to": to_address, + "value": value, + "token_contract": token_contract, + "transaction_hash": log["transactionHash"].hex() + } + + self.context.logger.debug( + f"Decoded transfer: From={from_address}, To={to_address}, " + f"Value={value}, Token={token_contract}" + ) + + return transfer_data + except Exception as e: + self.context.logger.error( + f"Error decoding transfer log: {str(e)}\n" + f"Log data: {log}\n" + f"Data field: {log.get('data', 'None')}" + ) + return None + + def format_whale_info(self, address: str, balance_eth: float, is_whale: bool) -> str: + """Format whale information for logging.""" + if is_whale: + return f"šŸ‹ Whale detected!\nAddress: {address}\nBalance: {balance_eth:.2f} ETH\nBasescan: {self.base_scan_url}/address/{address}" + return "" # Return empty string if not a whale + + def is_whale_address(self, address: str) -> bool: + """Check if an address has balance above threshold.""" + try: + # Skip burn address + if address.lower() == "0x0000000000000000000000000000000000000000": + return False + + if not self.w3: + self.w3 = self.context.shared_state.get("w3") + if not self.w3: + raise Exception("Web3 instance not found in shared state") + + # Convert to checksum address before getting balance + checksum_address = self.to_checksum_address(address) + balance = self.w3.eth.get_balance(checksum_address) + balance_eth = float(self.w3.from_wei(balance, 'ether')) + is_whale = balance_eth > self.threshold + if is_whale and checksum_address not in self.whale_addresses: + self.whale_addresses.add(checksum_address) + whale_info = self.format_whale_info(checksum_address, balance_eth, True) + self.context.logger.info(f"New whale discovered!\n{whale_info}") + return is_whale + except Exception as e: + self.context.logger.error(f"Error checking balance for {address}: {str(e)}") + return False + + def act(self) -> None: + """Analyze ERC20 transfer events and determine if whales are involved.""" + self.context.logger.info("BlockReceivedRound: Analyzing ERC20 transfers...") + + try: + if not self.w3: + self.w3 = self.context.shared_state.get("w3") + if not self.w3: + raise Exception("Web3 instance not found in shared state") + + block_data = self.context.shared_state.get("current_block", {}) + transfer_logs = block_data.get("transfer_logs", []) + + whale_transfers = [] + for log in transfer_logs: + transfer = self.decode_transfer_log(log) + if not transfer: + continue + + from_address = transfer["from"] + to_address = transfer["to"] + + # Skip if either address is the burn address + if (from_address.lower() == "0x0000000000000000000000000000000000000000" or + to_address.lower() == "0x0000000000000000000000000000000000000000"): + continue + + # Check if either address is a whale + from_is_whale = self.is_whale_address(from_address) if from_address else False + to_is_whale = self.is_whale_address(to_address) if to_address else False + + if from_is_whale or to_is_whale: + # Get balances using checksum addresses + from_balance = float(self.w3.from_wei( + self.w3.eth.get_balance(from_address), 'ether' + )) if from_address else 0 + to_balance = float(self.w3.from_wei( + self.w3.eth.get_balance(to_address), 'ether' + )) if to_address else 0 + + whale_tx = { + **transfer, + "from_is_whale": from_is_whale, + "to_is_whale": to_is_whale, + "from_balance": from_balance, + "to_balance": to_balance, + "base_scan_url": f"{self.base_scan_url}/tx/{transfer['transaction_hash']}" + } + whale_transfers.append(whale_tx) + + # Format detailed transfer log - only show whale information + log_parts = [ + f"\n🚨 Whale ERC20 Transfer Detected! 🚨\n" + f"Transaction: {self.base_scan_url}/tx/{transfer['transaction_hash']}\n" + f"Token Contract: {self.base_scan_url}/token/{transfer['token_contract']}\n" + ] + + if from_is_whale: + log_parts.append(f"\nFrom:\n{self.format_whale_info(from_address, from_balance, True)}\n") + if to_is_whale: + log_parts.append(f"\nTo:\n{self.format_whale_info(to_address, to_balance, True)}\n") + + log_parts.append(f"\nBlock: {block_data.get('number')}") + self.context.logger.info("".join(log_parts)) + + if whale_transfers: + self.context.logger.info(f"Found {len(whale_transfers)} whale ERC20 transfers in block {block_data.get('number')}") + self.context.shared_state["whale_transactions"] = whale_transfers + self._event = WhalewatcherabciappEvents.TX_OVER_THRESHOLD.value + else: + self.context.logger.info(f"No whale ERC20 transfers found in block {block_data.get('number')}") + self._event = WhalewatcherabciappEvents.TX_UNDER_THRESHOLD.value + + except Exception as e: + self.context.logger.error(f"Error processing block: {str(e)}") + self._event = WhalewatcherabciappEvents.TIMEOUT.value + + self._is_done = True + + def is_done(self) -> bool: + return self._is_done + + @property + def event(self) -> Optional[str]: + return self._event + + +class AlertRound(State): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._is_done = False + self._event = None + self.base_scan_url = "https://basescan.org" + + def format_alert_message(self, tx: dict) -> str: + """Format alert message with detailed whale information.""" + parts = [ + f"šŸ‹ Whale ERC20 Transfer Alert šŸ‹\n" + f"Transaction: {tx.get('base_scan_url')}\n" + f"Token Contract: {self.base_scan_url}/token/{tx.get('token_contract')}\n" + ] + + if tx.get('from_is_whale'): + parts.extend([ + f"\nFrom: {tx.get('from')} šŸ‹\n", + f"ETH Balance: {tx.get('from_balance', 0):.2f} ETH\n", + f"Profile: {self.base_scan_url}/address/{tx.get('from')}\n" + ]) + + if tx.get('to_is_whale'): + parts.extend([ + f"\nTo: {tx.get('to')} šŸ‹\n", + f"ETH Balance: {tx.get('to_balance', 0):.2f} ETH\n", + f"Profile: {self.base_scan_url}/address/{tx.get('to')}\n" + ]) + + return "".join(parts) + + def act(self) -> None: + """Send alerts for whale transactions.""" + self.context.logger.info("AlertRound: Sending alerts...") + + try: + whale_txs = self.context.shared_state.get("whale_transactions", []) + + for tx in whale_txs: + # Create alert content + alert_content = { + "type": "whale_alert", + "transaction": tx, + "timestamp": self.context.shared_state.get("current_block", {}).get("timestamp"), + "message": self.format_alert_message(tx) + } + + # Log the formatted alert + self.context.logger.info(f"\n{alert_content['message']}") + + # Create proper Message object + alert_msg = Message( + performative=Message.Performative.INFORM, + content=alert_content, + target=0, # Default target + sender=self.context.agent_address, + ) + + # Send alert through configured channels + self.context.outbox.put_message(message=alert_msg) + + self._event = WhalewatcherabciappEvents.DONE.value + + except Exception as e: + self.context.logger.error(f"Error sending alerts: {str(e)}") + self._event = WhalewatcherabciappEvents.TIMEOUT.value + + self._is_done = True + + def is_done(self) -> bool: + return self._is_done + + @property + def event(self) -> Optional[str]: + return self._event + + +class DoneRound(State): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._is_done = False + self._event = None + + def act(self) -> None: + """Clean up and reset for next cycle.""" + self.context.logger.info("DoneRound: Cleaning up...") + + # Reset state for next cycle + self.context.shared_state["new_block_available"] = False + self.context.shared_state["current_block"] = None + self.context.shared_state["large_transactions"] = [] + + self._is_done = True + self._event = WhalewatcherabciappEvents.DONE.value + + def is_done(self) -> bool: + return self._is_done + + @property + def event(self) -> Optional[str]: + return self._event + + +class ErrorRound(State): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._is_done = False + self._event = None + + def act(self) -> None: + """Handle error conditions and attempt recovery.""" + self.context.logger.error("ErrorRound: Handling error condition") + + # Log error details if available + error_details = self.context.shared_state.get("error_details") + if error_details: + self.context.logger.error(f"Error details: {error_details}") + + # Implement recovery logic here + self.context.shared_state["new_block_available"] = False + self.context.shared_state["current_block"] = None + self.context.shared_state["large_transactions"] = [] + + self._is_done = True + self._event = WhalewatcherabciappEvents.DONE.value + + def is_done(self) -> bool: + return self._is_done + + @property + def event(self) -> Optional[str]: + return self._event + +class WhalewatcherabciappEvents(Enum): + """Events for the FSM.""" + BLOCK_RECEIVED = 'BLOCK_RECEIVED' + TX_OVER_THRESHOLD = 'TX_OVER_THRESHOLD' + TX_UNDER_THRESHOLD = 'TX_UNDER_THRESHOLD' + DONE = 'DONE' + TIMEOUT = 'TIMEOUT' + + +class WhalewatcherabciappFsmBehaviour(FSMBehaviour): + """This class implements a simple Finite State Machine behaviour.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the FSM behavior.""" + # Initialize transition function before super().__init__ + self._transition_function = {} + + # First call super().__init__ + super().__init__(**kwargs) + + # Register all states + self.register_state('idleround', IdleRound(**kwargs)) + self.register_state('blockreceivedround', BlockReceivedRound(**kwargs)) + self.register_state('alertround', AlertRound(**kwargs)) + self.register_state('errorround', ErrorRound(**kwargs)) + self.register_state('doneround', DoneRound(**kwargs)) + + # Set initial state + self.current_state = 'idleround' + + # Initialize transitions dictionary for each state + self._transition_function = { + 'idleround': {}, + 'blockreceivedround': {}, + 'alertround': {}, + 'errorround': {}, + 'doneround': {} + } + + # Register all transitions + self.register_transition( + source='idleround', + destination='blockreceivedround', + event=WhalewatcherabciappEvents.BLOCK_RECEIVED.value + ) + self.register_transition( + source='blockreceivedround', + destination='alertround', + event=WhalewatcherabciappEvents.TX_OVER_THRESHOLD.value + ) + self.register_transition( + source='blockreceivedround', + destination='idleround', + event=WhalewatcherabciappEvents.TX_UNDER_THRESHOLD.value + ) + self.register_transition( + source='alertround', + destination='doneround', + event=WhalewatcherabciappEvents.DONE.value + ) + self.register_transition( + source='doneround', + destination='idleround', + event=WhalewatcherabciappEvents.DONE.value + ) + # Error transitions + self.register_transition( + source='errorround', + destination='idleround', + event=WhalewatcherabciappEvents.DONE.value + ) + # Timeout transitions + self.register_transition( + source='blockreceivedround', + destination='errorround', + event=WhalewatcherabciappEvents.TIMEOUT.value + ) + self.register_transition( + source='alertround', + destination='errorround', + event=WhalewatcherabciappEvents.TIMEOUT.value + ) + self.register_transition( + source='idleround', + destination='errorround', + event=WhalewatcherabciappEvents.TIMEOUT.value + ) + + def register_transition(self, source: str, destination: str, event: str) -> None: + """Register a transition in the FSM.""" + if source not in self._transition_function: + self._transition_function[source] = {} + self._transition_function[source][event] = destination + self.context.logger.info(f"Registered transition: {source} -> {destination} on {event}") + + def act(self) -> None: + """Execute one step of the FSM behaviour.""" + self.context.logger.info(f"FSM act() called. Current state: {self.current_state}") + + if self.current_state is None: + self.context.logger.warning("No current state set, defaulting to idleround") + self.current_state = 'idleround' + return + + try: + current_round = self._name_to_state[self.current_state] + self.context.logger.info(f"Executing state: {self.current_state}") + current_round.act() + + if current_round.is_done(): + next_state = self.get_next_state(current_round.event) + if next_state: + self.context.logger.info(f"Transitioning from {self.current_state} to {next_state}") + self.current_state = next_state + else: + self.context.logger.info(f"No transition found for event {current_round.event}") + # If no transition is found, return to idle state + if self.current_state != 'idleround': + self.context.logger.info("Returning to idle state") + self.current_state = 'idleround' + + except Exception as e: + self.context.logger.error(f"Error in FSM act: {str(e)}") + self.current_state = 'errorround' + + def get_next_state(self, event: Optional[str]) -> Optional[str]: + """Get the next state based on the current state and event.""" + if event is None: + return None + + transitions = self._transition_function.get(self.current_state, {}) + next_state = transitions.get(event) + self.context.logger.info(f"Current state: {self.current_state}, Event: {event}, Next state: {next_state}") + return next_state + + def setup(self) -> None: + """Set up the FSM behaviour.""" + self.context.logger.info("Setting up Whalewatcherabciapp FSM behaviour...") + if self.current_state is None: + self.current_state = 'idleround' + self.context.logger.info(f"Initial state set to: {self.current_state}") + super().setup() + self.context.logger.info("FSM setup complete") + # Ensure first act() is called + self.act() + + def is_done(self) -> bool: + """Override is_done to ensure behaviour continues running.""" + return False # Never done, keep running + + def teardown(self) -> None: + """Tear down the FSM behaviour.""" + self.context.logger.info("Tearing down Whalewatcherabciapp FSM behaviour.") + super().teardown() + + def terminate(self) -> None: + """Implement the termination.""" + print("Terminating the agent.") + sys.exit(0) +``` + ### 8. Fingerprint the Skill ```bash @@ -128,7 +732,7 @@ cd .. && adev run your_name/some_agent ## Environment Variables -Create a `.env` file in your agent's directory with: +Create a `.env` file in the root directory with: ```env RPC_URL=your_ethereum_rpc_endpoint @@ -183,6 +787,4 @@ The agent includes comprehensive error handling: Common issues and solutions: 1. RPC Connection Issues: Check your RPC endpoint and internet connection -2. Missing Environment Variables: Ensure all required environment variables are set -3. Decoding Errors: Verify contract ABI and event signatures -4. Performance Issues: Adjust batch sizes and polling intervals \ No newline at end of file +2. Missing Environment Variables: Ensure all required environment variables are set in .env \ No newline at end of file