stones/scripts/utils/resolve_all_ens.py

224 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""
Resolve ENS Names and Contact Information for All Contacts
This script resolves ENS names and additional contact information for all contacts
in the database that have Ethereum addresses. It uses the existing ENS resolver utility
to fetch ENS names and text records containing social profiles and contact information.
Usage:
python resolve_all_ens.py [--batch-size BATCH_SIZE] [--delay DELAY]
"""
import os
import sys
import logging
import time
import argparse
from typing import Dict, Any, List, Optional, Tuple
from web3 import Web3
from dotenv import load_dotenv
# Add parent directory to path to import utils
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils.db_connector import DatabaseConnector
from utils.ens_resolver import ENSResolver
from utils.logger import setup_logger
# Load environment variables
load_dotenv()
# Setup logging
logger = setup_logger("all_ens_resolver")
class AllContactsENSResolver:
"""Resolver for ENS names and contact information for all contacts"""
def __init__(self):
"""Initialize the resolver"""
# Initialize database
self.db = DatabaseConnector()
# Initialize Web3 and ENS resolver
alchemy_api_key = os.getenv("ALCHEMY_API_KEY")
if not alchemy_api_key:
raise ValueError("ALCHEMY_API_KEY not found in environment variables")
self.web3 = Web3(Web3.HTTPProvider(f"https://eth-mainnet.g.alchemy.com/v2/{alchemy_api_key}"))
self.ens_resolver = ENSResolver(self.web3)
# Register data source
self.data_source_id = self.register_data_source()
def register_data_source(self) -> str:
"""Register the ENS data source in the database"""
return self.db.upsert_data_source(
name="ENS Resolver",
source_type="blockchain",
description="ENS names and profile information resolved from Ethereum addresses"
)
def get_contacts_without_ens(self) -> List[Dict[str, Any]]:
"""Get all contacts that have an Ethereum address but no ENS name"""
query = """
SELECT id, "ethereumAddress", name
FROM "Contact"
WHERE "ethereumAddress" IS NOT NULL
AND "ensName" IS NULL
"""
result = self.db.execute_query(query)
logger.info(f"Found {len(result)} contacts without ENS names")
return result
def get_all_contacts_with_eth_address(self) -> List[Dict[str, Any]]:
"""Get all contacts that have an Ethereum address"""
query = """
SELECT id, "ethereumAddress", "ensName", name, twitter, discord, telegram, email, farcaster
FROM "Contact"
WHERE "ethereumAddress" IS NOT NULL
"""
result = self.db.execute_query(query)
logger.info(f"Found {len(result)} contacts with Ethereum addresses")
return result
def process_contact(self, contact: Dict[str, Any]) -> Tuple[bool, bool]:
"""
Process a single contact to resolve ENS name and contact info
Args:
contact: Contact data from the database
Returns:
Tuple of (ens_updated, info_updated) booleans
"""
contact_id = contact["id"]
address = contact["ethereumAddress"]
current_ens = contact.get("ensName")
ens_updated = False
info_updated = False
# Skip if no address
if not address:
return ens_updated, info_updated
# Resolve ENS name if not already set
if not current_ens:
ens_name = self.ens_resolver.get_ens_name(address)
if ens_name:
# Update contact with ENS name
self.db.update_contact(contact_id, {"ensName": ens_name})
logger.info(f"Updated contact {contact_id} with ENS name: {ens_name}")
current_ens = ens_name
ens_updated = True
# Get contact info from ENS text records if we have an ENS name
if current_ens:
# Update profile from ENS
self.ens_resolver.update_contact_from_ens(contact_id, current_ens)
info_updated = True
# Link to data source
self.db.link_contact_to_data_source(contact_id, self.data_source_id)
return ens_updated, info_updated
def run(self, batch_size: int = 50, delay_seconds: float = 0.5, resolve_all: bool = False):
"""
Run the resolver for contacts
Args:
batch_size: Number of contacts to process in each batch
delay_seconds: Delay between processing contacts
resolve_all: Whether to process all contacts or just those without ENS names
Returns:
Tuple of (ens_updated_count, info_updated_count)
"""
# Create a scraping job
job_id = self.db.create_scraping_job("ENS Resolver", "running")
logger.info(f"Created scraping job with ID: {job_id}")
try:
if resolve_all:
contacts = self.get_all_contacts_with_eth_address()
else:
contacts = self.get_contacts_without_ens()
if not contacts:
logger.info("No contacts found to process")
self.db.update_scraping_job(job_id, "completed")
return 0, 0
ens_updated_count = 0
info_updated_count = 0
# Process in batches to avoid rate limiting
for i in range(0, len(contacts), batch_size):
batch = contacts[i:i+batch_size]
logger.info(f"Processing batch {i//batch_size + 1}/{(len(contacts) + batch_size - 1)//batch_size}")
for contact in batch:
ens_updated, info_updated = self.process_contact(contact)
if ens_updated:
ens_updated_count += 1
if info_updated:
info_updated_count += 1
# Add a small delay to avoid rate limiting
time.sleep(delay_seconds)
# Update the scraping job
self.db.update_scraping_job(
job_id,
"running",
records_processed=len(batch),
records_updated=sum(1 for c in batch if self.process_contact(c)[0] or self.process_contact(c)[1])
)
# Complete the scraping job
self.db.update_scraping_job(
job_id,
"completed",
records_processed=len(contacts),
records_added=ens_updated_count,
records_updated=info_updated_count
)
logger.info(f"Updated ENS names for {ens_updated_count} contacts and contact info for {info_updated_count} contacts out of {len(contacts)} processed")
return ens_updated_count, info_updated_count
except Exception as e:
# Update the scraping job with error
self.db.update_scraping_job(job_id, "failed", error_message=str(e))
logger.exception(f"Error resolving ENS names: {e}")
raise
def main():
"""Main function"""
try:
parser = argparse.ArgumentParser(description="Resolve ENS names and contact information for all contacts")
parser.add_argument("--all", action="store_true", help="Process all contacts with Ethereum addresses, not just those without ENS names")
parser.add_argument("--batch-size", type=int, default=50, help="Number of contacts to process in each batch")
parser.add_argument("--delay", type=float, default=0.5, help="Delay in seconds between processing contacts")
args = parser.parse_args()
resolver = AllContactsENSResolver()
ens_count, info_count = resolver.run(
batch_size=args.batch_size,
delay_seconds=args.delay,
resolve_all=args.all
)
logger.info(f"ENS resolution completed successfully. Updated {ens_count} ENS names and {info_count} contact info records.")
return 0
except Exception as e:
logger.exception(f"Error running ENS resolver: {e}")
return 1
if __name__ == "__main__":
sys.exit(main())