# Guide #73 — Blockchain Monitor 주소 등록

> **대상**: core 모듈 + admin-api/partner-api (Spring Boot)
> **참고**: v1 `ExternalMonitorService`, `MonitoredAddressRequestDTO`
> **DDL 컬럼**: `wallet_addresses.monitor_registered`, `wallet_addresses.monitor_registration_id`

---

## 설계 요약

v1의 `ExternalMonitorService`를 v2 core 모듈로 포팅.
v1과 동일한 외부 monitor API를 호출하되, **chainType 하드코딩 → `blockchain_networks.chain_id` DB 조회**로 변경.

**호출 시점**:
- 지갑 생성 직후 (`WalletService.createAllWalletsForPartner()`)
- HOT 지갑 개별 생성 시 (`createHotWallet()`)

**실패 처리**: monitor 등록 실패해도 지갑 생성은 성공. 로그만 남기고 `monitor_registered=false` 유지.
이후 admin-api 수동 재등록 또는 배치로 보정.

---

## Part A. application.yml 설정 추가

**파일**: `core/src/main/resources/application.yml` (또는 공통 설정)

```yaml
external:
  monitor:
    service:
      base-url: http://localhost:8080
      service-id: 1
```

> partner-api, admin-api에서 core를 의존하므로 core에 설정하면 전파됨.
> 환경별로 다르면 `application-{profile}.yml`에서 오버라이드.

---

## Part B. MonitorRegistrationService 생성 (core 모듈)

**파일**: `core/src/main/java/com/cryptoments/core/monitor/MonitorRegistrationService.java`

v1의 `ExternalMonitorService`를 v2 패턴으로 포팅. RestTemplate 대신 **Axim RestClient** 사용.

```java
package com.cryptoments.core.monitor;

import com.cryptoments.common.entity.BlockchainNetwork;
import com.cryptoments.common.entity.WalletAddress;
import com.cryptoments.common.repository.BlockchainNetworkRepository;
import com.cryptoments.common.repository.WalletAddressRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.List;
import java.util.Map;

/**
 * 외부 blockchain_monitor 서비스에 주소 등록/삭제.
 *
 * <p>API: POST/DELETE {baseUrl}/api/v1/service/{serviceId}/addresses
 * <p>Body: { "chainId": 56, "address": "0x..." }
 */
@Slf4j
@Service
public class MonitorRegistrationService {

    private final RestClient restClient;
    private final BlockchainNetworkRepository blockchainNetworkRepository;
    private final WalletAddressRepository walletAddressRepository;

    @Value("${external.monitor.service.base-url:http://localhost:8080}")
    private String monitorBaseUrl;

    @Value("${external.monitor.service.service-id:1}")
    private Integer serviceId;

    public MonitorRegistrationService(RestClient restClient,
                                       BlockchainNetworkRepository blockchainNetworkRepository,
                                       WalletAddressRepository walletAddressRepository) {
        this.restClient = restClient;
        this.blockchainNetworkRepository = blockchainNetworkRepository;
        this.walletAddressRepository = walletAddressRepository;
    }

    /**
     * 단건 주소 등록.
     *
     * @return true=성공, false=실패 (지갑 생성에 영향 없음)
     */
    public boolean register(WalletAddress wallet) {
        BlockchainNetwork network = blockchainNetworkRepository.findOne(wallet.getNetworkId());
        if (network == null) {
            log.warn("Monitor 등록 실패 — 네트워크 없음: networkId={}", wallet.getNetworkId());
            return false;
        }

        try {
            String url = String.format("%s/api/v1/service/%d/addresses", monitorBaseUrl, serviceId);

            restClient.post()
                    .uri(url)
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(Map.of(
                            "chainId", network.getChainId(),
                            "address", wallet.getAddress()
                    ))
                    .retrieve()
                    .toBodilessEntity();

            // DB 플래그 업데이트
            wallet.setMonitorRegistered(true);
            walletAddressRepository.modify(wallet);

            log.info("Monitor 등록 성공: address={}, chain={}", wallet.getAddress(), network.getChainSymbol());
            return true;

        } catch (Exception e) {
            log.error("Monitor 등록 실패: address={}, chain={}", wallet.getAddress(), network.getChainSymbol(), e);
            return false;
        }
    }

    /**
     * 단건 주소 해제.
     */
    public boolean unregister(WalletAddress wallet) {
        BlockchainNetwork network = blockchainNetworkRepository.findOne(wallet.getNetworkId());
        if (network == null) return false;

        try {
            String url = String.format("%s/api/v1/service/%d/addresses", monitorBaseUrl, serviceId);

            restClient.method(org.springframework.http.HttpMethod.DELETE)
                    .uri(url)
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(Map.of(
                            "chainId", network.getChainId(),
                            "address", wallet.getAddress()
                    ))
                    .retrieve()
                    .toBodilessEntity();

            wallet.setMonitorRegistered(false);
            walletAddressRepository.modify(wallet);

            log.info("Monitor 해제 성공: address={}, chain={}", wallet.getAddress(), network.getChainSymbol());
            return true;

        } catch (Exception e) {
            log.error("Monitor 해제 실패: address={}, chain={}", wallet.getAddress(), network.getChainSymbol(), e);
            return false;
        }
    }

    /**
     * 다건 일괄 등록 (파트너 지갑 전체, 서비스 재기동 시 등).
     */
    public void registerAll(List<WalletAddress> wallets) {
        int success = 0;
        for (WalletAddress wallet : wallets) {
            if (register(wallet)) success++;
            try { Thread.sleep(100); } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        log.info("Monitor 일괄 등록 완료: 성공={}/{}", success, wallets.size());
    }

    /**
     * monitor_registered=false인 모든 주소 재등록 (배치/수동 보정용).
     */
    public void registerUnregistered() {
        List<WalletAddress> unregistered = walletAddressRepository.findByMonitorRegistered(false);
        if (unregistered.isEmpty()) {
            log.info("Monitor 미등록 주소 없음");
            return;
        }
        log.info("Monitor 미등록 주소 {}건 등록 시작", unregistered.size());
        registerAll(unregistered);
    }
}
```

---

## Part C. WalletAddressRepository에 findBy 메서드 추가

**파일**: `common/src/main/java/com/cryptoments/common/repository/WalletAddressRepository.java`

```java
/** monitor_registered=false인 주소 목록 */
List<WalletAddress> findByMonitorRegistered(Boolean monitorRegistered);
```

---

## Part D. WalletService에 monitor 등록 연동

**파일**: `core/src/main/java/com/cryptoments/core/wallet/WalletService.java`

### D-1. 의존성 주입

```java
private final MonitorRegistrationService monitorRegistrationService;

// 생성자에 추가
public WalletService(..., MonitorRegistrationService monitorRegistrationService) {
    ...
    this.monitorRegistrationService = monitorRegistrationService;
}
```

### D-2. createAllWalletsForPartner() 수정

지갑 생성 성공 후 monitor 등록 추가. **기존 216~247행** 내부, `results.add(...)` 직후에 추가:

```java
// 기존 코드
results.add(WalletCreationResult.builder()
        .networkId(networkId)
        .chainSymbol(network.getChainSymbol())
        .masterAddress(master.getAddress())
        .masterWalletId(master.getId())
        .hotAddress(hot.getAddress())
        .hotWalletId(hot.getId())
        .build());

// ↓ 추가: monitor 등록 (실패해도 지갑 생성은 유효)
monitorRegistrationService.register(master);
monitorRegistrationService.register(hot);

log.info("Partner wallets created: partnerId={}, network={}", partnerId, network.getChainSymbol());
```

### D-3. createHotWallet() 에도 동일 적용

HOT 지갑을 개별 생성하는 경우에도 monitor 등록 필요. `createHotWallet()` 메서드 반환 직전에 추가:

```java
// 기존 반환 직전에 추가
monitorRegistrationService.register(walletAddress);
return walletAddress;
```

---

## Part E. admin-api — 수동 재등록 엔드포인트 (선택)

monitor 서비스 장애 복구 후 미등록 주소 일괄 재등록용.

**파일**: `admin-api/.../controller/WalletController.java` (기존 컨트롤러에 추가)

```java
/**
 * Monitor 미등록 주소 일괄 재등록.
 *
 * @response 200 재등록 완료
 * @group 지갑
 * @auth true
 */
@PostMapping(name = "Monitor 미등록 주소 재등록", value = "/wallets/monitor/register-unregistered")
public void registerUnregisteredMonitor() {
    monitorRegistrationService.registerUnregistered();
}
```

---

## 체크리스트

| # | 항목 | 모듈 | 파일 |
|---|------|------|------|
| A | application.yml 설정 추가 | core | application.yml |
| B | MonitorRegistrationService 생성 | core | core/monitor/ |
| C | findByMonitorRegistered() 추가 | common | WalletAddressRepository |
| D-1 | WalletService DI 추가 | core | WalletService |
| D-2 | createAllWalletsForPartner()에 register 호출 | core | WalletService |
| D-3 | createHotWallet()에 register 호출 | core | WalletService |
| E | admin-api 수동 재등록 EP (선택) | admin-api | WalletController |

## 구현 순서

1. **Part A** — yml 설정
2. **Part C** — Repository 메서드
3. **Part B** — MonitorRegistrationService 생성
4. **Part D** — WalletService 연동
5. **Part E** — admin-api 재등록 EP (선택)
6. 빌드 확인: `./gradlew :core:compileJava :admin-api:compileJava :partner-api:compileJava`

## 참고: v1 → v2 차이점

| 항목 | v1 | v2 |
|------|----|----|
| chainId 매핑 | `MonitoredAddressRequestDTO.fromChainType()` 하드코딩 | `blockchain_networks.chain_id` DB 조회 |
| HTTP Client | `RestTemplate` | `RestClient` (Axim) |
| 설정 | `external.monitor.service.*` | **동일** |
| API 경로 | `POST/DELETE /api/v1/service/{serviceId}/addresses` | **동일** |
| Body | `{ chainId, address }` | **동일** |
| 등록 상태 추적 | 별도 테이블 `monitor_address_registrations` | `wallet_addresses.monitor_registered` 컬럼 |
| 호출 시점 | `WalletGenerationService` 지갑 생성 후 | `WalletService.createAllWalletsForPartner()` + `createHotWallet()` |
