Web3.py로 스마트 컨트랙트와 상호작용할 때 가장 빈번하게 마주치는 오류 중 하나가 바로 "invalid address" 오류에요. 이 오류는 주소 형식이 이더리움 표준을 따르지 않을 때 발생하는데, 특히 EIP-55 체크섬 주소 형식과 관련이 깊어요. 오늘은 이 문제를 완벽하게 해결하는 방법을 실전 코드와 함께 알아보겠어요.
개발 환경 세팅 (5분 완성)
먼저 필요한 패키지를 설치하고 기본 환경을 구성해요.
pip install web3 python-dotenv eth-account
프로젝트 구조를 다음과 같이 설정해요:
project/
├── .env
├── contracts/
│ └── SimpleToken.sol
├── abi/
│ └── SimpleToken.json
└── main.py
환경 변수 파일(.env)을 생성해요:
INFURA_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID
PRIVATE_KEY=your_private_key_here
CONTRACT_ADDRESS=0x...
Invalid Address 오류 재현하기
먼저 오류가 발생하는 상황을 살펴보겠어요. 다음 코드는 전형적인 invalid address 오류를 발생시켜요:
from web3 import Web3
import json
# Web3 연결
w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/YOUR_KEY'))
# 잘못된 주소 형식들
wrong_addresses = [
"0x5b38da6a701c568545dcfcb03fcb875f56beddc4", # 소문자 주소
"5B38Da6a701c568545dCfcB03FcB875f56beddC4", # 0x 빠진 주소
"0x5B38Da6a701c568545dCfcB03FcB875f56beddC", # 길이 부족
]
# ABI 로드
with open('abi/SimpleToken.json', 'r') as file:
abi = json.load(file)
# 오류 발생 코드
for addr in wrong_addresses:
try:
contract = w3.eth.contract(address=addr, abi=abi)
print(f"성공: {addr}")
except ValueError as e:
print(f"오류 발생: {addr}")
print(f"오류 메시지: {e}\n")
위 코드를 실행하면 다음과 같은 오류 메시지를 볼 수 있어요:
오류 발생: 0x5b38da6a701c568545dcfcb03fcb875f56beddc4
오류 메시지: {'code': -32602, 'message': 'invalid address'}
원인 분석: EIP-55 체크섬이란?
이더리움은 주소의 무결성을 검증하기 위해 EIP-55 체크섬 인코딩을 사용해요. 이는 주소의 대소문자를 특정 패턴으로 변환하여 타이핑 실수를 방지하는 메커니즘이에요.
체크섬 주소의 특징:
- 40자의 16진수 문자열
- 대소문자가 섞여 있음
- 주소의 해시값을 기반으로 대소문자 결정
체크섬 검증 원리를 코드로 이해해보겠어요:
from web3 import Web3
import hashlib
def manual_checksum_verification(address):
"""EIP-55 체크섬 검증 과정을 수동으로 구현"""
# 0x 제거하고 소문자로 변환
address = address.lower().replace('0x', '')
# Keccak-256 해시 계산
hash_object = Web3.keccak(text=address)
hash_hex = hash_object.hex()
# 체크섬 주소 생성
checksum = '0x'
for i in range(len(address)):
if hash_hex[i] in '89abcdef': # 해시값이 8 이상이면 대문자
checksum += address[i].upper()
else:
checksum += address[i]
return checksum
# 테스트
test_addr = "0x5b38da6a701c568545dcfcb03fcb875f56beddc4"
checksum_addr = manual_checksum_verification(test_addr)
print(f"원본: {test_addr}")
print(f"체크섬: {checksum_addr}")
해결 방법 1: toChecksumAddress() 사용
Web3.py는 주소를 체크섬 형식으로 변환하는 내장 함수를 제공해요:
from web3 import Web3
import json
from eth_account import Account
class SmartContractManager:
def __init__(self, provider_url):
self.w3 = Web3(Web3.HTTPProvider(provider_url))
if not self.w3.is_connected():
raise Exception("Web3 연결 실패")
def safe_contract_init(self, address, abi_path):
"""안전한 컨트랙트 초기화"""
# 주소 전처리
if not address.startswith('0x'):
address = '0x' + address
# 주소 길이 확인
if len(address) != 42:
raise ValueError(f"잘못된 주소 길이: {len(address)}")
# 체크섬 주소로 변환
try:
checksum_address = self.w3.to_checksum_address(address)
except Exception as e:
raise ValueError(f"체크섬 변환 실패: {e}")
# ABI 로드
with open(abi_path, 'r') as file:
abi = json.load(file)
# 컨트랙트 객체 생성
contract = self.w3.eth.contract(
address=checksum_address,
abi=abi
)
return contract
# 사용 예제
manager = SmartContractManager('https://mainnet.infura.io/v3/YOUR_KEY')
# 다양한 형식의 주소 테스트
test_addresses = [
"0x5b38da6a701c568545dcfcb03fcb875f56beddc4", # 소문자
"5B38Da6a701c568545dCfcB03FcB875f56beddC4", # 0x 없음
"0X5B38DA6A701C568545DCFCB03FCB875F56BEDDC4", # 대문자
]
for addr in test_addresses:
try:
contract = manager.safe_contract_init(addr, 'abi/Token.json')
print(f"✓ 성공: {contract.address}")
except Exception as e:
print(f"✗ 실패: {addr} - {e}")
해결 방법 2: 주소 유효성 검증 유틸리티
더 강력한 주소 검증 시스템을 구축해보겠어요:
from web3 import Web3
from typing import Optional, Union
import re
class AddressValidator:
"""이더리움 주소 유효성 검증 클래스"""
def __init__(self):
self.w3 = Web3()
self.address_pattern = re.compile(r'^(0x)?[0-9a-fA-F]{40}$')
def is_valid_format(self, address: str) -> bool:
"""주소 형식 검증"""
return bool(self.address_pattern.match(address))
def normalize_address(self, address: str) -> Optional[str]:
"""주소 정규화 및 체크섬 적용"""
# 빈 문자열 체크
if not address:
return None
# 공백 제거
address = address.strip()
# 0x 접두사 처리
if not address.startswith('0x'):
address = '0x' + address
# 형식 검증
if not self.is_valid_format(address):
return None
try:
# 체크섬 주소로 변환
return self.w3.to_checksum_address(address)
except:
return None
def validate_and_convert(self, address: str) -> dict:
"""종합 검증 결과 반환"""
result = {
'original': address,
'is_valid': False,
'checksum_address': None,
'errors': []
}
# 빈 값 체크
if not address:
result['errors'].append('주소가 비어있어요')
return result
# 길이 체크
clean_addr = address.replace('0x', '').replace('0X', '')
if len(clean_addr) != 40:
result['errors'].append(f'주소 길이 오류: {len(clean_addr)}자')
return result
# 16진수 체크
try:
int(clean_addr, 16)
except ValueError:
result['errors'].append('올바른 16진수가 아니에요')
return result
# 체크섬 변환
normalized = self.normalize_address(address)
if normalized:
result['is_valid'] = True
result['checksum_address'] = normalized
else:
result['errors'].append('체크섬 변환 실패')
return result
# 사용 예제
validator = AddressValidator()
test_cases = [
"0x5b38da6a701c568545dcfcb03fcb875f56beddc4",
"5b38da6a701c568545dcfcb03fcb875f56beddc4",
"0x5b38da6a701c568545dcfcb03fcb875f56bedd", # 짧음
"0xGGGGda6a701c568545dcfcb03fcb875f56beddc4", # 잘못된 문자
"", # 빈 문자열
]
for addr in test_cases:
result = validator.validate_and_convert(addr)
print(f"\n입력: {addr}")
print(f"유효: {result['is_valid']}")
if result['checksum_address']:
print(f"체크섬: {result['checksum_address']}")
if result['errors']:
print(f"오류: {', '.join(result['errors'])}")
실전 스마트 컨트랙트 연동
이제 실제 토큰 컨트랙트와 상호작용하는 완전한 예제를 만들어보겠어요:
from web3 import Web3
from web3.middleware import geth_poa_middleware
from eth_account import Account
import json
import os
from dotenv import load_dotenv
load_dotenv()
class TokenInteraction:
"""ERC-20 토큰 상호작용 클래스"""
def __init__(self, network='mainnet'):
self.network = network
self.setup_connection()
def setup_connection(self):
"""네트워크 연결 설정"""
if self.network == 'mainnet':
rpc_url = os.getenv('MAINNET_RPC')
elif self.network == 'goerli':
rpc_url = os.getenv('GOERLI_RPC')
else:
raise ValueError(f"지원하지 않는 네트워크: {self.network}")
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
# POA 네트워크용 미들웨어 (테스트넷)
if self.network != 'mainnet':
self.w3.middleware_onion.inject(geth_poa_middleware, layer=0)
if not self.w3.is_connected():
raise Exception("Web3 연결 실패")
print(f"연결 성공: {self.network}")
print(f"블록 번호: {self.w3.eth.block_number}")
def load_contract(self, contract_address: str, abi_path: str):
"""컨트랙트 로드 (주소 검증 포함)"""
# 주소 정규화
if not contract_address.startswith('0x'):
contract_address = '0x' + contract_address
# 체크섬 적용
try:
checksum_address = self.w3.to_checksum_address(contract_address)
except Exception as e:
raise ValueError(f"잘못된 컨트랙트 주소: {e}")
# ABI 로드
with open(abi_path, 'r') as f:
abi = json.load(f)
# 컨트랙트 객체 생성
contract = self.w3.eth.contract(address=checksum_address, abi=abi)
# 컨트랙트 코드 확인 (배포되었는지)
code = self.w3.eth.get_code(checksum_address)
if code == b'':
raise ValueError(f"해당 주소에 컨트랙트가 없어요: {checksum_address}")
return contract
def safe_address_param(self, address: str) -> str:
"""함수 파라미터용 주소 검증"""
# None 체크
if address is None:
raise ValueError("주소가 None이에요")
# 문자열 타입 체크
if not isinstance(address, str):
address = str(address)
# 체크섬 변환
return self.w3.to_checksum_address(address)
def get_balance(self, token_contract, owner_address: str) -> dict:
"""토큰 잔액 조회"""
# 주소 검증
owner = self.safe_address_param(owner_address)
try:
# 잔액 조회
balance = token_contract.functions.balanceOf(owner).call()
# decimals 조회
decimals = token_contract.functions.decimals().call()
# 심볼 조회
symbol = token_contract.functions.symbol().call()
# 실제 잔액 계산
actual_balance = balance / (10 ** decimals)
return {
'address': owner,
'balance_wei': balance,
'balance': actual_balance,
'symbol': symbol,
'decimals': decimals
}
except Exception as e:
raise Exception(f"잔액 조회 실패: {e}")
# 사용 예제
token_manager = TokenInteraction('mainnet')
# USDT 컨트랙트 (메인넷)
usdt_address = "0xdac17f958d2ee523a2206206994597c13d831ec7"
user_address = "0x5b38da6a701c568545dcfcb03fcb875f56beddc4"
try:
# 컨트랙트 로드
usdt = token_manager.load_contract(usdt_address, 'abi/USDT.json')
# 잔액 조회
balance_info = token_manager.get_balance(usdt, user_address)
print(f"\n토큰: {balance_info['symbol']}")
print(f"잔액: {balance_info['balance']:.6f}")
print(f"주소: {balance_info['address']}")
except Exception as e:
print(f"오류 발생: {e}")
프론트엔드 연동 (React + ethers.js)
웹 애플리케이션에서도 동일한 문제가 발생할 수 있어요. React와 ethers.js를 사용한 해결 방법을 살펴보겠어요:
// utils/addressValidator.js
import { ethers } from 'ethers';
export const validateAndChecksum = (address) => {
try {
// 빈 값 체크
if (!address || address.trim() === '') {
return { valid: false, error: '주소가 비어있어요' };
}
// 0x 접두사 추가
let normalized = address.trim();
if (!normalized.startsWith('0x')) {
normalized = '0x' + normalized;
}
// ethers.js의 getAddress는 자동으로 체크섬 적용
const checksumAddress = ethers.utils.getAddress(normalized);
return {
valid: true,
address: checksumAddress,
original: address
};
} catch (error) {
return {
valid: false,
error: error.message || '유효하지 않은 주소에요',
original: address
};
}
};
// 컨트랙트 연결 훅
export const useContract = (contractAddress, abi) => {
const [contract, setContract] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const initContract = async () => {
try {
// 주소 검증
const validation = validateAndChecksum(contractAddress);
if (!validation.valid) {
throw new Error(validation.error);
}
// Provider 설정
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
// 컨트랙트 초기화
const contractInstance = new ethers.Contract(
validation.address,
abi,
signer
);
setContract(contractInstance);
setError(null);
} catch (err) {
setError(err.message);
setContract(null);
}
};
if (contractAddress && abi) {
initContract();
}
}, [contractAddress, abi]);
return { contract, error };
};
테스트넷 배포 및 검증
Goerli 테스트넷에서 실제로 테스트해보는 과정이에요:
from web3 import Web3
from eth_account import Account
import json
class ContractDeployer:
"""스마트 컨트랙트 배포 및 검증"""
def __init__(self, private_key, rpc_url):
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
self.account = Account.from_key(private_key)
def deploy_and_verify(self, bytecode, abi):
"""컨트랙트 배포 후 주소 검증"""
# 트랜잭션 구성
Contract = self.w3.eth.contract(abi=abi, bytecode=bytecode)
# 가스 추정
gas_estimate = Contract.constructor().estimate_gas({
'from': self.account.address
})
# 배포 트랜잭션 생성
tx = Contract.constructor().build_transaction({
'from': self.account.address,
'gas': gas_estimate,
'gasPrice': self.w3.eth.gas_price,
'nonce': self.w3.eth.get_transaction_count(self.account.address),
})
# 서명 및 전송
signed_tx = self.account.sign_transaction(tx)
tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
# 영수증 대기
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
# 배포된 주소 검증
deployed_address = receipt.contractAddress
checksum_address = self.w3.to_checksum_address(deployed_address)
print(f"배포 완료: {checksum_address}")
print(f"트랜잭션: {tx_hash.hex()}")
return checksum_address
보안 고려사항
주소 처리 시 반드시 고려해야 할 보안 사항들이에요:
- Zero Address 체크: 0x0000...0000 주소로의 전송 방지
- Contract vs EOA 구분: 컨트랙트 주소와 일반 주소 구분
- Reentrancy 방지: 외부 호출 시 주소 검증
- Access Control: 관리자 주소 하드코딩 금지
def security_checks(w3, address):
"""보안 검증 함수"""
# 체크섬 적용
safe_address = w3.to_checksum_address(address)
# Zero address 체크
if safe_address == '0x0000000000000000000000000000000000000000':
raise ValueError("Zero address는 사용할 수 없어요")
# 컨트랙트 여부 확인
code = w3.eth.get_code(safe_address)
is_contract = len(code) > 0
return {
'address': safe_address,
'is_contract': is_contract,
'is_eoa': not is_contract
}
흔한 에러와 해결법
1. ENS 도메인 처리
def resolve_ens_or_address(w3, input_string):
"""ENS 도메인 또는 주소 처리"""
# .eth로 끝나면 ENS
if input_string.endswith('.eth'):
address = w3.ens.address(input_string)
if address:
return w3.to_checksum_address(address)
raise ValueError(f"ENS 도메인을 찾을 수 없어요: {input_string}")
# 일반 주소
return w3.to_checksum_address(input_string)
2. 멀티체인 주소 처리
def multichain_address_handler(chain, address):
"""체인별 주소 처리"""
chains = {
'ethereum': Web3.to_checksum_address,
'bsc': Web3.to_checksum_address,
'polygon': Web3.to_checksum_address,
}
if chain in chains:
return chains[chain](address)
raise ValueError(f"지원하지 않는 체인: {chain}")
메인넷 체크리스트
메인넷 배포 전 반드시 확인해야 할 사항들이에요:
- 모든 주소에 체크섬 적용 확인
- Zero address 체크 로직 구현
- 주소 타입 검증 (EOA vs Contract)
- 테스트넷에서 충분한 테스트
- 가스비 최적화 확인
- 에러 핸들링 구현
- 로깅 시스템 구축
중요 경고: 메인넷 배포 전 반드시 전문 감사를 받으세요. 실제 자금을 다룰 때는 극도로 신중해야 해요. 모든 책임은 사용자 본인에게 있어요.
마치며
Invalid address 오류는 단순해 보이지만 블록체인 개발에서 자주 마주치는 중요한 문제에요. Web3.py의 to_checksum_address()
함수를 적절히 활용하고, 체계적인 주소 검증 시스템을 구축하면 안정적인 디앱을 개발할 수 있어요.
테스트넷에서 충분히 테스트한 후 메인넷에 적용하는 것을 잊지 마세요. 블록체인 트랜잭션은 되돌릴 수 없으니 항상 신중하게 접근해야 해요.