์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” ํŒŒ์ด์ฌ์œผ๋กœ ENS ์ฃผ์†Œ ์ž๋™ ๊ฒ€์ฆ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿ” ํŒŒ์ด์ฌ์œผ๋กœ ENS ์ฃผ์†Œ ์ž๋™ ๊ฒ€์ฆ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์‹ค์ „ ๊ฐ€์ด๋“œ

๐Ÿ’ก ์ด ๊ธ€์„ ์ฝ์œผ๋ฉด ENS(Ethereum Name Service) ์ฃผ์†Œ ๊ฒ€์ฆ ์‹œ์Šคํ…œ์„ ํŒŒ์ด์ฌ์œผ๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด์š”. ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์— ๊ด€์‹ฌ ์žˆ๋Š” ๋ถ„๋“ค์ด๋ผ๋ฉด ๊ผญ ์•Œ์•„์•ผ ํ•  ์‹ค์ „ ๊ธฐ์ˆ ์ด์ฃ ! ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผ์˜ค๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค. ๐Ÿ˜Š

๐ŸŽฏ ENS๊ฐ€ ๋ญ๊ธธ๋ž˜?

์นœ๊ตฌ์•ผ, ๋จผ์ € ENS๊ฐ€ ๋ญ”์ง€๋ถ€ํ„ฐ ์•Œ์•„๋ณผ๊นŒ? ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ ๋ณด๋ฉด 0x71C7656EC7ab88b098defB751B7401B5f6d8976F ์ด๋Ÿฐ ์‹์œผ๋กœ ์—„์ฒญ ๊ธธ๊ณ  ๋ณต์žกํ•˜์ž–์•„? ๐Ÿ˜ต ์ด๊ฑธ ์™ธ์šฐ๊ฑฐ๋‚˜ ์ž…๋ ฅํ•˜๋Š” ๊ฑด ์ •๋ง ๊ณ ์—ญ์ด์—์š”.

ENS๋Š” ์ด๋Ÿฐ ๋ณต์žกํ•œ ์ฃผ์†Œ๋ฅผ vitalik.eth ๊ฐ™์€ ์ฝ๊ธฐ ์‰ฌ์šด ์ด๋ฆ„์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ์„œ๋น„์Šค์˜ˆ์š”. ๋งˆ์น˜ IP ์ฃผ์†Œ(192.168.0.1) ๋Œ€์‹  ๋„๋ฉ”์ธ ์ด๋ฆ„(google.com)์„ ์“ฐ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”! ๐ŸŒ

๐Ÿ’ก ์™œ ENS ๊ฒ€์ฆ์ด ํ•„์š”ํ• ๊นŒ? ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ENS ์ฃผ์†Œ๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š”์ง€, ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์ธ์ง€, ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ค ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ์™€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ด์š”. ์ž˜๋ชป๋œ ์ฃผ์†Œ๋กœ ์•”ํ˜ธํ™”ํ๋ฅผ ๋ณด๋‚ด๋ฉด ์˜์›ํžˆ ์žƒ์–ด๋ฒ„๋ฆด ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”! ๐Ÿ˜ฑ
๋ณต์žกํ•œ ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ 0x71C7656EC7ab88b098 defB751B7401B5f6d8976F ๐Ÿ˜ต โœจ ENS โœจ ์ฝ๊ธฐ ์‰ฌ์šด ์ด๋ฆ„ vitalik.eth ๐Ÿ˜Š

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”? ๋จผ์ € ํ•„์š”ํ•œ ๋„๊ตฌ๋“ค์„ ์ค€๋น„ํ•ด์•ผ ํ•ด์š”. ์š”๋ฆฌํ•˜๊ธฐ ์ „์— ์žฌ๋ฃŒ ์ค€๋น„ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”! ๐Ÿ‘จโ€๐Ÿณ

ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

ENS ๊ฒ€์ฆ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๋ ค๋ฉด ๋ช‡ ๊ฐ€์ง€ ํŒŒ์ด์ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด์š”:

pip install web3
pip install ens
pip install python-dotenv
๐Ÿ“ฆ ๊ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์—ญํ•  web3.py: ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๋Š” ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
ens: ENS ๊ด€๋ จ ์ž‘์—…์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ์ „๋ฌธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
python-dotenv: API ํ‚ค ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌ

Infura ๋˜๋Š” Alchemy API ํ‚ค ๋ฐœ๊ธ‰๋ฐ›๊ธฐ

์ด๋”๋ฆฌ์›€ ๋„คํŠธ์›Œํฌ์— ์ ‘๊ทผํ•˜๋ ค๋ฉด ๋…ธ๋“œ ์ œ๊ณต ์„œ๋น„์Šค๊ฐ€ ํ•„์š”ํ•ด์š”. ์ง์ ‘ ๋…ธ๋“œ๋ฅผ ์šด์˜ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ์ดˆ๋ณด์ž์—๊ฒŒ๋Š” Infura๋‚˜ Alchemy ๊ฐ™์€ ์„œ๋น„์Šค๋ฅผ ์ถ”์ฒœํ•ด์š”! ๐Ÿš€

1

Infura ํšŒ์›๊ฐ€์ž…
infura.io์— ์ ‘์†ํ•ด์„œ ๋ฌด๋ฃŒ ๊ณ„์ •์„ ๋งŒ๋“ค์–ด์š”. ์ด๋ฉ”์ผ ์ธ์ฆ๋งŒ ํ•˜๋ฉด ๋!

2

์ƒˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
๋Œ€์‹œ๋ณด๋“œ์—์„œ "Create New Project"๋ฅผ ํด๋ฆญํ•˜๊ณ  ํ”„๋กœ์ ํŠธ ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์š”. (์˜ˆ: "ENS-Validator")

3

API ํ‚ค ๋ณต์‚ฌ
ํ”„๋กœ์ ํŠธ ์„ค์ •์—์„œ Project ID๋ฅผ ๋ณต์‚ฌํ•ด์š”. ์ด๊ฒŒ ๋ฐ”๋กœ ์šฐ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ API ํ‚ค๋ž๋‹ˆ๋‹ค! ๐Ÿ”‘

ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •

ํ”„๋กœ์ ํŠธ ํด๋”์— .env ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  API ํ‚ค๋ฅผ ์ €์žฅํ•ด์š”:

INFURA_PROJECT_ID=your_project_id_here
ETHEREUM_NETWORK=mainnet
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ .env ํŒŒ์ผ์€ ์ ˆ๋Œ€ Git์— ์ปค๋ฐ‹ํ•˜๋ฉด ์•ˆ ๋ผ์š”! .gitignore ํŒŒ์ผ์— ๊ผญ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”. API ํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜๋ฉด ์•…์šฉ๋  ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿ”ง ๊ธฐ๋ณธ ENS ๊ฒ€์ฆ ์‹œ์Šคํ…œ ๊ตฌํ˜„

๋“œ๋””์–ด ์ฝ”๋”ฉ ์‹œ์ž‘! ๐Ÿ˜Ž ๋จผ์ € ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ENS ๊ฒ€์ฆ ๊ธฐ๋Šฅ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”.

Web3 ์—ฐ๊ฒฐ ์„ค์ •

์ด๋”๋ฆฌ์›€ ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์š”:

from web3 import Web3
from ens import ENS
from dotenv import load_dotenv
import os

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ
load_dotenv()

# Infura ์—ฐ๊ฒฐ ์„ค์ •
infura_url = f"https://mainnet.infura.io/v3/{os.getenv('INFURA_PROJECT_ID')}"
w3 = Web3(Web3.HTTPProvider(infura_url))

# ENS ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
ns = ENS.fromWeb3(w3)

# ์—ฐ๊ฒฐ ํ™•์ธ
if w3.is_connected():
    print("โœ… ์ด๋”๋ฆฌ์›€ ๋„คํŠธ์›Œํฌ์— ์„ฑ๊ณต์ ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!")
    print(f"ํ˜„์žฌ ๋ธ”๋ก ๋ฒˆํ˜ธ: {w3.eth.block_number}")
else:
    print("โŒ ์—ฐ๊ฒฐ ์‹คํŒจ")

์ด ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท์— ์—ฐ๊ฒฐ๋˜๊ณ , ํ˜„์žฌ ๋ธ”๋ก ๋ฒˆํ˜ธ๊ฐ€ ์ถœ๋ ฅ๋ผ์š”. ๋ธ”๋ก ๋ฒˆํ˜ธ๊ฐ€ ๋ณด์ธ๋‹ค๋ฉด ์„ฑ๊ณต! ๐ŸŽ‰

ENS ์ด๋ฆ„ ๊ฒ€์ฆ ํ•จ์ˆ˜

์ด์ œ ENS ์ด๋ฆ„์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:

def validate_ens_name(ens_name):
    """
    ENS ์ด๋ฆ„์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ํ•จ์ˆ˜
    
    Args:
        ens_name (str): ๊ฒ€์ฆํ•  ENS ์ด๋ฆ„ (์˜ˆ: 'vitalik.eth')
    
    Returns:
        dict: ๊ฒ€์ฆ ๊ฒฐ๊ณผ์™€ ๊ด€๋ จ ์ •๋ณด
    """
    result = {
        'is_valid': False,
        'ens_name': ens_name,
        'ethereum_address': None,
        'error': None
    }
    
    try:
        # ENS ์ด๋ฆ„ ํ˜•์‹ ๊ฒ€์ฆ
        if not ens_name.endswith('.eth'):
            result['error'] = "ENS ์ด๋ฆ„์€ .eth๋กœ ๋๋‚˜์•ผ ํ•ฉ๋‹ˆ๋‹ค"
            return result
        
        # ENS ์ด๋ฆ„ ๊ธธ์ด ๊ฒ€์ฆ (์ตœ์†Œ 3์ž)
        name_without_suffix = ens_name.replace('.eth', '')
        if len(name_without_suffix) < 3:
            result['error'] = "ENS ์ด๋ฆ„์€ ์ตœ์†Œ 3์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"
            return result
        
        # ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ ์กฐํšŒ
        address = ns.address(ens_name)
        
        if address:
            result['is_valid'] = True
            result['ethereum_address'] = address
            print(f"โœ… {ens_name} โ†’ {address}")
        else:
            result['error'] = "๋“ฑ๋ก๋˜์ง€ ์•Š์€ ENS ์ด๋ฆ„์ž…๋‹ˆ๋‹ค"
            print(f"โŒ {ens_name}๋Š” ๋“ฑ๋ก๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค")
            
    except Exception as e:
        result['error'] = f"๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
        print(f"โš ๏ธ ์˜ค๋ฅ˜: {str(e)}")
    
    return result

# ์‚ฌ์šฉ ์˜ˆ์‹œ
test_names = ['vitalik.eth', 'invalid.eth', 'ab.eth', 'test123.eth']

for name in test_names:
    print(f"\n๐Ÿ” ๊ฒ€์ฆ ์ค‘: {name}")
    result = validate_ens_name(name)
    print(f"๊ฒฐ๊ณผ: {result}")
๐ŸŽฏ ์ฝ”๋“œ ์„ค๋ช… ์ด ํ•จ์ˆ˜๋Š” ์„ธ ๊ฐ€์ง€๋ฅผ ๊ฒ€์ฆํ•ด์š”:
1๏ธโƒฃ ENS ์ด๋ฆ„์ด .eth๋กœ ๋๋‚˜๋Š”์ง€
2๏ธโƒฃ ์ด๋ฆ„ ๊ธธ์ด๊ฐ€ ์ตœ์†Œ 3์ž ์ด์ƒ์ธ์ง€
3๏ธโƒฃ ์‹ค์ œ๋กœ ๋“ฑ๋ก๋œ ENS ์ด๋ฆ„์ธ์ง€

๋ชจ๋“  ๊ฒ€์ฆ์„ ํ†ต๊ณผํ•˜๋ฉด ์—ฐ๊ฒฐ๋œ ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค!

๐Ÿš€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

๊ธฐ๋ณธ ๊ฒ€์ฆ์€ ์™„์„ฑํ–ˆ์–ด์š”! ์ด์ œ ๋” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณผ๊นŒ์š”? ๐Ÿ’ช

์—ญ๋ฐฉํ–ฅ ์กฐํšŒ (Reverse Resolution)

์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ์—ฐ๊ฒฐ๋œ ENS ์ด๋ฆ„์„ ์ฐพ์•„์ฃผ๋Š” ๊ธฐ๋Šฅ์ด์—์š”:

def reverse_lookup(ethereum_address):
    """
    ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ๋กœ ENS ์ด๋ฆ„ ์กฐํšŒ
    
    Args:
        ethereum_address (str): ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ
    
    Returns:
        str: ENS ์ด๋ฆ„ ๋˜๋Š” None
    """
    try:
        # ์ฃผ์†Œ ํ˜•์‹ ๊ฒ€์ฆ
        if not Web3.is_address(ethereum_address):
            print(f"โŒ ์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ: {ethereum_address}")
            return None
        
        # ์ฒดํฌ์„ฌ ์ฃผ์†Œ๋กœ ๋ณ€ํ™˜
        checksum_address = Web3.to_checksum_address(ethereum_address)
        
        # ENS ์ด๋ฆ„ ์กฐํšŒ
        ens_name = ns.name(checksum_address)
        
        if ens_name:
            print(f"โœ… {checksum_address} โ†’ {ens_name}")
            return ens_name
        else:
            print(f"โ„น๏ธ ์ด ์ฃผ์†Œ์— ์—ฐ๊ฒฐ๋œ ENS ์ด๋ฆ„์ด ์—†์Šต๋‹ˆ๋‹ค")
            return None
            
    except Exception as e:
        print(f"โš ๏ธ ์—ญ๋ฐฉํ–ฅ ์กฐํšŒ ์˜ค๋ฅ˜: {str(e)}")
        return None

# ์‚ฌ์šฉ ์˜ˆ์‹œ
vitalik_address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
result = reverse_lookup(vitalik_address)

๋ฐฐ์น˜ ๊ฒ€์ฆ ์‹œ์Šคํ…œ

์—ฌ๋Ÿฌ ๊ฐœ์˜ ENS ์ด๋ฆ„์„ ํ•œ ๋ฒˆ์— ๊ฒ€์ฆํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ๋งŒ๋“ค์–ด๋ด์š”. ์‹ค๋ฌด์—์„œ ์ •๋ง ์œ ์šฉํ•˜๋‹ต๋‹ˆ๋‹ค! ๐Ÿ“Š

import time
from typing import List, Dict

def batch_validate_ens(ens_names: List[str], delay: float = 0.5) -> List[Dict]:
    """
    ์—ฌ๋Ÿฌ ENS ์ด๋ฆ„์„ ๋ฐฐ์น˜๋กœ ๊ฒ€์ฆ
    
    Args:
        ens_names (List[str]): ๊ฒ€์ฆํ•  ENS ์ด๋ฆ„ ๋ฆฌ์ŠคํŠธ
        delay (float): ๊ฐ ์š”์ฒญ ์‚ฌ์ด์˜ ์ง€์—ฐ ์‹œ๊ฐ„ (์ดˆ)
    
    Returns:
        List[Dict]: ๊ฐ ENS ์ด๋ฆ„์˜ ๊ฒ€์ฆ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ
    """
    results = []
    total = len(ens_names)
    
    print(f"๐Ÿ”„ ์ด {total}๊ฐœ์˜ ENS ์ด๋ฆ„ ๊ฒ€์ฆ ์‹œ์ž‘...\n")
    
    for idx, ens_name in enumerate(ens_names, 1):
        print(f"[{idx}/{total}] ๊ฒ€์ฆ ์ค‘: {ens_name}")
        
        result = validate_ens_name(ens_name)
        results.append(result)
        
        # API ์š”์ฒญ ์ œํ•œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ง€์—ฐ
        if idx < total:
            time.sleep(delay)
    
    # ํ†ต๊ณ„ ์ถœ๋ ฅ
    valid_count = sum(1 for r in results if r['is_valid'])
    invalid_count = total - valid_count
    
    print(f"\n๐Ÿ“Š ๊ฒ€์ฆ ์™„๋ฃŒ!")
    print(f"โœ… ์œ ํšจ: {valid_count}๊ฐœ")
    print(f"โŒ ๋ฌดํšจ: {invalid_count}๊ฐœ")
    
    return results

# ์‚ฌ์šฉ ์˜ˆ์‹œ
test_batch = [
    'vitalik.eth',
    'ethereum.eth',
    'invalid123.eth',
    'test.eth',
    'opensea.eth'
]

batch_results = batch_validate_ens(test_batch)
๐Ÿ’ก API ์š”์ฒญ ์ œํ•œ ์ฃผ์˜ Infura ๋ฌด๋ฃŒ ํ”Œ๋žœ์€ ํ•˜๋ฃจ 10๋งŒ ๊ฑด์˜ ์š”์ฒญ ์ œํ•œ์ด ์žˆ์–ด์š”. ๋ฐฐ์น˜ ๊ฒ€์ฆํ•  ๋•Œ๋Š” ์ ์ ˆํ•œ ์ง€์—ฐ ์‹œ๊ฐ„์„ ๋‘๋Š” ๊ฒŒ ์ข‹์•„์š”. ๋„ˆ๋ฌด ๋น ๋ฅด๊ฒŒ ์š”์ฒญํ•˜๋ฉด ์ผ์‹œ์ ์œผ๋กœ ์ฐจ๋‹จ๋  ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”! โฑ๏ธ

ENS ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ

ENS ์ด๋ฆ„์— ์—ฐ๊ฒฐ๋œ ์ถ”๊ฐ€ ์ •๋ณด๋“ค๋„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด์š”:

def get_ens_metadata(ens_name):
    """
    ENS ์ด๋ฆ„์˜ ์ƒ์„ธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ
    
    Args:
        ens_name (str): ENS ์ด๋ฆ„
    
    Returns:
        dict: ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ •๋ณด
    """
    metadata = {
        'ens_name': ens_name,
        'ethereum_address': None,
        'owner': None,
        'resolver': None,
        'ttl': None,
        'text_records': {}
    }
    
    try:
        # ๊ธฐ๋ณธ ์ฃผ์†Œ ์กฐํšŒ
        address = ns.address(ens_name)
        if not address:
            print(f"โŒ {ens_name}๋Š” ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฆ„์ž…๋‹ˆ๋‹ค")
            return metadata
        
        metadata['ethereum_address'] = address
        
        # ์†Œ์œ ์ž ์กฐํšŒ
        owner = ns.owner(ens_name)
        metadata['owner'] = owner
        
        # Resolver ์ฃผ์†Œ ์กฐํšŒ
        resolver_address = w3.ens.resolver(ens_name).address
        metadata['resolver'] = resolver_address
        
        # ํ…์ŠคํŠธ ๋ ˆ์ฝ”๋“œ ์กฐํšŒ (์ผ๋ฐ˜์ ์ธ ํ•„๋“œ๋“ค)
        text_fields = ['email', 'url', 'avatar', 'description', 
                      'com.twitter', 'com.github']
        
        for field in text_fields:
            try:
                value = ns.get_text(ens_name, field)
                if value:
                    metadata['text_records'][field] = value
            except:
                pass
        
        print(f"โœ… {ens_name} ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ ์™„๋ฃŒ")
        
    except Exception as e:
        print(f"โš ๏ธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ ์˜ค๋ฅ˜: {str(e)}")
    
    return metadata

# ์‚ฌ์šฉ ์˜ˆ์‹œ
metadata = get_ens_metadata('vitalik.eth')

print("\n๐Ÿ“‹ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ:")
for key, value in metadata.items():
    if value:
        print(f"  {key}: {value}")

๐ŸŽจ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ENS ๊ฒ€์ฆ API ์„œ๋ฒ„

์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” API ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! Flask๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์˜ˆ์š”. ๐ŸŒŸ

Flask ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ •

pip install flask flask-cors

์™„์ „ํ•œ API ์„œ๋ฒ„ ์ฝ”๋“œ

from flask import Flask, request, jsonify
from flask_cors import CORS
from web3 import Web3
from ens import ENS
from dotenv import load_dotenv
import os

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ
load_dotenv()

# Flask ์•ฑ ์ดˆ๊ธฐํ™”
app = Flask(__name__)
CORS(app)  # CORS ํ™œ์„ฑํ™”

# Web3 ๋ฐ ENS ์„ค์ •
infura_url = f"https://mainnet.infura.io/v3/{os.getenv('INFURA_PROJECT_ID')}"
w3 = Web3(Web3.HTTPProvider(infura_url))
ns = ENS.fromWeb3(w3)

@app.route('/health', methods=['GET'])
def health_check():
    """์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ"""
    return jsonify({
        'status': 'healthy',
        'connected': w3.is_connected(),
        'block_number': w3.eth.block_number
    })

@app.route('/validate', methods=['POST'])
def validate():
    """ENS ์ด๋ฆ„ ๊ฒ€์ฆ ์—”๋“œํฌ์ธํŠธ"""
    data = request.get_json()
    ens_name = data.get('ens_name')
    
    if not ens_name:
        return jsonify({
            'error': 'ens_name ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'
        }), 400
    
    result = {
        'is_valid': False,
        'ens_name': ens_name,
        'ethereum_address': None,
        'error': None
    }
    
    try:
        if not ens_name.endswith('.eth'):
            result['error'] = "ENS ์ด๋ฆ„์€ .eth๋กœ ๋๋‚˜์•ผ ํ•ฉ๋‹ˆ๋‹ค"
            return jsonify(result), 400
        
        address = ns.address(ens_name)
        
        if address:
            result['is_valid'] = True
            result['ethereum_address'] = address
        else:
            result['error'] = "๋“ฑ๋ก๋˜์ง€ ์•Š์€ ENS ์ด๋ฆ„์ž…๋‹ˆ๋‹ค"
            
    except Exception as e:
        result['error'] = str(e)
        return jsonify(result), 500
    
    return jsonify(result)

@app.route('/reverse', methods=['POST'])
def reverse():
    """์—ญ๋ฐฉํ–ฅ ์กฐํšŒ ์—”๋“œํฌ์ธํŠธ"""
    data = request.get_json()
    address = data.get('address')
    
    if not address:
        return jsonify({
            'error': 'address ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'
        }), 400
    
    try:
        if not Web3.is_address(address):
            return jsonify({
                'error': '์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค'
            }), 400
        
        checksum_address = Web3.to_checksum_address(address)
        ens_name = ns.name(checksum_address)
        
        return jsonify({
            'address': checksum_address,
            'ens_name': ens_name
        })
        
    except Exception as e:
        return jsonify({
            'error': str(e)
        }), 500

@app.route('/metadata', methods=['POST'])
def metadata():
    """ENS ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ ์—”๋“œํฌ์ธํŠธ"""
    data = request.get_json()
    ens_name = data.get('ens_name')
    
    if not ens_name:
        return jsonify({
            'error': 'ens_name ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'
        }), 400
    
    try:
        address = ns.address(ens_name)
        if not address:
            return jsonify({
                'error': '๋“ฑ๋ก๋˜์ง€ ์•Š์€ ENS ์ด๋ฆ„์ž…๋‹ˆ๋‹ค'
            }), 404
        
        metadata = {
            'ens_name': ens_name,
            'ethereum_address': address,
            'owner': ns.owner(ens_name),
            'text_records': {}
        }
        
        # ํ…์ŠคํŠธ ๋ ˆ์ฝ”๋“œ ์กฐํšŒ
        text_fields = ['email', 'url', 'avatar', 'description']
        for field in text_fields:
            try:
                value = ns.get_text(ens_name, field)
                if value:
                    metadata['text_records'][field] = value
            except:
                pass
        
        return jsonify(metadata)
        
    except Exception as e:
        return jsonify({
            'error': str(e)
        }), 500

if __name__ == '__main__':
    print("๐Ÿš€ ENS ๊ฒ€์ฆ API ์„œ๋ฒ„ ์‹œ์ž‘!")
    print(f"๐Ÿ“ก ์ด๋”๋ฆฌ์›€ ์—ฐ๊ฒฐ ์ƒํƒœ: {w3.is_connected()}")
    app.run(debug=True, host='0.0.0.0', port=5000)

API ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๊ณ  curl์ด๋‚˜ Postman์œผ๋กœ ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ์–ด์š”:

# ์„œ๋ฒ„ ์‹คํ–‰
python app.py

# ๋‹ค๋ฅธ ํ„ฐ๋ฏธ๋„์—์„œ ํ…Œ์ŠคํŠธ
curl -X POST http://localhost:5000/validate \
  -H "Content-Type: application/json" \
  -d '{"ens_name": "vitalik.eth"}'

# ์—ญ๋ฐฉํ–ฅ ์กฐํšŒ ํ…Œ์ŠคํŠธ
curl -X POST http://localhost:5000/reverse \
  -H "Content-Type: application/json" \
  -d '{"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}'
๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! ์ด์ œ ์™„์ „ํžˆ ์ž‘๋™ํ•˜๋Š” ENS ๊ฒ€์ฆ API ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์—ˆ์–ด์š”! ์ด ์„œ๋ฒ„๋Š”:
โœ… ENS ์ด๋ฆ„ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
โœ… ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ๋กœ ENS ์กฐํšŒ
โœ… ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ
โœ… RESTful API ์ œ๊ณต

์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ธ์ฆ, ๋กœ๊น…, ์—๋Ÿฌ ํ•ธ๋“ค๋ง์„ ๋” ๊ฐ•ํ™”ํ•ด์•ผ ํ•ด์š”!

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

ENS ๊ฒ€์ฆ ์‹œ์Šคํ…œ์„ ๋” ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๐Ÿš„

์บ์‹ฑ ๊ตฌํ˜„ํ•˜๊ธฐ

๊ฐ™์€ ENS ์ด๋ฆ„์„ ๋ฐ˜๋ณตํ•ด์„œ ์กฐํšŒํ•˜๋ฉด ๋น„ํšจ์œจ์ ์ด์—์š”. Redis๋ฅผ ์‚ฌ์šฉํ•œ ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•ด๋ด์š”:

import redis
import json
from datetime import timedelta

# Redis ์—ฐ๊ฒฐ
redis_client = redis.Redis(
    host='localhost',
    port=6379,
    db=0,
    decode_responses=True
)

def validate_ens_with_cache(ens_name, cache_ttl=3600):
    """
    ์บ์‹ฑ์„ ์ ์šฉํ•œ ENS ๊ฒ€์ฆ
    
    Args:
        ens_name (str): ENS ์ด๋ฆ„
        cache_ttl (int): ์บ์‹œ ์œ ํšจ ์‹œ๊ฐ„ (์ดˆ)
    
    Returns:
        dict: ๊ฒ€์ฆ ๊ฒฐ๊ณผ
    """
    cache_key = f"ens:{ens_name}"
    
    # ์บ์‹œ ํ™•์ธ
    cached_result = redis_client.get(cache_key)
    if cached_result:
        print(f"๐Ÿ’พ ์บ์‹œ์—์„œ ์กฐํšŒ: {ens_name}")
        return json.loads(cached_result)
    
    # ์บ์‹œ ๋ฏธ์Šค - ์‹ค์ œ ์กฐํšŒ
    print(f"๐Ÿ” ๋ธ”๋ก์ฒด์ธ์—์„œ ์กฐํšŒ: {ens_name}")
    result = validate_ens_name(ens_name)
    
    # ๊ฒฐ๊ณผ ์บ์‹ฑ (์œ ํšจํ•œ ๊ฒฝ์šฐ๋งŒ)
    if result['is_valid']:
        redis_client.setex(
            cache_key,
            cache_ttl,
            json.dumps(result)
        )
    
    return result

# ์‚ฌ์šฉ ์˜ˆ์‹œ
result1 = validate_ens_with_cache('vitalik.eth')  # ๋ธ”๋ก์ฒด์ธ ์กฐํšŒ
result2 = validate_ens_with_cache('vitalik.eth')  # ์บ์‹œ์—์„œ ์กฐํšŒ (๋น ๋ฆ„!)
๐Ÿ’ก ์บ์‹ฑ ์ „๋žต ํŒ โ€ข ์œ ํšจํ•œ ENS๋งŒ ์บ์‹ฑํ•˜์„ธ์š” (๋ฌดํšจํ•œ ์ด๋ฆ„์€ ๋‚˜์ค‘์— ๋“ฑ๋ก๋  ์ˆ˜ ์žˆ์–ด์š”)
โ€ข TTL์€ 1์‹œ๊ฐ„~24์‹œ๊ฐ„์ด ์ ๋‹นํ•ด์š”
โ€ข ์ค‘์š”ํ•œ ํŠธ๋žœ์žญ์…˜ ์ „์—๋Š” ์บ์‹œ๋ฅผ ์šฐํšŒํ•˜๊ณ  ์‹ค์‹œ๊ฐ„ ์กฐํšŒํ•˜์„ธ์š”
โ€ข ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜์„ธ์š”

๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

์—ฌ๋Ÿฌ ENS๋ฅผ ๋™์‹œ์— ๊ฒ€์ฆํ•  ๋•Œ๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ํ›จ์”ฌ ๋น ๋ฅด๋‹ต๋‹ˆ๋‹ค:

import asyncio
from web3 import AsyncWeb3
from web3.providers.async_rpc import AsyncHTTPProvider

async def async_validate_ens(ens_name, w3_async, ns_async):
    """๋น„๋™๊ธฐ ENS ๊ฒ€์ฆ"""
    try:
        address = await ns_async.address(ens_name)
        return {
            'ens_name': ens_name,
            'is_valid': address is not None,
            'ethereum_address': address
        }
    except Exception as e:
        return {
            'ens_name': ens_name,
            'is_valid': False,
            'error': str(e)
        }

async def batch_validate_async(ens_names):
    """์—ฌ๋Ÿฌ ENS๋ฅผ ๋น„๋™๊ธฐ๋กœ ๊ฒ€์ฆ"""
    # ๋น„๋™๊ธฐ Web3 ์„ค์ •
    infura_url = f"https://mainnet.infura.io/v3/{os.getenv('INFURA_PROJECT_ID')}"
    w3_async = AsyncWeb3(AsyncHTTPProvider(infura_url))
    ns_async = ENS.fromWeb3(w3_async)
    
    # ๋ชจ๋“  ๊ฒ€์ฆ ์ž‘์—…์„ ๋™์‹œ์— ์‹คํ–‰
    tasks = [async_validate_ens(name, w3_async, ns_async) for name in ens_names]
    results = await asyncio.gather(*tasks)
    
    return results

# ์‚ฌ์šฉ ์˜ˆ์‹œ
async def main():
    test_names = ['vitalik.eth', 'ethereum.eth', 'test.eth']
    
    import time
    start = time.time()
    results = await batch_validate_async(test_names)
    elapsed = time.time() - start
    
    print(f"โšก {len(test_names)}๊ฐœ ๊ฒ€์ฆ ์™„๋ฃŒ: {elapsed:.2f}์ดˆ")
    for result in results:
        print(result)

# ์‹คํ–‰
asyncio.run(main())
๐Ÿš€ ์„ฑ๋Šฅ ๋น„๊ต ๋™๊ธฐ ๋ฐฉ์‹: 10๊ฐœ ๊ฒ€์ฆ โ†’ ์•ฝ 5์ดˆ
๋น„๋™๊ธฐ ๋ฐฉ์‹: 10๊ฐœ ๊ฒ€์ฆ โ†’ ์•ฝ 1์ดˆ

5๋ฐฐ ๋น ๋ฅธ ์†๋„! ๋Œ€๋Ÿ‰ ๊ฒ€์ฆ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋น„๋™๊ธฐ ๋ฐฉ์‹์„ ๊ผญ ์‚ฌ์šฉํ•˜์„ธ์š”.

๐Ÿ”’ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

ENS ๊ฒ€์ฆ ์‹œ์Šคํ…œ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์šด์˜ํ•˜๊ธฐ ์œ„ํ•œ ์ค‘์š”ํ•œ ๋ณด์•ˆ ์‚ฌํ•ญ๋“ค์ด์—์š”! ๐Ÿ›ก๏ธ

์ž…๋ ฅ ๊ฒ€์ฆ ๊ฐ•ํ™”

import re

def sanitize_ens_input(ens_name):
    """
    ENS ์ž…๋ ฅ๊ฐ’ ์ •์ œ ๋ฐ ๊ฒ€์ฆ
    
    Args:
        ens_name (str): ์‚ฌ์šฉ์ž ์ž…๋ ฅ ENS ์ด๋ฆ„
    
    Returns:
        tuple: (์ •์ œ๋œ ์ด๋ฆ„, ์—๋Ÿฌ ๋ฉ”์‹œ์ง€)
    """
    # ๊ณต๋ฐฑ ์ œ๊ฑฐ
    ens_name = ens_name.strip().lower()
    
    # ๊ธธ์ด ๊ฒ€์ฆ
    if len(ens_name) > 255:
        return None, "ENS ์ด๋ฆ„์ด ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค"
    
    # .eth ์ ‘๋ฏธ์‚ฌ ํ™•์ธ
    if not ens_name.endswith('.eth'):
        return None, "ENS ์ด๋ฆ„์€ .eth๋กœ ๋๋‚˜์•ผ ํ•ฉ๋‹ˆ๋‹ค"
    
    # ํ—ˆ์šฉ๋œ ๋ฌธ์ž๋งŒ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธ
    # ENS๋Š” ์˜๋ฌธ ์†Œ๋ฌธ์ž, ์ˆซ์ž, ํ•˜์ดํ”ˆ๋งŒ ํ—ˆ์šฉ
    name_part = ens_name.replace('.eth', '')
    if not re.match(r'^[a-z0-9-]+$', name_part):
        return None, "ENS ์ด๋ฆ„์— ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค"
    
    # ํ•˜์ดํ”ˆ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ ๋๋‚˜๋Š”์ง€ ํ™•์ธ
    if name_part.startswith('-') or name_part.endswith('-'):
        return None, "ENS ์ด๋ฆ„์€ ํ•˜์ดํ”ˆ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ ๋๋‚  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
    
    # ์—ฐ์†๋œ ํ•˜์ดํ”ˆ ํ™•์ธ
    if '--' in name_part:
        return None, "์—ฐ์†๋œ ํ•˜์ดํ”ˆ์€ ํ—ˆ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"
    
    return ens_name, None

# ์‚ฌ์šฉ ์˜ˆ์‹œ
test_inputs = [
    'Vitalik.ETH',  # ๋Œ€๋ฌธ์ž
    'test..eth',    # ์ž˜๋ชป๋œ ํ˜•์‹
    'valid-name.eth',  # ์ •์ƒ
    '-invalid.eth',  # ํ•˜์ดํ”ˆ์œผ๋กœ ์‹œ์ž‘
    'test@name.eth'  # ํŠน์ˆ˜๋ฌธ์ž
]

for input_name in test_inputs:
    sanitized, error = sanitize_ens_input(input_name)
    if error:
        print(f"โŒ {input_name}: {error}")
    else:
        print(f"โœ… {input_name} โ†’ {sanitized}")

Rate Limiting ๊ตฌํ˜„

API ๋‚จ์šฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์š”์ฒญ ์ œํ•œ ๊ธฐ๋Šฅ์ด์—์š”:

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

# Flask ์•ฑ์— Rate Limiter ์ถ”๊ฐ€
limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["100 per hour"]
)

@app.route('/validate', methods=['POST'])
@limiter.limit("10 per minute")  # ๋ถ„๋‹น 10ํšŒ ์ œํ•œ
def validate():
    # ... ๊ธฐ์กด ์ฝ”๋“œ ...
    pass

@app.route('/batch-validate', methods=['POST'])
@limiter.limit("5 per minute")  # ๋ฐฐ์น˜๋Š” ๋” ์—„๊ฒฉํ•˜๊ฒŒ
def batch_validate():
    # ... ๋ฐฐ์น˜ ๊ฒ€์ฆ ์ฝ”๋“œ ...
    pass
โš ๏ธ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๊ฒ€์ฆํ•˜๊ณ  ์ •์ œํ•˜์„ธ์š”
  • โœ… API ํ‚ค๋ฅผ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜์„ธ์š”
  • โœ… Rate Limiting์„ ์ ์šฉํ•˜์„ธ์š”
  • โœ… HTTPS๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”
  • โœ… ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”
  • โœ… ๋กœ๊น…์„ ๊ตฌํ˜„ํ•˜๋˜ ๊ฐœ์ธ์ •๋ณด๋Š” ๊ธฐ๋กํ•˜์ง€ ๋งˆ์„ธ์š”
  • โœ… ์ •๊ธฐ์ ์œผ๋กœ ์˜์กด์„ฑ ํŒจํ‚ค์ง€๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์„ธ์š”

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น…

์‹œ์Šคํ…œ์ด ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ๋ฌธ์ œ๋ฅผ ๋น ๋ฅด๊ฒŒ ๋ฐœ๊ฒฌํ•˜๋ ค๋ฉด ๋ชจ๋‹ˆํ„ฐ๋ง์ด ํ•„์ˆ˜์˜ˆ์š”! ๐Ÿ“ˆ

๊ตฌ์กฐํ™”๋œ ๋กœ๊น…

import logging
import json
from datetime import datetime

# ๋กœ๊ฑฐ ์„ค์ •
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('ens_validator.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger('ENSValidator')

def log_validation(ens_name, result, duration_ms):
    """๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๊ตฌ์กฐํ™”๋œ ํ˜•์‹์œผ๋กœ ๋กœ๊น…"""
    log_data = {
        'timestamp': datetime.utcnow().isoformat(),
        'action': 'validate_ens',
        'ens_name': ens_name,
        'is_valid': result.get('is_valid', False),
        'duration_ms': duration_ms,
        'ethereum_address': result.get('ethereum_address')
    }
    
    logger.info(json.dumps(log_data))

# ์‚ฌ์šฉ ์˜ˆ์‹œ
import time

start = time.time()
result = validate_ens_name('vitalik.eth')
duration = (time.time() - start) * 1000

log_validation('vitalik.eth', result, duration)

์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘

from prometheus_client import Counter, Histogram, generate_latest
from flask import Response

# Prometheus ๋ฉ”ํŠธ๋ฆญ ์ •์˜
validation_counter = Counter(
    'ens_validations_total',
    'Total number of ENS validations',
    ['status']
)

validation_duration = Histogram(
    'ens_validation_duration_seconds',
    'Time spent validating ENS names'
)

@app.route('/validate', methods=['POST'])
@validation_duration.time()
def validate():
    data = request.get_json()
    ens_name = data.get('ens_name')
    
    try:
        result = validate_ens_name(ens_name)
        
        if result['is_valid']:
            validation_counter.labels(status='success').inc()
        else:
            validation_counter.labels(status='invalid').inc()
        
        return jsonify(result)
        
    except Exception as e:
        validation_counter.labels(status='error').inc()
        return jsonify({'error': str(e)}), 500

@app.route('/metrics')
def metrics():
    """Prometheus ๋ฉ”ํŠธ๋ฆญ ์—”๋“œํฌ์ธํŠธ"""
    return Response(generate_latest(), mimetype='text/plain')
๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ Grafana์™€ Prometheus๋ฅผ ์—ฐ๋™ํ•˜๋ฉด ๋ฉ‹์ง„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”:
โ€ข ์‹œ๊ฐ„๋‹น ๊ฒ€์ฆ ์š”์ฒญ ์ˆ˜
โ€ข ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„
โ€ข ์„ฑ๊ณต/์‹คํŒจ ๋น„์œจ
โ€ข ์บ์‹œ ํžˆํŠธ์œจ
โ€ข API ์—๋Ÿฌ์œจ

์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹œ์Šคํ…œ ์ƒํƒœ๋ฅผ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค!

๐ŸŒ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

ENS ๊ฒ€์ฆ ์‹œ์Šคํ…œ์„ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„๋ณผ๊นŒ์š”? ๐Ÿ’ผ

1. DApp ์‚ฌ์šฉ์ž ์ธ์ฆ

ํƒˆ์ค‘์•™ํ™” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ENS ์ด๋ฆ„์œผ๋กœ ๋กœ๊ทธ์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์š”:

def authenticate_with_ens(ens_name, signature):
    """
    ENS ๊ธฐ๋ฐ˜ ์‚ฌ์šฉ์ž ์ธ์ฆ
    
    Args:
        ens_name (str): ์‚ฌ์šฉ์ž์˜ ENS ์ด๋ฆ„
        signature (str): ์„œ๋ช…๋œ ๋ฉ”์‹œ์ง€
    
    Returns:
        dict: ์ธ์ฆ ๊ฒฐ๊ณผ
    """
    # ENS ๊ฒ€์ฆ
    result = validate_ens_name(ens_name)
    
    if not result['is_valid']:
        return {
            'authenticated': False,
            'error': 'Invalid ENS name'
        }
    
    ethereum_address = result['ethereum_address']
    
    # ์„œ๋ช… ๊ฒ€์ฆ (์‹ค์ œ๋กœ๋Š” ๋” ๋ณต์žกํ•œ ๋กœ์ง ํ•„์š”)
    message = f"Login to DApp with {ens_name}"
    
    try:
        # ์„œ๋ช… ๋ณต๊ตฌ ๋ฐ ๊ฒ€์ฆ
        recovered_address = w3.eth.account.recover_message(
            encode_defunct(text=message),
            signature=signature
        )
        
        if recovered_address.lower() == ethereum_address.lower():
            return {
                'authenticated': True,
                'ens_name': ens_name,
                'address': ethereum_address
            }
        else:
            return {
                'authenticated': False,
                'error': 'Signature verification failed'
            }
            
    except Exception as e:
        return {
            'authenticated': False,
            'error': str(e)
        }

2. NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ํ†ตํ•ฉ

NFT ๊ฑฐ๋ž˜ ์‹œ ๋ณต์žกํ•œ ์ฃผ์†Œ ๋Œ€์‹  ENS ์ด๋ฆ„์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:

def send_nft_to_ens(nft_contract, token_id, recipient_ens):
    """
    ENS ์ด๋ฆ„์œผ๋กœ NFT ์ „์†ก
    
    Args:
        nft_contract (str): NFT ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ
        token_id (int): ํ† ํฐ ID
        recipient_ens (str): ์ˆ˜์‹ ์ž ENS ์ด๋ฆ„
    
    Returns:
        dict: ์ „์†ก ๊ฒฐ๊ณผ
    """
    # ENS ๊ฒ€์ฆ
    result = validate_ens_name(recipient_ens)
    
    if not result['is_valid']:
        return {
            'success': False,
            'error': f'{recipient_ens}๋Š” ์œ ํšจํ•˜์ง€ ์•Š์€ ENS ์ด๋ฆ„์ž…๋‹ˆ๋‹ค'
        }
    
    recipient_address = result['ethereum_address']
    
    print(f"๐Ÿ“ฆ NFT ์ „์†ก ์ค€๋น„")
    print(f"  ์ˆ˜์‹ ์ž: {recipient_ens} ({recipient_address})")
    print(f"  ํ† ํฐ ID: {token_id}")
    
    # ์‹ค์ œ NFT ์ „์†ก ๋กœ์ง
    # (์—ฌ๊ธฐ์„œ๋Š” ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
    
    return {
        'success': True,
        'recipient_ens': recipient_ens,
        'recipient_address': recipient_address,
        'token_id': token_id,
        'tx_hash': '0x...'  # ์‹ค์ œ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ
    }

# ์‚ฌ์šฉ ์˜ˆ์‹œ
result = send_nft_to_ens(
    nft_contract='0x...',
    token_id=1234,
    recipient_ens='vitalik.eth'
)
print(result)

3. ์†Œ์…œ ๋ฏธ๋””์–ด ํ†ตํ•ฉ

ENS ํ”„๋กœํ•„ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€์„œ ์†Œ์…œ ํ”„๋กœํ•„์„ ์ž๋™์œผ๋กœ ๊ตฌ์„ฑํ•ด์š”:

def create_social_profile(ens_name):
    """
    ENS ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋กœ ์†Œ์…œ ํ”„๋กœํ•„ ์ƒ์„ฑ
    
    Args:
        ens_name (str): ENS ์ด๋ฆ„
    
    Returns:
        dict: ์†Œ์…œ ํ”„๋กœํ•„ ์ •๋ณด
    """
    metadata = get_ens_metadata(ens_name)
    
    if not metadata['ethereum_address']:
        return None
    
    profile = {
        'username': ens_name,
        'display_name': ens_name.replace('.eth', ''),
        'ethereum_address': metadata['ethereum_address'],
        'avatar': None,
        'bio': None,
        'social_links': {}
    }
    
    # ํ…์ŠคํŠธ ๋ ˆ์ฝ”๋“œ์—์„œ ์ •๋ณด ์ถ”์ถœ
    text_records = metadata.get('text_records', {})
    
    if 'avatar' in text_records:
        profile['avatar'] = text_records['avatar']
    
    if 'description' in text_records:
        profile['bio'] = text_records['description']
    
    if 'com.twitter' in text_records:
        profile['social_links']['twitter'] = text_records['com.twitter']
    
    if 'com.github' in text_records:
        profile['social_links']['github'] = text_records['com.github']
    
    if 'url' in text_records:
        profile['social_links']['website'] = text_records['url']
    
    return profile

# ์‚ฌ์šฉ ์˜ˆ์‹œ
profile = create_social_profile('vitalik.eth')
if profile:
    print(f"๐Ÿ‘ค ํ”„๋กœํ•„ ์ƒ์„ฑ ์™„๋ฃŒ: {profile['username']}")
    print(f"   ์ฃผ์†Œ: {profile['ethereum_address']}")
    if profile['bio']:
        print(f"   ์†Œ๊ฐœ: {profile['bio']}")
    if profile['social_links']:
        print(f"   ์†Œ์…œ ๋งํฌ: {profile['social_links']}")
๐Ÿ’ผ ๋น„์ฆˆ๋‹ˆ์Šค ํ™œ์šฉ ํŒ ENS ๊ฒ€์ฆ ์‹œ์Šคํ…œ์€ ๋‹ค์–‘ํ•œ Web3 ์„œ๋น„์Šค์— ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ์–ด์š”:
โ€ข ์•”ํ˜ธํ™”ํ ์ง€๊ฐ‘ ์•ฑ
โ€ข DeFi ํ”„๋กœํ† ์ฝœ
โ€ข NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค
โ€ข ํƒˆ์ค‘์•™ํ™” ์†Œ์…œ ๋ฏธ๋””์–ด
โ€ข DAO ๊ฑฐ๋ฒ„๋„Œ์Šค ํ”Œ๋žซํผ

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ฐ˜ ๊ฒฐ์ œ๋‚˜ ์‹ ์› ์ธ์ฆ์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค!

๐Ÿ› ๋””๋ฒ„๊น… ๋ฐ ๋ฌธ์ œ ํ•ด๊ฒฐ

๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋ฅผ ๋งŒ๋‚˜๊ฒŒ ๋ผ์š”. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์ด์Šˆ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ–ˆ์–ด์š”! ๐Ÿ”ง

์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋“ค

โŒ ์˜ค๋ฅ˜ 1: "Connection refused" ๋˜๋Š” "Timeout" ์›์ธ: Infura API ํ‚ค๊ฐ€ ์ž˜๋ชป๋˜์—ˆ๊ฑฐ๋‚˜, ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ฌธ์ œ
ํ•ด๊ฒฐ:
โ€ข .env ํŒŒ์ผ์˜ API ํ‚ค ํ™•์ธ
โ€ข Infura ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ”„๋กœ์ ํŠธ ์ƒํƒœ ํ™•์ธ
โ€ข ๋ฐฉํ™”๋ฒฝ ์„ค์ • ํ™•์ธ
โ€ข ๋‹ค๋ฅธ ๋„คํŠธ์›Œํฌ๋กœ ์‹œ๋„ (์˜ˆ: Goerli ํ…Œ์ŠคํŠธ๋„ท)
โŒ ์˜ค๋ฅ˜ 2: "ENS name not found" ์›์ธ: ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ENS ์ด๋ฆ„์ด๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋จ
ํ•ด๊ฒฐ:
โ€ข ENS ์•ฑ(app.ens.domains)์—์„œ ์ง์ ‘ ํ™•์ธ
โ€ข ์ฒ ์ž ํ™•์ธ (๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์Œ)
โ€ข .eth ์ ‘๋ฏธ์‚ฌ ํ™•์ธ
โŒ ์˜ค๋ฅ˜ 3: "Rate limit exceeded" ์›์ธ: API ์š”์ฒญ ํ•œ๋„ ์ดˆ๊ณผ
ํ•ด๊ฒฐ:
โ€ข ์š”์ฒญ ์‚ฌ์ด์— ์ง€์—ฐ ์‹œ๊ฐ„ ์ถ”๊ฐ€
โ€ข ์บ์‹ฑ ๊ตฌํ˜„
โ€ข Infura ์œ ๋ฃŒ ํ”Œ๋žœ ๊ณ ๋ ค
โ€ข ์—ฌ๋Ÿฌ API ํ‚ค ๋กœํ…Œ์ด์…˜

๋””๋ฒ„๊น… ๋„๊ตฌ

def debug_ens_resolution(ens_name):
    """
    ENS ํ•ด์„ ๊ณผ์ •์„ ์ƒ์„ธํžˆ ๋””๋ฒ„๊น…
    
    Args:
        ens_name (str): ๋””๋ฒ„๊น…ํ•  ENS ์ด๋ฆ„
    """
    print(f"\n๐Ÿ” ๋””๋ฒ„๊น… ์‹œ์ž‘: {ens_name}")
    print("=" * 50)
    
    # 1. ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ
    print(f"\n1๏ธโƒฃ ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ")
    print(f"   ์—ฐ๊ฒฐ๋จ: {w3.is_connected()}")
    print(f"   ํ˜„์žฌ ๋ธ”๋ก: {w3.eth.block_number}")
    
    # 2. ENS ์ด๋ฆ„ ํ˜•์‹ ๊ฒ€์ฆ
    print(f"\n2๏ธโƒฃ ENS ์ด๋ฆ„ ํ˜•์‹ ๊ฒ€์ฆ")
    sanitized, error = sanitize_ens_input(ens_name)
    if error:
        print(f"   โŒ ์˜ค๋ฅ˜: {error}")
        return
    else:
        print(f"   โœ… ์œ ํšจํ•œ ํ˜•์‹: {sanitized}")
    
    # 3. Resolver ํ™•์ธ
    print(f"\n3๏ธโƒฃ Resolver ์กฐํšŒ")
    try:
        resolver = w3.ens.resolver(ens_name)
        print(f"   Resolver ์ฃผ์†Œ: {resolver.address}")
    except Exception as e:
        print(f"   โŒ Resolver ์กฐํšŒ ์‹คํŒจ: {str(e)}")
        return
    
    # 4. ์ฃผ์†Œ ํ•ด์„
    print(f"\n4๏ธโƒฃ ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ ํ•ด์„")
    try:
        address = ns.address(ens_name)
        if address:
            print(f"   โœ… ์ฃผ์†Œ: {address}")
            
            # 5. ์—ญ๋ฐฉํ–ฅ ์กฐํšŒ
            print(f"\n5๏ธโƒฃ ์—ญ๋ฐฉํ–ฅ ์กฐํšŒ (Reverse Resolution)")
            reverse_name = ns.name(address)
            if reverse_name:
                print(f"   โœ… ์—ญ๋ฐฉํ–ฅ ์ด๋ฆ„: {reverse_name}")
                if reverse_name == ens_name:
                    print(f"   โœ… ์ •๋ฐฉํ–ฅ/์—ญ๋ฐฉํ–ฅ ์ผ์น˜!")
                else:
                    print(f"   โš ๏ธ ๋ถˆ์ผ์น˜: {reverse_name} != {ens_name}")
            else:
                print(f"   โ„น๏ธ ์—ญ๋ฐฉํ–ฅ ๋ ˆ์ฝ”๋“œ ์—†์Œ")
        else:
            print(f"   โŒ ์ฃผ์†Œ ์—†์Œ")
    except Exception as e:
        print(f"   โŒ ์ฃผ์†Œ ํ•ด์„ ์‹คํŒจ: {str(e)}")
    
    # 6. ํ…์ŠคํŠธ ๋ ˆ์ฝ”๋“œ
    print(f"\n6๏ธโƒฃ ํ…์ŠคํŠธ ๋ ˆ์ฝ”๋“œ")
    text_fields = ['email', 'url', 'avatar', 'description']
    for field in text_fields:
        try:
            value = ns.get_text(ens_name, field)
            if value:
                print(f"   {field}: {value}")
        except:
            pass
    
    print("\n" + "=" * 50)
    print("๐Ÿ ๋””๋ฒ„๊น… ์™„๋ฃŒ\n")

# ์‚ฌ์šฉ ์˜ˆ์‹œ
debug_ens_resolution('vitalik.eth')

๐Ÿš€ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๊ฐ€์ด๋“œ

๊ฐœ๋ฐœ์ด ๋๋‚ฌ๋‹ค๋ฉด ์ด์ œ ์‹ค์ œ ์„œ๋น„์Šค๋กœ ๋ฐฐํฌํ•  ์ฐจ๋ก€์˜ˆ์š”! ์•ˆ์ •์ ์ธ ์šด์˜์„ ์œ„ํ•œ ํŒ๋“ค์ด์—์š”. ๐ŸŒŸ

Docker ์ปจํ…Œ์ด๋„ˆํ™”

Dockerfile์„ ๋งŒ๋“ค์–ด์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ปจํ…Œ์ด๋„ˆํ™”ํ•ด์š”:

# Dockerfile
FROM python:3.10-slim

WORKDIR /app

# ์˜์กด์„ฑ ์„ค์น˜
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
ENV FLASK_APP=app.py
ENV FLASK_ENV=production

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 5000

# ํ—ฌ์Šค์ฒดํฌ
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:5000/health || exit 1

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]
# docker-compose.yml
version: '3.8'

services:
  ens-validator:
    build: .
    ports:
      - "5000:5000"
    environment:
      - INFURA_PROJECT_ID=${INFURA_PROJECT_ID}
      - REDIS_HOST=redis
    depends_on:
      - redis
    restart: unless-stopped
    
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    restart: unless-stopped
    
  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    restart: unless-stopped

volumes:
  redis-data:

ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๊ด€๋ฆฌ

# config.py
import os

class Config:
    """๊ธฐ๋ณธ ์„ค์ •"""
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
    INFURA_PROJECT_ID = os.getenv('INFURA_PROJECT_ID')
    REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
    REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
    CACHE_TTL = int(os.getenv('CACHE_TTL', 3600))

class DevelopmentConfig(Config):
    """๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •"""
    DEBUG = True
    TESTING = False
    ETHEREUM_NETWORK = 'goerli'  # ํ…Œ์ŠคํŠธ๋„ท ์‚ฌ์šฉ

class ProductionConfig(Config):
    """ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ์„ค์ •"""
    DEBUG = False
    TESTING = False
    ETHEREUM_NETWORK = 'mainnet'
    
    # ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ•„์š”
    if not Config.INFURA_PROJECT_ID:
        raise ValueError("INFURA_PROJECT_ID must be set in production")

class TestingConfig(Config):
    """ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ •"""
    DEBUG = True
    TESTING = True
    ETHEREUM_NETWORK = 'goerli'

# ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์„ค์ • ์„ ํƒ
config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
    'default': DevelopmentConfig
}

def get_config():
    env = os.getenv('FLASK_ENV', 'development')
    return config.get(env, config['default'])

CI/CD ํŒŒ์ดํ”„๋ผ์ธ

GitHub Actions๋ฅผ ์‚ฌ์šฉํ•œ ์ž๋™ ๋ฐฐํฌ ์„ค์ •์ด์—์š”:

# .github/workflows/deploy.yml
name: Deploy ENS Validator

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.10'
    
    - name: Install dependencies
      run: |
        pip install -r requirements.txt
        pip install pytest pytest-cov
    
    - name: Run tests
      run: pytest --cov=./ --cov-report=xml
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to production
      env:
        DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
      run: |
        # ์‹ค์ œ ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ
        echo "Deploying to production..."
๐ŸŽฏ ํ”„๋กœ๋•์…˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ์™„๋ฃŒ
  • โœ… HTTPS ์ธ์ฆ์„œ ์„ค์น˜
  • โœ… ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •
  • โœ… ๋ฐฑ์—… ์ „๋žต ์ˆ˜๋ฆฝ
  • โœ… Rate limiting ํ™œ์„ฑํ™”
  • โœ… ์—๋Ÿฌ ์•Œ๋ฆผ ์„ค์ • (Sentry, Slack ๋“ฑ)
  • โœ… ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ
  • โœ… ๋ฌธ์„œํ™” ์™„๋ฃŒ
  • โœ… ๋กค๋ฐฑ ๊ณ„ํš ์ˆ˜๋ฆฝ

๐Ÿ“š ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

์•ˆ์ •์ ์ธ ์‹œ์Šคํ…œ์„ ์œ„ํ•ด์„œ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์ˆ˜์˜ˆ์š”! pytest๋ฅผ ์‚ฌ์šฉํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ด์š”. โœ…

# test_ens_validator.py
import pytest
from unittest.mock import Mock, patch
from app import validate_ens_name, reverse_lookup

class TestENSValidation:
    """ENS ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ"""
    
    @patch('app.ns.address')
    def test_valid_ens_name(self, mock_address):
        """์œ ํšจํ•œ ENS ์ด๋ฆ„ ํ…Œ์ŠคํŠธ"""
        mock_address.return_value = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
        
        result = validate_ens_name('vitalik.eth')
        
        assert result['is_valid'] == True
        assert result['ethereum_address'] is not None
        assert result['error'] is None
    
    def test_invalid_suffix(self):
        """์ž˜๋ชป๋œ ์ ‘๋ฏธ์‚ฌ ํ…Œ์ŠคํŠธ"""
        result = validate_ens_name('test.com')
        
        assert result['is_valid'] == False
        assert 'eth' in result['error'].lower()
    
    def test_short_name(self):
        """์งง์€ ์ด๋ฆ„ ํ…Œ์ŠคํŠธ"""
        result = validate_ens_name('ab.eth')
        
        assert result['is_valid'] == False
        assert '3์ž' in result['error']
    
    @patch('app.ns.address')
    def test_unregistered_name(self, mock_address):
        """๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฆ„ ํ…Œ์ŠคํŠธ"""
        mock_address.return_value = None
        
        result = validate_ens_name('unregistered123456.eth')
        
        assert result['is_valid'] == False
        assert '๋“ฑ๋ก๋˜์ง€ ์•Š์€' in result['error']

class TestReverseLookup:
    """์—ญ๋ฐฉํ–ฅ ์กฐํšŒ ํ…Œ์ŠคํŠธ"""
    
    @patch('app.ns.name')
    def test_valid_address(self, mock_name):
        """์œ ํšจํ•œ ์ฃผ์†Œ ํ…Œ์ŠคํŠธ"""
        mock_name.return_value = 'vitalik.eth'
        
        result = reverse_lookup('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
        
        assert result == 'vitalik.eth'
    
    def test_invalid_address(self):
        """์œ ํšจํ•˜์ง€ ์•Š์€ ์ฃผ์†Œ ํ…Œ์ŠคํŠธ"""
        result = reverse_lookup('invalid_address')
        
        assert result is None

class TestAPIEndpoints:
    """API ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ"""
    
    @pytest.fixture
    def client(self):
        """ํ…Œ์ŠคํŠธ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ"""
        from app import app
        app.config['TESTING'] = True
        with app.test_client() as client:
            yield client
    
    def test_health_check(self, client):
        """ํ—ฌ์Šค์ฒดํฌ ํ…Œ์ŠคํŠธ"""
        response = client.get('/health')
        
        assert response.status_code == 200
        data = response.get_json()
        assert 'status' in data
    
    @patch('app.validate_ens_name')
    def test_validate_endpoint(self, mock_validate, client):
        """๊ฒ€์ฆ ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ"""
        mock_validate.return_value = {
            'is_valid': True,
            'ens_name': 'test.eth',
            'ethereum_address': '0x...',
            'error': None
        }
        
        response = client.post('/validate',
                              json={'ens_name': 'test.eth'})
        
        assert response.status_code == 200
        data = response.get_json()
        assert data['is_valid'] == True

# ํ…Œ์ŠคํŠธ ์‹คํ–‰
# pytest test_ens_validator.py -v --cov
๐Ÿงช ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ โ€ข ํ•ต์‹ฌ ๊ธฐ๋Šฅ: 90% ์ด์ƒ
โ€ข ์ „์ฒด ์ฝ”๋“œ: 80% ์ด์ƒ
โ€ข ์—ฃ์ง€ ์ผ€์ด์Šค ํฌํ•จ
โ€ข ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ์ž‘์„ฑ๋˜์–ด ์žˆ์œผ๋ฉด ๋ฆฌํŒฉํ† ๋ง์ด๋‚˜ ๊ธฐ๋Šฅ ์ถ”๊ฐ€๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ์š”!

๐Ÿ’ก ์ตœ์ ํ™” ๋ฐ ํ™•์žฅ ์•„์ด๋””์–ด

๊ธฐ๋ณธ ์‹œ์Šคํ…œ์„ ๋”์šฑ ๊ฐ•๋ ฅํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ์•„์ด๋””์–ด๋“ค์ด์—์š”! ๐Ÿš€

1. ๋ฉ€ํ‹ฐ์ฒด์ธ ์ง€์›

์ด๋”๋ฆฌ์›€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋‹ค๋ฅธ ์ฒด์ธ์˜ ๋„ค์ž„ ์„œ๋น„์Šค๋„ ์ง€์›ํ•ด์š”:

class MultiChainENSValidator:
    """๋ฉ€ํ‹ฐ์ฒด์ธ ENS ๊ฒ€์ฆ๊ธฐ"""
    
    def __init__(self):
        self.validators = {
            'ethereum': EthereumENSValidator(),
            'polygon': PolygonENSValidator(),
            'bnb': BNBENSValidator()
        }
    
    def validate(self, name, chain='ethereum'):
        """์ฒด์ธ๋ณ„ ๊ฒ€์ฆ"""
        validator = self.validators.get(chain)
        if not validator:
            raise ValueError(f"Unsupported chain: {chain}")
        
        return validator.validate(name)
    
    def validate_all_chains(self, name):
        """๋ชจ๋“  ์ฒด์ธ์—์„œ ๊ฒ€์ฆ"""
        results = {}
        for chain, validator in self.validators.items():
            try:
                results[chain] = validator.validate(name)
            except Exception as e:
                results[chain] = {'error': str(e)}
        
        return results

2. ์›นํ›… ์•Œ๋ฆผ ์‹œ์Šคํ…œ

ENS ๋“ฑ๋ก์ด๋‚˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์•Œ๋ ค์ค˜์š”:

import requests

class ENSWebhookNotifier:
    """ENS ์ด๋ฒคํŠธ ์›นํ›… ์•Œ๋ฆผ"""
    
    def __init__(self, webhook_url):
        self.webhook_url = webhook_url
    
    def notify_registration(self, ens_name, address):
        """์ƒˆ ENS ๋“ฑ๋ก ์•Œ๋ฆผ"""
        payload = {
            'event': 'ens_registered',
            'ens_name': ens_name,
            'ethereum_address': address,
            'timestamp': datetime.utcnow().isoformat()
        }
        
        try:
            response = requests.post(
                self.webhook_url,
                json=payload,
                timeout=5
            )
            response.raise_for_status()
            print(f"โœ… ์›นํ›… ์ „์†ก ์„ฑ๊ณต: {ens_name}")
        except Exception as e:
            print(f"โŒ ์›นํ›… ์ „์†ก ์‹คํŒจ: {str(e)}")
    
    def notify_transfer(self, ens_name, from_address, to_address):
        """ENS ์†Œ์œ ๊ถŒ ์ด์ „ ์•Œ๋ฆผ"""
        payload = {
            'event': 'ens_transferred',
            'ens_name': ens_name,
            'from_address': from_address,
            'to_address': to_address,
            'timestamp': datetime.utcnow().isoformat()
        }
        
        requests.post(self.webhook_url, json=payload)

3. GraphQL API

๋” ์œ ์—ฐํ•œ ์ฟผ๋ฆฌ๋ฅผ ์œ„ํ•œ GraphQL ์ธํ„ฐํŽ˜์ด์Šค์˜ˆ์š”:

from ariadne import QueryType, make_executable_schema, graphql_sync
from ariadne.constants import PLAYGROUND_HTML

type_defs = """
    type Query {
        validateENS(name: String!): ENSValidationResult!
        reverseLookup(address: String!): String
        getMetadata(name: String!): ENSMetadata
    }
    
    type ENSValidationResult {
        isValid: Boolean!
        ensName: String!
        ethereumAddress: String
        error: String
    }
    
    type ENSMetadata {
        ensName: String!
        ethereumAddress: String!
        owner: String
        textRecords: [TextRecord!]!
    }
    
    type TextRecord {
        key: String!
        value: String!
    }
"""

query = QueryType()

@query.field("validateENS")
def resolve_validate_ens(_, info, name):
    result = validate_ens_name(name)
    return {
        'isValid': result['is_valid'],
        'ensName': result['ens_name'],
        'ethereumAddress': result['ethereum_address'],
        'error': result['error']
    }

@query.field("reverseLookup")
def resolve_reverse_lookup(_, info, address):
    return reverse_lookup(address)

schema = make_executable_schema(type_defs, query)

@app.route('/graphql', methods=['GET'])
def graphql_playground():
    return PLAYGROUND_HTML, 200

@app.route('/graphql', methods=['POST'])
def graphql_server():
    data = request.get_json()
    success, result = graphql_sync(
        schema,
        data,
        context_value=request,
        debug=app.debug
    )
    return jsonify(result), 200 if success else 400

4. ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ์‚ฌ๊ธฐ ํƒ์ง€

์˜์‹ฌ์Šค๋Ÿฌ์šด ENS ์ด๋ฆ„์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ด์š”:

import difflib

class ENSFraudDetector:
    """ENS ์‚ฌ๊ธฐ ํƒ์ง€๊ธฐ"""
    
    def __init__(self):
        # ์œ ๋ช…ํ•œ ENS ์ด๋ฆ„๋“ค
        self.known_names = [
            'vitalik.eth',
            'ethereum.eth',
            'opensea.eth',
            'uniswap.eth'
        ]
    
    def check_typosquatting(self, ens_name):
        """ํƒ€์ดํฌ์Šค์ฟผํŒ… ๊ฒ€์‚ฌ"""
        name_without_suffix = ens_name.replace('.eth', '')
        
        for known_name in self.known_names:
            known_without_suffix = known_name.replace('.eth', '')
            
            # ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ
            similarity = difflib.SequenceMatcher(
                None,
                name_without_suffix,
                known_without_suffix
            ).ratio()
            
            # ๋งค์šฐ ์œ ์‚ฌํ•˜์ง€๋งŒ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ์˜์‹ฌ
            if 0.8 < similarity < 1.0:
                return {
                    'is_suspicious': True,
                    'reason': 'typosquatting',
                    'similar_to': known_name,
                    'similarity': similarity
                }
        
        return {'is_suspicious': False}
    
    def check_homograph_attack(self, ens_name):
        """ํ˜ธ๋ชจ๊ทธ๋ž˜ํ”„ ๊ณต๊ฒฉ ๊ฒ€์‚ฌ"""
        # ํ˜ผ๋™ํ•˜๊ธฐ ์‰ฌ์šด ๋ฌธ์ž๋“ค
        confusables = {
            'o': ['0', 'ฮฟ', 'ะพ'],  # o, ์ˆซ์ž 0, ๊ทธ๋ฆฌ์Šค ์˜ค๋ฏธํฌ๋ก , ํ‚ค๋ฆด o
            'l': ['1', 'I', 'ั–'],  # l, ์ˆซ์ž 1, ๋Œ€๋ฌธ์ž I, ํ‚ค๋ฆด i
            'a': ['ะฐ', 'ฮฑ'],       # a, ํ‚ค๋ฆด a, ๊ทธ๋ฆฌ์Šค ์•ŒํŒŒ
        }
        
        for char in ens_name:
            for original, confusable_chars in confusables.items():
                if char in confusable_chars:
                    return {
                        'is_suspicious': True,
                        'reason': 'homograph_attack',
                        'suspicious_char': char
                    }
        
        return {'is_suspicious': False}

# ์‚ฌ์šฉ ์˜ˆ์‹œ
detector = ENSFraudDetector()

test_names = [
    'vitalik.eth',      # ์ •์ƒ
    'vital1k.eth',      # ํƒ€์ดํฌ์Šค์ฟผํŒ… ์˜์‹ฌ
    'vั–talik.eth',      # ํ˜ธ๋ชจ๊ทธ๋ž˜ํ”„ ๊ณต๊ฒฉ (ํ‚ค๋ฆด ั–)
]

for name in test_names:
    typo_result = detector.check_typosquatting(name)
    homo_result = detector.check_homograph_attack(name)
    
    print(f"\n๐Ÿ” ๊ฒ€์‚ฌ: {name}")
    if typo_result['is_suspicious']:
        print(f"   โš ๏ธ ํƒ€์ดํฌ์Šค์ฟผํŒ… ์˜์‹ฌ: {typo_result}")
    if homo_result['is_suspicious']:
        print(f"   โš ๏ธ ํ˜ธ๋ชจ๊ทธ๋ž˜ํ”„ ๊ณต๊ฒฉ ์˜์‹ฌ: {homo_result}")
๐ŸŽฏ ํ™•์žฅ ์•„์ด๋””์–ด ๋” ๋ณด๊ธฐ โ€ข ENS ๋„๋ฉ”์ธ ๋งŒ๋ฃŒ ์•Œ๋ฆผ ์„œ๋น„์Šค
โ€ข ENS ๊ฐ€๊ฒฉ ์ถ”์  ๋ฐ ๋ถ„์„
โ€ข ENS ๊ธฐ๋ฐ˜ ์ด๋ฉ”์ผ ์„œ๋น„์Šค
โ€ข ENS ํ”„๋กœํ•„ ํŽ˜์ด์ง€ ์ƒ์„ฑ๊ธฐ
โ€ข ENS ๊ฑฐ๋ž˜ ํžˆ์Šคํ† ๋ฆฌ ๋ถ„์„
โ€ข ์„œ๋ธŒ๋„๋ฉ”์ธ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ

์—ฌ๋Ÿฌ๋ถ„์˜ ์ฐฝ์˜๋ ฅ์œผ๋กœ ๋” ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”! ๐Ÿ’ก

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค ๋ฐ ์ปค๋ฎค๋‹ˆํ‹ฐ

ENS์™€ Web3 ๊ฐœ๋ฐœ์„ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๋ฆฌ์†Œ์Šค๋“ค์„ ํ™œ์šฉํ•ด๋ณด์„ธ์š”! ๐Ÿ“–

๊ณต์‹ ๋ฌธ์„œ

โ€ข ENS ๊ณต์‹ ๋ฌธ์„œ: docs.ens.domains - ๊ฐ€์žฅ ์ •ํ™•ํ•˜๊ณ  ์ตœ์‹  ์ •๋ณด
โ€ข Web3.py ๋ฌธ์„œ: web3py.readthedocs.io - ํŒŒ์ด์ฌ ์ด๋”๋ฆฌ์›€ ๊ฐœ๋ฐœ์˜ ๋ฐ”์ด๋ธ”
โ€ข Ethereum.org: ethereum.org/developers - ์ด๋”๋ฆฌ์›€ ๊ฐœ๋ฐœ ์ „๋ฐ˜

์ปค๋ฎค๋‹ˆํ‹ฐ

โ€ข ENS Discord: ENS ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ์ง์ ‘ ์†Œํ†ต
โ€ข Ethereum Stack Exchange: ๊ธฐ์ˆ ์  ์งˆ๋ฌธ๊ณผ ๋‹ต๋ณ€
โ€ข Reddit r/ethereum: ์ด๋”๋ฆฌ์›€ ์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข GitHub: ensdomains ์กฐ์ง์˜ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ๋“ค

์ถ”์ฒœ ํ•™์Šต ๊ฒฝ๋กœ

1

๊ธฐ์ดˆ ๋‹ค์ง€๊ธฐ
๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ณธ ๊ฐœ๋…, ์ด๋”๋ฆฌ์›€ ์ž‘๋™ ์›๋ฆฌ, ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ดํ•ด

2

Web3 ๊ฐœ๋ฐœ
Web3.py ๋งˆ์Šคํ„ฐํ•˜๊ธฐ, ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹

3

ENS ์‹ฌํ™”
ENS ์ปจํŠธ๋ž™ํŠธ ๊ตฌ์กฐ, ์„œ๋ธŒ๋„๋ฉ”์ธ, ์ปค์Šคํ…€ Resolver

4

์‹ค์ „ ํ”„๋กœ์ ํŠธ
์‹ค์ œ ์„œ๋น„์Šค ๊ฐœ๋ฐœ, ๋ฐฐํฌ, ์šด์˜ ๊ฒฝํ—˜ ์Œ“๊ธฐ

๐Ÿ’ผ ์ปค๋ฆฌ์–ด ํŒ Web3 ๊ฐœ๋ฐœ์ž๋Š” ํ˜„์žฌ ์ˆ˜์š”๊ฐ€ ๋งค์šฐ ๋†’์•„์š”! ENS ๊ฒ€์ฆ ์‹œ์Šคํ…œ ๊ฐ™์€ ํ”„๋กœ์ ํŠธ๋ฅผ ํฌํŠธํด๋ฆฌ์˜ค์— ์ถ”๊ฐ€ํ•˜๋ฉด ์ทจ์—…์ด๋‚˜ ํ”„๋ฆฌ๋žœ์„œ ํ™œ๋™์— ํฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜, ๊ด€๋ จ ํ”„๋กœ์ ํŠธ๋ฅผ ์˜๋ขฐ๋ฐ›์„ ์ˆ˜๋„ ์žˆ์–ด์š”! ๐Ÿš€

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์˜ค๋А๋ผ ์ˆ˜๊ณ  ๋งŽ์•˜์–ด์š”! ๐Ÿ˜Š ์šฐ๋ฆฌ๋Š” ENS ์ฃผ์†Œ ์ž๋™ ๊ฒ€์ฆ ์‹œ์Šคํ…œ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋งŒ๋“ค์–ด๋ดค์–ด์š”.

์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šด ๊ฒƒ๋“ค

โœ… ENS์˜ ๊ฐœ๋…๊ณผ ์ค‘์š”์„ฑ
โœ… Web3.py๋ฅผ ์‚ฌ์šฉํ•œ ์ด๋”๋ฆฌ์›€ ์—ฐ๊ฒฐ
โœ… ๊ธฐ๋ณธ ENS ๊ฒ€์ฆ ํ•จ์ˆ˜ ๊ตฌํ˜„
โœ… ์—ญ๋ฐฉํ–ฅ ์กฐํšŒ์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ
โœ… Flask๋ฅผ ์‚ฌ์šฉํ•œ RESTful API ์„œ๋ฒ„
โœ… ์บ์‹ฑ๊ณผ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
โœ… ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
โœ… ํ…Œ์ŠคํŠธ ์ž‘์„ฑ๊ณผ ๋””๋ฒ„๊น…
โœ… Docker๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐฐํฌ
โœ… ํ™•์žฅ ์•„์ด๋””์–ด์™€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

๋‹ค์Œ ๋‹จ๊ณ„

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ ENS ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ๐Ÿ’ช

โ€ข ๊ธฐ์กด ์›น ์„œ๋น„์Šค์— ENS ๋กœ๊ทธ์ธ ์ถ”๊ฐ€ํ•˜๊ธฐ
โ€ข NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์— ENS ํ†ตํ•ฉํ•˜๊ธฐ
โ€ข ENS ๊ธฐ๋ฐ˜ ์†Œ์…œ ๋„คํŠธ์›Œํฌ ๋งŒ๋“ค๊ธฐ
โ€ข DAO ๊ฑฐ๋ฒ„๋„Œ์Šค์— ENS ํ™œ์šฉํ•˜๊ธฐ

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ์กฐ์–ธ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์€ ๊ณ„์† ์ง„ํ™”ํ•˜๊ณ  ์žˆ์–ด์š”. ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ๊ณผ ํ‘œ์ค€์ด ๋Š์ž„์—†์ด ๋‚˜์˜ค์ฃ . ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ž์ฃผ ํ™•์ธํ•˜๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ฐธ์—ฌํ•˜๋ฉฐ, ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ข‹์€ ํ•™์Šต ๋ฐฉ๋ฒ•์ด์—์š”!

๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ๋ถ€๋„๋Ÿฌ์›Œํ•˜์ง€ ๋ง๊ณ  ์งˆ๋ฌธํ•˜์„ธ์š”. ๋ชจ๋“  ์ „๋ฌธ๊ฐ€๋„ ์ฒ˜์Œ์—” ์ดˆ๋ณด์˜€๋‹ต๋‹ˆ๋‹ค. ๐Ÿ˜Š

์ด ๊ธ€์ด ์—ฌ๋Ÿฌ๋ถ„์˜ Web3 ๊ฐœ๋ฐœ ์—ฌ์ •์— ๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ผ์š”! ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๋ฌผ์–ด๋ณด์„ธ์š”. ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์–ด๋ด์š”! ๐Ÿš€

Happy Coding! ๐ŸŽ‰

๐ŸŒŸ ์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๋‹ค๋ฉด ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ๊ณต์œ ํ•ด์ฃผ์„ธ์š”!
ํ•จ๊ป˜ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๋Š” Web3 ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€์š”! ๐Ÿ’™

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.