# 인프라 지갑 생성 API 구현 지침서

**대상**: VS Code (blockchain-api) + IntelliJ (admin-api, common)
**우선순위**: Phase 0-3 필수
**날짜**: 2026-03-19

---

## 설계 요약

`create-admin`을 범용화하여 **`POST /api/admin/wallet/create-infra`** 하나로 **ADMIN/GAS** 지갑을 생성.
기존 `create-admin`은 유지하되, 내부적으로 범용 로직을 공유.

### 지갑 유형별 생성 + approve 정책

| 구분 | 지갑 타입 | 생성 API | approve 필요 | GAS Support |
|------|----------|---------|-------------|-------------|
| **인프라 (수동 관리)** | ADMIN | `create-infra` | ❌ | ❌ |
| | GAS | `create-infra` | ❌ | ❌ |
| | Relayer (SYSTEM) | Relayer 전용 API | ❌ | ❌ (수동 충전) |
| **비즈니스 (자동 관리)** | HOT | 파트너 설정 시 자동 | ✅ `transferFrom(HOT→MASTER)` | ✅ |
| | POOL | 시스템 설정 | ✅ `transferFrom(POOL→MASTER)` | ✅ |
| | MASTER | 파트너 설정 시 자동 | ✅ `transferFrom(MASTER→external)` | ✅ |
| | SETTLEMENT | 파트너/시스템 설정 | ✅ `transferFrom(SETTLEMENT→external)` | ✅ |

**핵심 원칙**: `create-infra`는 **approve 불필요 + native coin 수동 충전** 대상만 (ADMIN, GAS).
HOT/POOL/MASTER/SETTLEMENT은 생성 후 `wallet_approvals`에 PENDING 등록 → wallet-activator가 GAS 지원 + approve 실행.

### 중복 제한 정책

| 지갑 타입 | 네트워크당 제한 | 이미 존재 시 |
|----------|---------------|-------------|
| ADMIN | 1개 | 기존 정보 반환 |
| GAS | 1개 | 기존 정보 반환 |

---

## Node.js 변경 (blockchain-api)

### 1. 범용 지갑 생성 라우트 추가

**파일**: `node-service/packages/blockchain-api/src/routes/system-admin.ts`

기존 `POST /wallet/create-admin` 아래에 범용 엔드포인트 추가:

```typescript
// ═══════════════════════════════════════════════════════════════════
//  1-2. 범용 인프라 지갑 생성 (ADMIN/GAS — approve 불필요 대상만)
// ═══════════════════════════════════════════════════════════════════

const INFRA_WALLET_TYPES = ['ADMIN', 'GAS'];

router.post('/wallet/create-infra', async (req, res) => {
  const { networkId, hdWalletId, walletType } = req.body;

  if (!networkId || !hdWalletId || !walletType) {
    return res.status(400).json({ error: 'networkId, hdWalletId, and walletType are required' });
  }

  if (!INFRA_WALLET_TYPES.includes(walletType)) {
    return res.status(400).json({
      error: `Invalid walletType: ${walletType}. Must be one of: ${INFRA_WALLET_TYPES.join(', ')}`,
    });
  }

  try {
    // 이미 존재하는지 확인 (네트워크당 1개 제한)
    const existing = await walletAddressRepo.findByNetworkAndType(networkId, walletType);
    if (existing) {
      return res.json({
        addressId: existing.id,
        address: existing.address,
        networkId,
        walletType,
        derivationPath: existing.derivation_path,
        existing: true,
      });
    }

    // HD 인덱스 할당
    const derivationIndex = await walletIndexManagerRepo.getNextIndex(hdWalletId, networkId);

    // HD 지갑 조회
    const hdWallet = await hdWalletRepo.findById(hdWalletId);
    if (!hdWallet) {
      return res.status(404).json({ error: 'HD wallet not found' });
    }

    // 시드 복호화
    const seedHex = await keyManager.decrypt(hdWallet.master_seed_encrypted);
    const seed = Buffer.from(seedHex, 'hex');

    // 체인 심볼 조회
    const chainSymbol = await hdDerivation.getChainSymbol(networkId);

    // HD 파생
    const derived = hdDerivation.derive(seed, hdWallet.derivation_base_path, chainSymbol, derivationIndex);

    // 개인키 암호화 저장
    const encryptedKey = await keyManager.encrypt(derived.privateKey);

    // wallet_addresses INSERT
    const addressId = await walletAddressRepo.insert({
      address: derived.address,
      network_id: networkId,
      hd_wallet_id: hdWalletId,
      derivation_index: derivationIndex,
      derivation_path: derived.derivationPath,
      wallet_type: walletType,
      partner_id: null,
    });

    // wallet_keys INSERT
    await walletKeyRepo.insert({
      wallet_address_id: addressId,
      encrypted_private_key: encryptedKey,
    });

    logger.info(`${walletType} wallet created`, {
      networkId,
      addressId,
      address: derived.address,
      walletType,
    });

    res.json({
      addressId,
      address: derived.address,
      networkId,
      walletType,
      derivationPath: derived.derivationPath,
      existing: false,
    });
  } catch (error) {
    const msg = error instanceof Error ? error.message : 'Unknown error';
    logger.error(`Failed to create ${walletType} wallet`, { networkId, walletType, error: msg });
    res.status(500).json({ error: `Failed to create ${walletType} wallet` });
  }
});
```

### 2. 기존 `create-admin` 유지

하위 호환성을 위해 기존 `POST /wallet/create-admin`은 그대로 유지.
향후 `create-infra`로 통합할 수 있지만, Phase 0에서는 둘 다 동작하도록 유지.

---

## Spring Boot 변경 (common + admin-api)

### 1. DTO 추가: `InfraWalletCreateApiRequest.java`

**경로**: `common/src/main/java/com/cryptoments/common/client/dto/InfraWalletCreateApiRequest.java`

```java
package com.cryptoments.common.client.dto;

import lombok.*;

/**
 * blockchain-api 범용 인프라 지갑 생성 요청 DTO.
 */
@Getter @Setter @Builder
@NoArgsConstructor @AllArgsConstructor
public class InfraWalletCreateApiRequest {

    /** 네트워크 ID */
    private Integer networkId;

    /** HD 지갑 마스터 ID */
    private Integer hdWalletId;

    /** 지갑 타입 (ADMIN, GAS) */
    private String walletType;
}
```

### 2. DTO 추가: `InfraWalletCreateApiResponse.java`

**경로**: `common/src/main/java/com/cryptoments/common/client/dto/InfraWalletCreateApiResponse.java`

```java
package com.cryptoments.common.client.dto;

import lombok.*;

/**
 * blockchain-api 범용 인프라 지갑 생성 응답 DTO.
 */
@Getter @Setter @Builder
@NoArgsConstructor @AllArgsConstructor
public class InfraWalletCreateApiResponse {

    /** wallet_addresses.id */
    private Integer addressId;

    /** 지갑 주소 */
    private String address;

    /** 네트워크 ID */
    private Integer networkId;

    /** 지갑 타입 */
    private String walletType;

    /** HD 파생 경로 */
    private String derivationPath;

    /** 기존 지갑 여부 (true면 이미 존재하던 것) */
    private Boolean existing;
}
```

### 3. `BlockchainApiClient.java` — 새 API 추가

**경로**: `common/src/main/java/com/cryptoments/common/client/BlockchainApiClient.java`

기존 `createAdminWallet` 아래에 추가:

```java
    /** 범용 인프라 지갑 생성 (ADMIN/GAS — approve 불필요 대상만) */
    @XRestAPI(value = "/api/admin/wallet/create-infra", method = XHttpMethod.POST)
    InfraWalletCreateApiResponse createInfraWallet(@RequestBody InfraWalletCreateApiRequest request);
```

### 4. `InfraWalletService.java` — GAS 구현 (SETTLEMENT 제거)

**경로**: `admin-api/src/main/java/com/cryptoments/adminapi/service/InfraWalletService.java`

GAS만 `create-infra` 연동. SETTLEMENT 메서드는 **삭제** (별도 흐름으로 이동 예정).

```java
    /**
     * GAS 지갑 생성 (네트워크당 1개).
     * 해당 네트워크에 이미 GAS 지갑이 존재하면 기존 지갑 정보를 반환한다.
     */
    public InfraWalletCreateApiResponse createGasWallet(Long networkId, Long hdWalletId, Long adminId) {
        InfraWalletCreateApiResponse response;
        try {
            response = blockchainApiClient.createInfraWallet(
                    InfraWalletCreateApiRequest.builder()
                            .networkId(networkId.intValue())
                            .hdWalletId(hdWalletId.intValue())
                            .walletType("GAS")
                            .build());
        } catch (XRestException e) {
            log.error("GAS 지갑 생성 실패: networkId={}, error={}", networkId, e.getMessage());
            throw new ExternalServiceException(ErrorCodes.BLOCKCHAIN_API_ERROR, e.getMessage());
        }

        if (!Boolean.TRUE.equals(response.getExisting())) {
            auditLogService.log(adminId, AuditAction.CREATE_GAS_WALLET,
                    response.getAddressId() != null ? response.getAddressId().longValue() : null,
                    Map.of("networkId", networkId,
                            "walletType", "GAS",
                            "address", response.getAddress()));
        }

        return response;
    }

    // SETTLEMENT 지갑은 approve + GAS Support 대상이므로 create-infra 사용 안 함.
    // MASTER/SETTLEMENT/HOT/POOL은 생성 후 wallet_approvals PENDING 등록 → wallet-activator 처리.
```

### 5. `InfraWalletController.java` — SETTLEMENT 엔드포인트 제거

**경로**: `admin-api/src/main/java/com/cryptoments/adminapi/controller/InfraWalletController.java`

GAS만 유지. SETTLEMENT 엔드포인트는 **삭제**.

```java
    /**
     * GAS 지갑 생성 (네트워크당 1개, blockchain-api 연동).
     * 해당 네트워크에 이미 GAS 지갑이 존재하면 기존 지갑 정보를 반환한다.
     *
     * @param request 지갑 생성 요청 (networkId, hdWalletId)
     * @return GAS 지갑 정보 (신규 생성 또는 기존)
     * @response 200 성공
     * @group 인프라 지갑
     * @auth true
     */
    @PostMapping(name = "GAS 지갑 생성", value = "/gas")
    public InfraWalletCreateApiResponse createGasWallet(@Valid @RequestBody AdminWalletCreateRequest request) {
        return infraWalletService.createGasWallet(request.getNetworkId(), request.getHdWalletId(), getSession().getAdminId());
    }

    // SETTLEMENT 엔드포인트 삭제 — approve + GAS Support 대상이므로
    // MASTER/SETTLEMENT/HOT/POOL 지갑은 별도 생성 흐름 (wallet-activator 자동 처리)
```

**참고**: `AdminWalletCreateRequest`를 재사용 (이미 `networkId`, `hdWalletId` 필드 포함).
`InfraWalletCreateRequest` (기존, networkId만 있음)는 삭제하거나 deprecate.

---

## Relayer 지갑은 별도 흐름

Relayer 지갑은 단순 HD 파생 외에 추가 작업이 필요하므로 기존 `RelayerManagementController`에서 처리:

1. HD 파생으로 지갑 생성 (create-infra 활용 가능, walletType=SYSTEM)
2. `relayer_wallets` 테이블 INSERT (role, priority, contract_address 등)
3. 온체인 `addRelayer(relayer_address)` TX 실행

현재 `RelayerManagementController.createRelayer()` → `RelayerManagementService.createRelayer()` 경로가 이미 존재하며, relayer-api를 호출하는 구조.

---

## ⚠️ 이미 적용된 코드 정리 필요

IntelliJ에서 이미 적용한 SETTLEMENT 관련 코드를 제거해야 합니다:

### InfraWalletService.java
- `createSettlementWallet()` 메서드 **삭제**

### InfraWalletController.java
- `@PostMapping("/settlement")` 엔드포인트 **삭제**

### blockchain-api system-admin.ts
- `INFRA_WALLET_TYPES` 배열에서 `'SETTLEMENT'` **제거** → `['ADMIN', 'GAS']`만 남김

---

## 빌드 & 테스트

### Node.js

```bash
cd node-service
pnpm build
# blockchain-api 재시작

# 직접 테스트
curl -s -X POST http://localhost:3001/api/admin/wallet/create-infra \
  -H "Content-Type: application/json" \
  -d '{"networkId": 2, "hdWalletId": 1, "walletType": "GAS"}'
```

### Spring Boot

```bash
./gradlew :common:compileJava :admin-api:compileJava
# admin-api 재시작

# admin-api 경유 테스트
TOKEN=<login-token>
curl -s -X POST http://localhost:8080/api/admin/infra-wallets/gas \
  -H "Content-Type: application/json" \
  -H "Access-Token: $TOKEN" \
  -d '{"networkId": 2, "hdWalletId": 1}'
```

### DB 확인

```sql
SELECT id, address, network_id, wallet_type, derivation_path
FROM wallet_addresses
WHERE wallet_type IN ('ADMIN', 'GAS')
ORDER BY network_id, wallet_type;
```
