224 lines
8.4 KiB
Python
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()) |