# Cryptoments V2 — Node.js 서버 API 연동 가이드

> **대상**: Spring Boot Admin Console / Partner Console 개발자
> **버전**: v1.2 (2026-03-16)
> **Node.js 서버 4개**: blockchain-api, relayer-api, wallet-activator, telegram-bot

---

## 목차

1. [서버 개요 및 포트](#1-서버-개요-및-포트)
2. [blockchain-api (Port 3001)](#2-blockchain-api-port-3001)
3. [relayer-api (Port 3002)](#3-relayer-api-port-3002)
4. [wallet-activator (Worker)](#4-wallet-activator-worker)
5. [telegram-bot (Worker)](#5-telegram-bot-worker)
6. [시퀀스 다이어그램](#6-시퀀스-다이어그램)
7. [DB 테이블 연동 정보](#7-db-테이블-연동-정보)
8. [에러 처리 공통 규격](#8-에러-처리-공통-규격)
9. [Spring Boot 연동 구현 — Axim REST Framework](#9-spring-boot-연동-구현--axim-rest-framework)

---

## 1. 서버 개요 및 포트

| 서버 | 포트 | 유형 | 역할 |
|------|------|------|------|
| **blockchain-api** | 3001 | REST API | 지갑 파생, 잔액 조회, TX 조회, 가스 추정, 컨트랙트 등록/관리 |
| **relayer-api** | 3002 | REST API + Poller | Relayer 관리 API + 집금(Collection)/출금(Withdrawal) 폴러 |
| **wallet-activator** | — | Worker (API 없음) | 지갑 활성화: 가스 전송 → ERC-20 approve 실행 |
| **telegram-bot** | — | Worker (API 없음) | 텔레그램 알림 봇 (Long Polling) |

> **호출 방향**: Spring Boot → `blockchain-api`, `relayer-api` (HTTP)
> **DB 직접 접근**: Spring Boot → MySQL (wallet-activator, telegram-bot은 DB 트리거/상태 변경으로 간접 연동)

---

## 2. blockchain-api (Port 3001)

### 2.1 지갑 — `/api/wallet`

#### `POST /api/wallet/derive` — HD 지갑 주소 파생

```
POST http://blockchain-api:3001/api/wallet/derive
Content-Type: application/json
```

**Request Body**

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `networkId` | number | ✅ | blockchain_networks.id |
| `hdWalletId` | number | ✅ | hd_wallets.id |
| `derivationIndex` | number | — | 생략 시 자동 증가 (wallet_index_manager 기반) |

**Response** `200 OK`

```json
{
  "address": "0x1234...abcd",
  "publicKey": "0x04...",
  "walletAddressId": 42,
  "derivationPath": "m/44'/60'/0'/0/5"
}
```

**DB 변경**: `wallet_addresses` INSERT, `wallet_keys` INSERT, `wallet_index_manager` UPDATE

---

#### `GET /api/wallet/approve-status/:address` — approve 상태 조회

```
GET http://blockchain-api:3001/api/wallet/approve-status/0x1234...?networkId=1&tokenContract=0xdAC17...&spender=0xABCD...
```

**Query Parameters**

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `networkId` | number | ✅ | 네트워크 ID |
| `tokenContract` | string | ✅ | ERC-20/TRC-20 토큰 컨트랙트 주소 |
| `spender` | string | ✅ | 승인 대상 주소 (= CryptoRelayer 컨트랙트 주소) |

**Response** `200 OK`

```json
{
  "allowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
  "isApproved": true
}
```

---

### 2.2 잔액 — `/api/balance`

#### `GET /api/balance/token/:address` — 토큰 잔액 조회 (캐시 30초)

```
GET http://blockchain-api:3001/api/balance/token/0x1234...?networkId=1&tokenContract=0xdAC17...
```

| Query | 타입 | 설명 |
|-------|------|------|
| `networkId` | number | 네트워크 ID |
| `tokenContract` | string | 토큰 컨트랙트 주소 |

**Response**

```json
{ "balance": "1000000000", "decimals": 6 }
```

---

#### `GET /api/balance/native/:address` — 네이티브 잔액 조회

```
GET http://blockchain-api:3001/api/balance/native/0x1234...?networkId=1
```

**Response**

```json
{ "balance": "50000000000000000" }
```

---

#### `POST /api/balance/sync` — 온체인 잔액 일괄 동기화

```
POST http://blockchain-api:3001/api/balance/sync
```

**Request Body**

```json
{ "walletAddressIds": [1, 2, 3, 5] }
```

**Response**

```json
{
  "results": [
    { "walletAddressId": 1, "currencyId": 1, "onchainBalance": "5000000", "diff": "100000" },
    { "walletAddressId": 2, "currencyId": 1, "onchainBalance": "0", "diff": "0" }
  ]
}
```

**DB 변경**: `wallet_balances.onchain_balance` UPDATE, `last_synced_at` UPDATE

---

### 2.3 트랜잭션 — `/api/tx`

#### `GET /api/tx/status/:txHash` — TX 상태 조회

```
GET http://blockchain-api:3001/api/tx/status/0xabc...?networkId=1
```

**Response**

```json
{
  "txHash": "0xabc...",
  "status": "confirmed",
  "blockNumber": 18000000,
  "confirmations": 15,
  "gasUsed": "65000"
}
```

> `status` 값: `"pending"` | `"confirmed"` | `"failed"`

---

#### `GET /api/tx/receipt/:txHash` — TX 영수증 조회

```
GET http://blockchain-api:3001/api/tx/receipt/0xabc...?networkId=1
```

**Response**

```json
{
  "txHash": "0xabc...",
  "status": true,
  "blockNumber": 18000000,
  "gasUsed": "65000",
  "effectiveGasPrice": "20000000000",
  "logs": [
    {
      "address": "0xdAC17...",
      "topics": ["0xddf2..."],
      "data": "0x...",
      "logIndex": 0
    }
  ]
}
```

---

### 2.4 가스 — `/api/gas`

#### `GET /api/gas/price` — 가스 가격 조회 (캐시 15초)

```
GET http://blockchain-api:3001/api/gas/price?networkId=1
```

**Response** (EIP-1559 체인)

```json
{
  "gasPrice": "20000000000",
  "maxFeePerGas": "25000000000",
  "maxPriorityFeePerGas": "1500000000"
}
```

---

#### `POST /api/gas/estimate` — 가스 추정

```
POST http://blockchain-api:3001/api/gas/estimate
```

**Request Body**

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `networkId` | number | ✅ | |
| `from` | string | ✅ | 발신 주소 |
| `to` | string | ✅ | 수신 주소 |
| `data` | string | — | TX input data (hex) |
| `value` | string | — | wei 단위 전송량 |

**Response**

```json
{ "gasLimit": "65000", "estimatedFee": "1300000000000000" }
```

---

### 2.5 블록 — `/api/block`

#### `GET /api/block/latest` — 최신 블록 번호 (캐시 5초)

```
GET http://blockchain-api:3001/api/block/latest?networkId=1
```

**Response**

```json
{ "blockNumber": 18000000, "timestamp": 1709827200 }
```

---

### 2.6 시스템 관리 — `/api/admin` ⚠️ 내부 API

> **주의**: 이 API는 Admin Console에서만 호출해야 합니다. 인증/인가 미들웨어 추가 필요.

#### `POST /api/admin/wallet/create-admin` — ADMIN 지갑 생성

```
POST http://blockchain-api:3001/api/admin/wallet/create-admin
```

| 필드 | 타입 | 설명 |
|------|------|------|
| `networkId` | number | 네트워크 ID (네트워크당 1개만 생성 가능) |
| `hdWalletId` | number | HD 지갑 마스터 ID |

**Response**

```json
{
  "addressId": 1,
  "address": "0xADMIN...",
  "networkId": 1,
  "derivationPath": "m/44'/60'/0'/0/0"
}
```

**이미 존재 시**: `200 OK` — 기존 ADMIN 지갑 정보 반환 (생성 스킵)
**DB 변경**: `wallet_addresses` INSERT (wallet_type=ADMIN), `wallet_keys` INSERT

---

#### `POST /api/admin/contract/register` — CryptoRelayer 컨트랙트 등록 (수동 배포 후) 🔑

> **⚠️ v1.2 변경**: `deploy` → `register`. Node.js가 직접 배포하지 않음.
> 운영자가 Hardhat(EVM)/Tronbox(TRON)으로 수동 배포 → `transferOwnership(ADMIN지갑)` → 이 API로 등록.

```
POST http://blockchain-api:3001/api/admin/contract/register
```

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `networkId` | number | ✅ | 네트워크 ID |
| `contractAddress` | string | ✅ | 배포된 컨트랙트 주소 |
| `deployTxHash` | string | ✅ | 배포 트랜잭션 해시 |
| `ownerAddressId` | number | ✅ | ADMIN 지갑 wallet_addresses.id |
| `abiVersion` | string | — | ABI 버전 |

**Response**

```json
{
  "contractId": 1,
  "contractAddress": "0xCONTRACT...",
  "deployTxHash": "0xDEPLOY...",
  "networkId": 1,
  "ownerAddressId": 5,
  "status": "ACTIVE"
}
```

**전제조건**: 해당 네트워크에 ADMIN 지갑이 존재해야 하며, 온체인 `contract.owner()` == ADMIN 지갑 주소 검증
**DB 변경**: 기존 ACTIVE → DEPRECATED, 새 `relayer_contracts` INSERT (status=ACTIVE)

---

#### `POST /api/admin/contract/add-relayer` — Relayer EOA 컨트랙트 등록

```
POST http://blockchain-api:3001/api/admin/contract/add-relayer
```

| 필드 | 타입 | 설명 |
|------|------|------|
| `networkId` | number | 네트워크 ID |
| `relayerWalletId` | number | relayer_wallets.id |

**Response**

```json
{
  "txHash": "0xADD...",
  "relayerAddress": "0xRELAYER...",
  "contractAddress": "0xCONTRACT..."
}
```

**온체인 동작**: ADMIN 지갑이 `CryptoRelayer.addRelayer(relayerAddress)` 호출
**DB 변경**: `relayer_wallets.registration_status` → REGISTERED

---

#### `POST /api/admin/contract/remove-relayer` — Relayer EOA 컨트랙트 해제

```
POST http://blockchain-api:3001/api/admin/contract/remove-relayer
```

| 필드 | 타입 | 설명 |
|------|------|------|
| `networkId` | number | 네트워크 ID |
| `relayerWalletId` | number | relayer_wallets.id |

**Response**

```json
{ "txHash": "0xREMOVE..." }
```

**DB 변경**: `relayer_wallets.registration_status` → REVOKED

---

#### `POST /api/admin/contract/pause` — 긴급 정지

| 필드 | 타입 | 설명 |
|------|------|------|
| `networkId` | number | 네트워크 ID |
| `reason` | string | 정지 사유 (필수) |

**Response**: `{ "txHash": "0x...", "status": "PAUSED" }`
**DB 변경**: `relayer_contracts.status` → PAUSED, `paused_at`, `paused_reason` 설정

---

#### `POST /api/admin/contract/unpause` — 긴급 정지 해제

| 필드 | 타입 | 설명 |
|------|------|------|
| `networkId` | number | 네트워크 ID |

**Response**: `{ "txHash": "0x...", "status": "ACTIVE" }`
**DB 변경**: `relayer_contracts.status` → ACTIVE, `paused_at` NULL

---

#### `GET /api/admin/contract/status/:networkId` — 컨트랙트 상태 조회 (온체인+DB)

```
GET http://blockchain-api:3001/api/admin/contract/status/1
```

**Response**

```json
{
  "contractAddress": "0xCONTRACT...",
  "owner": "0xADMIN...",
  "paused": false,
  "relayerCount": 3,
  "relayers": ["0xR1...", "0xR2...", "0xR3..."],
  "abiVersion": "1.0",
  "dbStatus": "ACTIVE"
}
```

---

#### `GET /api/admin/contract/list` — 전체 ACTIVE 컨트랙트 목록

```
GET http://blockchain-api:3001/api/admin/contract/list
```

**Response**

```json
{
  "contracts": [
    {
      "id": 1, "network_id": 1, "contract_address": "0x...",
      "owner_address_id": 5, "deploy_tx_hash": "0x...",
      "abi_version": "1.0", "status": "ACTIVE",
      "paused_at": null, "paused_reason": null,
      "created_at": "2026-03-01T...", "updated_at": "2026-03-01T..."
    }
  ]
}
```

---

## 3. relayer-api (Port 3002)

### 3.1 Relayer 관리 — `/api/relayer`

#### `POST /api/relayer/register` — Relayer 등록 (DB + 온체인)

```
POST http://relayer-api:3002/api/relayer/register
```

**Request Body**

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `networkId` | number | ✅ | 네트워크 ID |
| `walletAddressId` | number | ✅ | wallet_addresses.id (Relayer EOA) |
| `relayerRole` | string | ✅ | `"COLLECTION"` 또는 `"WITHDRAWAL"` |

**Response** `200 OK`

```json
{
  "relayerId": 7,
  "address": "0xRELAYER...",
  "status": "ACTIVE",
  "registrationStatus": "REGISTERED",
  "txHash": "0xADD_RELAYER...",
  "contractAddress": "0xCONTRACT..."
}
```

**온체인 동작**: Contract Owner가 `CryptoRelayer.addRelayer(relayerAddress)` 호출 (EVM/TVM 분기)
**DB 변경**: `relayer_wallets` INSERT → status=ACTIVE, registration_status=REGISTERED

---

#### `POST /api/relayer/unregister` — Relayer 해제

```
POST http://relayer-api:3002/api/relayer/unregister
```

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `relayerId` | number | ✅ | relayer_wallets.id |

**Response (pending TX 있을 때)**

```json
{
  "relayerId": 7,
  "registrationStatus": "DEREGISTERING",
  "message": "Waiting for 3 pending TXs to complete"
}
```

**Response (즉시 해제)**

```json
{ "relayerId": 7, "status": "PAUSED", "registrationStatus": "REVOKED", "txHash": "0xREMOVE..." }
```

**온체인 동작**: Contract Owner가 `CryptoRelayer.removeRelayer(relayerAddress)` 호출

---

#### `GET /api/relayer/list` — Relayer 목록 조회

```
GET http://relayer-api:3002/api/relayer/list
```

**Response**

```json
{
  "count": 4,
  "relayers": [
    {
      "id": 1,
      "networkId": 1,
      "address": "0xR1...",
      "role": "COLLECTION",
      "status": "ACTIVE",
      "registrationStatus": "REGISTERED",
      "priority": 10,
      "pendingTxCount": 2,
      "maxPendingTxCount": 50
    }
  ]
}
```

---

#### `GET /api/relayer/status/:walletId` — Relayer 상태 조회

```
GET http://relayer-api:3002/api/relayer/status/42
```

**Response**: 단일 Relayer 객체 (list 엔드포인트와 동일 구조)

---

### 3.2 백그라운드 서비스 (API 없음 — DB 상태 변경으로 트리거)

#### CollectionPoller — 집금 처리 (3초 간격, 배치 20건)

> Spring Boot가 `collection_queue` 테이블에 `status='QUEUED'` 레코드를 INSERT하면 자동 처리

**상태 흐름**: `QUEUED → COLLECTING → BROADCASTING → CONFIRMED/FAILED`

**처리 로직**:
1. QUEUED 레코드 배치 조회
2. approve 상태 확인 (wallet_approvals.status = APPROVED 여부)
3. COLLECTION 역할의 ACTIVE Relayer 선택
4. TRON인 경우: TronZap API로 에너지 위임 (Relayer EOA 대상)
5. `CryptoRelayer.executeTransfer(token, fromWallet, masterWallet, amount)` 호출
6. gas_cost_records에 비용 기록

---

#### WithdrawalPoller — 출금 처리 (3초 간격, 배치 10건)

> Spring Boot가 `withdrawals` 테이블의 `status`를 `'APPROVED'`로 변경하면 자동 처리

**상태 흐름**: `APPROVED → PROCESSING → BROADCASTING → CONFIRMED/FAILED`

**처리 로직**:
1. APPROVED 출금 배치 조회
2. 파트너 MASTER 지갑 조회
3. WITHDRAWAL 역할의 ACTIVE Relayer 선택
4. TRON인 경우: TronZap API로 에너지 위임
5. `CryptoRelayer.executeTransfer(token, masterWallet, toAddress, amount)` 호출
6. gas_cost_records에 비용 기록

---

## 4. wallet-activator (Worker)

> **HTTP API 없음** — DB 상태 변경으로 트리거

### ApprovalPoller — 지갑 활성화 + approve (5초 간격, 배치 10건)

> Spring Boot가 `wallet_approvals` 테이블에 `status='PENDING'` 레코드를 INSERT하면 자동 처리

**상태 흐름**:

```
PENDING → GAS_SUPPORTING → GAS_READY → APPROVING → APPROVED
                                                  ↗
                              GAS_READY (재시도) ──┘
실패 시 → FAILED (또는 DEFERRED)
```

#### EVM 경로 (ETH/BSC/Polygon)

| 단계 | 동작 | DB 변경 |
|------|------|---------|
| 1 | FEE 지갑 → HOT 지갑에 가스비 전송 | `status → GAS_SUPPORTING`, gas_cost_records INSERT (GAS_SUPPORT) |
| 2 | 가스 TX 확인 대기 | `status → GAS_READY` |
| 3 | HOT 지갑에서 ERC-20 approve(CryptoRelayer, MAX_UINT256) | `status → APPROVING`, gas_cost_records INSERT (APPROVE) |
| 4 | approve TX 확인 대기 | `status → APPROVED` |

#### TRON 경로

| 단계 | 동작 | DB 변경 |
|------|------|---------|
| 1 | TronZap API로 에너지 임대 | `status → GAS_SUPPORTING`, gas_cost_records INSERT (ENERGY_RENTAL) |
| 2 | FEE 지갑 → HOT 지갑에 0.1 TRX 전송 (계정 활성화) | gas_cost_records INSERT (GAS_SUPPORT) |
| 3 | TRX TX 확인 대기 | `status → GAS_READY` |
| 4 | HOT 지갑에서 TRC-20 approve(CryptoRelayer, MAX_UINT256) | `status → APPROVING`, gas_cost_records INSERT (APPROVE) |
| 5 | approve TX 확인 대기 | `status → APPROVED` |

---

## 5. telegram-bot (Worker)

> **HTTP API 없음** — Telegram Long Polling 방식

### 텔레그램 명령어

| 명령어 | 파라미터 | 설명 |
|--------|----------|------|
| `/start {partner_code}` | 파트너 코드 | 채팅방을 파트너 알림에 연결 |
| `/stop` | — | 알림 연결 해제 |
| `/balance` | — | MASTER 지갑 잔액 조회 |
| `/recent` | — | 최근 입금 3건 + 출금 3건 |
| `/status` | — | 네트워크 상태 + Relayer 상태 |
| `/subscribe {event}` | 이벤트명 | 이벤트 알림 구독 |
| `/unsubscribe {event}` | 이벤트명 | 이벤트 알림 해제 |
| `/help` | — | 도움말 |

**구독 가능 이벤트**: `DEPOSIT_CONFIRMED`, `WITHDRAWAL_CONFIRMED`, `WITHDRAWAL_FAILED`, `LARGE_DEPOSIT`, `BALANCE_LOW`

**DB 참조**: `telegram_configs`, `telegram_subscriptions`, `partners`, `wallet_balances`, `deposits`, `withdrawals`

---

## 6. 시퀀스 다이어그램

### 6.1 시스템 초기 세팅

```
Admin Console                blockchain-api                 DB
     │                            │                          │
     │─ POST /admin/wallet/       │                          │
     │  create-admin {networkId}  │                          │
     │                            │── HD derive ────────────>│ wallet_addresses INSERT
     │                            │── encrypt key ──────────>│ wallet_keys INSERT
     │<─── { address, addressId } │                          │
     │                            │                          │
     │                            │                          │
     │  [운영자: Hardhat/Tronbox로 수동 배포 → transferOwnership(ADMIN)]
     │                            │                          │
     │─ POST /admin/contract/     │                          │
     │  register {networkId,      │                          │
     │   contractAddress,         │                          │
     │   deployTxHash,            │                          │
     │   ownerAddressId}          │                          │
     │                            │── owner 검증 ──> Chain   │
     │                            │──────────────────────────>│ relayer_contracts INSERT
     │<─── { contractId,          │                          │
     │       status: "ACTIVE" }   │                          │
     │                            │                          │
     │─ POST /admin/contract/     │                          │
     │  add-relayer               │                          │
     │  {networkId, relayerWalletId}                         │
     │                            │── addRelayer() ──> Chain │
     │                            │──────────────────────────>│ relayer_wallets UPDATE
     │<─── { txHash }             │                          │
```

### 6.2 입금 처리 (전체 플로우)

```
Webhook(Spring)         DB                wallet-activator    relayer-api(Poller)   Chain
     │                   │                      │                   │                 │
     │── 입금 감지 ──>    │                      │                   │                 │
     │   deposits INSERT  │                      │                   │                 │
     │                   │                      │                   │                 │
     │── 미승인 지갑? ──>│ wallet_approvals     │                   │                 │
     │   INSERT PENDING   │                      │                   │                 │
     │                   │                      │                   │                 │
     │                   │  (5초 폴링)           │                   │                 │
     │                   │<── PENDING 조회 ──────│                   │                 │
     │                   │                      │── FEE→HOT 가스 ──>│                 │──> Chain
     │                   │<─ GAS_SUPPORTING ────│                   │                 │
     │                   │<─ GAS_READY ─────────│                   │                 │
     │                   │                      │── approve TX ────>│                 │──> Chain
     │                   │<─ APPROVED ──────────│                   │                 │
     │                   │                      │                   │                 │
     │── 입금 확정 ──>   │ collection_queue     │                   │                 │
     │   INSERT QUEUED   │                      │                   │                 │
     │                   │                      │   (3초 폴링)      │                 │
     │                   │<── QUEUED 조회 ──────────────────────────│                 │
     │                   │                      │                   │── executeTransfer ──> Chain
     │                   │<── BROADCASTING ─────────────────────────│                 │
     │                   │                      │                   │                 │
     │── TX 확인 ──>     │<── CONFIRMED ────────────────────────────│                 │
```

### 6.3 출금 처리 (Spring Boot → Node.js)

```
Partner Console      Admin Console(SB)       DB              relayer-api(Poller)   Chain
     │                      │                 │                    │                  │
     │── 출금 요청 ──>     │                 │                    │                  │
     │                      │── 승인 ────────>│ withdrawals       │                  │
     │                      │   status=APPROVED│                   │                  │
     │                      │                 │                    │                  │
     │                      │                 │  (3초 폴링)        │                  │
     │                      │                 │<── APPROVED 조회 ──│                  │
     │                      │                 │                    │── executeTransfer ──> Chain
     │                      │                 │<── BROADCASTING ───│                  │
     │                      │                 │                    │                  │
     │                      │                 │<── CONFIRMED ──────│                  │
     │<── Webhook/알림 ─────│                 │                    │                  │
```

### 6.4 컨트랙트 온체인 호출 구조

```
                        ┌──────────────────────────┐
                        │   CryptoRelayer Contract  │
                        │   (네트워크당 1개)         │
                        │                          │
   ADMIN 지갑 (Owner)──>│  addRelayer(eoa)         │
   ADMIN 지갑 (Owner)──>│  removeRelayer(eoa)      │
   ADMIN 지갑 (Owner)──>│  pause() / unpause()     │
                        │                          │
   Relayer EOA ────────>│  executeTransfer(         │
                        │    token, from, to, amt)  │──> IERC20.transferFrom(from, to, amt)
                        │                          │
   Relayer EOA ────────>│  executeBatchTransfer(    │
                        │    token, from,           │
                        │    toList[], amounts[])   │──> 반복 transferFrom
                        └──────────────────────────┘
```

> **TRON 에너지**: TVM에서는 tx origin(Relayer EOA)이 에너지를 소비하므로, TronZap 에너지 위임 대상은 **Relayer EOA 주소**입니다 (컨트랙트 주소가 아님).

---

## 7. DB 테이블 연동 정보

### 7.1 Spring Boot가 INSERT/UPDATE → Node.js가 폴링 처리하는 테이블

| 테이블 | Spring Boot 역할 | Node.js 역할 |
|--------|------------------|--------------|
| `wallet_approvals` | INSERT (status=PENDING) | wallet-activator가 폴링 → APPROVED까지 처리 |
| `collection_queue` | INSERT (status=QUEUED) | relayer-api CollectionPoller가 폴링 → CONFIRMED까지 처리 |
| `withdrawals` | UPDATE status=APPROVED | relayer-api WithdrawalPoller가 폴링 → CONFIRMED까지 처리 |

### 7.2 Node.js가 관리하는 핵심 테이블

| 테이블 | 관리 서버 | 설명 |
|--------|-----------|------|
| `wallet_addresses` | blockchain-api | HD 파생 주소 저장 |
| `wallet_keys` | blockchain-api | 암호화된 개인키 저장 |
| `wallet_balances` | blockchain-api | 온체인 잔액 캐시 (sync API) |
| `wallet_index_manager` | blockchain-api | HD 파생 인덱스 카운터 |
| `relayer_contracts` | blockchain-api | CryptoRelayer 배포 정보 |
| `relayer_wallets` | relayer-api | Relayer EOA 관리 |
| `nonce_tracker` | relayer-api | 논스 관리 (분산 락 기반) |
| `gas_cost_records` | relayer-api, wallet-activator | 가스비 추적 |
| `telegram_configs` | telegram-bot | 텔레그램 채팅방 연결 |
| `telegram_subscriptions` | telegram-bot | 이벤트 구독 |

### 7.3 주요 상태 전이표

#### `wallet_approvals.status`

```
PENDING → GAS_SUPPORTING → GAS_READY → APPROVING → APPROVED
                                                  → FAILED
                                                  → DEFERRED (재시도 초과)
```

#### `collection_queue.status`

```
QUEUED → COLLECTING → BROADCASTING → CONFIRMED
                                   → FAILED
```

#### `withdrawals.status` (Node.js 관련 부분)

```
APPROVED → PROCESSING → BROADCASTING → CONFIRMED
                                     → FAILED
```

#### `relayer_wallets.status` (운영 상태)

```
ACTIVE ↔ PAUSED
```

#### `relayer_wallets.registration_status` (온체인 등록 상태)

```
PENDING → REGISTERING → REGISTERED → DEREGISTERING → REVOKED
```

#### `relayer_contracts.status`

```
ACTIVE → PAUSED → ACTIVE (unpause)
ACTIVE → DEPRECATED (신규 배포 시)
```

### 7.4 `gas_cost_records` TX 타입

| tx_type | 발생 서버 | 설명 |
|---------|-----------|------|
| `GAS_SUPPORT` | wallet-activator | FEE 지갑 → HOT 지갑 가스비 전송 |
| `ENERGY_RENTAL` | wallet-activator, relayer-api | TronZap 에너지 임대 비용 |
| `APPROVE` | wallet-activator | ERC-20/TRC-20 approve TX |
| `COLLECTION` | relayer-api | 집금 transferFrom TX |
| `WITHDRAWAL` | relayer-api | 출금 transferFrom TX |

---

## 8. 에러 처리 공통 규격

모든 에러 응답은 다음 형식을 따릅니다:

```json
{
  "error": "에러 메시지 (영문)"
}
```

### HTTP 상태 코드

| 코드 | 의미 | 사용 예 |
|------|------|---------|
| `200` | 성공 | 정상 응답 |
| `400` | Bad Request | 필수 파라미터 누락, 유효하지 않은 값 |
| `404` | Not Found | 지갑/Relayer/컨트랙트 없음 |
| `409` | Conflict | (사용 안 함 — ADMIN 지갑 중복 시 기존 반환) |
| `500` | Internal Server Error | 온체인 TX 실패, 시스템 에러 |

### 주의사항

1. **BigInt 직렬화**: 잔액, 가스 등의 큰 숫자 값은 `string`으로 반환됩니다 (JSON의 number 정밀도 한계)
2. **온체인 TX 포함 API**: deploy, addRelayer, removeRelayer, pause, unpause, register, unregister 등은 온체인 TX를 포함하므로 응답 시간이 수~수십 초 소요될 수 있습니다
3. **폴러 기반 처리**: collection_queue, withdrawals, wallet_approvals는 폴러가 주기적으로 확인하므로 INSERT/UPDATE 후 즉시 처리되지 않을 수 있습니다 (최대 지연: 폴링 주기)
4. **TRON 에너지**: TRON 네트워크 TX 전에는 TronZap API로 에너지 위임이 선행되며, 이 비용은 `gas_cost_records`에 ENERGY_RENTAL로 기록됩니다

---

## 9. Spring Boot 연동 구현 — Axim REST Framework

> 이 프로젝트는 **Axim REST Framework** 기반입니다. Spring 기본 `RestTemplate`이 아닌 `XWebClient` 또는 `@XRestService`를 사용합니다.

### 9.1 설정 — application.properties

```properties
# ── Node.js 서버 연결 ──
axim.web-client.services.blockchainApi=http://blockchain-api:3001
axim.web-client.services.relayerApi=http://relayer-api:3002

# ── HTTP Client 튜닝 ──
axim.rest.client.pool-size=200
axim.rest.client.connection-request-timeout=30
axim.rest.client.response-timeout=30
axim.rest.debug=false
```

### 9.2 Application 클래스 설정

```java
@ComponentScan({"one.axim.framework.rest", "one.axim.framework.mybatis", "com.cryptoments"})
@SpringBootApplication
@XRepositoryScan("com.cryptoments.repository")
@MapperScan({"one.axim.framework.mybatis.mapper", "com.cryptoments.mapper"})
@XRestServiceScan("com.cryptoments.client")  // ← Node.js REST 클라이언트 스캔
public class CryptomentsApplication {
    public static void main(String[] args) {
        SpringApplication.run(CryptomentsApplication.class, args);
    }
}
```

### 9.3 방법 A — `@XRestService` 선언형 인터페이스

```java
// common/src/main/java/com/cryptoments/client/BlockchainApiClient.java

@XRestService(value = "blockchain-api",
    host = "${axim.web-client.services.blockchainApi:http://blockchain-api:3001}")
public interface BlockchainApiClient {

    // ── 지갑 ──
    @XRestAPI(value = "/api/wallet/derive", method = XHttpMethod.POST)
    DeriveResponse deriveAddress(@RequestBody DeriveRequest request);

    @XRestAPI(value = "/api/wallet/approve-status/{address}", method = XHttpMethod.GET)
    ApproveStatusResponse getApproveStatus(
        @PathVariable("address") String address,
        @RequestParam("networkId") int networkId,
        @RequestParam("tokenContract") String tokenContract,
        @RequestParam("spender") String spender);

    // ── 잔액 ──
    @XRestAPI(value = "/api/balance/token/{address}", method = XHttpMethod.GET)
    BalanceResponse getTokenBalance(
        @PathVariable("address") String address,
        @RequestParam("networkId") int networkId,
        @RequestParam("tokenContract") String tokenContract);

    @XRestAPI(value = "/api/balance/native/{address}", method = XHttpMethod.GET)
    NativeBalanceResponse getNativeBalance(
        @PathVariable("address") String address,
        @RequestParam("networkId") int networkId);

    @XRestAPI(value = "/api/balance/sync", method = XHttpMethod.POST)
    SyncBalanceResponse syncBalances(@RequestBody SyncBalanceRequest request);

    // ── 트랜잭션 ──
    @XRestAPI(value = "/api/tx/status/{txHash}", method = XHttpMethod.GET)
    TxStatusResponse getTxStatus(
        @PathVariable("txHash") String txHash,
        @RequestParam("networkId") int networkId);

    @XRestAPI(value = "/api/tx/receipt/{txHash}", method = XHttpMethod.GET)
    TxReceiptResponse getTxReceipt(
        @PathVariable("txHash") String txHash,
        @RequestParam("networkId") int networkId);

    // ── 가스 ──
    @XRestAPI(value = "/api/gas/price", method = XHttpMethod.GET)
    GasPriceResponse getGasPrice(@RequestParam("networkId") int networkId);

    @XRestAPI(value = "/api/gas/estimate", method = XHttpMethod.POST)
    GasEstimateResponse estimateGas(@RequestBody GasEstimateRequest request);

    // ── 블록 ──
    @XRestAPI(value = "/api/block/latest", method = XHttpMethod.GET)
    LatestBlockResponse getLatestBlock(@RequestParam("networkId") int networkId);

    // ── 관리자 (Admin API에서만 사용) ──
    @XRestAPI(value = "/api/admin/wallet/create-admin", method = XHttpMethod.POST)
    AdminWalletResponse createAdminWallet(@RequestBody AdminWalletRequest request);

    // ⚠️ deploy → register 변경 (v1.2)
    @XRestAPI(value = "/api/admin/contract/register", method = XHttpMethod.POST)
    ContractRegisterResponse registerContract(@RequestBody ContractRegisterRequest request);

    @XRestAPI(value = "/api/admin/contract/add-relayer", method = XHttpMethod.POST)
    AddRelayerResponse addRelayer(@RequestBody AddRelayerRequest request);

    @XRestAPI(value = "/api/admin/contract/remove-relayer", method = XHttpMethod.POST)
    RemoveRelayerResponse removeRelayer(@RequestBody RemoveRelayerRequest request);

    @XRestAPI(value = "/api/admin/contract/pause", method = XHttpMethod.POST)
    ContractStatusResponse pauseContract(@RequestBody PauseContractRequest request);

    @XRestAPI(value = "/api/admin/contract/unpause", method = XHttpMethod.POST)
    ContractStatusResponse unpauseContract(@RequestBody UnpauseContractRequest request);

    @XRestAPI(value = "/api/admin/contract/status/{networkId}", method = XHttpMethod.GET)
    ContractDetailResponse getContractStatus(@PathVariable("networkId") int networkId);

    @XRestAPI(value = "/api/admin/contract/list", method = XHttpMethod.GET)
    ContractListResponse getContractList();
}
```

```java
// common/src/main/java/com/cryptoments/client/RelayerApiClient.java

@XRestService(value = "relayer-api",
    host = "${axim.web-client.services.relayerApi:http://relayer-api:3002}")
public interface RelayerApiClient {

    // ★ Admin API에서만 사용 — Relayer 등록/해제 전용
    @XRestAPI(value = "/api/relayer/register", method = XHttpMethod.POST)
    RelayerResponse registerRelayer(@RequestBody RegisterRelayerRequest request);

    @XRestAPI(value = "/api/relayer/unregister", method = XHttpMethod.POST)
    RelayerResponse unregisterRelayer(@RequestBody UnregisterRelayerRequest request);

    @XRestAPI(value = "/api/relayer/list", method = XHttpMethod.GET)
    RelayerListResponse getRelayerList();

    @XRestAPI(value = "/api/relayer/status/{walletId}", method = XHttpMethod.GET)
    RelayerResponse getRelayerStatus(@PathVariable("walletId") int walletId);
}
```

### 9.4 방법 B — `XWebClient` 직접 사용

```java
@Service
@RequiredArgsConstructor
public class BlockchainQueryService {

    @Qualifier("blockchainApi")
    private final XWebClient blockchainApi;

    // 단순 GET
    public BalanceResponse getTokenBalance(int networkId, String address, String contract) {
        return blockchainApi.get(
            "/api/balance/token/{address}?networkId={nid}&tokenContract={contract}",
            BalanceResponse.class,
            address, networkId, contract);
    }

    // POST with body
    public DeriveResponse deriveAddress(DeriveRequest req) {
        return blockchainApi.post("/api/wallet/derive", req, DeriveResponse.class);
    }

    // spec() 빌더 (커스텀 헤더가 필요한 경우)
    public ContractDetailResponse getContractWithAuth(int networkId, String adminToken) {
        return blockchainApi.spec()
            .get("/api/admin/contract/status/{networkId}", networkId)
            .header("X-Admin-Token", adminToken)
            .retrieve(ContractDetailResponse.class);
    }
}
```

### 9.5 서비스에서의 사용 예

```java
// core/src/main/java/com/cryptoments/service/WalletService.java

@Service
@RequiredArgsConstructor
public class WalletService {
    private final BlockchainApiClient blockchainApiClient;  // @XRestService 인터페이스 주입

    /**
     * 새 입금 지갑 주소 발급
     */
    public WalletAddress createDepositWallet(int partnerId, int networkId, int hdWalletId) {
        // 1. Node.js에 HD 파생 요청
        DeriveRequest req = new DeriveRequest(networkId, hdWalletId, null);
        DeriveResponse res;
        try {
            res = blockchainApiClient.deriveAddress(req);
        } catch (XRestException e) {
            log.error("지갑 파생 실패: networkId={}, error={}", networkId, e.getMessage());
            throw new BusinessException("WALLET_DERIVE_FAILED");
        }

        // 2. wallet_addresses에 이미 INSERT됨 (blockchain-api가 처리)
        //    → partner_id만 UPDATE
        walletAddressMapper.updatePartnerId(res.getWalletAddressId(), partnerId);

        // 3. wallet_approvals에 PENDING INSERT → wallet-activator가 approve 처리
        walletApprovalMapper.insert(WalletApproval.builder()
            .walletAddressId(res.getWalletAddressId())
            .networkId(networkId)
            .status("PENDING")
            .build());

        return walletAddressMapper.findById(res.getWalletAddressId());
    }
}
```

### 9.6 에러 처리

```java
// Node.js 서버 에러 응답 형식: { "error": "메시지" }
// Axim REST Framework는 XRestException으로 전파

try {
    blockchainApiClient.registerContract(new ContractRegisterRequest(networkId, contractAddress, deployTxHash, ownerAddressId, null));
} catch (XRestException e) {
    // e.getCode()   → HTTP 상태 코드 (400, 404, 409, 500 등)
    // e.getMessage() → 에러 메시지
    switch (e.getCode()) {
        case 409 -> throw new BusinessException("CONTRACT_ALREADY_EXISTS");
        case 404 -> throw new BusinessException("ADMIN_WALLET_NOT_FOUND");
        default  -> throw new BusinessException("BLOCKCHAIN_API_ERROR", e.getMessage());
    }
}
```

### 9.7 DTO 예시 (common/dto/blockchain/)

```java
// DeriveRequest.java
@Data @AllArgsConstructor @NoArgsConstructor
public class DeriveRequest {
    private Integer networkId;
    private Integer hdWalletId;
    private Integer derivationIndex;  // null이면 자동 증가
}

// DeriveResponse.java
@Data
public class DeriveResponse {
    private String address;
    private String publicKey;
    private Integer walletAddressId;
    private String derivationPath;
}

// BalanceResponse.java
@Data
public class BalanceResponse {
    private String balance;   // String → BigDecimal로 변환 필요
    private Integer decimals;
}

// TxStatusResponse.java
@Data
public class TxStatusResponse {
    private String txHash;
    private String status;        // "pending" | "confirmed" | "failed"
    private Long blockNumber;
    private Integer confirmations;
    private String gasUsed;
}
```

### 9.8 ❌ 하지 말아야 할 것

```java
// ❌ RestTemplate 직접 사용 — 이 프로젝트는 Axim REST Framework 사용
@Bean
public RestTemplate restTemplate() { ... }  // 사용 금지

// ❌ WebClient 사용 — XWebClient를 대신 사용
WebClient.builder().baseUrl("http://blockchain-api:3001").build();

// ❌ OpenFeign 사용 — @XRestService가 동일한 역할
@FeignClient(name = "blockchain-api") ...

// ✅ 올바른 방법
// → @XRestService 인터페이스 선언 또는 @Qualifier XWebClient 주입
```

### 9.9 온체인 TX 포함 API의 타임아웃

`deploy`, `addRelayer`, `removeRelayer`, `pause`, `unpause` 등 온체인 TX가 포함된 API는 응답 시간이 **수~수십 초** 소요됩니다. 기본 `response-timeout=30`으로 부족할 수 있으므로, 해당 호출은 `spec()` 빌더로 타임아웃을 늘리거나, 별도 `XWebClient` 빈을 등록하세요:

```properties
# 관리자 API 전용 — 긴 타임아웃
axim.web-client.services.blockchainAdmin=http://blockchain-api:3001
```

```java
@Qualifier("blockchainAdmin")
private final XWebClient blockchainAdmin;  // response-timeout을 별도 설정 가능
```

---

## 변경 이력

| 버전 | 날짜 | 변경 내용 |
|------|------|----------|
| v1.0 | 2026-03-07 | 초안 — Node.js 4개 서버 API 스펙, 시퀀스 다이어그램, DB 연동 정보 |
| v1.1 | 2026-03-10 | §9 추가 — Spring Boot 연동 구현 (Axim REST Framework XWebClient/@XRestService) |
| v1.2 | 2026-03-16 | deploy→register 변경 반영, relayer_wallets status/registrationStatus 분리, 시퀀스 다이어그램 업데이트 |
