# Guide #64 — 지갑 생성 → 활성화 E2E 테스트 가이드

**작성일**: 2026-03-26
**대상**: core (Spring), partner-api (Spring), node-service (wallet-activator)

---

## 1. 전체 흐름 요약

```
                     Spring Boot                          Node.js
                 ┌─────────────────┐              ┌──────────────────────┐
  POST /wallets  │  WalletService   │              │   wallet-activator   │
  ─────────────► │  1. deriveWallet ──(HTTP)──►    │                      │
                 │     → blockchain-api            │                      │
                 │  2. initBalances  │              │                      │
                 │  3. registerApprovals ──────►   │  ApprovalPoller (5s) │
                 │     → wallet_approvals PENDING  │    │                 │
                 │  4. deposit_address_pool (POOL)│    ▼                 │
                 └─────────────────┘              │  ApprovalProcessor   │
                                                   │    ├─ GAS 전송      │
                                                   │    ├─ TX 확인 대기   │
                                                   │    ├─ approve TX    │
                                                   │    └─ APPROVED      │
                                                   └──────────────────────┘
```

### 지갑 타입별 처리

| 타입 | HD 파생 | 잔액 초기화 | Approve 등록 | deposit_address_pool | 활성화 완료 기준 |
|------|---------|------------|-------------|---------------------|----------------|
| **MASTER** | ✅ | ✅ | ✅ 즉시 | ❌ | 모든 토큰 approve APPROVED |
| **POOL** | ✅ | ✅ | ✅ 즉시 | ✅ | 모든 토큰 approve APPROVED |
| **HOT** | ✅ | ✅ | ❌ 지연 | ❌ | 생성 즉시 (approve 불필요) |

---

## 2. 전제 조건 (테스트 전 반드시 확인)

### 2-1. 인프라 지갑 (네트워크당 1개씩)

```sql
-- ① ADMIN 지갑 존재 확인
SELECT id, network_id, wallet_type, address
  FROM wallet_addresses
 WHERE wallet_type = 'ADMIN';

-- ② GAS 지갑 존재 + 잔액 충분 확인
SELECT wa.id, wa.network_id, wa.address,
       bn.chain_symbol
  FROM wallet_addresses wa
  JOIN blockchain_networks bn ON wa.network_id = bn.id
 WHERE wa.wallet_type = 'GAS';
```

**GAS 지갑 최소 잔액 기준:**

| 네트워크 | 최소 권장 잔액 | 용도 |
|---------|--------------|------|
| BSC | 0.05 BNB | approve 1건당 ~0.002 BNB |
| Polygon | 1 MATIC | approve 1건당 ~0.01 MATIC |
| TRON | 100 TRX | 활성화 0.1 TRX + 에너지 렌탈 ~30 TRX |

> ⚠️ **GAS 지갑 잔액 부족 시**: wallet-activator가 GAS_SUPPORTING 단계에서 실패 → FAILED 상태. 잔액 충전 후 admin-api에서 retryApproval 호출.

### 2-2. HD 지갑 (네트워크당 1개)

```sql
-- HD 지갑 (마스터 시드) 등록 확인
SELECT id, network_id, derivation_base_path, current_index
  FROM hd_wallets
 WHERE is_active = TRUE;
```

> HD 지갑이 없으면 `deriveWallet()` 호출 시 `NotFoundException: HD_WALLET_NOT_FOUND`

### 2-3. CryptoRelayer 컨트랙트 (네트워크당 1개)

```sql
-- Relayer 컨트랙트 배포 + 등록 확인
SELECT id, network_id, contract_address, is_active
  FROM relayer_contracts
 WHERE is_active = TRUE;
```

> Relayer 컨트랙트가 없으면 `registerTokenApprovals()`가 0건 반환 (skip) — **approve가 등록되지 않음!**
> 이 경우 MASTER/POOL 지갑은 생성되지만 Relayer가 transferFrom을 실행할 수 없으므로 **집금/출금 불가**.

### 2-4. 활성 통화 (네트워크별)

```sql
-- 네트워크별 활성 통화 확인
SELECT c.id, c.symbol, c.contract_address, c.is_active, bn.chain_symbol
  FROM currencies c
  JOIN blockchain_networks bn ON c.network_id = bn.id
 WHERE c.is_active = TRUE
 ORDER BY bn.id;
```

> `contract_address`가 NULL인 것은 native coin → approve 대상 아님.
> 토큰(USDT, USDC)만 approve 등록됨.

### 2-5. Relayer 지갑 (최소 1개)

```sql
-- Relayer 지갑 등록 확인
SELECT wa.id, wa.network_id, wa.address, rw.role, rw.is_active
  FROM relayer_wallets rw
  JOIN wallet_addresses wa ON rw.wallet_address_id = wa.id
 WHERE rw.is_active = TRUE;
```

> Relayer가 없으면 approve는 되지만 실제 집금/출금 TX를 실행할 주체가 없음.

### 2-6. 서비스 기동 상태

| 서비스 | 포트 | 확인 방법 |
|--------|------|----------|
| partner-api | 8082 | `curl localhost:8082/actuator/health` |
| blockchain-api | 3001 | `curl localhost:3001/health` |
| wallet-activator | — (worker) | 로그 확인: `ApprovalPoller started` |

---

## 3. MASTER 지갑 테스트

### 3-1. 생성 요청

```bash
curl -X POST http://localhost:8082/api/partner/wallets/master \
  -H "Authorization: Bearer {PARTNER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"networkId": 2}'
```

**성공 응답:**
```json
{
  "walletAddressId": 101,
  "address": "0xABC...123",
  "networkId": 2,
  "walletType": "MASTER",
  "derivationPath": "m/44'/60'/0'/0/5"
}
```

### 3-2. 생성 직후 DB 확인

```sql
-- ① wallet_addresses 레코드
SELECT id, address, wallet_type, partner_id, network_id, derivation_path
  FROM wallet_addresses WHERE id = 101;

-- ② wallet_balances 초기화 (통화 수만큼)
SELECT wb.id, wb.wallet_address_id, wb.currency_id, wb.balance, c.symbol
  FROM wallet_balances wb
  JOIN currencies c ON wb.currency_id = c.id
 WHERE wb.wallet_address_id = 101;
-- 기대: BNB(0), USDT(0), USDC(0) 등

-- ③ wallet_approvals PENDING 등록 (토큰 수만큼)
SELECT wa.id, wa.wallet_address_id, wa.currency_id, wa.status,
       wa.spender_address, c.symbol
  FROM wallet_approvals wa
  JOIN currencies c ON wa.currency_id = c.id
 WHERE wa.wallet_address_id = 101;
-- 기대: USDT=PENDING, USDC=PENDING (native coin 제외)
```

### 3-3. 활성화 진행 확인

wallet-activator 로그를 모니터링하거나, DB 상태를 폴링:

```sql
-- 5초 간격으로 상태 변화 추적
SELECT id, currency_id, status, gas_tx_hash, approve_tx_hash, updated_at
  FROM wallet_approvals
 WHERE wallet_address_id = 101
 ORDER BY id;
```

**상태 전이 순서:**

```
PENDING → GAS_SUPPORTING → GAS_READY → APPROVING → APPROVED
```

| 상태 | 의미 | 소요 시간 |
|------|------|----------|
| PENDING | 대기 (poller 미감지) | 0~5초 |
| GAS_SUPPORTING | GAS 지갑 → 대상 지갑 네이티브 전송 중 | ~15초 (BSC) |
| GAS_READY | 가스 TX 확인 완료, approve 실행 대기 | 즉시 |
| APPROVING | ERC-20/TRC-20 approve TX 전송 중 | ~15초 (BSC) |
| APPROVED | 완료 | — |

### 3-4. 활성화 완료 확인

```sql
-- 모든 토큰이 APPROVED인지 확인
SELECT currency_id, status, approve_tx_hash
  FROM wallet_approvals
 WHERE wallet_address_id = 101
   AND status != 'APPROVED';
-- 기대: 0건 (모두 APPROVED)

-- gas_cost_records 확인 (GAS_SUPPORT + APPROVE)
SELECT id, tx_type, tx_hash, fee_native, fee_usd, reference_id
  FROM gas_cost_records
 WHERE reference_type = 'WALLET_APPROVAL'
   AND reference_id IN (SELECT id FROM wallet_approvals WHERE wallet_address_id = 101);
-- 기대: 토큰 수 × 2건 (GAS_SUPPORT 1건 + APPROVE 1건)
```

### 3-5. 중복 생성 방어

```bash
# 같은 파트너 + 같은 네트워크로 재요청
curl -X POST http://localhost:8082/api/partner/wallets/master \
  -H "Authorization: Bearer {PARTNER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"networkId": 2}'
```

> 기대: 기존 지갑 반환 (새로 생성하지 않음). 로그: `"MASTER wallet already exists"`

---

## 4. POOL 지갑 테스트

### 4-1. 생성 요청

```bash
# 통화 지정 (USDT, USDC)
curl -X POST http://localhost:8082/api/partner/wallets/pool \
  -H "Authorization: Bearer {PARTNER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"networkId": 2, "currencyIds": [5, 6]}'

# 또는 미지정 → 네트워크 활성 통화 전체
curl -X POST http://localhost:8082/api/partner/wallets/pool \
  -H "Authorization: Bearer {PARTNER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"networkId": 2}'
```

### 4-2. 생성 직후 DB 확인

```sql
-- ① wallet_addresses
SELECT id, address, wallet_type, partner_id, network_id
  FROM wallet_addresses
 WHERE partner_id = ? AND wallet_type = 'POOL' AND network_id = 2
 ORDER BY id DESC LIMIT 1;

-- ② wallet_balances
SELECT wb.currency_id, wb.balance, c.symbol
  FROM wallet_balances wb
  JOIN currencies c ON wb.currency_id = c.id
 WHERE wb.wallet_address_id = ?;

-- ③ wallet_approvals PENDING
SELECT wa.id, wa.currency_id, wa.status, c.symbol
  FROM wallet_approvals wa
  JOIN currencies c ON wa.currency_id = c.id
 WHERE wa.wallet_address_id = ?;

-- ④ ★ deposit_address_pool 레코드 (Guide #63 Part A 핵심)
SELECT id, partner_id, network_id, currency_id,
       wallet_address_id, address, decimal_digits, is_active
  FROM deposit_address_pool
 WHERE wallet_address_id = ?;
-- 기대: currencyIds에 지정한 수만큼 (예: USDT=1건, USDC=1건 → 2건)
```

### 4-3. 활성화 진행

MASTER와 동일 — wallet-activator가 PENDING 건을 처리.

### 4-4. 소수점 매칭 입금 세션 테스트 (활성화 완료 후)

```bash
# 소수점 매칭 입금 세션 생성 요청
curl -X POST http://localhost:8082/api/partner/deposits/sessions \
  -H "Authorization: Bearer {PARTNER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "depositMethod": "DECIMAL_MATCH",
    "networkId": 2,
    "currencyId": 5,
    "expectedAmount": "100"
  }'
```

> **이전 버그** (Guide #63 Part A): `deposit_address_pool` 미생성 → `NotFoundException: 사용 가능한 풀 주소가 없습니다`
> **수정 후**: 정상적으로 소수점 매칭 세션 생성 + 고유 소수점 할당 (예: 100.0042 USDT)

---

## 5. HOT 지갑 테스트

### 5-1. 생성 요청

```bash
# partnerUserId 지정 (고객별 주소 할당)
curl -X POST http://localhost:8082/api/partner/wallets/hot \
  -H "Authorization: Bearer {PARTNER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"networkId": 2, "partnerUserId": "user-001"}'

# partnerUserId 미지정 (범용 HOT)
curl -X POST http://localhost:8082/api/partner/wallets/hot \
  -H "Authorization: Bearer {PARTNER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"networkId": 2}'
```

### 5-2. 생성 직후 DB 확인

```sql
-- ① wallet_addresses
SELECT id, address, wallet_type, partner_id, partner_user_id, network_id
  FROM wallet_addresses
 WHERE partner_id = ? AND wallet_type = 'HOT' AND network_id = 2
 ORDER BY id DESC LIMIT 1;

-- ② wallet_balances (초기화됨)
SELECT wb.currency_id, wb.balance, c.symbol
  FROM wallet_balances wb
  JOIN currencies c ON wb.currency_id = c.id
 WHERE wb.wallet_address_id = ?;

-- ③ wallet_approvals — 없어야 정상
SELECT * FROM wallet_approvals WHERE wallet_address_id = ?;
-- 기대: 0건 (HOT은 첫 입금 시까지 approve 지연)
```

### 5-3. HOT 활성화 시점 (설계 의도)

HOT 지갑은 **첫 입금 감지 후** 집금(collection) 시점에 approve가 필요:

```
① 유저가 HOT 주소로 USDT 전송
② blockchain_monitor 감지 → webhook → Cryptoments
③ DepositService.confirmDeposit() → 집금 enqueue
④ CollectionPoller가 집금 실행 시:
   - HOT 지갑의 approve 상태 확인
   - APPROVED 아니면 → registerTokenApprovals() → wallet-activator 처리
   - APPROVED 확인 후 → Relayer.transferFrom(HOT→MASTER)
```

> HOT은 생성 즉시 "사용 가능" 상태. approve 없이도 입금 수신은 가능.
> 집금 실행 시 자동으로 approve 등록 → 활성화 → 집금 순서로 진행.

---

## 6. 일괄 생성 테스트 (파트너 온보딩)

`createAllWalletsForPartner()`는 모든 활성 네트워크에 대해 MASTER + HOT 자동 생성:

```sql
-- 현재 활성 네트워크 확인
SELECT id, chain_symbol, network_type, is_active
  FROM blockchain_networks WHERE is_active = TRUE;
```

admin-api 또는 내부 호출로 실행 후:

```sql
-- 파트너별 지갑 생성 현황 확인
SELECT wa.network_id, bn.chain_symbol, wa.wallet_type,
       wa.address, wa.created_at
  FROM wallet_addresses wa
  JOIN blockchain_networks bn ON wa.network_id = bn.id
 WHERE wa.partner_id = ?
 ORDER BY wa.network_id, wa.wallet_type;
-- 기대: 네트워크 수 × 2건 (MASTER + HOT)
```

---

## 7. 실패 시나리오 및 복구

### 7-1. FAILED 상태 복구

```sql
-- FAILED 건 조회
SELECT wa.id, wa.wallet_address_id, wa.currency_id, wa.status,
       wa.error_message, wa.retry_count, c.symbol
  FROM wallet_approvals wa
  JOIN currencies c ON wa.currency_id = c.id
 WHERE wa.status = 'FAILED';
```

**복구 방법:**

```bash
# admin-api에서 retry (FAILED → PENDING 전환)
curl -X POST http://localhost:8081/api/admin/wallets/approvals/{id}/retry \
  -H "Authorization: Bearer {ADMIN_TOKEN}"
```

또는 직접 DB 업데이트 (개발 환경):
```sql
UPDATE wallet_approvals SET status = 'PENDING', error_message = NULL WHERE id = ?;
```

### 7-2. 흔한 실패 원인

| 실패 시점 | 원인 | 해결 |
|----------|------|------|
| GAS_SUPPORTING | GAS 지갑 잔액 부족 | GAS 지갑에 네이티브 코인 충전 |
| GAS_SUPPORTING | GAS 지갑 key 복호화 실패 | `wallet_keys` 암호화 키 확인 |
| APPROVING | Relayer 컨트랙트 미배포 | 온체인 컨트랙트 배포 + `relayer_contracts` 등록 |
| APPROVING | 가스비 부족 (전송된 양 < 실제 소요) | `GAS_PRICE_MULTIPLIER` 상향 조정 |
| TRON GAS_SUPPORTING | TronZap API 오류 | TronZap 서비스 상태 확인 |
| DEFERRED | 5회 재시도 초과 | 근본 원인 해결 후 status → PENDING 수동 전환 |

### 7-3. 부분 실패 시 재시도 안전성

- `requestApproval()`: 이미 APPROVED/GAS_SUPPORTING/GAS_READY/APPROVING 상태이면 skip → **멱등성 보장**
- `createMasterWallet()`: 동일 partner+network에 이미 존재하면 기존 반환 → **중복 생성 방지**
- `initWalletBalances()`: 이미 존재하는 balance는 skip → **멱등성 보장**

---

## 8. 전체 E2E 체크리스트

| # | 테스트 항목 | 확인 방법 | 상태 |
|---|-----------|---------|------|
| 1 | 전제조건: HD 지갑 존재 | `hd_wallets` 조회 | ☐ |
| 2 | 전제조건: GAS 지갑 + 충분한 잔액 | `wallet_addresses` + 온체인 확인 | ☐ |
| 3 | 전제조건: Relayer 컨트랙트 등록 | `relayer_contracts` 조회 | ☐ |
| 4 | 전제조건: 활성 통화 등록 | `currencies` 조회 | ☐ |
| 5 | 전제조건: blockchain-api 기동 | health check | ☐ |
| 6 | 전제조건: wallet-activator 기동 | 로그 확인 | ☐ |
| 7 | MASTER 생성 → wallet_addresses 확인 | DB 조회 | ☐ |
| 8 | MASTER 생성 → wallet_balances 초기화 확인 | DB 조회 | ☐ |
| 9 | MASTER 생성 → wallet_approvals PENDING 확인 | DB 조회 | ☐ |
| 10 | MASTER 활성화 → 모든 토큰 APPROVED | DB 폴링 (~30초) | ☐ |
| 11 | MASTER 활성화 → gas_cost_records 생성 확인 | DB 조회 | ☐ |
| 12 | MASTER 중복 생성 → 기존 반환 확인 | API 재호출 | ☐ |
| 13 | POOL 생성 → wallet_addresses 확인 | DB 조회 | ☐ |
| 14 | POOL 생성 → deposit_address_pool 레코드 확인 | DB 조회 | ☐ |
| 15 | POOL 생성 → wallet_approvals PENDING 확인 | DB 조회 | ☐ |
| 16 | POOL 활성화 → 모든 토큰 APPROVED | DB 폴링 (~30초) | ☐ |
| 17 | POOL 활성화 후 → 소수점 매칭 입금 세션 생성 | API 호출 | ☐ |
| 18 | HOT 생성 → wallet_addresses 확인 | DB 조회 | ☐ |
| 19 | HOT 생성 → wallet_balances 초기화 확인 | DB 조회 | ☐ |
| 20 | HOT 생성 → wallet_approvals 0건 확인 | DB 조회 | ☐ |
| 21 | HOT 중복 생성 (같은 partnerUserId) → 기존 반환 | API 재호출 | ☐ |
| 22 | 일괄 생성 → 네트워크 수 × 2건 확인 | DB 조회 | ☐ |
| 23 | FAILED 복구 → retry 후 APPROVED 전이 | retry API + DB | ☐ |

---

## 9. 모니터링 쿼리 (운영용)

```sql
-- 전체 approve 상태 요약
SELECT status, COUNT(*) as cnt
  FROM wallet_approvals
 GROUP BY status;

-- PENDING 건 체류 시간 (5분 이상이면 wallet-activator 문제)
SELECT id, wallet_address_id, currency_id, status,
       TIMESTAMPDIFF(SECOND, updated_at, NOW()) as pending_seconds
  FROM wallet_approvals
 WHERE status IN ('PENDING', 'GAS_SUPPORTING', 'GAS_READY', 'APPROVING')
   AND TIMESTAMPDIFF(MINUTE, updated_at, NOW()) > 5;

-- 파트너별 활성화 미완료 지갑
SELECT wa.partner_id, wa.wallet_type, wa.address,
       COUNT(CASE WHEN wap.status != 'APPROVED' THEN 1 END) as pending_approvals
  FROM wallet_addresses wa
  LEFT JOIN wallet_approvals wap ON wa.id = wap.wallet_address_id
 WHERE wa.wallet_type IN ('MASTER', 'POOL')
 GROUP BY wa.id
HAVING pending_approvals > 0;
```
