# Node.js Contract 구조 리팩토링 가이드

**작성일**: 2026-03-16
**목적**: 컨트랙트 수동 배포 + 시스템 등록 방식으로 전환, TRON(TVM) 분기 추가
**대상 파일**: 6개 파일 수정 + 1개 신규

---

## 설계 변경 요약

```
[변경 전]
  Admin Console → POST /api/admin/contract/deploy
  → Node.js가 ethers.js로 직접 배포 (EVM only)
  → 기존 컨트랙트 pause → deprecate → 신규 배포

[변경 후]
  1. 운영자가 Hardhat(EVM) / Tronbox(TRON)으로 수동 배포
  2. Admin Console → POST /api/admin/contract/register (신규 API)
     → { networkId, contractAddress, deployTxHash, ownerAddressId, abiVersion }
  3. Node.js는 등록된 컨트랙트로 addRelayer/removeRelayer/pause 등 관리만 수행
  4. 모든 컨트랙트 인터랙션에 EVM/TVM 분기 추가
```

**삭제 대상**: `deployContract()` 함수, `CRYPTO_RELAYER_BYTECODE` (배포용 바이트코드)
**추가 대상**: `registerContract()` 함수, TVM 분기 헬퍼, TronContractHelper

---

## ⚠️ Phase 1/1B 이후 코드 상태 (필독)

이 가이드는 Phase 1/1B 이전에 작성되었습니다. 아래 변경사항은 이미 적용 완료:

| 항목 | 이 가이드의 코드 | 현재 코드 (이미 반영됨) |
|------|-----------------|----------------------|
| `relayerWalletRepo.updateStatus(id, 'INACTIVE')` | 여러 곳에서 사용 | `updateRegistrationStatus(id, 'REVOKED')` + `updateOperationStatus(id, 'PAUSED')` |
| `RelayerStatus` 타입 | 단일 타입 | `RelayerOperationStatus` + `RegistrationStatus` 분리 |
| `RelayerWallet` 인터페이스 | `status` 하나 | `status` (운영) + `registration_status` (등록) + `contract_address` + `registration_tx_hash` |

**따라서 이 가이드에서 `updateStatus(id, 'INACTIVE')` 가 나오면, 현재 코드의 `updateRegistrationStatus()` + `updateOperationStatus()` 조합으로 읽을 것.**

---

## 수동 배포 후 Ownership 이전 (중요)

### 배경

CryptoRelayer 컨트랙트는 OpenZeppelin `Ownable` 패턴을 사용합니다.
`addRelayer`, `removeRelayer`, `pause`, `unpause` 모두 **owner-only** 함수입니다.

수동 배포 시 **배포자 지갑**이 자동으로 owner가 됩니다.
하지만 Node.js의 `getAdminSigner()` / `getAdminTronWeb()`은 시스템 **ADMIN 지갑**의 개인키로 TX를 서명합니다.

→ **배포 후 반드시 `transferOwnership(시스템 ADMIN 지갑 주소)` 호출 필요**

### 프로세스

```
┌──────────────────────────────────────────────────────────┐
│ 1. 수동 배포 (Hardhat / Tronbox)                          │
│    → 배포자 지갑이 owner                                   │
│                                                          │
│ 2. transferOwnership 호출 (배포 직후, 같은 스크립트에서)    │
│    EVM:  contract.transferOwnership(systemAdminAddress)   │
│    TRON: contract.transferOwnership(systemAdminAddress)   │
│          .send({ feeLimit: 100_000_000 })                │
│    → 시스템 ADMIN 지갑이 owner                             │
│                                                          │
│ 3. Admin Console → registerContract()                     │
│    → 온체인 검증: contract.owner() === ADMIN 지갑 주소     │
│    → verified = true                                      │
└──────────────────────────────────────────────────────────┘
```

### Hardhat 배포 스크립트 예시

```typescript
// scripts/deploy.ts (Hardhat)
import { ethers } from "hardhat";

async function main() {
  const SYSTEM_ADMIN_ADDRESS = "0x...";  // 시스템 ADMIN 지갑 주소

  // 1. 배포
  const CryptoRelayer = await ethers.getContractFactory("CryptoRelayer");
  const contract = await CryptoRelayer.deploy();
  await contract.waitForDeployment();

  const contractAddress = await contract.getAddress();
  console.log("CryptoRelayer deployed:", contractAddress);

  // 2. Ownership 이전
  const tx = await contract.transferOwnership(SYSTEM_ADMIN_ADDRESS);
  await tx.wait();
  console.log("Ownership transferred to:", SYSTEM_ADMIN_ADDRESS);

  // 3. 검증
  const newOwner = await contract.owner();
  console.log("Verified owner:", newOwner);

  // 결과 출력 — Admin Console 등록에 사용
  console.log("\n=== Register Info ===");
  console.log("contractAddress:", contractAddress);
  console.log("deployTxHash:", contract.deploymentTransaction()?.hash);
  console.log("owner:", newOwner);
}

main().catch(console.error);
```

### Tronbox 배포 스크립트 예시

```javascript
// migrations/2_deploy_crypto_relayer.js (Tronbox)
const CryptoRelayer = artifacts.require("CryptoRelayer");

const SYSTEM_ADMIN_ADDRESS = "T...";  // 시스템 ADMIN 지갑 주소 (Base58)

module.exports = async function (deployer) {
  // 1. 배포
  await deployer.deploy(CryptoRelayer);
  const contract = await CryptoRelayer.deployed();
  console.log("CryptoRelayer deployed:", contract.address);

  // 2. Ownership 이전
  await contract.transferOwnership(SYSTEM_ADMIN_ADDRESS);
  console.log("Ownership transferred to:", SYSTEM_ADMIN_ADDRESS);

  // 3. 검증
  const newOwner = await contract.owner();
  console.log("Verified owner:", newOwner);
};
```

### transferOwnership 누락 시 영향

| 상황 | 결과 |
|------|------|
| ownership 이전 안 함 | `registerContract()` 온체인 검증 실패 (verified=false, 등록은 진행되지만 경고) |
| ownership 이전 안 한 상태에서 addRelayer 호출 | **TX 실패** — owner-only 함수를 non-owner가 호출 |
| ownership 이전 안 한 상태에서 pause 호출 | **TX 실패** — 동일 이유 |

→ **ownership 이전 없이는 시스템이 컨트랙트를 관리할 수 없음**

---

## 파일별 수정 지침

### 1. ContractDeployService.ts → ContractManagementService.ts (리네임)

**경로**: `blockchain-api/src/services/ContractDeployService.ts`
**리네임**: `ContractManagementService.ts`

#### 1-1. import 변경

```typescript
// ── 삭제 ──
import { CRYPTO_RELAYER_BYTECODE } from '@cryptoments/common';

// ── 추가 ──
import { TronWeb } from 'tronweb';
import { CRYPTO_RELAYER_ABI_JSON } from '@cryptoments/common';
```

#### 1-2. getAdminSigner() — 네트워크 타입 분기 추가

현재 `getAdminSigner()`는 EVM 전용. TVM인 경우 tronweb 인스턴스를 반환하는 헬퍼 추가.

```typescript
// ── 네트워크 타입 조회 헬퍼 (추가) ──
const networkTypeCache = new Map<number, string>();
async function getNetworkType(networkId: number): Promise<string> {
  if (networkTypeCache.has(networkId)) return networkTypeCache.get(networkId)!;
  const rows = await query<({ network_type: string } & import('mysql2/promise').RowDataPacket)[]>(
    'SELECT network_type FROM blockchain_networks WHERE id = ?',
    [networkId],
  );
  if (rows.length === 0) throw new Error(`Network not found: ${networkId}`);
  networkTypeCache.set(networkId, rows[0].network_type);
  return rows[0].network_type;
}

// ── EVM Signer (기존 유지) ──
async function getAdminSigner(networkId: number): Promise<{
  signer: ethers.Wallet;
  adminAddressId: number;
  adminAddress: string;
}> {
  // ... 기존 코드 그대로
}

// ── TVM Signer (추가) ──
async function getAdminTronWeb(networkId: number): Promise<{
  tronWeb: any;
  adminAddressId: number;
  adminAddress: string;
}> {
  const adminWallet = await walletAddressRepo.findByNetworkAndType(networkId, 'ADMIN');
  if (!adminWallet) throw new Error(`ADMIN wallet not found for network ${networkId}`);

  const walletEncryptedKey = await walletKeyRepo.findByAddressId(adminWallet.id);
  if (!walletEncryptedKey) throw new Error(`Wallet key not found for ADMIN wallet: ${adminWallet.id}`);
  const privateKey = await keyManager.decrypt(walletEncryptedKey);

  const provider = chainProviderFactory.get(networkId);
  const rpcUrl = provider.getRpcUrl();
  const tronWeb = new TronWeb({ fullHost: rpcUrl, privateKey });

  return {
    tronWeb,
    adminAddressId: adminWallet.id,
    adminAddress: adminWallet.address,
  };
}
```

#### 1-3. deployContract() → registerContract() (교체)

```typescript
// ── 삭제: deployContract() 전체 함수 ──

// ── 추가: registerContract() ──
export interface RegisterContractRequest {
  /** blockchain_networks.id */
  networkId: number;
  /** 수동 배포된 컨트랙트 주소 */
  contractAddress: string;
  /** 배포 TX 해시 */
  deployTxHash: string;
  /** owner 지갑의 wallet_addresses.id (ADMIN 지갑) */
  ownerAddressId: number;
  /** ABI 버전 (기본값 '1.0') */
  abiVersion?: string;
}

export interface RegisterResult {
  contractId: number;
  contractAddress: string;
  networkId: number;
  ownerAddress: string;
  verified: boolean;
}

export async function registerContract(req: RegisterContractRequest): Promise<RegisterResult> {
  logger.info('Registering externally deployed contract', {
    networkId: req.networkId,
    contractAddress: req.contractAddress,
  });

  // 1. owner 지갑 확인
  const ownerWallet = await walletAddressRepo.findById(req.ownerAddressId);
  if (!ownerWallet) throw new Error(`Owner wallet not found: ${req.ownerAddressId}`);
  if (ownerWallet.wallet_type !== 'ADMIN') {
    throw new Error(`Owner wallet must be ADMIN type, got: ${ownerWallet.wallet_type}`);
  }

  // 2. 기존 ACTIVE 컨트랙트 처리 (DEPRECATED)
  const existing = await relayerContractRepo.findActiveByNetwork(req.networkId);
  if (existing) {
    logger.warn('Deprecating existing contract', {
      oldContract: existing.contract_address,
      newContract: req.contractAddress,
    });
    await relayerContractRepo.deprecateByNetwork(req.networkId);
  }

  // 3. 온체인 검증 — owner가 실제 컨트랙트 owner인지 확인
  let verified = false;
  try {
    const networkType = await getNetworkType(req.networkId);

    if (networkType === 'TVM') {
      const { tronWeb } = await getAdminTronWeb(req.networkId);
      const contract = await tronWeb.contract(CRYPTO_RELAYER_ABI_JSON, req.contractAddress);
      const onchainOwner = await contract.owner().call();
      // TRON 주소 비교 (hex → base58 변환 필요할 수 있음)
      verified = tronWeb.address.toHex(onchainOwner).toLowerCase()
        === tronWeb.address.toHex(ownerWallet.address).toLowerCase();
    } else {
      const { signer } = await getAdminSigner(req.networkId);
      const contract = new ethers.Contract(req.contractAddress, CRYPTO_RELAYER_ABI, signer.provider!);
      const onchainOwner = await contract.owner();
      verified = onchainOwner.toLowerCase() === ownerWallet.address.toLowerCase();
    }

    if (!verified) {
      logger.warn('Contract owner verification failed', {
        expectedOwner: ownerWallet.address,
        networkId: req.networkId,
      });
      // 경고만 — 등록은 진행 (네트워크 문제로 검증 실패할 수 있음)
    }
  } catch (verifyError) {
    logger.warn('Contract verification skipped (network error)', {
      error: (verifyError as Error).message,
    });
  }

  // 4. DB 등록
  const contractId = await relayerContractRepo.insert({
    network_id: req.networkId,
    contract_address: req.contractAddress,
    owner_address_id: req.ownerAddressId,
    deploy_tx_hash: req.deployTxHash,
    abi_version: req.abiVersion,
  });

  logger.info('Contract registered', {
    contractId,
    contractAddress: req.contractAddress,
    networkId: req.networkId,
    verified,
  });

  return {
    contractId,
    contractAddress: req.contractAddress,
    networkId: req.networkId,
    ownerAddress: ownerWallet.address,
    verified,
  };
}
```

#### 1-4. addRelayer() — TVM 분기 추가

```typescript
export async function addRelayer(
  networkId: number,
  relayerWalletId: number,
): Promise<RelayerRegistrationResult> {
  const relayerContract = await relayerContractRepo.findActiveByNetwork(networkId);
  if (!relayerContract) throw new Error(`No active CryptoRelayer contract for network ${networkId}`);

  const relayerWallet = await relayerWalletRepo.findById(relayerWalletId);
  if (!relayerWallet) throw new Error(`Relayer wallet not found: ${relayerWalletId}`);

  // relayerAddr는 JOIN으로 이미 포함됨 (relayerWallet.address)
  const relayerAddress = relayerWallet.address;
  if (!relayerAddress) throw new Error(`Relayer address not resolved: ${relayerWalletId}`);

  const networkType = await getNetworkType(networkId);
  let txHash: string;

  if (networkType === 'TVM') {
    // ── TRON: tronweb ──
    const { tronWeb } = await getAdminTronWeb(networkId);
    const contract = await tronWeb.contract(CRYPTO_RELAYER_ABI_JSON, relayerContract.contract_address);

    try {
      txHash = await contract.addRelayer(relayerAddress).send({ feeLimit: 100_000_000 });
    } catch (txError) {
      logger.error('addRelayer TX failed (TRON)', { relayerWalletId, error: (txError as Error).message });
      await relayerWalletRepo.updateRegistrationStatus(relayerWalletId, 'REVOKED');
      await relayerWalletRepo.updateOperationStatus(relayerWalletId, 'PAUSED');
      throw txError;
    }
  } else {
    // ── EVM: ethers.js ──
    const { signer } = await getAdminSigner(networkId);
    const contract = new ethers.Contract(relayerContract.contract_address, CRYPTO_RELAYER_ABI, signer);

    const tx = await contract.addRelayer(relayerAddress);
    try {
      await tx.wait();
    } catch (txError) {
      logger.error('addRelayer TX failed (EVM)', { relayerWalletId, error: (txError as Error).message });
      await relayerWalletRepo.updateRegistrationStatus(relayerWalletId, 'REVOKED');
      await relayerWalletRepo.updateOperationStatus(relayerWalletId, 'PAUSED');
      throw txError;
    }
    txHash = tx.hash;
  }

  // DB 업데이트 (공통)
  await relayerWalletRepo.updateRegistration(
    relayerWalletId,
    relayerContract.contract_address,
    txHash,
    'REGISTERED',
  );

  logger.info('Relayer registered on contract', { relayerWalletId, relayerAddress, txHash });

  return {
    txHash,
    relayerAddress,
    contractAddress: relayerContract.contract_address,
  };
}
```

#### 1-5. removeRelayer() — 동일 패턴으로 TVM 분기

```typescript
export async function removeRelayer(networkId: number, relayerWalletId: number): Promise<{ txHash: string }> {
  // ... 기존 검증 로직 동일 ...

  const networkType = await getNetworkType(networkId);
  let txHash: string;

  if (networkType === 'TVM') {
    const { tronWeb } = await getAdminTronWeb(networkId);
    const contract = await tronWeb.contract(CRYPTO_RELAYER_ABI_JSON, relayerContract.contract_address);
    txHash = await contract.removeRelayer(relayerAddr.address).send({ feeLimit: 100_000_000 });
  } else {
    const { signer } = await getAdminSigner(networkId);
    const contract = new ethers.Contract(relayerContract.contract_address, CRYPTO_RELAYER_ABI, signer);
    const tx = await contract.removeRelayer(relayerAddr.address);
    await tx.wait();
    txHash = tx.hash;
  }

  await relayerWalletRepo.updateRegistration(relayerWalletId, relayerContract.contract_address, txHash, 'REVOKED');
  return { txHash };
}
```

#### 1-6. pauseContract() / unpauseContract() — 동일 패턴

```typescript
// pause
if (networkType === 'TVM') {
  const { tronWeb } = await getAdminTronWeb(networkId);
  const contract = await tronWeb.contract(CRYPTO_RELAYER_ABI_JSON, relayerContract.contract_address);
  txHash = await contract.pause().send({ feeLimit: 100_000_000 });
} else {
  const { signer } = await getAdminSigner(networkId);
  const contract = new ethers.Contract(relayerContract.contract_address, CRYPTO_RELAYER_ABI, signer);
  const tx = await contract.pause();
  await tx.wait();
  txHash = tx.hash;
}

// unpause — 동일 패턴, contract.unpause()
```

#### 1-7. getContractStatus() — TVM 분기

```typescript
export async function getContractStatus(networkId: number): Promise<ContractStatus | null> {
  const relayerContract = await relayerContractRepo.findActiveByNetwork(networkId);
  if (!relayerContract) return null;

  const networkType = await getNetworkType(networkId);

  if (networkType === 'TVM') {
    const { tronWeb } = await getAdminTronWeb(networkId);
    const contract = await tronWeb.contract(CRYPTO_RELAYER_ABI_JSON, relayerContract.contract_address);

    const [owner, paused, relayerCount, relayers] = await Promise.all([
      contract.owner().call(),
      contract.paused().call(),
      contract.relayerCount().call(),
      contract.getRelayers().call(),
    ]);

    return {
      contractAddress: relayerContract.contract_address,
      owner: String(owner),
      paused: Boolean(paused),
      relayerCount: Number(relayerCount),
      relayers: relayers.map(String),
      abiVersion: relayerContract.abi_version,
      dbStatus: relayerContract.status,
    };
  }

  // EVM — 기존 로직 그대로
  // ...
}
```

---

### 2. system-admin.ts — deploy → register 교체

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

#### 2-1. import 변경

```typescript
// ── 삭제 ──
import { deployContract, ... } from '../services/ContractDeployService.js';

// ── 변경 ──
import {
  registerContract,    // ← 신규
  addRelayer,
  removeRelayer,
  pauseContract,
  unpauseContract,
  getContractStatus,
} from '../services/ContractManagementService.js';  // ← 파일명 변경
```

#### 2-2. deploy 라우트 → register 라우트

```typescript
// ── 삭제 ──
router.post('/contract/deploy', async (req, res) => { ... });

// ── 추가 ──
router.post('/contract/register', async (req, res) => {
  const { networkId, contractAddress, deployTxHash, ownerAddressId, abiVersion } = req.body;

  if (!networkId || !contractAddress || !deployTxHash || !ownerAddressId) {
    return res.status(400).json({
      error: 'networkId, contractAddress, deployTxHash, ownerAddressId are required',
    });
  }

  try {
    const result = await registerContract({
      networkId,
      contractAddress,
      deployTxHash,
      ownerAddressId,
      abiVersion,
    });

    logger.info('Contract registered via admin API', result);
    res.json(result);
  } catch (error) {
    const msg = error instanceof Error ? error.message : 'Unknown error';
    logger.error('Contract registration failed', { networkId, error: msg });
    res.status(500).json({ error: 'Contract registration failed', detail: msg });
  }
});
```

**JSDoc 주석도 업데이트**:
```
 *   POST /api/admin/contract/register     — 수동 배포한 컨트랙트 등록 (신규)
```

---

### 3. CollectionPoller.ts — executeTransfer TVM 분기

**경로**: `relayer-api/src/services/CollectionPoller.ts`

`executeCollection()` 함수의 Step 9 (line 125-148):

```typescript
// 기존: ethers.js로만 호출 (EVM only)

// 변경: 네트워크 타입 분기
let txHash: string;

if (networkType === 'TVM') {
  // ── TRON: tronweb ──
  const { TronWeb } = await import('tronweb');
  const provider = chainProviderFactory.get(entry.network_id);
  const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: relayerPrivateKey });

  const contract = await tronWeb.contract(
    CRYPTO_RELAYER_ABI_JSON,
    relayerContract.contract_address,
  );

  txHash = await contract.executeTransfer(
    tokenContract,
    fromWallet.address,
    masterWallet.address,
    entry.amount,    // TRON은 string으로 전달
  ).send({ feeLimit: 100_000_000 });

} else {
  // ── EVM: ethers.js (기존 로직) ──
  const rpcUrl = provider.getRpcUrl();
  const jsonRpcProvider = new ethers.JsonRpcProvider(rpcUrl);
  const signer = new ethers.Wallet(relayerPrivateKey, jsonRpcProvider);
  const contract = new ethers.Contract(
    relayerContract.contract_address,
    CRYPTO_RELAYER_ABI,
    signer,
  );

  const tx = await contract.executeTransfer(
    tokenContract,
    fromWallet.address,
    masterWallet.address,
    BigInt(entry.amount),
    { nonce: acquired.nonce },
  );
  txHash = tx.hash;
}

// 이후 DB 업데이트 로직은 공통으로 txHash 사용
```

**import 추가**:
```typescript
import { CRYPTO_RELAYER_ABI_JSON } from '@cryptoments/common';
```

**주의사항**:
- TRON은 nonce 개념이 없으므로 `{ nonce: acquired.nonce }` 옵션 불필요
- 단, `nonceManager`는 TRON에서도 내부 순서 관리용으로 사용하므로 `acquireNonce`/`confirmNonce` 호출은 유지
- TRON `executeTransfer`의 `amount`는 string으로 전달 (BigInt 아님)

---

### 4. WithdrawalPoller.ts — 동일 패턴

**경로**: `relayer-api/src/services/WithdrawalPoller.ts`

CollectionPoller와 동일한 TVM 분기를 `executeWithdrawal()` Step 8 (line 119-143)에 적용.

```typescript
let txHash: string;

if (networkType === 'TVM') {
  const { TronWeb } = await import('tronweb');
  const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: relayerPrivateKey });
  const contract = await tronWeb.contract(CRYPTO_RELAYER_ABI_JSON, relayerContract.contract_address);

  txHash = await contract.executeTransfer(
    tokenContract,
    masterWallet.address,
    withdrawal.to_address,
    withdrawal.amount,
  ).send({ feeLimit: 100_000_000 });
} else {
  // 기존 EVM 로직
}
```

---

### 5. relayer.ts (relayer-api) — unregister TVM 분기

**경로**: `relayer-api/src/routes/relayer.ts`

`unregister` 라우트 (line 168-188):

```typescript
// 기존: ethers.js로만 removeRelayer
// 변경:

const networkType = await getNetworkType(relayer.network_id);

if (networkType === 'TVM') {
  const { TronWeb } = await import('tronweb');
  const tronWeb = new TronWeb({ fullHost: provider.getRpcUrl(), privateKey: ownerPrivateKey });
  const contract = await tronWeb.contract(CRYPTO_RELAYER_ABI_JSON, relayerContract.contract_address);
  const txResult = await contract.removeRelayer(relayer.address).send({ feeLimit: 100_000_000 });
  // txResult는 txHash 문자열
} else {
  // 기존 ethers.js 로직
}
```

**그리고 register 라우트 (line 97-106)에도 TX 실패 롤백 추가**:
```typescript
try {
  const tx = await contract.addRelayer(walletAddress.address);
  const receipt = await tx.wait();
  // ...성공 처리
} catch (txError) {
  await relayerWalletRepo.updateRegistrationStatus(relayerId, 'REVOKED');
  await relayerWalletRepo.updateOperationStatus(relayerId, 'PAUSED');
  return res.status(500).json({
    error: 'addRelayer TX failed',
    relayerId,
    detail: (txError as Error).message,
  });
}
```

---

### 6. CryptoRelayerAbi.ts — 바이트코드 정리

**경로**: `common/src/contracts/CryptoRelayerAbi.ts`

```typescript
// ── 삭제 또는 주석 처리 ──
// export const CRYPTO_RELAYER_BYTECODE = '0x6080...';
// → 수동 배포 방식이므로 Node.js에서 바이트코드 불필요

// ── 유지 ──
export const CRYPTO_RELAYER_ABI = [ ... ];           // ethers.js Human-Readable
export const CRYPTO_RELAYER_ABI_JSON = [ ... ];      // tronweb JSON 포맷
```

**common/src/contracts/index.ts 에서 export도 정리**:
```typescript
// 삭제
export { CRYPTO_RELAYER_BYTECODE } from './CryptoRelayerAbi.js';
```

---

### 7. TronZap 에너지 추정 from_address 수정 (추가 수정)

**CollectionPoller.ts:195-199** 및 **WithdrawalPoller.ts:189-193**:

```typescript
// ── 변경 전 ──
const estimate = await tronZap.estimateEnergy({
  from_address: fromWallet.address,    // HOT 지갑 — 실제 에너지 소비자 아님
  to_address: toWallet.address,
  contract_address: tokenContract,
});

// ── 변경 후 ──
// tx.origin = Relayer EOA → 에너지 소비 주체
// TronZap estimateEnergy의 from_address는 에너지 추정 대상
const estimate = await tronZap.estimateEnergy({
  from_address: relayerAddress,        // Relayer EOA — 실제 에너지 소비자
  to_address: toWallet.address,
  contract_address: tokenContract,
});
```

---

## API 변경 요약 (Spring Boot Integration Guide 반영 필요)

| 변경 | 이전 | 이후 |
|------|------|------|
| 엔드포인트 | `POST /api/admin/contract/deploy` | `POST /api/admin/contract/register` |
| Request Body | `{ networkId }` | `{ networkId, contractAddress, deployTxHash, ownerAddressId, abiVersion? }` |
| Response | `{ contractAddress, deployTxHash, ... }` | `{ contractId, contractAddress, verified, ... }` |

나머지 엔드포인트(add-relayer, remove-relayer, pause, unpause, status, list)는 **URL 변경 없음**.

---

## 시스템 초기 세팅 플로우 (최종)

```
┌─────────────────────────────────────────────────────────────┐
│ 사전 준비 (1회성)                                           │
│  1. HD Wallet seed 생성                                     │
│  2. TronZap API 토큰/시크릿 환경변수 세팅                    │
│  3. 각 네트워크 FEE 지갑에 소량의 네이티브 토큰 입금         │
│  4. ADMIN 지갑에 컨트랙트 배포 가스비 입금                   │
├─────────────────────────────────────────────────────────────┤
│ Step 1: ADMIN 지갑 생성 (시스템에서 자동)                    │
│  POST /api/admin/wallet/create-admin                        │
│  → HD 파생 → DB 저장                                        │
├─────────────────────────────────────────────────────────────┤
│ Step 2: CryptoRelayer 수동 배포 + Ownership 이전 (운영자 CLI)│
│  EVM:  npx hardhat run scripts/deploy.ts --network bsc      │
│  TRON: tronbox migrate --network mainnet                    │
│  → 배포 완료                                                │
│  → transferOwnership(시스템 ADMIN 지갑 주소) 호출 (필수!)    │
│  → 컨트랙트 주소 + TX 해시 + owner 확인 기록                │
│  → Etherscan/Tronscan에서 소스 코드 Verify                  │
├─────────────────────────────────────────────────────────────┤
│ Step 3: 컨트랙트 시스템 등록 (Admin Console)                 │
│  POST /api/admin/contract/register                          │
│  { networkId, contractAddress, deployTxHash, ownerAddressId }│
│  → owner 검증 → DB 등록 (status=ACTIVE)                     │
├─────────────────────────────────────────────────────────────┤
│ Step 4: Relayer EOA 생성 + 온체인 등록                       │
│  HD 파생으로 Relayer 지갑 생성                               │
│  POST /api/admin/contract/add-relayer                       │
│  → ADMIN 지갑으로 CryptoRelayer.addRelayer() TX              │
│  → relayer_wallets UPDATE (REGISTERED→ACTIVE)               │
├─────────────────────────────────────────────────────────────┤
│ Step 5: 파트너 등록 → 지갑 활성화 (자동)                     │
│  HOT/MASTER/POOL 지갑 생성 + approve 큐                     │
│  wallet-activator가 폴링하여 자동 approve                   │
│  EVM: FEE→가스비→approve                                    │
│  TRON: TronZap 에너지→0.1TRX→approve                       │
└─────────────────────────────────────────────────────────────┘
```

---

## 체크리스트

- [ ] ContractDeployService.ts → ContractManagementService.ts 리네임
- [ ] deployContract() 삭제, registerContract() 추가
- [ ] addRelayer() TVM 분기 추가
- [ ] removeRelayer() TVM 분기 추가
- [ ] pauseContract() TVM 분기 추가
- [ ] unpauseContract() TVM 분기 추가
- [ ] getContractStatus() TVM 분기 추가
- [ ] system-admin.ts: deploy 라우트 → register 라우트
- [ ] CollectionPoller.ts: executeTransfer TVM 분기
- [ ] WithdrawalPoller.ts: executeTransfer TVM 분기
- [ ] relayer.ts: register/unregister TVM 분기 + TX 실패 롤백
- [ ] CryptoRelayerAbi.ts: BYTECODE export 삭제
- [ ] common/contracts/index.ts: BYTECODE re-export 삭제
- [ ] CollectionPoller: TronZap estimateEnergy from_address 수정
- [ ] WithdrawalPoller: TronZap estimateEnergy from_address 수정
- [ ] NODEJS_API_INTEGRATION_GUIDE.md 업데이트 (deploy→register)
