# Node.js v2 Fresh Start 지침서

**대상**: VS Code (node-service)
**배경**: v1 마이그레이션 데이터를 전체 리셋하고 v2 방식으로 재생성
**날짜**: 2026-03-19

---

## 변경 사항 요약

| # | 내용 | 이유 |
|---|------|------|
| 1 | `seed-hd-wallet.ts` — `blockchain_networks` DB에서 `derivation_base_path` 읽기 | 하드코딩 `COIN_TYPES` 제거 |
| 2 | `KeyManager.ts` — v1 호환 코드 제거, v2 단순 버전으로 원복 | v1 데이터 없으므로 불필요 |
| 3 | `config/index.ts` — `ENCRYPTION_MASTER_KEY_V1` 제거 | v1 키 불필요 |
| 4 | `system-admin.ts` — `0x` 접두사 제거 코드 정리 | v2 encrypt는 0x 없음 |

---

## 변경 1: `seed-hd-wallet.ts` — DB에서 derivation_base_path 읽기

**경로**: `node-service/packages/blockchain-api/scripts/seed-hd-wallet.ts`

### 전체 교체 코드:

```typescript
/**
 * HD 지갑 시드 생성 & hd_wallets 테이블 삽입
 *
 * blockchain_networks.derivation_base_path를 읽어서 hd_wallets에 저장.
 * 하드코딩 없이 DB 마스터 데이터 기반으로 동작.
 *
 * 실행: pnpm tsx scripts/seed-hd-wallet.ts
 */

import { ethers } from 'ethers';
import crypto from 'node:crypto';
import { keyManager, getPool } from '@cryptoments/common';

async function main() {
  const pool = getPool();

  // 1. 활성 네트워크 조회 (derivation_base_path 포함)
  const [networks] = await pool.query(
    'SELECT id, chain_symbol, name, derivation_base_path FROM blockchain_networks WHERE is_active = 1',
  );
  console.log('Active networks:', networks);

  // 2. 니모닉 생성 (24단어)
  const wallet = ethers.Wallet.createRandom();
  const mnemonic = wallet.mnemonic!.phrase;
  console.log('\n⚠️  MNEMONIC (안전한 곳에 백업 필수):');
  console.log(`   ${mnemonic}\n`);

  // 3. BIP-39 시드 생성 (PBKDF2)
  const seed = mnemonicToSeed(mnemonic);
  const seedHex = Buffer.from(seed).toString('hex');
  console.log('Seed hex length:', seedHex.length, '(expected 128 = 64 bytes)');

  // 4. 시드 암호화 (v2: AES-256-GCM, hex 인코딩)
  const encryptedSeed = await keyManager.encrypt(seedHex);
  console.log('Encrypted seed length:', encryptedSeed.length);

  // 5. 복호화 검증
  const decrypted = await keyManager.decrypt(encryptedSeed);
  if (decrypted !== seedHex) {
    throw new Error('Encrypt/decrypt round-trip failed!');
  }
  console.log('Encrypt/decrypt round-trip OK');

  // 6. 네트워크별 hd_wallets 삽입 — DB의 derivation_base_path 사용
  for (const net of networks as any[]) {
    if (!net.derivation_base_path) {
      console.log(`Skip ${net.chain_symbol}: derivation_base_path not set`);
      continue;
    }

    // 중복 체크
    const [existing] = await pool.query(
      'SELECT id FROM hd_wallets WHERE network_id = ? AND status = ?',
      [net.id, 'ACTIVE'],
    );
    if ((existing as any[]).length > 0) {
      console.log(`HD wallet already exists for ${net.chain_symbol} (network_id=${net.id}), skipping`);
      continue;
    }

    const [result] = await pool.execute(
      `INSERT INTO hd_wallets
       (network_id, master_seed_encrypted, encryption_algorithm, derivation_base_path, current_index, max_index, status)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
      [net.id, encryptedSeed, 'AES-256-GCM', net.derivation_base_path, 0, 10000000, 'ACTIVE'],
    );
    const insertId = (result as any).insertId;
    console.log(`Inserted hd_wallet id=${insertId} for ${net.chain_symbol} (network_id=${net.id}, path=${net.derivation_base_path})`);
  }

  console.log('\nDone! HD wallets seeded.');
  await pool.end();
}

/**
 * BIP-39 니모닉 → 64바이트 시드 (PBKDF2)
 */
function mnemonicToSeed(mnemonic: string, passphrase = ''): Buffer {
  const mnemonicBuffer = Buffer.from(mnemonic.normalize('NFKD'), 'utf8');
  const salt = Buffer.from(`mnemonic${passphrase}`.normalize('NFKD'), 'utf8');
  return crypto.pbkdf2Sync(mnemonicBuffer, salt, 2048, 64, 'sha512');
}

main().catch((err) => {
  console.error('Error:', err);
  process.exit(1);
});
```

### 변경 요점:
- `COIN_TYPES` 하드코딩 맵 제거
- `blockchain_networks.derivation_base_path`를 SELECT로 읽어서 `hd_wallets`에 그대로 저장
- `basePath` 생성 로직 없음 — DB 값 그대로 사용

---

## 변경 2: `KeyManager.ts` — v2 단순 버전으로 원복

**경로**: `node-service/packages/common/src/crypto/KeyManager.ts`

v1 데이터가 없으므로 v1 호환 코드(`decryptV1`, `getKeyBufferV1`, `isHexString`) 모두 제거.

### 전체 교체 코드:

```typescript
/**
 * 키 관리 모듈 — AES-256-GCM 로컬 암복호화
 *
 * 지갑 개인키를 DB에 암호화 저장/복호화 조회하는 단일 모듈.
 * 암호화 키(ENCRYPTION_MASTER_KEY)는 환경 변수로 주입.
 */

import crypto from 'node:crypto';
import { config } from '../config/index.js';

const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;

export class KeyManager {

  /** 암호화된 키 복호화 */
  async decrypt(encryptedHex: string): Promise<string> {
    const localKey = this.getKeyBuffer();
    const data = Buffer.from(encryptedHex, 'hex');

    const iv = data.subarray(0, IV_LENGTH);
    const authTag = data.subarray(data.length - AUTH_TAG_LENGTH);
    const ciphertext = data.subarray(IV_LENGTH, data.length - AUTH_TAG_LENGTH);

    const decipher = crypto.createDecipheriv(ALGORITHM, localKey, iv);
    decipher.setAuthTag(authTag);

    const decrypted = Buffer.concat([
      decipher.update(ciphertext),
      decipher.final(),
    ]);

    return decrypted.toString('utf8');
  }

  /** 평문 키 암호화 */
  async encrypt(plainText: string): Promise<string> {
    const localKey = this.getKeyBuffer();
    const iv = crypto.randomBytes(IV_LENGTH);

    const cipher = crypto.createCipheriv(ALGORITHM, localKey, iv);
    const encrypted = Buffer.concat([
      cipher.update(plainText, 'utf8'),
      cipher.final(),
    ]);
    const authTag = cipher.getAuthTag();

    // iv + ciphertext + authTag → hex
    return Buffer.concat([iv, encrypted, authTag]).toString('hex');
  }

  private getKeyBuffer(): Buffer {
    const keyHex = config.encryption.masterKey;
    if (!keyHex || keyHex.length !== 64) {
      throw new Error('ENCRYPTION_MASTER_KEY must be a 64-character hex string (256 bits)');
    }
    return Buffer.from(keyHex, 'hex');
  }
}

// 싱글톤
export const keyManager = new KeyManager();
```

---

## 변경 3: `config/index.ts` — v1 키 제거

**경로**: `node-service/packages/common/src/config/index.ts`

```typescript
// 변경 전
encryption: {
    masterKey: optionalEnv('ENCRYPTION_MASTER_KEY', ''),
    masterKeyV1: optionalEnv('ENCRYPTION_MASTER_KEY_V1', ''),  // v1 Java 원본 마스터 키
},

// 변경 후
encryption: {
    masterKey: optionalEnv('ENCRYPTION_MASTER_KEY', ''),
},
```

`.env`에서도 `ENCRYPTION_MASTER_KEY_V1` 줄 삭제 (또는 주석 처리).

---

## 변경 4: `system-admin.ts` — 0x 제거 코드 정리

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

v2에서 encrypt한 seed hex는 `0x` 접두사가 없으므로 `.replace(/^0x/, '')` 불필요.

```typescript
// 변경 전
const decryptedSeedHex = await keyManager.decrypt(hdWallet.master_seed_encrypted);
const seed = Buffer.from(decryptedSeedHex.replace(/^0x/, ''), 'hex');

// 변경 후
const seedHex = await keyManager.decrypt(hdWallet.master_seed_encrypted);
const seed = Buffer.from(seedHex, 'hex');
```

### `WalletDerivationService.ts`도 동일 적용

**경로**: `node-service/packages/blockchain-api/src/services/WalletDerivationService.ts`

```typescript
// 변경 전
const decryptedSeedHex = await keyManager.decrypt(hdWallet.master_seed_encrypted);
const seed = Buffer.from(decryptedSeedHex.replace(/^0x/, ''), 'hex');

// 변경 후
const seedHex = await keyManager.decrypt(hdWallet.master_seed_encrypted);
const seed = Buffer.from(seedHex, 'hex');
```

---

## 변경 5 (Spring Boot): `BlockchainNetwork` Entity 필드 추가

**대상**: IntelliJ (common 모듈)
**경로**: `common/src/main/java/com/cryptoments/common/entity/BlockchainNetwork.java`

DDL에 `coin_type`, `derivation_base_path` 컬럼이 추가되었으므로 Entity에 반영.

`blockConfirmationCount` 필드 아래, `isActive` 위에 추가:

```java
    /** 입금 최소 블록 확인 수 */
    private Integer blockConfirmationCount;

    // ★ v1.5 추가: HD 지갑 파생 경로
    /** BIP-44 coin type — ETH/BSC/POLYGON=60, TRON=195 */
    private Integer coinType;

    /** BIP-44 기본 파생 경로 (account index까지, 예: m/44'/60'/1') */
    private String derivationBasePath;

    /** 활성 여부 */
    private Boolean isActive;
```

### 수정 불필요

- `BlockchainNetworkUpdateRequest.java` — `coinType`/`derivationBasePath`는 초기 설정값이므로 수정 API 불필요
- `BlockchainNetworkRepository.java` — IXRepository 자동 매핑이므로 변경 없음

---

## 실행 순서

```bash
cd node-service

# 1. 위 4개 파일 수정

# 2. 빌드
pnpm build

# 3. seed-hd-wallet.ts 실행 (HD 지갑 생성)
pnpm tsx packages/blockchain-api/scripts/seed-hd-wallet.ts

# ⚠️ 출력되는 MNEMONIC을 반드시 안전한 곳에 백업!

# 4. blockchain-api 재시작

# 5. ADMIN 지갑 생성 테스트
curl -s -X POST http://localhost:3001/api/admin/wallet/create-admin \
  -H "Content-Type: application/json" \
  -d '{"networkId": 2, "hdWalletId": ?}'
# hdWalletId는 seed-hd-wallet.ts 출력에서 확인
```

---

## DB 상태 확인

```sql
-- HD 지갑 확인
SELECT id, network_id, derivation_base_path, current_index, status
FROM hd_wallets ORDER BY id;

-- 기대 결과:
-- id=1, network_id=2(BSC),     path=m/44'/60'/1',   index=0
-- id=2, network_id=3(POLYGON), path=m/44'/60'/3',   index=0
-- id=3, network_id=4(TRON),    path=m/44'/195'/2',  index=0
-- (ETH는 is_active=0이므로 생성 안 됨)
```
