# HD Derivation — Partner Namespace 재설계

## 문제

현재 모든 지갑(인프라 + 파트너)이 단일 공간 `m/basePath/0/{index}`에서 순차 파생.
`wallet_index_manager`가 `(hd_wallet_id, network_id, wallet_type)` 별 독립 카운터 →
동일 index가 중복 사용되어 주소 충돌 발생.

## 새 설계: Partner Namespace

BIP-44 change 레벨을 파트너 네임스페이스로 활용:

```
현재:  m/44'/60'/1'/0/{index}                  ← 전부 한 공간
변경:  m/44'/60'/1'/{partnerId | 0}/{index}    ← 파트너별 독립 공간
```

### 파생 경로 구조

```
m / 44' / coinType' / account' / {partnerId | 0} / {index}
                                  ↑ BIP-44 change 레벨을 namespace로 활용

partnerId = 0   → 시스템/인프라 지갑 (ADMIN, GAS, RELAYER)
partnerId = N   → 파트너 N의 모든 지갑 (MASTER, HOT, POOL)
```

### 파트너별 주소 공간 예시

```
=== 시스템 (partnerId=0) ===
m/44'/60'/1'/0/0  → ADMIN
m/44'/60'/1'/0/1  → GAS
m/44'/60'/1'/0/2  → RELAYER

=== 파트너 7 ===
m/44'/60'/1'/7/0  → MASTER
m/44'/60'/1'/7/1  → HOT
m/44'/60'/1'/7/2  → POOL

=== 파트너 8 ===
m/44'/60'/1'/8/0  → MASTER
m/44'/60'/1'/8/1  → HOT
m/44'/60'/1'/8/2  → POOL
```

### wallet_index_manager 키 구조

| 키 | 변경 |
|---|---|
| **UNIQUE KEY** | `(hd_wallet_id, network_id, wallet_type)` → `(hd_wallet_id, network_id, partner_id)` |
| **partner_id** | 0 = 시스템 인프라, N = 파트너 ID |
| **wallet_type** | 컬럼 제거 — 인덱스 관리에 불필요 |

---

## 기존 데이터 마이그레이션

### 현재 wallet_addresses (network_id=2)

| derivation_index | wallet_type | partner_id | 현재 경로 |
|---|---|---|---|
| 0 | ADMIN | NULL | m/44'/60'/1'/0/0 |
| 1 | GAS | NULL | m/44'/60'/1'/0/1 |
| 3 | RELAYER | NULL | m/44'/60'/1'/0/3 |
| 4 | MASTER | 7 | m/44'/60'/1'/0/4 |
| 5 | HOT | 7 | m/44'/60'/1'/0/5 |
| 6 | POOL | 7 | m/44'/60'/1'/0/6 |

### 마이그레이션 후 (새 파생만 새 경로 적용)

기존 지갑은 이미 `m/.../0/{index}` 로 파생되어 있고, 개인키가 이 경로 기반.
**기존 데이터는 그대로 유지** — DB의 `derivation_path` 컬럼에 원래 경로가 기록되어 있으므로
키 복원 시 이 값을 사용하면 됨.

**새로 파생하는 지갑부터** 새 경로 적용:
- 파트너 7의 다음 지갑 → `m/44'/60'/1'/7/0` (새 namespace에서 index 0부터)
- 시스템의 다음 지갑 → `m/44'/60'/1'/0/{기존 max+1}`

### wallet_index_manager 마이그레이션

기존 데이터를 새 구조로 통합:

```sql
-- 1. 임시 테이블: partner_id별 MAX(derivation_index) from wallet_addresses
CREATE TEMPORARY TABLE tmp_index_new AS
SELECT
    wa.hd_wallet_id,
    wa.network_id,
    COALESCE(wa.partner_id, 0) AS partner_id,
    MAX(wa.derivation_index) AS last_index
FROM wallet_addresses wa
WHERE wa.hd_wallet_id IS NOT NULL
GROUP BY wa.hd_wallet_id, wa.network_id, COALESCE(wa.partner_id, 0);

-- 시스템 지갑 (hd_wallet_id IS NULL → 네트워크의 기본 hd_wallet 사용)
INSERT INTO tmp_index_new (hd_wallet_id, network_id, partner_id, last_index)
SELECT
    bn.id AS hd_wallet_id,  -- hd_wallets.id = blockchain_networks.id (현재 구조)
    wa.network_id,
    0 AS partner_id,
    MAX(wa.derivation_index) AS last_index
FROM wallet_addresses wa
JOIN blockchain_networks bn ON wa.network_id = bn.id
WHERE wa.hd_wallet_id IS NULL
  AND wa.wallet_type IN ('ADMIN', 'GAS', 'RELAYER')
GROUP BY bn.id, wa.network_id
ON DUPLICATE KEY UPDATE last_index = GREATEST(last_index, VALUES(last_index));

-- 2. 기존 wallet_index_manager 데이터 교체
TRUNCATE TABLE wallet_index_manager;

-- 3. ALTER TABLE
ALTER TABLE wallet_index_manager
    DROP KEY `uk_combo`,
    DROP COLUMN `wallet_type`,
    ADD COLUMN `partner_id` BIGINT NOT NULL DEFAULT 0 COMMENT '0=시스템, N=파트너ID',
    ADD UNIQUE KEY `uk_hd_network_partner` (`hd_wallet_id`, `network_id`, `partner_id`);

-- 4. 통합 데이터 복원
INSERT INTO wallet_index_manager (hd_wallet_id, network_id, partner_id, last_index)
SELECT hd_wallet_id, network_id, partner_id, last_index FROM tmp_index_new;

DROP TEMPORARY TABLE tmp_index_new;

-- 5. 코멘트 업데이트
ALTER TABLE wallet_index_manager
    COMMENT = '지갑 인덱스 카운터 — HD지갑 × 네트워크 × 파트너 (v1.8)';
```

### 마이그레이션 후 예상 데이터

**주의**: 기존 지갑은 전부 `partnerId=0` 공간(change=0)에서 파생됨.
파트너 7 지갑도 `m/.../0/4`, `m/.../0/5`, `m/.../0/6` 으로 파생되어 있음.
→ `partner_id=0`의 last_index가 가장 큰 값(6)을 포함해야 함.

| hd_wallet_id | network_id | partner_id | last_index | 설명 |
|---|---|---|---|---|
| 2 | 2 | 0 | 6 | 기존 모든 지갑 (system + partner 7 기존분) |
| 3 | 3 | 0 | 4 | 기존 모든 지갑 |
| 4 | 4 | 0 | 4 | 기존 모든 지갑 |

→ 파트너 7의 새 namespace 행은 아직 없음 (첫 파생 시 `partner_id=7, last_index=0` INSERT)

---

## 코드 변경

### 1. Node.js — HdDerivation.ts

```typescript
// Before
derive(seed: Buffer, basePath: string, chainSymbol: ChainSymbol, index: number): DeriveResult {
    const path = `${basePath}/0/${index}`;
    // ...
}

// After — partnerId 파라미터 추가
derive(seed: Buffer, basePath: string, chainSymbol: ChainSymbol, partnerId: number, index: number): DeriveResult {
    const path = `${basePath}/${partnerId}/${index}`;
    // ...
}
```

### 2. Node.js — WalletIndexManagerRepo.ts

```typescript
// Before
async getNextIndex(hdWalletId: number, networkId: number, walletType = 'HOT'): Promise<number> {
    // WHERE hd_wallet_id = ? AND network_id = ? AND wallet_type = ?

// After — partnerId 기반
async getNextIndex(hdWalletId: number, networkId: number, partnerId: number = 0): Promise<number> {
    const rows = await query<WalletIndexManagerRow[]>(
        `SELECT * FROM wallet_index_manager
         WHERE hd_wallet_id = ? AND network_id = ? AND partner_id = ?`,
        [hdWalletId, networkId, partnerId],
    );

    if (!rows[0]) {
        // 첫 파생: index 0부터 시작 (새 namespace이므로 기존 주소와 겹칠 일 없음)
        await execute(
            `INSERT INTO wallet_index_manager (hd_wallet_id, network_id, partner_id, last_index)
             VALUES (?, ?, ?, 0)`,
            [hdWalletId, networkId, partnerId],
        );
        return 0;
    }

    const nextIndex = Number(rows[0].last_index) + 1;
    await execute(
        `UPDATE wallet_index_manager SET last_index = ?
         WHERE hd_wallet_id = ? AND network_id = ? AND partner_id = ?`,
        [nextIndex, hdWalletId, networkId, partnerId],
    );
    return nextIndex;
}
```

**핵심 변경**: 새 namespace(partner_id > 0)는 항상 index 0부터 시작.
기존 `MAX(derivation_index)` 조회 로직 불필요 — namespace가 다르면 주소 공간이 분리됨.

### 3. Node.js — WalletDerivationService.ts

```typescript
// Before
export interface DeriveRequest {
    networkId: number;
    hdWalletId: number;
    derivationIndex?: number;
    walletType?: string;
    partnerId?: number | null;
    partnerUserId?: string | null;
}

export async function deriveWallet(req: DeriveRequest): Promise<DeriveResponse> {
    // ...
    const derivationIndex =
        req.derivationIndex ?? (await walletIndexManagerRepo.getNextIndex(
            req.hdWalletId, req.networkId, req.walletType || 'HOT'));

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

// After
export async function deriveWallet(req: DeriveRequest): Promise<DeriveResponse> {
    // ...
    const namespaceId = req.partnerId ?? 0;  // 0 = system, N = partner

    const derivationIndex =
        req.derivationIndex ?? (await walletIndexManagerRepo.getNextIndex(
            req.hdWalletId, req.networkId, namespaceId));

    const derived = hdDerivation.derive(
        seed, hdWallet.derivation_base_path, chainSymbol, namespaceId, derivationIndex);
    // ...
}
```

### 4. Node.js — system-admin.ts (인프라 지갑)

ADMIN, GAS, RELAYER 생성 시 `partnerId=0` 명시:

```typescript
// Before (ADMIN wallet)
const derivationIndex = await walletIndexManagerRepo.getNextIndex(hdWalletId, networkId);
const derived = hdDerivation.derive(seed, hdWallet.derivation_base_path, chainSymbol, derivationIndex);

// After
const derivationIndex = await walletIndexManagerRepo.getNextIndex(hdWalletId, networkId, 0);
const derived = hdDerivation.derive(seed, hdWallet.derivation_base_path, chainSymbol, 0, derivationIndex);
```

### 5. Node.js — relayer.ts (Relayer 지갑)

```typescript
// Before
const derivationIndex = await walletIndexManagerRepo.getNextIndex(hdWalletId, networkId);
const derived = hdDerivation.derive(seed, hdWallet.derivation_base_path, chainSymbol, derivationIndex);

// After
const derivationIndex = await walletIndexManagerRepo.getNextIndex(hdWalletId, networkId, 0);
const derived = hdDerivation.derive(seed, hdWallet.derivation_base_path, chainSymbol, 0, derivationIndex);
```

### 6. Node.js — wallet.ts 타입

```typescript
// wallet_index_manager 타입에서 wallet_type → partner_id
export interface WalletIndexManager {
    id: number;
    network_id: number;
    hd_wallet_id: number;
    partner_id: number;  // 0=system, N=partner
    last_index: number;
    created_at: string;
    updated_at: string;
}
```

### 7. Spring Boot — WalletIndexManager Entity

```java
// Before
@XColumn("wallet_type")
private String walletType;

// After
@XColumn("partner_id")
private Long partnerId;  // 0=시스템, N=파트너ID
```

### 8. Spring Boot — WalletIndexManagerRepository

```java
// Before
WalletIndexManager findByHdWalletIdAndNetworkIdAndWalletType(Long hdWalletId, Long networkId, String walletType);

// After
WalletIndexManager findByHdWalletIdAndNetworkIdAndPartnerId(Long hdWalletId, Long networkId, Long partnerId);
```

---

## 호출부 변경 요약

| 파일 | 현재 호출 | 변경 후 |
|---|---|---|
| `WalletDerivationService.ts` | `getNextIndex(hd, net, walletType)` | `getNextIndex(hd, net, partnerId ?? 0)` |
| `system-admin.ts` (ADMIN) | `getNextIndex(hd, net)` | `getNextIndex(hd, net, 0)` |
| `system-admin.ts` (create-infra) | `getNextIndex(hd, net)` | `getNextIndex(hd, net, 0)` |
| `relayer.ts` | `getNextIndex(hd, net)` | `getNextIndex(hd, net, 0)` |
| `HdDerivation.derive()` | `(seed, base, chain, index)` | `(seed, base, chain, partnerId, index)` |

---

## v2-docs/CRYPTOMENTS_V2_DDL.sql 변경

```sql
CREATE TABLE wallet_index_manager (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'PK',
    hd_wallet_id BIGINT NOT NULL DEFAULT 0 COMMENT 'hd_wallets.id — HD 지갑 마스터 참조',
    network_id BIGINT NOT NULL COMMENT 'blockchain_networks.id 참조',
    partner_id BIGINT NOT NULL DEFAULT 0
        COMMENT '파트너 네임스페이스 — 0=시스템(ADMIN/GAS/RELAYER), N=파트너ID',
    last_index BIGINT NOT NULL DEFAULT 0 COMMENT '마지막 할당된 인덱스',
    created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
    updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    UNIQUE KEY uk_hd_network_partner (hd_wallet_id, network_id, partner_id)
) COMMENT = '지갑 인덱스 카운터 — HD지갑 × 네트워크 × 파트너 namespace (v1.8)';
```

---

## 적용 순서

1. **Node.js 코드 수정** — WalletIndexManagerRepo, HdDerivation, WalletDerivationService, system-admin, relayer 호출부
2. **Node.js 배포** (이 시점에는 아직 DB에 wallet_type 컬럼 존재 — 하지만 코드가 사용 안 함)
3. **DDL ALTER 실행** — wallet_type 제거 + partner_id 추가 + 데이터 마이그레이션
4. **Spring Boot 코드 수정** — Entity, Repository
5. **Spring Boot 배포**

## 체크리스트

### DDL
- [ ] 마이그레이션 SQL 작성 + 테스트 (dev 환경)
- [ ] 운영 DB 적용
- [ ] `v2-docs/CRYPTOMENTS_V2_DDL.sql` 업데이트 (v1.8)

### Node.js
- [ ] `HdDerivation.ts` — derive() 에 partnerId 파라미터 추가
- [ ] `WalletIndexManagerRepo.ts` — walletType → partnerId, 새 namespace는 index 0 시작
- [ ] `WalletDerivationService.ts` — namespaceId = partnerId ?? 0
- [ ] `system-admin.ts` — ADMIN/GAS create에 partnerId=0 명시
- [ ] `relayer.ts` — RELAYER create에 partnerId=0 명시
- [ ] `wallet.ts` 타입 — wallet_type → partner_id
- [ ] 빌드 + 테스트

### Spring Boot
- [ ] `WalletIndexManager` Entity — walletType → partnerId
- [ ] `WalletIndexManagerRepository` — findBy 메서드 변경
- [ ] admin-api Mapper — wallet_type 관련 필터 변경
- [ ] 빌드 확인

### 테스트
- [ ] 시스템 지갑 파생 (ADMIN, GAS) → partnerId=0 경로, 기존 인덱스 이후부터
- [ ] 파트너 7 신규 지갑 → partnerId=7, index=0 부터 새 경로
- [ ] 파트너 8 신규 지갑 → partnerId=8, index=0 부터 새 경로
- [ ] 같은 파트너 내 MASTER+HOT+POOL → 인덱스 순차 증가, 주소 충돌 없음
