# 🔗 前后端集成指南

## 概述

本文档说明如何将后端的 Holder 获取方案与前端页面集成。

---

## 📊 数据对应关系

### 后端提供的数据类型

根据 `holder获取方案文档.txt`，后端会获取三种类型的持有者：

1. **YT 代币持有者** (YT-A, YT-B, YT-C)
   ```typescript
   interface YTHolderData {
     address: string;
     balance: string; // 格式化后的余额 (ether)
   }
   ```

2. **LP 代币持有者** (ytLP)
   ```typescript
   interface LPHolderData {
     address: string;
     balance: string; // 格式化后的余额 (ether)
     share: string;   // 占总量的百分比 (如 "5.23%")
   }
   ```

3. **Lending 提供者**
   ```typescript
   interface LendingSupplierData {
     address: string;
     supply: string;  // 供应金额 (USDC)
     borrow: string;  // 借款金额 (USDC)
     net: string;     // 净额 (供应 - 借款)
   }
   ```

### 前端期望的数据格式

前端定义的接口（来自 `index.tsx`）：

```typescript
interface Holder {
  id: number;              // 数据库记录 ID
  holder_address: string;  // 持有者地址
  token_type: string;      // 代币类型
  token_address: string;   // 代币合约地址
  balance: string;         // 余额 (wei 格式字符串)
  first_seen: number;      // 首次持有时间戳 (秒)
  last_updated: number;    // 最后更新时间戳 (秒)
}

interface Stats {
  token_type: string;      // 代币类型
  holder_count: number;    // 持有者数量
  total_balance: number;   // 总余额
}
```

---

## 🔄 数据转换需求

### 后端需要实现的转换

1. **添加数据库字段**
   - `id`: 自增主键
   - `first_seen`: 从 Transfer 事件的首次出现时间获取
   - `last_updated`: 每次更新时的时间戳
   - `token_address`: 添加代币合约地址

2. **余额格式转换**
   - 后端脚本中使用 `ethers.formatEther(balance)` 得到的是格式化后的字符串
   - 前端需要 **wei 格式** 的原始字符串
   - **建议**：后端存储原始 wei 值，不要格式化

3. **Lending 数据特殊处理**
   - Lending 有 `supply`、`borrow`、`net` 三个字段
   - 前端只有一个 `balance` 字段
   - **建议**：使用 `net` (净额) 作为 balance 值

---

## 🏗️ 后端数据库设计建议

### 表结构：`holders`

```sql
CREATE TABLE holders (
  id BIGSERIAL PRIMARY KEY,
  holder_address VARCHAR(42) NOT NULL,
  token_type VARCHAR(20) NOT NULL,
  token_address VARCHAR(42) NOT NULL,
  balance VARCHAR(78) NOT NULL,        -- wei 格式字符串 (最大 2^256)
  first_seen INTEGER NOT NULL,         -- Unix timestamp (秒)
  last_updated INTEGER NOT NULL,       -- Unix timestamp (秒)

  -- Lending 特有字段（可选）
  supply VARCHAR(78),                  -- USDC supply (可选)
  borrow VARCHAR(78),                  -- USDC borrow (可选)

  UNIQUE(holder_address, token_type)   -- 组合唯一索引
);

CREATE INDEX idx_token_type ON holders(token_type);
CREATE INDEX idx_holder_address ON holders(holder_address);
CREATE INDEX idx_last_updated ON holders(last_updated);
```

### 表结构：`holder_stats` (可选缓存表)

```sql
CREATE TABLE holder_stats (
  id SERIAL PRIMARY KEY,
  token_type VARCHAR(20) UNIQUE NOT NULL,
  holder_count INTEGER NOT NULL DEFAULT 0,
  total_balance NUMERIC(30, 0) NOT NULL DEFAULT 0,
  last_updated INTEGER NOT NULL
);
```

---

## 🔌 API 实现建议

### 1. GET /api/holders/stats

**功能**：返回所有代币的统计信息

**实现逻辑**：
```typescript
// 从缓存表读取（推荐）
SELECT token_type, holder_count, total_balance
FROM holder_stats
ORDER BY token_type;

// 或实时计算
SELECT
  token_type,
  COUNT(*) as holder_count,
  SUM(CAST(balance AS NUMERIC)) as total_balance
FROM holders
WHERE balance != '0'
GROUP BY token_type;
```

**返回格式**：
```json
{
  "success": true,
  "data": [
    {
      "token_type": "YT-A",
      "holder_count": 156,
      "total_balance": 1250000000000000000000
    },
    {
      "token_type": "YT-B",
      "holder_count": 89,
      "total_balance": 780000000000000000000
    },
    {
      "token_type": "YT-C",
      "holder_count": 124,
      "total_balance": 950000000000000000000
    },
    {
      "token_type": "ytLP",
      "holder_count": 67,
      "total_balance": 450000000000000000000
    },
    {
      "token_type": "Lending",
      "holder_count": 43,
      "total_balance": 320000000000
    }
  ]
}
```

---

### 2. GET /api/holders/:type

**功能**：返回特定代币的持有者列表

**参数**：
- `type`: YT-A | YT-B | YT-C | ytLP | Lending

**实现逻辑**：
```typescript
SELECT
  id,
  holder_address,
  token_type,
  token_address,
  balance,
  first_seen,
  last_updated
FROM holders
WHERE token_type = :type
  AND balance != '0'
ORDER BY CAST(balance AS NUMERIC) DESC;
```

**返回格式**：
```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "holder_address": "0x1234567890abcdef1234567890abcdef12345678",
      "token_type": "YT-A",
      "token_address": "0x97204190B35D9895a7a47aa7BaC61ac08De3cF05",
      "balance": "100000000000000000000",
      "first_seen": 1707800000,
      "last_updated": 1707900000
    }
  ]
}
```

---

### 3. POST /api/holders/update

**功能**：手动触发区块链数据同步

**实现逻辑**：
```typescript
// 触发后台任务
triggerBlockchainSync();

// 立即返回
return {
  success: true,
  message: "Update triggered successfully"
};
```

**返回格式**：
```json
{
  "success": true,
  "message": "Update triggered successfully"
}
```

---

## 🔧 后端代码集成步骤

### 1. 修改后端扫描脚本

将原脚本中的数据保存到数据库：

```typescript
// 在 getAllHolders 函数中添加数据库操作

// YT 代币持有者
for (const vault of YT_VAULTS) {
  // ... 获取 holders 数组 ...

  // 保存到数据库
  for (const holder of holders) {
    await db.query(`
      INSERT INTO holders (
        holder_address,
        token_type,
        token_address,
        balance,
        first_seen,
        last_updated
      ) VALUES ($1, $2, $3, $4, $5, $6)
      ON CONFLICT (holder_address, token_type)
      DO UPDATE SET
        balance = $4,
        last_updated = $6
    `, [
      holder.address,
      vault.name,
      vault.address,
      balance.toString(), // ⚠️ 原始 wei 值，不要格式化
      firstSeenTimestamp,
      Math.floor(Date.now() / 1000)
    ]);
  }
}

// LP 和 Lending 同理
```

### 2. 添加 first_seen 追踪

需要记录每个地址首次出现的时间：

```typescript
// 扩展全局地址集合，保存首次出现时间
const allYTAddresses: Map<string, Map<string, number>> = new Map();
// vault address -> (holder address -> first seen timestamp)

for (const event of events) {
  if ("args" in event && event.args.to !== ethers.ZeroAddress) {
    const address = event.args.to as string;
    const vaultAddresses = allYTAddresses.get(vault.address)!;

    if (!vaultAddresses.has(address)) {
      // 获取事件的区块时间戳
      const block = await provider.getBlock(event.blockNumber);
      vaultAddresses.set(address, block?.timestamp || 0);
    }
  }
}
```

### 3. 更新统计缓存表

```typescript
// 在每次扫描完成后更新统计表
await db.query(`
  INSERT INTO holder_stats (token_type, holder_count, total_balance, last_updated)
  VALUES ($1, $2, $3, $4)
  ON CONFLICT (token_type)
  DO UPDATE SET
    holder_count = $2,
    total_balance = $3,
    last_updated = $4
`, [tokenType, holderCount, totalBalance, Math.floor(Date.now() / 1000)]);
```

---

## 📝 合约地址配置

根据文档，需要更新合约地址配置：

### 最新合约地址

```typescript
// YT Vaults (新地址)
const YT_VAULTS = [
  { name: "YT-A", address: "0x97204190B35D9895a7a47aa7BaC61ac08De3cF05" },
  { name: "YT-B", address: "0x181ef4011c35C4a2Fda08eBC5Cf509Ef58E553fF" },
  { name: "YT-C", address: "0xE9A5b9f3a2Eda4358f81d4E2eF4f3280A664e5B0" },
];

// ytLP Token
const YTLP_ADDRESS = "0x102e3F25Ef0ad9b0695C8F2daF8A1262437eEfc3";

// Lending Contract
const LENDING_ADDRESS = "0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D";

// 部署区块号
const DEPLOYMENT_BLOCKS = {
  ytVaults: 227339300,
  ytlp: 227230270,
  lending: 227746053,
};
```

**⚠️ 注意**：这些地址与前端配置可能不一致，需要统一！

---

## 🎯 前端调整建议

### 1. 更新 Mock 数据格式

将 Mock 数据的余额改为 wei 格式：

```typescript
// mockData.ts
export const mockHolders = {
  'YT-A': [
    {
      id: 1,
      holder_address: '0x1234567890abcdef1234567890abcdef12345678',
      token_type: 'YT-A',
      token_address: '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05',
      balance: '100000000000000000000', // ✅ wei 格式
      first_seen: Math.floor(Date.now() / 1000) - 86400 * 30,
      last_updated: Math.floor(Date.now() / 1000),
    },
  ],
};
```

### 2. 添加 Lending 特殊显示（可选）

如果需要显示 Lending 的供应和借款信息，可以扩展前端接口：

```typescript
interface LendingHolder extends Holder {
  supply?: string;  // 供应金额
  borrow?: string;  // 借款金额
}
```

---

## ✅ 集成检查清单

- [ ] 后端数据库表已创建
- [ ] 后端扫描脚本已修改为保存数据库
- [ ] 后端 API 接口已实现 (3个接口)
- [ ] 合约地址已统一（前后端一致）
- [ ] 余额格式统一为 wei 格式
- [ ] first_seen 时间戳已实现
- [ ] 前端 Mock 数据格式已更新
- [ ] 前端关闭 Mock 模式 (USE_MOCK_DATA = false)
- [ ] 联调测试通过

---

## 🚀 部署步骤

1. **启动后端扫描服务**
   ```bash
   # 运行扫描脚本（建议使用 PM2 管理）
   pm2 start holder-scanner.ts --name holder-scanner
   ```

2. **启动后端 API 服务**
   ```bash
   # 启动 Golang API 服务器
   go run main.go
   ```

3. **配置前端**
   ```typescript
   // mockData.ts
   export const USE_MOCK_DATA = false; // 关闭 Mock
   ```

4. **测试验证**
   ```bash
   # 测试 API
   curl http://localhost:8080/api/holders/stats
   curl http://localhost:8080/api/holders/YT-A
   ```

---

**文档创建时间：** 2024-02-13
**最后更新：** 2024-02-13
