04. 백엔드 (Python 3.12 · FastAPI · web3.py · SQLite)
ドキュメント本文は現在、韓国語のみで提供しています。
디렉터리 구조
backend/
├─ app/
│ ├─ main.py # FastAPI 앱, CORS, 라우터 등록, 시작 시 DB 초기화
│ ├─ config.py # pydantic-settings (.env)
│ ├─ db.py # sqlite3 연결, 스키마, 쿼리 함수
│ ├─ chain.py # web3 인스턴스, BEP20 ABI, Transfer 로그 디코딩
│ ├─ schemas.py # 요청/응답 pydantic 모델, 주소 검증
│ ├─ ratelimit.py # 프로세스 내 per-IP 고정 윈도 레이트리미터
│ ├─ indexer.py # Transfer 이벤트 폴링 인덱서 (python -m app.indexer)
│ └─ routers/
│ ├─ token.py # /api/token, /api/token/balance/{address}
│ └─ transfers.py # /api/transfers (GET 목록, POST 기록)
├─ requirements.txt
└─ .env.example
엔드포인트
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /health | 상태 확인 |
| GET | /api/token | 이름·심볼·소수점·총발행량 (체인에서 읽고 프로세스 캐시) |
| GET | /api/token/balance/{address} | 주소의 ACN 잔액(wei 문자열) |
| GET | /api/transfers?address=&limit=&offset= | 거래 내역. address는 보낸/받은 주소 모두 매치 |
| GET | /api/indexer | 인덱서 동기화 상태: 체인 헤드, 안전 헤드, 마지막 인덱싱 블록·시각, 남은 블록 수, 저장된 전송 건수, 현재 RPC, 시작 시각, 연속 실패 수, 마지막 오류. dApp의 "네트워크 동기화" 표시와 원격 진단에 사용 |
| POST | /api/transfers { "txHash": "0x…" } | tx 영수증을 체인에서 조회·검증 후 ACN Transfer 로그 저장 |
OpenAPI 문서: http://localhost:8000/docs
POST /api/transfers 검증 규칙
- 클라이언트 IP당 분당
RECORD_RATE_LIMIT_PER_MINUTE(기본 10) 초과 → 429 txHash형식(0x + 64 hex) 검증 → 422 (pydantic)- 영수증 없음(미채굴/존재하지 않음,
TransactionNotFound) → 404 (클라이언트가 재시도) - RPC 장애/타임아웃 → 502
status != 1(revert) → 422- 확정 수 부족:
head - blockNumber < INDEXER_CONFIRMATIONS(기본 15) → 404 (재시도). reorg로 고아 행이 남지 않게 하기 위함 - 영수증 로그 중 ACN 컨트랙트 주소에서 발생한 Transfer 토픽 로그만 선별, 없으면 → 422 (이 경우 블록 조회 RPC 호출을 하지 않음)
- from/to/amount/blockTime은 전부 영수증·블록에서 읽는다. 클라이언트 입력은 hash 하나뿐이다.
큰 정수(wei)는 JSON 정밀도 문제를 피하려고 항상 문자열로 직렬화한다.
인덱서 (app/indexer.py)
indexer_state.last_indexed_block다음 블록부터INDEXER_BATCH_SIZE(기본 500) 블록씩eth_getLogs.- 재조직 안전 마진:
head - INDEXER_CONFIRMATIONS(기본 15) 까지만 인덱싱한다. BSC 최종성 이전 블록은 다루지 않으므로 짧은 reorg로 고아 이벤트가 남지 않는다. 블록 해시 검증까지는 하지 않으므로 15블록을 넘는 reorg는 수동 재색인이 필요하다. - 첫 실행 시
INDEXER_START_BLOCK이 0이면head - confirmations - INDEXER_BACKFILL_BLOCKS부터 시작한다. 영구 디스크가 없는 호스트(Render 무료)에서는 재시작마다 DB가 비므로INDEXER_BACKFILL_BLOCKS(운영 100000 ≈ 약 21시간)만큼 최근 구간을 다시 채운다. 과거 전체 백필은INDEXER_START_BLOCK에 값 지정. - 블록 타임스탬프는 블록 단위로 캐시하며, 조회 실패 시 배치를 버리지 않고
block_time=NULL로 저장한다. - RPC 오류 시 배치를 절반씩 줄여(최소 1블록) 재시도하고, 성공하면 다시 늘린다. 최소 배치에서도 실패하면 지수 백오프(최대 120초). 403/429 응답이나 3회 연속 실패 시
BSC_RPC_URL목록의 다음 엔드포인트로 회전한다. - 인덱서 스레드가 예외로 죽으면 30초 후 자동 재시작하며, 마지막 오류를
indexer_state.last_error에 남긴다. - 기동 시
eth_chainId가CHAIN_ID와 다르면 즉시 종료한다(잘못된 RPC 방지). API 서버도 동일하게 검사한다.
환경 변수 (backend/.env)
| 변수 | 설명 | 기본값 |
|---|---|---|
BSC_RPC_URL | JSON-RPC 엔드포인트. 콤마로 여러 개 지정 가능: API는 첫 번째를 쓰고, 인덱서는 403/429 또는 연속 실패 시 다음 URL로 회전 | https://bsc-rpc.publicnode.com (운영: publicnode, 1rpc.io/bnb, drpc, blastapi) |
CHAIN_ID | 56 / 97 | 56 |
TOKEN_ADDRESS | ACN 컨트랙트(형식 검증 후 체크섬으로 정규화) | 0x84f9…6236 |
DATABASE_PATH | SQLite 파일 | ./data/alice.db |
CORS_ORIGINS | 허용 오리진(콤마 구분) | http://localhost:3000 |
INDEXER_START_BLOCK | 인덱서 시작 블록(0=현재) | 0 |
INDEXER_BACKFILL_BLOCKS | 체크포인트가 없을 때 헤드에서 뒤로 되감아 시작할 블록 수 | 0 (운영 100000) |
INDEXER_BATCH_SIZE | 배치 블록 수(최대치, 실패 시 자동 축소) | 500 |
INDEXER_POLL_SECONDS | 폴링 간격 | 5 |
INDEXER_CONFIRMATIONS | 헤드에서 뒤로 둘 안전 블록 수. POST 기록에도 동일 적용 | 15 |
RECORD_RATE_LIMIT_PER_MINUTE | POST /api/transfers IP당 분당 허용 횟수 | 10 |
모든 숫자 설정은 pydantic 제약(ge=0/1)으로 검증되며 CHAIN_ID는 56/97만 허용한다. 잘못된 값이면 기동 시 실패한다.
실행
uvicorn app.main:app --reload --port 8000 # API
python -m app.indexer # 인덱서(별도 프로세스)
