316 lines
11 KiB
Python
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) |