# TRON Approve 플로우 리팩토링 지침서

> 기존: TronZap activate_address:true (1.4 TRX) + 직접 0.1 TRX 전송 = 이중 비용
> 변경: 활성화/리소스 체크 → 조건부 활성화 → 조건부 에너지 렌탈 → approve

## 변경된 TRON Approve 플로우

```
Step 1. 지갑 활성화 상태 확인 — getAccount()
  └─ 미활성 → GAS 지갑에서 0.1 TRX 전송 → 확인 대기
  └─ 이미 활성 → 스킵

Step 2. 에너지 리소스 확인 — getAccountResources()
  └─ 보유 에너지 조회

Step 3. 렌탈 필요량 계산
  └─ needed = max(100,000 - 보유 에너지, 0)
  └─ if needed == 0 → 렌탈 스킵 (Step 4로)
  └─ rental = max(needed, 65,000)  ← TronZap 최소 렌탈 단위

Step 4. 에너지 렌탈 (필요 시) — TronZap API
  └─ activate_address: false (항상)
  └─ energy_amount: rental (계산된 값)
  └─ waitForCompletion → status: 'success'

Step 5. Approve TX 실행
```

---

## 수정 1: TronProvider — getAccount / getAccountResources 추가

**파일**: `packages/common/src/chain/TronProvider.ts`

```typescript
// ── 계정 상태 조회 ──

/**
 * TRON 계정 정보 조회
 * @returns 계정 객체 또는 null (미활성)
 */
async getAccount(address: string): Promise<{ address: string; balance: number } | null> {
  try {
    const account = await this.tronWeb.trx.getAccount(address);
    // 미활성 계정은 빈 객체 {} 또는 address 필드 없음
    if (!account || !account.address) {
      return null;
    }
    return {
      address: account.address,
      balance: account.balance ?? 0,
    };
  } catch {
    return null;
  }
}

/**
 * TRON 계정 에너지/대역폭 리소스 조회
 * @returns { energyLimit, energyUsed, freeNetLimit, freeNetUsed, ... }
 */
async getAccountResources(address: string): Promise<{
  energyLimit: number;
  energyUsed: number;
  availableEnergy: number;
}> {
  try {
    const resources = await this.tronWeb.trx.getAccountResources(address);
    const energyLimit = resources.EnergyLimit ?? 0;
    const energyUsed = resources.EnergyUsed ?? 0;
    return {
      energyLimit,
      energyUsed,
      availableEnergy: energyLimit - energyUsed,
    };
  } catch {
    // 미활성 계정은 리소스 조회 실패 가능
    return { energyLimit: 0, energyUsed: 0, availableEnergy: 0 };
  }
}
```

> ChainProvider 인터페이스에 추가하지 않아도 됨 — TRON 전용 메서드.
> ApprovalProcessor에서 `provider as TronProvider`로 캐스팅하여 사용.

## 수정 2: 상수 변경

**파일**: `packages/common/src/constants/gas.ts`

```typescript
// BEFORE
export const TRON_APPROVE_ENERGY = 65_000;

// AFTER
export const TRON_APPROVE_ENERGY = 100_000;
export const TRON_MIN_RENTAL_ENERGY = 65_000;  // TronZap 최소 렌탈 단위
```

**export 추가** — `packages/common/src/index.ts`:
```typescript
export {
  ...,
  TRON_MIN_RENTAL_ENERGY as TRON_MIN_RENTAL_ENERGY,
} from './constants/gas.js';
```

## 수정 3: TronZapClient 수정 (이전 지침서 내용 통합)

**파일**: `packages/common/src/tronzap/TronZapClient.ts`

### 3-1. waitForCompletion — status 값 수정
```typescript
// BEFORE
if (result.status === 'completed') {

// AFTER
if (result.status === 'success' || result.status === 'completed') {
```

### 3-2. createEnergyTransaction — service+params 구조
```typescript
// AFTER
return this.request<CreateTransactionResult>('/v1/transaction/new', {
  service: 'energy',
  ...(req.external_id && { external_id: req.external_id }),
  params: {
    address: req.address,
    amount: req.energy_amount,
    duration: req.duration,
    ...(req.activate_address !== undefined && { activate_address: req.activate_address }),
  },
});
```

### 3-3. 인터페이스 업데이트 (실측 기반)

```typescript
export interface CreateTransactionResult {
  id: string;
  status: string;       // 'new' | 'pending' | 'processing' | 'success' | 'failed'
  created_at: string;
  external_id?: string;
  service: string;
  params: { address: string; amount: number; duration: number; activate_address?: boolean };
  amount: number;        // 총 비용 (TRX)
}

export interface CheckTransactionResult {
  id: string;
  status: string;
  created_at: string;
  external_id?: string;
  service: string;
  params: { address: string; amount: number; duration: number; activate_address?: boolean };
  amount: number | string;  // transaction/check은 string "3.000000" 형태
  hash?: string;
}
```

## 수정 4: processTronApproval 리팩토링

**파일**: `packages/wallet-activator/src/services/ApprovalProcessor.ts`

import 추가:
```typescript
import {
  ...,
  TRON_MIN_RENTAL_ENERGY as TRON_MIN_RENTAL_ENERGY_CONST,
} from '@cryptoments/common';
import type { TronProvider } from '@cryptoments/common';

const TRON_MIN_RENTAL_ENERGY = TRON_MIN_RENTAL_ENERGY_CONST;
```

### processTronApproval() 전체 리팩토링:

```typescript
export async function processTronApproval(approval: WalletApproval): Promise<void> {
  const provider = chainProviderFactory.get(approval.network_id) as TronProvider;
  const walletAddress = await walletAddressRepo.findById(approval.wallet_address_id);
  if (!walletAddress) throw new Error(`WalletAddress not found: ${approval.wallet_address_id}`);

  const gasWallet = await walletAddressRepo.findGasWallet(approval.network_id);
  if (!gasWallet) throw new Error(`GAS wallet not found for network: ${approval.network_id}`);

  const partnerId = await getPartnerId(walletAddress);

  // 통화 정보 (토큰 컨트랙트)
  const currency = await currencyRepo.findById(approval.currency_id);
  if (!currency) throw new Error(`Currency not found: ${approval.currency_id}`);
  const tokenContract = currency.contract_address;
  if (!tokenContract) throw new Error(`No contract_address: ${approval.currency_id}`);

  // CryptoRelayer 컨트랙트 (approve spender)
  const relayerContract = await relayerContractRepo.findActiveByNetwork(approval.network_id);
  if (!relayerContract) {
    throw new Error(`Active CryptoRelayer not found: network ${approval.network_id}`);
  }

  const tronZap = getTronZapClient();
  const trxPriceUsd = await getNativePriceUsd(approval.network_id);

  // ══════════════════════════════════════════════════════════════
  // Step 1: 지갑 활성화 상태 확인 → 미활성이면 GAS에서 0.1 TRX 전송
  // ══════════════════════════════════════════════════════════════
  await walletApprovalRepo.updateStatus(approval.id, 'GAS_SUPPORTING');

  const account = await provider.getAccount(walletAddress.address);

  if (!account) {
    logger.info('TRON: Wallet not activated, sending activation TRX', {
      approvalId: approval.id,
      target: walletAddress.address,
    });

    const gasEncryptedKey = await walletKeyRepo.findByAddressId(gasWallet.id);
    if (!gasEncryptedKey) throw new Error(`GAS wallet key not found: ${gasWallet.id}`);
    const gasPrivateKey = await keyManager.decrypt(gasEncryptedKey);

    const activationTxHash = await provider.sendNative({
      from: gasWallet.address,
      to: walletAddress.address,
      amount: BigInt(TRON_ACTIVATION_AMOUNT),
      privateKey: gasPrivateKey,
    });
```

    logger.info('TRON: Activation TRX sent', { approvalId: approval.id, txHash: activationTxHash });

    // 💰 GAS_SUPPORT 비용 기록 (0.1 TRX 활성화)
    await recordGasCost({
      partner_id: partnerId,
      network_id: approval.network_id,
      tx_type: 'GAS_SUPPORT',
      tx_hash: activationTxHash,
      reference_type: 'WALLET_APPROVAL',
      reference_id: approval.id,
      fee_native: TRON_ACTIVATION_AMOUNT,   // 100,000 sun = 0.1 TRX
      native_price_usd: trxPriceUsd ?? undefined,
      fee_usd: trxPriceUsd ? calcFeeUsd(TRON_ACTIVATION_AMOUNT, trxPriceUsd, 6) : undefined,
    });

    await provider.waitForConfirmation(activationTxHash);
    logger.info('TRON: Activation confirmed', { approvalId: approval.id });
  } else {
    logger.info('TRON: Wallet already activated, skipping activation', {
      approvalId: approval.id,
      address: walletAddress.address,
    });
  }

  // ══════════════════════════════════════════════════════════════
  // Step 2: 에너지 리소스 확인 → 렌탈 필요량 계산
  // ══════════════════════════════════════════════════════════════
  const resources = await provider.getAccountResources(walletAddress.address);
  const currentEnergy = resources.availableEnergy;
  const neededEnergy = Math.max(TRON_APPROVE_ENERGY - currentEnergy, 0);

  logger.info('TRON: Energy resource check', {
    approvalId: approval.id,
    currentEnergy,
    targetEnergy: TRON_APPROVE_ENERGY,
    neededEnergy,
  });
```

  // ══════════════════════════════════════════════════════════════
  // Step 3: 에너지 렌탈 (필요 시)
  // ══════════════════════════════════════════════════════════════
  if (neededEnergy > 0) {
    // 최소 렌탈 단위 = 65,000
    const rentalEnergy = Math.max(neededEnergy, TRON_MIN_RENTAL_ENERGY);

    logger.info('TRON: Renting energy via TronZap', {
      approvalId: approval.id,
      rentalEnergy,
      minRental: TRON_MIN_RENTAL_ENERGY,
    });

    const energyTx = await tronZap.createEnergyTransaction({
      address: walletAddress.address,
      energy_amount: rentalEnergy,
      duration: 1,
      activate_address: false,          // ← 활성화는 Step 1에서 직접 처리
      external_id: `approval-${approval.id}`,
    });

    // gas_tx_hash에 TronZap 트랜잭션 ID 저장 (추적용)
    await walletApprovalRepo.updateGasTxHash(approval.id, energyTx.id);

    logger.info('TRON: TronZap energy transaction created', {
      approvalId: approval.id,
      tronZapTxId: energyTx.id,
      rentalEnergy,
      cost: energyTx.amount,
    });

    // 💰 ENERGY_RENTAL 비용 기록
    const energyFeeNative = trxToSun(Number(energyTx.amount));  // TRX→SUN
    await recordGasCost({
      partner_id: partnerId,
      network_id: approval.network_id,
      tx_type: 'ENERGY_RENTAL',
      tx_hash: energyTx.id,
      reference_type: 'WALLET_APPROVAL',
      reference_id: approval.id,
      fee_native: energyFeeNative,
      native_price_usd: trxPriceUsd ?? undefined,
      fee_usd: trxPriceUsd ? calcFeeUsd(energyFeeNative, trxPriceUsd, 6) : undefined,
      energy_fee_trx: energyFeeNative,
    });

    // 에너지 위임 완료 대기
    await tronZap.waitForCompletion(energyTx.id, {
      pollIntervalMs: 3_000,
      timeoutMs: 120_000,
    });

    logger.info('TRON: Energy delegation completed', {
      approvalId: approval.id,
      tronZapTxId: energyTx.id,
    });
  } else {
    logger.info('TRON: Sufficient energy available, skipping rental', {
      approvalId: approval.id,
      currentEnergy,
    });
  }

  await walletApprovalRepo.updateStatus(approval.id, 'GAS_READY');
```

  // ══════════════════════════════════════════════════════════════
  // Step 4: TRC-20 Approve TX 실행
  // ══════════════════════════════════════════════════════════════
  await executeApprove(approval, walletAddress.address, partnerId);
}
```

## 수정 요약

| # | 파일 | 수정 |
|---|------|------|
| 1 | TronProvider.ts | `getAccount()`, `getAccountResources()` 추가 |
| 2 | gas.ts | `TRON_APPROVE_ENERGY=100,000`, `TRON_MIN_RENTAL_ENERGY=65,000` |
| 3 | TronZapClient.ts | waitForCompletion `success` 추가 |
| 4 | TronZapClient.ts | createEnergyTransaction service+params 구조 |
| 5 | TronZapClient.ts | Result 인터페이스 실측 기반 |
| 6 | ApprovalProcessor.ts | processTronApproval 전체 리팩토링 |

## 핵심 차이: 기존 vs 변경

| 항목 | 기존 | 변경 |
|------|------|------|
| 활성화 방식 | TronZap activate:true (1.4 TRX) + 직접 0.1 TRX (이중) | 직접 0.1 TRX만 (조건부) |
| 에너지량 | 65,000 고정 | 100,000 - 보유분 (최소 65,000) |
| 리소스 체크 | 없음 | getAccountResources() 호출 |
| 활성화 체크 | 없음 | getAccount() 호출 |
| 이미 활성화된 2차 approve | 다시 활성화 + 에너지 | 활성화 스킵 + 보유분 차감 렌탈 |
| gas_tx_hash | 미저장 | TronZap result.id 저장 |

## 비용 시나리오

### 신규 주소 첫 approve (미활성, 에너지 0)
```
활성화: 0.1 TRX (직접 전송)
에너지: max(100,000 - 0, 65,000) = 100,000 렌탈 → ~4.6 TRX
합계: ~4.7 TRX (기존 5.5+ TRX에서 절감)
```

### 같은 주소 두 번째 approve (활성화됨, 에너지 잔여 50,000)
```
활성화: 스킵 (0 TRX)
에너지: max(100,000 - 50,000, 65,000) = 65,000 렌탈 → ~3 TRX
합계: ~3 TRX (기존 대비 대폭 절감)
```

### 같은 주소 두 번째 approve (활성화됨, 에너지 잔여 110,000)
```
활성화: 스킵 (0 TRX)
에너지: max(100,000 - 110,000, 0) = 0 → 렌탈 스킵
합계: 0 TRX (무료!)
```

## 이전 지침서 폐기

이 지침서가 아래 문서를 대체합니다:
- ~~TRONZAP_REQUEST_FORMAT_FIX_GUIDE.md~~ (수정 3~5가 여기 통합)
- ~~PHASE3_PREPARATION_TODO_GUIDE.md TODO-5~~ (TRON approve 항목)
