# Node.js TRON Private Key 0x 접두사 수정 지침서

**대상**: VS Code (node-service 전체)
**우선순위**: Phase 0-3 블로커 — TRON Relayer 등록 실패 원인
**날짜**: 2026-03-19

---

## 문제

`ethers.HDNodeWallet.privateKey`는 항상 `0x` 접두사 포함 hex를 반환하고,
`keyManager.encrypt()`는 이를 그대로 저장한다.

- **EVM**: `ethers.Wallet(pk)` → `0x` 자동 처리 ✅
- **TronWeb**: `new TronWeb({ privateKey })` → `0x` **허용하지 않음** ❌ → `"Invalid private key provided"`

### 영향 범위

TronWeb에 private key를 전달하는 **모든 코드**에서 동일한 문제 발생:

| 파일 | 위치 | 용도 |
|------|------|------|
| `ContractManagementService.ts` | L88 `getAdminTronWeb()` | 컨트랙트 관리 전체 |
| `relayer.ts` (relayer-api) | L137, L247 | Relayer 등록/해제 |
| `CollectionPoller.ts` | L142 | 집금 TX 실행 |
| `WithdrawalPoller.ts` | L136 | 출금 TX 실행 |
| `TronProvider.ts` | L150, L166, L183 | approve, transferFrom, sendTrx |

---

## 수정 방안: 공통 유틸리티 함수

### 1. `common/src/utils/hex.ts` 생성

**경로**: `node-service/packages/common/src/utils/hex.ts`

```typescript
/**
 * 0x 접두사를 제거한 순수 hex 문자열 반환.
 * TronWeb은 0x 없는 hex만 허용.
 * EVM(ethers.js)은 0x 유무 모두 허용.
 */
export function stripHexPrefix(hex: string): string {
  return hex.startsWith('0x') ? hex.slice(2) : hex;
}
```

### 2. `common/src/index.ts`에 export 추가

```typescript
export { stripHexPrefix } from './utils/hex.js';
```

### 3. 각 파일에서 TronWeb에 PK 전달 시 `stripHexPrefix()` 적용

#### `ContractManagementService.ts` — `getAdminTronWeb()`

```typescript
// 변경 전 (L84, L88)
const privateKey = await keyManager.decrypt(walletEncryptedKey);
const tronWeb = new TronWeb({ fullHost: rpcUrl, privateKey });

// 변경 후
import { stripHexPrefix } from '@cryptoments/common';
// ...
const privateKey = await keyManager.decrypt(walletEncryptedKey);
const tronWeb = new TronWeb({ fullHost: rpcUrl, privateKey: stripHexPrefix(privateKey) });
```

#### `relayer.ts` (relayer-api) — register 핸들러 TVM 분기

```typescript
// 변경 전 (L128, L137)
const ownerPrivateKey = await keyManager.decrypt(encryptedOwnerKey);
// ...
const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: ownerPrivateKey });

// 변경 후
import { stripHexPrefix } from '@cryptoments/common';
// ...
const ownerPrivateKey = await keyManager.decrypt(encryptedOwnerKey);
// ...
const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: stripHexPrefix(ownerPrivateKey) });
```

#### `relayer.ts` — unregister 핸들러 TVM 분기 (동일 패턴)

```typescript
// L247
const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: stripHexPrefix(ownerPrivateKey) });
```

#### `CollectionPoller.ts` — 집금 TVM 분기

```typescript
// 변경 전 (L142)
const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: relayerPrivateKey });

// 변경 후
import { stripHexPrefix } from '@cryptoments/common';
// ...
const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: stripHexPrefix(relayerPrivateKey) });
```

#### `WithdrawalPoller.ts` — 출금 TVM 분기 (동일 패턴)

```typescript
// L136
const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: stripHexPrefix(relayerPrivateKey) });
```

#### `TronProvider.ts` — setPrivateKey 호출부

```typescript
// 변경 전 (L150, L166, L183)
this.tronWeb.setPrivateKey(params.privateKey);

// 변경 후
import { stripHexPrefix } from '../utils/hex.js';  // 또는 상대 경로
// ...
this.tronWeb.setPrivateKey(stripHexPrefix(params.privateKey));
// L166
this.tronWeb.setPrivateKey(stripHexPrefix(params.relayerPrivateKey));
// L183
this.tronWeb.setPrivateKey(stripHexPrefix(params.privateKey));
```

---

## 수정 대상 요약

| # | 파일 | 수정 내용 |
|---|------|----------|
| 1 | `common/src/utils/hex.ts` | **신규 생성** — `stripHexPrefix()` |
| 2 | `common/src/index.ts` | export 추가 |
| 3 | `ContractManagementService.ts` | L88 `stripHexPrefix(privateKey)` |
| 4 | `relayer.ts` (relayer-api) | L137, L247 `stripHexPrefix(ownerPrivateKey)` |
| 5 | `CollectionPoller.ts` | L142 `stripHexPrefix(relayerPrivateKey)` |
| 6 | `WithdrawalPoller.ts` | L136 `stripHexPrefix(relayerPrivateKey)` |
| 7 | `TronProvider.ts` | L150, L166, L183 `stripHexPrefix(params.privateKey)` |

---

## 빌드 & 검증

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

# 1. 컨트랙트 상태 조회 (TRON) — getAdminTronWeb 검증
curl -s http://localhost:3001/api/admin/contract/status/4

# 2. TRON Relayer 등록 재시도
#    먼저 실패한 relayer (id=5,6) 의 DB 상태를 정리하거나, 새로 생성
```

---

## TRON Relayer 재등록 절차

기존 TRON Relayer (id=5, id=6)가 REVOKED/PAUSED 상태이므로:

### 방법 A: DB 정리 후 재생성

```sql
-- 실패한 relayer_wallets 삭제
DELETE FROM relayer_wallets WHERE id IN (5, 6);
-- 실패한 SYSTEM 지갑도 삭제 (wallet_keys → wallet_addresses 순)
DELETE FROM wallet_keys WHERE wallet_address_id IN (11, 12);
DELETE FROM wallet_addresses WHERE id IN (11, 12);
```

그 후 admin-api로 재호출:
```bash
POST /api/admin/relayer-wallets { networkId: 4, hdWalletId: 3, relayerRole: "COLLECTION" }
POST /api/admin/relayer-wallets { networkId: 4, hdWalletId: 3, relayerRole: "WITHDRAWAL" }
```

### 방법 B: blockchain-api contract/add-relayer로 재등록

기존 SYSTEM 지갑은 유지하고, DB 상태만 복원 후 addRelayer 재시도:
```sql
UPDATE relayer_wallets SET registration_status = 'REGISTERING', status = 'ACTIVE' WHERE id IN (5, 6);
```
```bash
curl -s -X POST http://localhost:3001/api/admin/contract/add-relayer \
  -H "Content-Type: application/json" -d '{"networkId": 4, "relayerWalletId": 5}'
curl -s -X POST http://localhost:3001/api/admin/contract/add-relayer \
  -H "Content-Type: application/json" -d '{"networkId": 4, "relayerWalletId": 6}'
```

**방법 B 권장** — 이미 생성된 SYSTEM 지갑(주소)을 재사용, DB 정리 최소화.
