2020class OracleMode (Enum ):
2121 """Oracle operating modes."""
2222
23- EVENT_LISTENER = "event_listener" # Listen for BlockHeaderRequested events
24- PUSH = "push" # Push latest block headers periodically
25- WATCHER = "watcher" # Watch specific addresses for interactions
23+ EVENT_LISTENER = "event_listener"
24+ PUSH = "push"
25+ WATCHER = "watcher"
26+ TOKEN_WATCHER = "token_watcher"
2627
2728
2829@dataclass (frozen = True , slots = True )
@@ -202,6 +203,71 @@ def __post_init__(self) -> None:
202203 object .__setattr__ (self , "watch_addresses" , checksummed_addresses )
203204
204205
206+ @dataclass (frozen = True , slots = True )
207+ class TokenWatcherModeConfig :
208+ """Configuration specific to token watcher mode.
209+
210+ This mode watches for ERC20 token transfers to specific recipient addresses
211+ and pushes block headers when such transfers are detected.
212+
213+ :cvar token_addresses: List of ERC20 token contract addresses to monitor (env: TOKEN_ADDRESSES, comma-separated)
214+ :cvar recipient_addresses: List of recipient addresses to watch for (env: RECIPIENT_ADDRESSES, comma-separated)
215+ :cvar scan_interval: Seconds between scanning for token transfers (env: SCAN_INTERVAL)
216+ :cvar max_blocks_per_scan: Maximum number of blocks to scan per iteration (env: MAX_BLOCKS_PER_SCAN, default: 10)
217+ """
218+
219+ token_addresses : list [str ]
220+ recipient_addresses : list [str ]
221+ scan_interval : int
222+ max_blocks_per_scan : int = 10
223+
224+ def __post_init__ (self ) -> None :
225+ """Validate token watcher configuration."""
226+ if self .scan_interval <= 0 :
227+ raise ValueError (
228+ f"Scan interval must be positive, got { self .scan_interval } "
229+ )
230+ if self .scan_interval > 300 :
231+ raise ValueError (
232+ f"Scan interval too long (max 300s), got { self .scan_interval } "
233+ )
234+
235+ if self .max_blocks_per_scan <= 0 :
236+ raise ValueError (
237+ f"Max blocks per scan must be positive, got { self .max_blocks_per_scan } "
238+ )
239+ if self .max_blocks_per_scan > 10000 :
240+ raise ValueError (
241+ f"Max blocks per scan too high (max 10000), got { self .max_blocks_per_scan } "
242+ )
243+
244+ if not self .token_addresses or len (self .token_addresses ) == 0 :
245+ raise ValueError (
246+ "Token watcher mode requires at least one token address"
247+ )
248+
249+ if not self .recipient_addresses or len (self .recipient_addresses ) == 0 :
250+ raise ValueError (
251+ "Token watcher mode requires at least one recipient address"
252+ )
253+
254+ # Validate and checksum token addresses
255+ checksummed_tokens = []
256+ for addr in self .token_addresses :
257+ if not Web3 .is_address (addr ):
258+ raise ValueError (f"Invalid token address: { addr } " )
259+ checksummed_tokens .append (Web3 .to_checksum_address (addr ))
260+ object .__setattr__ (self , "token_addresses" , checksummed_tokens )
261+
262+ # Validate and checksum recipient addresses
263+ checksummed_recipients = []
264+ for addr in self .recipient_addresses :
265+ if not Web3 .is_address (addr ):
266+ raise ValueError (f"Invalid recipient address: { addr } " )
267+ checksummed_recipients .append (Web3 .to_checksum_address (addr ))
268+ object .__setattr__ (self , "recipient_addresses" , checksummed_recipients )
269+
270+
205271@dataclass (frozen = True , slots = True )
206272class CommonConfig :
207273 """Configuration common to all oracle modes.
@@ -294,14 +360,19 @@ class OracleConfig:
294360
295361 :cvar common_config: Common configuration shared across all modes
296362 :cvar oracle_mode: The operating mode of the oracle
297- :cvar mode_config: Mode-specific configuration (EventListenerModeConfig, PushModeConfig, or WatcherModeConfig )
363+ :cvar mode_config: Mode-specific configuration (EventListenerModeConfig, PushModeConfig, WatcherModeConfig, or TokenWatcherModeConfig )
298364 :cvar local_mode: Whether running in local mode (for testing)
299365 :cvar local_private_key: Private key for local mode (optional)
300366 """
301367
302368 common_config : CommonConfig
303369 oracle_mode : OracleMode
304- mode_config : EventListenerModeConfig | PushModeConfig | WatcherModeConfig
370+ mode_config : (
371+ EventListenerModeConfig
372+ | PushModeConfig
373+ | WatcherModeConfig
374+ | TokenWatcherModeConfig
375+ )
305376 local_mode : bool = False
306377 local_private_key : str | None = None
307378
@@ -346,6 +417,12 @@ def __post_init__(self) -> None:
346417 self .mode_config , WatcherModeConfig
347418 ):
348419 raise ValueError ("Watcher mode requires WatcherModeConfig" )
420+ elif self .oracle_mode == OracleMode .TOKEN_WATCHER and not isinstance (
421+ self .mode_config , TokenWatcherModeConfig
422+ ):
423+ raise ValueError (
424+ "Token watcher mode requires TokenWatcherModeConfig"
425+ )
349426
350427 @classmethod
351428 def from_env (cls , local_mode : bool = False ) -> "OracleConfig" :
@@ -395,15 +472,20 @@ def from_env(cls, local_mode: bool = False) -> "OracleConfig":
395472 oracle_mode = OracleMode .PUSH
396473 elif mode_str == "watcher" :
397474 oracle_mode = OracleMode .WATCHER
475+ elif mode_str == "token_watcher" :
476+ oracle_mode = OracleMode .TOKEN_WATCHER
398477 else :
399478 raise ValueError (
400479 f"Invalid ORACLE_MODE: { mode_str } . "
401- "Must be one of: event_listener, push, watcher"
480+ "Must be one of: event_listener, push, watcher, token_watcher "
402481 )
403482
404483 # Load mode-specific configuration
405484 mode_config : (
406- EventListenerModeConfig | PushModeConfig | WatcherModeConfig
485+ EventListenerModeConfig
486+ | PushModeConfig
487+ | WatcherModeConfig
488+ | TokenWatcherModeConfig
407489 )
408490
409491 if oracle_mode == OracleMode .EVENT_LISTENER :
@@ -417,7 +499,7 @@ def from_env(cls, local_mode: bool = False) -> "OracleConfig":
417499 push_interval = int (os .environ .get ("PUSH_INTERVAL" , "60" )),
418500 batch_size = int (os .environ .get ("PUSH_BATCH_SIZE" , "20" )),
419501 )
420- else : # OracleMode.WATCHER
502+ elif oracle_mode == OracleMode .WATCHER :
421503 watch_addresses_str = os .environ .get ("WATCH_ADDRESSES" , "" )
422504 if not watch_addresses_str :
423505 raise ValueError (
@@ -450,6 +532,46 @@ def from_env(cls, local_mode: bool = False) -> "OracleConfig":
450532 os .environ .get ("HEARTBEAT_INTERVAL_SECONDS" , "3600" )
451533 ),
452534 )
535+ else : # OracleMode.TOKEN_WATCHER
536+ token_addresses_str = os .environ .get ("TOKEN_ADDRESSES" , "" )
537+ if not token_addresses_str :
538+ raise ValueError (
539+ "TOKEN_ADDRESSES is required for token watcher mode. "
540+ "Provide comma-separated token contract addresses (e.g., USDC, USDT)."
541+ )
542+
543+ recipient_addresses_str = os .environ .get ("RECIPIENT_ADDRESSES" , "" )
544+ if not recipient_addresses_str :
545+ raise ValueError (
546+ "RECIPIENT_ADDRESSES is required for token watcher mode. "
547+ "Provide comma-separated recipient addresses to watch."
548+ )
549+
550+ # Parse comma-separated token addresses
551+ token_addresses = [
552+ addr .strip ()
553+ for addr in token_addresses_str .split ("," )
554+ if addr .strip ()
555+ ]
556+
557+ if len (token_addresses ) == 0 :
558+ raise ValueError ("TOKEN_ADDRESSES cannot be empty" )
559+
560+ # Parse comma-separated recipient addresses
561+ recipient_addresses = [
562+ addr .strip ()
563+ for addr in recipient_addresses_str .split ("," )
564+ if addr .strip ()
565+ ]
566+
567+ if len (recipient_addresses ) == 0 :
568+ raise ValueError ("RECIPIENT_ADDRESSES cannot be empty" )
569+
570+ mode_config = TokenWatcherModeConfig (
571+ token_addresses = token_addresses ,
572+ recipient_addresses = recipient_addresses ,
573+ scan_interval = int (os .environ .get ("SCAN_INTERVAL" , "5" )),
574+ )
453575
454576 # Load oracle-level config
455577 local_private_key = (
@@ -527,6 +649,26 @@ def log_config(self) -> None:
527649 for addr in self .mode_config .watch_addresses :
528650 logger .info (f" - { addr } " )
529651
652+ elif self .oracle_mode == OracleMode .TOKEN_WATCHER :
653+ assert isinstance (self .mode_config , TokenWatcherModeConfig )
654+ logger .info ("Token Watcher Settings:" )
655+ logger .info (
656+ f" Scan Interval: { self .mode_config .scan_interval } seconds"
657+ )
658+ logger .info (
659+ " Mode: Real-time (scans all new blocks since last check)"
660+ )
661+ logger .info (
662+ f" Watching { len (self .mode_config .token_addresses )} token(s):"
663+ )
664+ for addr in self .mode_config .token_addresses :
665+ logger .info (f" - { addr } " )
666+ logger .info (
667+ f" Monitoring { len (self .mode_config .recipient_addresses )} recipient(s):"
668+ )
669+ for addr in self .mode_config .recipient_addresses :
670+ logger .info (f" - { addr } " )
671+
530672 logger .info ("Oracle Settings:" )
531673 logger .info (f" Mode: { 'LOCAL' if self .local_mode else 'PRODUCTION' } " )
532674
0 commit comments