🔗 StableCoin BC Adapter

v1.1 · 최종 업데이트: 2026-04-13

📋 시스템 개요

ERC-4337 Account Abstraction 기반 스테이블코인 블록체인 어댑터 서비스

StableCoin BC Adapter는 블록체인과 비즈니스 서비스 사이의 중간 계층(Adapter)으로, 입금 감지 · 출금 · 결제 · 정산 · 집금 · 대사 등 스테이블코인 관련 모든 On-chain 오퍼레이션을 처리합니다.

헥사고날 아키텍처(Ports & Adapters)를 채택하여 비즈니스 로직과 인프라를 분리하였으며, NestJS 없이 순수 TypeScript로 수동 조합(Composition Root) 방식을 사용합니다.

12
핵심 서비스
20
Kafka 토픽
11
인바운드 포트
9
아웃바운드 포트

🏗️ 아키텍처

헥사고날 아키텍처 (Ports & Adapters Pattern)

🔌 Adapter (In) Kafka Consumer · WebSocket · HTTP Health
⚙️ Application 12개 UseCase 서비스
💎 Domain Port 인터페이스 · Model · Error
🗄️ Infrastructure SQLite (better-sqlite3) · Redis (ioredis) · Kafka (kafkajs) · Ethers.js v6 · Bundler HTTP · NHN KMS
💡
의존성 방향: Adapter → Application → Domain ← Infrastructure
Domain 계층은 외부에 의존하지 않으며, Port 인터페이스를 통해 인프라와 소통합니다.

🛠️ 기술 스택

카테고리기술용도
런타임Node.js 22 + TypeScript (strict)메인 런타임
메시징kafkajs비동기 이벤트 기반 통신
블록체인ethers.js v6RPC 호출, CREATE2, ABI 인코딩
데이터베이스better-sqlite3계정/설정/아웃박스/키 관리 (4개 DB)
분산 락Redis (ioredis)동시성 제어, Nonce 충돌 방지
키 관리NHN Cloud SKMAES-256-GCM 암호화/복호화
로깅pino구조화 JSON 로깅 (KST)
검증zod입력 스키마 유효성 검사
번들러ERC-4337 Bundler HTTP APIUserOperation 제출

🔄 전체 플로우

Adapter 서비스의 전체 메시지 흐름 개요

메시지 처리 파이프라인

Kafka 메시지 수신 (snake_case) JSON 파싱 + camelCase 변환 AsyncLocalStorage 컨텍스트 바인딩 Zod 스키마 유효성 검사 비즈니스 로직 실행 Kafka 응답 발행 (snake_case)

데이터 포맷 변환 규칙

  • 외부 → 내부snake_case JSON → parseJsonToCamel()camelCase 객체
  • 내부 처리모든 내부 로직은 camelCase 사용
  • 내부 → 외부camelCasetoSnakePayload()snake_case JSON

부트스트랩 시퀀스

SQLite 4개 DB 초기화 Redis 연결 (KMS 비밀번호) 활성 체인/토큰 설정 로드 체인별 RPC Provider 생성 12개 서비스 조립 Kafka/WebSocket/Outbox/Health 시작
💡
Graceful Shutdown: SIGINT/SIGTERM 수신 시 10초 타임아웃 내 순차 종료 (Outbox → Health → Kafka → WebSocket → DB → Logger)

🗺️ E2E 유즈케이스 흐름

App(프론트) → Wallet BE → BC Adapter → Blockchain 전체 흐름을 유즈케이스별로 정리

💡
BC Adapter의 위치: App과 직접 통신하지 않습니다. Wallet BE가 Kafka를 통해 BC Adapter에 요청하고, BC Adapter는 블록체인과 직접 통신한 결과를 Kafka로 반환합니다.
🆕
1. 계정 생성 → 배포
사용자 가입 시 블록체인 지갑 주소를 생성하고 스마트 컨트랙트를 배포하는 흐름
📱 App (Front)
회원가입 요청
🏢 Wallet BE
사용자 생성 처리
Kafka 발행adapter.account.create
주소 수신 & 저장
Kafka 발행adapter.account.deploy
⚙️ BC Adapter
CREATE2 주소 계산salt = requestId
SQLite 저장status: CREATED
Kafka 응답adapter.account.created
분산 락 → Nonce 조회
Bundler 배포 제출
Registry 폴링 확인
자동 집금 트리거(비동기)
Kafka 응답adapter.account.deployed
⛓️ Blockchain
주소 계산(오프체인, 온체인 X)
프록시 컨트랙트 배포CREATE2 + EOFS
Registry 등록 확인
요청 토픽
adapter.account.create / deploy
응답 토픽
adapter.account.created / deployed
동시성 제어
Redis 분산 락 (배포 시)
후속 처리
자동 집금 (비동기)
💰
2. 입금 감지 → 알림
외부에서 사용자 지갑으로 토큰이 입금되면 실시간 감지하여 BE에 알리는 흐름
🌐 외부
사용자 주소로
토큰 전송
⛓️ Blockchain
트랜잭션 컨펌
Deposit Listener가
이벤트 감지
⚙️ BC Adapter
WebSocket 수신ws://host:8080/ws
Zod 검증 + 컨텍스트
Outbox 저장at-least-once 보장
Kafka 발행adapter.deposit.detected
자동 집금 트리거(비동기, 에러 swallow)
🏢 Wallet BE
입금 이벤트 수신
사용자 잔액 업데이트
Push 알림 전송
📱 App
입금 완료 알림
⚡ 입금은 Kafka 요청이 아닌 WebSocket 이벤트 기반입니다. Deposit Listener(외부 서비스)가 블록체인 이벤트를 감지하여 BC Adapter로 전달합니다.
수신 방식
WebSocket (ws://)
발행 토픽
adapter.deposit.detected
메시지 보장
Outbox 패턴 (at-least-once)
후속 처리
자동 집금 → HotWallet
📤
3. 출금
사용자가 외부 주소로 토큰을 전송 요청하면 블록체인에 트랜잭션을 제출하는 흐름
📱 App
출금 요청주소, 금액, 토큰
결과 확인(상태 폴링)
🏢 Wallet BE
출금 검증잔액·한도·KYC
Kafka 발행adapter.withdraw.request
결과 수신adapter.withdraw.result
Kafka 발행adapter.common.confirm
컨펌 결과 수신TXCF이면 확정
⚙️ BC Adapter
Zod 검증
분산 락 획득withdraw:nonce:{chainId}:{addr}
Pending Nonce 대기
Native/ERC-20 분기
Bundler UserOp 제출
Kafka 응답status: TXPD
Confirm 처리receipt + blockNumber → TXCF
⛓️ Blockchain
UserOp 실행ERC-4337
토큰 전송 처리
블록 컨펌
📌 출금 응답은 TXPD(제출됨)로 즉시 반환됩니다. 최종 확정은 Wallet BE가 별도로 adapter.common.confirm을 요청하여 TXCF 여부를 확인합니다.
요청 토픽
adapter.withdraw.request
응답 토픽
adapter.withdraw.result
Nonce 재시도
1회 (100ms 대기)
최종 확정
Confirm 별도 요청 → TXCF
💳
4. 결제
가맹점 결제 시 HotWallet에서 Settlement 주소로 토큰을 전송하는 흐름
📱 App
결제 요청금액, 토큰
🏢 Wallet BE
결제 검증
Kafka 발행adapter.payment.request
결과 수신adapter.payment.result
⚙️ BC Adapter
토큰 정보 해석DB → ENV → Default
분산 락 획득withdraw:nonce: (출금과 공유)
HotWallet Nonce 조회
ERC-20 Transfer
Receipt 즉시 조회
Kafka 응답status: TXCF
⛓️ Blockchain
HotWallet →
Settlement 전송
💡 결제는 출금과 다르게 Receipt 즉시 조회를 시도합니다. Receipt를 바로 받으면 TXCF, 못 받으면 TXPD로 반환합니다.
요청 토픽
adapter.payment.request
응답 토픽
adapter.payment.result
전송 주체
HotWallet → Settlement
락 키 공유
출금과 동일 (Nonce 충돌 방지)
🏦
5. 정산 (EOFS Split)
Settlement 주소에서 가맹점+수수료 주소로 토큰을 동시 분배하는 흐름
🏢 Wallet BE / 정산 시스템
정산 요청가맹점, 금액, 수수료
Kafka 발행adapter.settlement.request
결과 수신
⚙️ BC Adapter
토큰 정보 해석
분산 락 획득settlement:nonce:{chainId}
payerNonce ethCall 조회
EOFS Split 실행가맹점 + 수수료 동시 분배
On-chain 상세 조회
Kafka 응답adapter.settlement.result
⛓️ Blockchain
EOFS 프록시 실행
가맹점 주소 입금
수수료 주소 입금
요청 토픽
adapter.settlement.request
분배 방식
EOFS Split (1 tx = 2 전송)
Nonce
payerNonce (ethCall) — 별도 관리
재시도
EFS024 시 1회 (100ms 대기)
💎
6. 잔액 조회
멀티 체인 × 멀티 토큰 잔액을 병렬로 조회하여 반환하는 흐름
📱 App
잔액 조회 요청
🏢 Wallet BE
Kafka 발행adapter.balance.inquiry
잔액 결과 수신
⚙️ BC Adapter
활성 체인/토큰 조회
병렬 RPC 호출Native: eth_getBalance
ERC-20: ethCall(balanceOf)
결과 집계 & 응답adapter.balance.result
⛓️ Blockchain
각 체인 RPC 응답
요청 토픽
adapter.balance.inquiry
응답 토픽
adapter.balance.result
조회 방식
모든 체인×토큰 병렬 조회
동시성 제어
없음 (읽기 전용)
📊
7. 대사 (Reconciliation)
특정 시점 기준 온체인 잔액을 집계하여 대사 검증용 데이터를 생성하는 흐름
🏢 정산/대사 시스템
대사 요청기준 시점 (targetDt)
Kafka 발행adapter.reconciliation.inquiry
대사 결과 수신심볼별 잔액 집계
⚙️ BC Adapter (별도 Worker)
Binary SearchKST → Unix → 블록 번호
계정 목록 조회deployed / undeployed
배치 잔액 조회100개씩, blockTag 기준
심볼별 집계 & 응답
⛓️ Blockchain
point-in-time
잔액 반환특정 블록 기준
🔧 대사 Worker는 메인 Adapter와 독립 프로세스로 실행됩니다. Consumer Group: bc-reconcile
Consumer Group
bc-reconcile (독립)
시점 탐색
Binary Search (블록 타임스탬프)
배치 크기
100개 주소씩
집계 단위
심볼별 (userOn / platformOn / reserveOn)
🧹
8. 집금 (Collection)
사용자 지갑 → HotWallet로 토큰을 자동 수거하는 내부 흐름 (입금·배포 후 비동기 트리거)
⚙️ BC Adapter (내부 트리거)
입금/배포 완료 후
비동기 호출
분산 락 획득collection:{chainId}:{addr}
잔액 확인0이면 스킵
approve + transferFromUserOp 2개 순차
실패 시 swallow에러 로그만 기록
⛓️ Blockchain
approve 실행
transferFrom 실행User → HotWallet
🔇 집금은 Kafka 요청/응답 없이 내부적으로 실행됩니다. 실패해도 에러를 throw하지 않고 swallow하여 상위 흐름(입금/배포)에 영향을 주지 않습니다.
트리거
입금 감지 / 계정 배포 완료
Kafka 토픽
없음 (내부 처리)
UserOp 수
2개 (approve → transferFrom)
에러 정책
swallow (실패 무시)
9. 트랜잭션 컨펌
제출된 트랜잭션(TXPD)의 최종 확정 여부를 확인하는 흐름
🏢 Wallet BE
TXPD 상태 TX 확인 요청
Kafka 발행adapter.common.confirm
결과 수신TXPD / TXCF / TXFA
⚙️ BC Adapter
Receipt 조회getTransactionReceipt
Receipt 없으면→ TXPD 유지
Receipt 있으면Block Number 확인
latest - txBlock ≥ threshold→ TXCF 확정
status=0 (revert)→ TXFA 실패
Kafka 응답adapter.common.confirmed
⛓️ Blockchain
Receipt + Block 반환
💡 컨펌은 idempotent합니다. Wallet BE가 동일 txHash로 반복 요청하여 블록 확정을 확인할 수 있습니다.
요청 토픽
adapter.common.confirm
응답 토픽
adapter.common.confirmed
상태 변환
TXPD → TXCF (확정) / TXFA (실패)
컨펌 기준
latestBlock - txBlock ≥ 1
⚙️
10. 설정 등록 (Config Register)
신규 체인/토큰 컨트랙트 정보를 BC Adapter에 등록하는 관리자 흐름
🏢 Wallet BE / Admin
신규 토큰 등록 요청chainId, contractAddress
Kafka 발행adapter.config.register
(응답 없음)
⚙️ BC Adapter
RPC 호출symbol(), decimals()
TokenConfig DB 저장SQLite config.db
인메모리 캐시 갱신
⛓️ Blockchain
컨트랙트 메타 반환symbol, decimals
📌 설정 등록은 응답 토픽이 없는 Fire-and-Forget 패턴입니다. 등록 결과는 BC Adapter 로그에서만 확인할 수 있습니다.
요청 토픽
adapter.config.register
응답 토픽
없음 (NoReply)
저장소
SQLite config.db + 인메모리
RPC 호출
symbol() + decimals()

💰 입금 (Deposit)

WebSocket 기반 입금 감지 및 Kafka 이벤트 발행

💰

입금 처리 플로우

WebSocket 입금 이벤트 수신 Zod 스키마 검증 컨텍스트 바인딩 (txHash) Outbox 저장 (보장) Kafka 발행 시도 집금 서비스 트리거 (비동기)

입금 메시지 스키마

필드타입설명
chainIdstring체인 ID
txHash0x + 64자 hex트랜잭션 해시
fromAddressEVM 주소송신자 주소
toAddressEVM 주소수신자 주소
amountnumeric string입금 금액
symbolstring(1-20)토큰 심볼
transactionDatetimeyyyyMMddHHmmss트랜잭션 일시 (KST)

핵심 동작 방식

  • 메시지 보장Outbox 테이블에 먼저 저장 → Kafka 발행 시도 → 성공 시 Outbox 삭제
  • 발행 실패 시Outbox에 남아 5초 간격 스케줄러가 재시도 (무한 재시도, at-least-once)
  • 후속 처리입금 감지 후 collectDeposit() 비동기 호출 → HotWallet로 자동 집금
  • 발행 토픽adapter.deposit.detected

🔍 입금 감지 (Deposit Detection)

WebSocket 서버를 통한 실시간 입금 이벤트 수신

🔍

WebSocket 서버 설정

  • Host0.0.0.0 (기본값, 환경변수 DEPOSIT_WS_HOST)
  • Port8080 (기본값, 환경변수 DEPOSIT_WS_PORT)
  • Path/ws (기본값, 환경변수 DEPOSIT_WS_PATH)
  • Ping 간격30초마다 ping 프레임 전송
  • 비응답 처리30초 내 pong 미수신 시 연결 강제 종료

이벤트 처리 파이프라인

WebSocket Raw Data parseJsonToCamel() Zod 유효성 검사 runWithContext(txHash) publishDetected()
⚠️
Keep-Alive 메커니즘: 30초마다 ping 프레임을 보내고, 응답이 없으면 해당 클라이언트 연결을 강제 종료하여 좀비 커넥션을 방지합니다.

📤 출금 (Withdraw)

사용자 계정에서 외부 주소로 Native/ERC-20 토큰 전송

📤

출금 처리 플로우

adapter.withdraw.request 수신 분산 락 획득 (nonce:{chainId}:{sender}) Pending Nonce 조회 Native / ERC-20 분기 Bundler UserOp 제출 On-chain 상세 조회 (선택) adapter.withdraw.result 발행

동시성 제어 상세

  • 락 키withdraw:nonce:{chainId}:{senderAddress}
  • 락 TTL30초 (자동 해제)
  • 락 재시도최대 10회, 100ms 간격
  • Nonce 충돌 재시도1회 (100ms 대기 후 새 Nonce 조회하여 재실행)
  • Pending Nonce 대기100ms 폴링, 최대 3초 (이전 tx 컨펌 대기)

실행 분기 로직

조건실행 경로번들러 메서드
Native 토큰 (ETH 등)Native TransfersendNativeTransfer
ERC-20 토큰 (USDC 등)ERC-20 TransfersendErc20Transfer

에러 처리

🔴
Nonce 충돌 (EFS024): 번들러가 "nonce" 또는 "replacement_underpriced" 에러를 반환하면, 100ms 대기 후 새 Nonce로 1회 재시도합니다. 재시도도 실패하면 에러를 상위로 전파합니다.
⚠️
Pending Nonce 미진행: 3초 내 이전 트랜잭션 Nonce가 증가하지 않으면 경고 로그 후 현재 Nonce로 진행합니다.

💳 결제 (Payment)

HotWallet에서 Settlement 주소로 ERC-20 토큰 전송

💳

결제 처리 플로우

adapter.payment.request 수신 토큰 정보 조회 (DB → ENV 폴백) 분산 락 획득 (nonce:{chainId}:{hotWallet}) ERC-20 Transfer UserOp On-chain 상세 조회 adapter.payment.result 발행

핵심 처리 규칙

  • 전송 주체HotWallet 주소 → Settlement 주소
  • 토큰 유형ERC-20만 지원 (Native 미지원)
  • 토큰 해석DB 조회 → 환경변수 폴백 → DEFAULT_DECIMALS 폴백
  • 결제 체인환경변수 PAYMENT_CHAIN_NAME (기본: KCP)
  • 락 키 공유출금과 동일한 withdraw:nonce: 프리픽스 사용 (의도적)
  • Nonce 재시도충돌 시 1회 재시도 (출금과 동일 로직)
💡
락 키 공유 이유: Payment와 Withdraw가 동일 HotWallet에서 전송하므로, 같은 락 프리픽스를 사용하여 Nonce 충돌을 원천적으로 방지합니다.

🏦 정산 (Settlement)

EOFS Split을 통한 가맹점/수수료 분배

🏦

정산 처리 플로우

adapter.settlement.request 수신 토큰 정보 조회 분산 락 획득 (nonce:{chainId}:{settlement}) payerNonce ethCall 조회 executeEoaFundedSplit (EOFS) On-chain 상세 조회 adapter.settlement.result 발행

EOFS Split 상세

  • Split 방식하나의 트랜잭션에서 가맹점 주소 + 수수료 주소로 동시 분배
  • Nonce 조회블록체인 직접 ethCall (payerNonce 함수) - 번들러 Nonce와 별도
  • 락 키settlement:nonce:{chainId}:{settlementAddress}
  • 에러 재시도EFS024 에러 시 100ms 대기 → Nonce 재조회 → 1회 재시도
⚠️
EFS024 에러: Bundler가 반환하는 Nonce 관련 에러 코드. 100ms 대기 후 payerNonce를 다시 ethCall로 조회하여 재시도합니다.

📥 정산 집금 (Settlement Collect)

외부 주소에서 HotWallet으로 자금 회수

📥

정산 집금 처리 플로우

adapter.settlement.collect.request 수신 토큰 정보 조회 분산 락 획득 payerNonce ethCall 조회 executeEoaFundedSplit → HotWallet On-chain 상세 조회 adapter.settlement.collect.result 발행
  • 수집 방향fromAddress → HotWallet 주소로 자금 회수
  • 번들러 메서드executeEoaFundedSplit (Settlement과 동일)
  • 응답 토픽adapter.settlement.collect.result

🗂️ 집금 (Collection)

사용자 계정의 입금된 토큰을 HotWallet로 수집

🗂️

집금 처리 플로우

입금 감지 / 계정 배포 완료 분산 락 획득 (collection:{chainId}:{address}) Native 잔액 확인 (3회 재시도) 인메모리 Nonce 조회 ERC-20 Transfer → HotWallet Nonce 캐시 업데이트

두 가지 실행 모드

모드트리거범위
collectDeposit입금 감지 후 자동 호출특정 1개 토큰만 집금
collectAfterDeploy계정 배포 완료 후 자동 호출해당 계정의 모든 토큰 집금

재시도 전략

  • Native 잔액 재시도잔액이 0이면 1초 대기 후 재시도, 최대 3회
  • Nonce 충돌 재시도100ms 대기 → Nonce 캐시 무효화 → 1회 재시도
  • Nonce 관리인메모리 캐시 (성공: advance, 실패: invalidate)
  • 에러 처리실패 시 에러를 삼키고(swallow) 로그만 기록 (비동기 호출이므로)
⚠️
에러 전파 없음: 집금은 입금/배포의 후속 처리로 비동기 호출되므로, 실패해도 원래 요청에 영향을 주지 않습니다. 재시도 메커니즘이 없으므로, 실패한 집금은 다음 입금 감지 시 재시도됩니다.

컨펌 (Confirm)

트랜잭션 컨펌 상태 확인 및 확정

컨펌 처리 플로우

adapter.common.confirm 수신 getTransactionReceipt 조회 getBlockNumber 조회 confirmCount 계산 adapter.common.confirmed 발행

컨펌 상태 판정 로직

조건상태confirmCount설명
Receipt 없음PENDING (TXPD)0아직 마이닝되지 않음
Receipt 있음 + status nullPENDING (TXPD)0상태 미확정
Receipt status = FAILEDBusinessError 발생-트랜잭션 실패
Receipt status = SUCCESS, confirmCount > 0CONFIRMED (TXCF)latest - receipt.blockNumber + 1확정 완료
💡
컨펌 수 계산: confirmCount = latestBlockNumber - receipt.blockNumber + 1
체인별 최소 컨펌 수는 ConfigRegister에서 설정됩니다.

💎 잔액 조회 (Balance)

멀티 체인 Native + ERC-20 토큰 잔액 조회

💎

잔액 조회 플로우

adapter.balance.inquiry 수신 활성 체인/토큰 순회 Native: eth_getBalance ERC-20: ethCall(balanceOf) adapter.balance.result 발행
  • 병렬 처리모든 체인 × 토큰 조합을 병렬로 조회
  • Native 예외KRW 심볼은 Native 잔액 조회 제외
  • ERC-20 조회BALANCE_OF_SELECTOR + address 인코딩으로 ethCall
  • 응답 포맷배열: [{chainId, symbol, address, balance}]

📊 대사 (Reconciliation)

특정 시점의 온체인 잔액을 집계하여 대사 데이터 생성

📊

대사 처리 플로우

adapter.reconciliation.inquiry 수신 Binary Search → 기준 블록 탐색 계정 목록 조회 (deployed/undeployed) 배치 잔액 조회 (100개씩) 심볼별 집계 adapter.reconciliation.result 발행

핵심 로직

  • 별도 프로세스메인 Adapter와 분리된 독립 Worker로 실행 (reconcile.ts)
  • Consumer Groupbc-reconcile (메인과 분리)
  • 시점 검색KST 날짜시간 → Unix 타임스탬프 → Binary Search로 블록 번호 특정
  • 배치 크기100개 주소씩 잔액 조회
  • blockTag특정 블록 번호를 기준으로 point-in-time 잔액 조회

집계 그룹

그룹대상설명
userRawHotWalletHotWallet 잔액
platformRawSettlement Fee 주소플랫폼 수수료 주소 잔액
notDeployRaw미배포 계정아직 배포 안 된 계정들의 잔액 합산

🆕 계정 생성 (Account Create)

CREATE2를 이용한 스마트 계정 주소 예측

adapter.account.create 수신 requestId를 salt로 사용 Promise.allSettled 병렬 CREATE2 계산 SQLite 저장 (중복 감지) adapter.account.created 발행
  • 주소 생성CREATE2 (factory + salt + bytecodeHash) → 결정적 주소 계산
  • 병렬 처리모든 활성 체인에 대해 Promise.allSettled로 동시 계산
  • 체인 선택FUJI 체인이 있으면 우선 선택, 없으면 첫 번째 체인
  • 검증계산된 주소가 EVM 패턴 (0x + 40자 hex) 일치 확인

🚀 계정 배포 (Account Deploy)

CREATE2 프록시 기반 스마트 계정 온체인 배포

adapter.account.deploy 수신 배포 락 획득 Owner Nonce 조회 deployAccount (proxyBytecodeHex) Registry 폴링 (2초 × 10회) 계정 상태 업데이트 자동 집금 트리거 (비동기) adapter.account.deployed 발행
  • 배포 방식CREATE2 프록시 + EOFS (EOA Funded Split) 멀티스텝
  • Registry 확인2초 간격 폴링, 최대 10회 (총 20초)
  • 후속 처리배포 완료 후 collectAfterDeploy 비동기 호출 → 모든 토큰 집금
  • 중복 방지Redis 분산 락으로 동시 배포 요청 차단

🗑️ 계정 삭제 (Account Delete)

SQLite에서 계정 레코드 삭제

adapter.account.delete 수신 SQLite 계정 삭제
  • 응답없음 (NoReply 패턴)
  • 처리DB 삭제만 수행, 온체인 작업 없음

⚙️ 설정 등록 (Config Register)

체인/토큰 설정을 RPC로 조회하여 DB 등록

adapter.config.create 수신 ERC-20 symbol/decimals 조회 블록 타임 계산 (100블록 샘플) Finalized 블록 조회 체인 정보 저장 토큰 정보 저장

블록 타임 계산 방법

  • 샘플 크기100블록 (fallback: 1블록)
  • 계산(latestTimestamp - pastTimestamp) / 100 → 초 단위 floor
  • FinalizedFinalized 블록 지원 시 confirmation 자동 계산, 미지원 시 1
  • 응답없음 (NoReply 패턴)

📨 Kafka 토픽 목록

전체 Kafka 토픽 매핑 및 핸들러 연결

요청 토픽 (Inbound)

토픽핸들러응답 패턴
adapter.account.createAccountServiceRequest-Reply
adapter.account.deployAccountDeployServiceRequest-Reply
adapter.account.deleteAccountDeleteServiceNoReply
adapter.withdraw.requestWithdrawServiceRequest-Reply
adapter.payment.requestPaymentServiceRequest-Reply
adapter.common.confirmConfirmServiceRequest-Reply
adapter.balance.inquiryBalanceServiceRequest-Reply
adapter.settlement.requestSettlementServiceRequest-Reply
adapter.settlement.collect.requestSettlementCollectServiceRequest-Reply
adapter.config.createConfigRegisterServiceNoReply
adapter.reconciliation.inquiryReconciliationServiceRequest-Reply (별도 프로세스)

응답/이벤트 토픽 (Outbound)

토픽발행 주체
adapter.account.created계정 생성 완료
adapter.account.deployed계정 배포 완료
adapter.withdraw.result출금 결과
adapter.payment.result결제 결과
adapter.common.confirmed컨펌 결과
adapter.balance.result잔액 조회 결과
adapter.settlement.result정산 결과
adapter.settlement.collect.result정산 집금 결과
adapter.reconciliation.result대사 결과
adapter.deposit.detected입금 감지 이벤트
adapter.dlq파싱/유효성 에러 DLQ

⚠️ 에러 처리

에러 계층 구조 및 처리 전략

에러 계층 구조

ApplicationError (base: code + statusCode + retryable + cause) ├── ValidationError (400, retryable=false, field?) ├── NotFoundError (404, retryable=false, identifier?) ├── BusinessError (422, retryable=false) └── InfraError (502, retryable=true) └── RPC / Kafka / DB / Bundler 실패

에러 코드 분류 (총 49개)

카테고리설명예시 코드
Validation입력값 유효성 실패VALIDATION_ERROR
RPC블록체인 RPC 호출 실패RPC_CALL_FAILED
Bundler번들러 API 호출 실패BUNDLER_SEND_FAILED
Kafka메시지 발행 실패KAFKA_PUBLISH_FAILED
DB데이터베이스 작업 실패DB_QUERY_FAILED
Lock분산 락 획득/해제 실패LOCK_ACQUIRE_FAILED
KMS키 관리 서비스 실패KMS_DECRYPT_FAILED
Business비즈니스 규칙 위반TX_FAILED

Kafka 에러 응답 구조

{ "request_id": "uuid-...", "message": "에러 메시지", "address": null, // 핸들러별 기본값 "status": "FAILED" // 핸들러별 기본값 }

🔒 분산 락 전략

Redis 기반 분산 락과 프로세스 큐 조합

2단계 직렬화 메커니즘

① 프로세스 내 큐 대기 (키 기반) ② Redis 분산 락 획득 (SET NX EX) ③ 비즈니스 로직 실행 ④ 락 해제 (Lua 스크립트) ⑤ 큐 다음 대기자 통보

서비스별 락 키 매핑

서비스락 키 패턴TTL재시도
Withdrawwithdraw:nonce:{chainId}:{senderAddress}30초10회 × 100ms
Paymentwithdraw:nonce:{chainId}:{hotWalletAddress}30초10회 × 100ms
Settlementsettlement:nonce:{chainId}:{settlementAddr}30초10회 × 100ms
Collectioncollection:{chainId}:{address}30초10회 × 100ms
AccountDeploydeploy:{chainId}:{address}30초10회 × 100ms
💡
안전한 락 해제: Redis Lua 스크립트로 "자신이 획득한 락만 해제"를 보장합니다. TTL 만료 후 다른 프로세스가 획득한 락을 실수로 해제하는 것을 방지합니다.

🔢 Nonce 관리

블록체인 트랜잭션 Nonce 충돌 방지 전략

Nonce 재시도 로직 (executeWithNonceRetry)

① 현재 Nonce 조회 ② UserOp 제출 ③ Nonce 충돌? ④ 100ms 대기 ⑤ 새 Nonce 조회 ⑥ 1회 재시도

Pending Nonce 진행 대기 (waitForPendingNonceAdvance)

  • 폴링 간격100ms
  • 최대 대기3,000ms (3초)
  • 성공 조건currentNonce > previousNonce
  • 타임아웃 시경고 로그 후 현재 Nonce로 진행 (실패하지 않음)

Nonce 충돌 감지 기준

// InfraError + BUNDLER_SEND_FAILED 코드 // + cause 메시지에 아래 키워드 포함 시 "nonce" | "replacement_underpriced"

인메모리 Nonce 캐시 (Collection 전용)

  • 키 형식{chainId}:{address}
  • 성공 시advance(key) → 캐시 +1 증가
  • 실패 시invalidate(key) → 캐시 삭제 (다음 호출 시 재조회)
  • 동시 조회 방지fetchInProgress Map으로 중복 RPC 조회 차단

📦 Outbox 패턴

메시지 발행 보장을 위한 Outbox 패턴 구현

① 비즈니스 로직 완료 ② Outbox 테이블 저장 ③ 즉시 Kafka 발행 시도 ④ 성공: Outbox 삭제 / 실패: 유지

Outbox 스케줄러

  • 폴링 간격5초 (5,000ms)
  • 배치 크기500건
  • 재시도 횟수무한 (성공할 때까지)
  • 실패 처리개별 건 실패 시 경고 로그 → 다음 폴링에서 재시도
  • 보장 수준At-Least-Once (최소 1회 발행 보장)
메시지 유실 방지: DB에 먼저 저장 후 Kafka 발행을 시도하므로, 서비스가 갑자기 종료되어도 재시작 후 미발행 메시지를 자동으로 재발행합니다.

❤️ Health Check

서비스 상태 모니터링 엔드포인트

  • 엔드포인트GET /health
  • Host0.0.0.0 (기본, 환경변수 HEALTH_HOST)
  • Port8081 (기본, 환경변수 HEALTH_PORT)
  • 체크 타임아웃3,000ms (환경변수 HEALTH_CHECK_TIMEOUT_MS)

점검 항목

컴포넌트점검 방법타임아웃
Kafka ProducerConnected 상태 확인-
RedisPING 명령3,000ms
Account DBSELECT 1동기
Config DBSELECT 1동기
Outbox DBSELECT 1동기
RPC (체인별)getBlockNumber()3,000ms

응답 코드

  • 200 OK모든 컴포넌트 정상
  • 503 Unavailable1개 이상 컴포넌트 이상
🆕 계정 생성 시뮬레이션
Kafka 메시지 수신부터 응답 발행까지의 데이터 흐름을 단계별로 시뮬레이션합니다
네트워크
토큰 유형
속도: