stones/scripts/utils/ens_resolver.py

316 lines
11 KiB
Python

#!/usr/bin/env python3
"""
ENS Resolver
Utility for resolving Ethereum addresses to ENS names and extracting profile information.
"""
import os
import sys
import json
from typing import Dict, Optional, Any
import requests
from web3 import Web3
from web3.exceptions import BadFunctionCallOutput
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
class ENSResolver:
"""Resolver for ENS names and profiles."""
def __init__(self, web3_instance: Web3):
"""
Initialize the ENS resolver.
Args:
web3_instance: Web3 instance to use for ENS resolution
"""
self.web3 = web3_instance
self.alchemy_api_key = os.getenv("ALCHEMY_API_KEY")
if not self.alchemy_api_key:
raise ValueError("ALCHEMY_API_KEY not found in environment variables")
self.alchemy_url = f"https://eth-mainnet.g.alchemy.com/v2/{self.alchemy_api_key}"
# ENS Registry contract address
self.ens_registry_address = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
self.ens_registry_abi = [
{
"constant": True,
"inputs": [{"name": "node", "type": "bytes32"}],
"name": "resolver",
"outputs": [{"name": "", "type": "address"}],
"type": "function"
}
]
self.ens_registry = self.web3.eth.contract(
address=self.ens_registry_address,
abi=self.ens_registry_abi
)
# ENS Resolver ABI (partial)
self.ens_resolver_abi = [
{
"constant": True,
"inputs": [{"name": "node", "type": "bytes32"}],
"name": "addr",
"outputs": [{"name": "", "type": "address"}],
"type": "function"
},
{
"constant": True,
"inputs": [{"name": "node", "type": "bytes32"}, {"name": "key", "type": "string"}],
"name": "text",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
}
]
def namehash(self, name: str) -> bytes:
"""
Compute the namehash of an ENS name.
Args:
name: ENS name
Returns:
Namehash as bytes
"""
if name == '':
return bytes([0] * 32)
# Handle names that start with '0x' differently
if name.startswith('0x') and len(name) > 2 and all(c in '0123456789abcdefABCDEF' for c in name[2:]):
# This is a hex string, not an ENS name
return bytes.fromhex(name[2:])
# For actual ENS names (even if they start with numbers like 0xcorini.eth)
labels = name.split('.')
labels.reverse()
node = bytes([0] * 32)
for label in labels:
label_hash = self.web3.keccak(text=label)
node = self.web3.keccak(node + label_hash)
return node
def get_ens_name(self, address: str) -> Optional[str]:
"""
Resolve an Ethereum address to an ENS name using reverse lookup.
Args:
address: Ethereum address to resolve
Returns:
ENS name if found, None otherwise
"""
try:
# Ensure the address is checksummed
address = Web3.to_checksum_address(address)
# Use web3.py's built-in ENS functionality
ens_name = self.web3.ens.name(address)
# Verify the name resolves back to the address
if ens_name:
resolved_address = self.get_ens_address(ens_name)
if resolved_address and resolved_address.lower() == address.lower():
return ens_name
return None
except Exception as e:
# Log errors but don't fail
print(f"Error resolving ENS name for {address}: {str(e)}")
return None
def get_ens_address(self, ens_name: str) -> Optional[str]:
"""
Resolve an ENS name to an Ethereum address.
Args:
ens_name: ENS name to resolve
Returns:
Ethereum address if found, None otherwise
"""
try:
# Use web3.py's built-in ENS functionality
address = self.web3.ens.address(ens_name)
return address
except Exception as e:
# Log errors but don't fail
print(f"Error resolving ENS address for {ens_name}: {str(e)}")
return None
def get_resolver_for_name(self, ens_name: str) -> Optional[str]:
"""
Get the resolver contract address for an ENS name.
Args:
ens_name: ENS name
Returns:
Resolver contract address if found, None otherwise
"""
try:
node = self.namehash(ens_name)
resolver_address = self.ens_registry.functions.resolver(node).call()
if resolver_address == "0x0000000000000000000000000000000000000000":
return None
return resolver_address
except Exception as e:
print(f"Error getting resolver for {ens_name}: {str(e)}")
return None
def get_text_record(self, ens_name: str, key: str) -> Optional[str]:
"""
Get a text record for an ENS name.
Args:
ens_name: ENS name
key: Text record key
Returns:
Text record value if found, None otherwise
"""
try:
resolver_address = self.get_resolver_for_name(ens_name)
if not resolver_address:
return None
resolver = self.web3.eth.contract(
address=resolver_address,
abi=self.ens_resolver_abi
)
node = self.namehash(ens_name)
value = resolver.functions.text(node, key).call()
return value if value else None
except Exception as e:
print(f"Error getting text record {key} for {ens_name}: {str(e)}")
return None
def get_ens_profile(self, ens_name: str) -> Dict[str, Any]:
"""
Get profile information for an ENS name.
Args:
ens_name: ENS name to get profile for
Returns:
Dictionary of profile information
"""
profile = {}
try:
# Common text record keys
text_keys = [
"name", "email", "url", "avatar", "description",
"com.twitter", "twitter",
"com.github", "github",
"org.telegram", "telegram",
"com.discord", "discord",
"com.farcaster", "social.farcaster", "farcaster"
]
# Get text records
for key in text_keys:
value = self.get_text_record(ens_name, key)
if value:
# Handle Farcaster variants
if key in ["com.farcaster", "social.farcaster", "farcaster"]:
profile["farcaster"] = value
# Handle Twitter variants
elif key in ["com.twitter", "twitter"]:
profile["twitter"] = value
# Handle Discord variants
elif key in ["com.discord", "discord"]:
profile["discord"] = value
# Handle Telegram variants
elif key in ["org.telegram", "telegram"]:
profile["telegram"] = value
# Handle GitHub variants
elif key in ["com.github", "github"]:
profile["github"] = value
# Handle other common fields
elif key in ["email", "url", "avatar", "description", "name"]:
profile[key] = value
# Try to get additional social media records
other_social = {}
for prefix in ["com.", "social."]:
for platform in ["reddit", "linkedin", "instagram", "facebook", "youtube", "tiktok", "lens"]:
key = f"{prefix}{platform}"
value = self.get_text_record(ens_name, key)
if value:
other_social[key] = value
if other_social:
profile["otherSocial"] = json.dumps(other_social)
except Exception as e:
# Log errors but don't fail
print(f"Error getting ENS profile for {ens_name}: {str(e)}")
return profile
def update_contact_from_ens(self, contact_id: str, ens_name: str) -> None:
"""
Update a contact with information from their ENS profile.
Args:
contact_id: ID of the contact to update
ens_name: ENS name of the contact
"""
# Import here to avoid circular imports
from utils.db_connector import DatabaseConnector
# Get the profile
profile = self.get_ens_profile(ens_name)
if not profile:
return
# Map ENS profile fields to database fields
# Only include fields that exist in the Contact model
db_fields = {
"name": profile.get("name"),
"email": profile.get("email"),
"farcaster": profile.get("farcaster"),
"twitter": profile.get("twitter"),
"discord": profile.get("discord"),
"telegram": profile.get("telegram"),
"otherSocial": profile.get("otherSocial")
}
# Filter out None values
db_fields = {k: v for k, v in db_fields.items() if v is not None}
if db_fields:
# Update the contact
db = DatabaseConnector()
db.update_contact(contact_id, db_fields)
# Add a note with additional profile information
note_content = f"ENS Profile Information for {ens_name}:\n"
for key, value in profile.items():
if key not in ["name", "email", "farcaster", "twitter", "discord", "telegram"]:
note_content += f"{key}: {value}\n"
if note_content != f"ENS Profile Information for {ens_name}:\n":
db.add_note_to_contact(contact_id, note_content)