Skip to content

Commit bf5d385

Browse files
committed
feat: support abi & api
1 parent 9bfb2b4 commit bf5d385

26 files changed

Lines changed: 2445 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Prime Leaderboard V2 — API ↔ Frontend 比对 & 计划
2+
3+
权威来源: **venus-protocol-api PR #962 `feat/prime-v2`**(真实实现,非 stub;取代旧的 feat/prime-leaderboard-v2-api 分析)
4+
FE branch: `feat/prime-leaderboard-api`(off rank-card,链尾)
5+
6+
## 关键约定
7+
- **chainId 必传**(query param)。当前**只支持 BSC_TESTNET (97)**(`SNAPSHOT_PG_SUPPORTED_CHAINS=[BSC_TESTNET]`)。非法 chain → 400。
8+
- 分页:`?page&limit`,响应含 `page/limit/total`
9+
- 金额:`...UsdCents`(string,美分)/ `...Mantissa`(string)。时间:Date(序列化为 ISO)。
10+
- 地址:响应里 `hexToAddress` 转成 checksum。
11+
12+
## 端点(6 个,真实查询 snapshot DB)
13+
14+
### 1. `GET /prime/leaderboard?chainId&page&limit&address?`
15+
当前榜单。`result[]`: `{ userAddress, rank, effectiveStakeMantissa, totalStakedMantissa, isPrimeHolder }`;外层 `{ chainId, blockNumber, computedAt, page, limit, total, result }``address?` 传了可定位该用户。
16+
**RankTable**
17+
18+
### 2. `GET /prime/cycle/current?chainId`
19+
`{ chainId, cycle, pendingPool }`
20+
- `cycle`: `{ cycleIndex, status, startsAt, endsAt, anchorBlockNum, mintLimitUsed }`
21+
- `pendingPool`: `{ blockNumber, computedAt, primeHolderCount, totalPendingUsdCents }`
22+
**endOfCycleDate**(`cycle.endsAt`)、EndOfCycle 周期起(`startsAt`)、**TotalRewards 池**(`pendingPool.totalPendingUsdCents`)、刷新时间(`computedAt`)。
23+
24+
### 3. `GET /prime/users/:address/pending-rewards?chainId`
25+
`{ chainId, userAddress, blockNumber, isPrimeHolder, rank, totalPendingUsdCents, rewards[] }`
26+
`rewards[]`: `{ marketAddress, rewardTokenAddress, pendingAmountMantissa, pendingUsdCents }`
27+
**RankCard**(rank/isPrimeHolder)、**UserRewards 卡**(totalPendingUsdCents + 每市场 rewards)。
28+
29+
### 4. `GET /prime/cycles?chainId&page&limit`
30+
已结算周期列表。`result[]`: `{ cycleIndex, startsAt, endsAt, mintLimitUsed, totalRewardPoolUsdCents, finalizedAt }`
31+
→ 判断「是否有上一周期」(决定 first-cycle 形态)+ 取 last cycle index。
32+
33+
### 5. `GET /prime/cycles/:cycleIndex?chainId` (cycleIndex 可为 `latest`)
34+
`{ chainId, cycle, markets[], ranking[] }`
35+
- `cycle`: FinalizedCycleRow + `status`
36+
- `markets[]`: `{ marketAddress, rewardTokenAddress, tokenDistributionSpeedMantissa, supplyMultiplierMantissa, borrowMultiplierMantissa, totalRewardMantissa, totalRewardUsdCents }`
37+
- `ranking[]`: `{ userAddress, finalRank, finalEffectiveStakeMantissa, finalTotalStakedMantissa }`
38+
**LastCycleSummaryModal**(用 `latest` 或上一 index)。
39+
40+
### 6. `GET /prime/cycles/:cycleIndex/users/:address?chainId`
41+
`{ chainId, cycleIndex, userAddress, totalRewardUsdCents, markets[] }`
42+
markets[]: `{ marketAddress, rewardTokenAddress, totalRewardMantissa, totalRewardUsdCents }`
43+
→ LastCycleSummaryModal 里的「用户上周期奖励」。
44+
45+
## 比对:前端占位 → API
46+
47+
| FE 占位 | API 来源 | 状态 |
48+
|---|---|---|
49+
| `useGetPrimeRank`.rank / isPrime | #3 rank / isPrimeHolder ||
50+
| RankTable placeholderRanks | #1 leaderboard ||
51+
| RewardTable placeholderRewards | #1 leaderboard(+ 各用户 reward 需 #3?或 leaderboard 不含 reward → 用 #1 排名 + ?) | ⚠️ 见缺口 |
52+
| useGetPrimeTotalRewards | #2 pendingPool.totalPendingUsdCents | ✅ 总额;**每市场拆分**见缺口 |
53+
| useGetPrimeUserRewards | #3 totalPendingUsdCents + rewards[] ||
54+
| LastCycleSummaryModal total/user | #5 + #6 ||
55+
| endOfCycleDate | #2 cycle.endsAt ||
56+
| lastRefreshedAt | #2 pendingPool.computedAt / #1 computedAt ||
57+
| primeScore(榜单显示的分数) | #1 `effectiveStakeMantissa`(≈ score) | ✅(注意是 mantissa,不是 cents) |
58+
59+
## 缺口处理(已和后端/PRD 敲定)
60+
61+
### ✅ 已解决
62+
1. **`hasStakedXvs`** = `totalStakedMantissa > 0`(#1`address` 取自己那行)或合约 `getEffectiveStake(user) > 0`
63+
2. **`hasSupplied`** = 项目现有 pools 数据:合格 Prime 市场(vUSDT/vUSDC/vBTC/vETH)里 `asset.userSupplyBalanceTokens > 0`**不是** API 字段。
64+
3. **`gapXvsTokens`**:Gleiser 确认后端出**第 500 名(榜底)的量**;前端算 `gap = bottomStake − userStake + 1`(他举例 10000 − 7000 + 1 = 3001)。
65+
- 榜底量 ← **新路由(待 Gleiser 加)**
66+
- 用户自己 stake ← 合约 `getEffectiveStake(user)`#1 `effectiveStakeMantissa`
67+
- 注意:这是**按 effective stake 的简化算法**(非 PRD 那套 boost×days 投影);后端 & FE 已对齐用简化版。
68+
- 显示规则:`gap ≤ 100_000`(`TOP_500_GAP_THRESHOLD_XVS`,已存在常量)→ 显示具体数;否则泛化文案。
69+
4. **每市场 Prime APY (`apyPercentage`)**:用项目现有 **Prime APY**(非 supply apy):
70+
- `getHypotheticalPrimeApys` → Base & Prime **模拟** APY(非供给/未连接态)
71+
- `useGetPools``getUserPrimeApys`**实际** Prime APY(已供给态)
72+
- 展示组件:`pages/Market/OperationForm/ApyBreakdown`
73+
6. **API base URL**:用项目现有 `clients/api` / restService 约定,无需单独 base。
74+
75+
### ⏳ 仍等后端(Gleiser)
76+
A. **Prime Score(排名分 542.5M)**:#1 行只有 `effectiveStakeMantissa`,无加权 score。
77+
- 自己那行:可前端用合约 `getDeposits`+`getMultiplier` 算;
78+
-**整张榜单每行(最多 500)算不了**(ranking 是链下的,文档 §1 "Ranking is OFF-CHAIN")→ **必须后端在 #1 行里返回 prime score**
79+
B. **榜底(第 500 名)量** 的新路由(gap 用)—— Gleiser 已答应加。
80+
C. **当前周期每市场 reward 拆分**(Card B 明细):#2 `pendingPool` 只有总额。过去周期(#5)有每市场,当前周期没有 → 确认从哪取。
81+
82+
## 合约(BSC 测试网 97,来自 PrimeV2-testnet-integration 文档)
83+
- PrimeLeaderboard proxy: `0x1a4408613eec291f2d338F7A88E9D550fa9cD8dC`
84+
- PrimeV2 proxy: `0xeC22366d2572e52BCB29B50C905b945BA421B9b2`
85+
- XVS Vault proxy: `0x9aB56bAD2D7631B2A857ccf36d998232A8b82280`(Prime pool id 测试网 = 1)
86+
- 合格市场:vUSDT `0xb7526572FFE56AB9D7489838Bf2E18e3323b441A`、vUSDC `0xD5C4C2e2facBEB59D0216D0595d63FcDc6F9A1a7`、vBTC `0xb6e9322C49FD75a367Fcb17B0Fcd62C5070EbCBe`、vETH `0x162D005F0Fff510E54958Cfc5CF32A3180A84aab`
87+
- 倍率:<30d=1.0x / ≥30d=1.3x / ≥60d=1.6x / ≥90d=2.0x(90d 封顶);mintThreshold=1 XVS;tokenLimit=500
88+
- PrimeLeaderboard 读接口:`getEffectiveStake(user)` / `getEffectiveStakeBatch` / `getTotalStaked` / `getDeposits` / `getMultiplier` / `getMultiplierTiers`
89+
- ⚠️ 前端目前**只有 Prime v1 ABI**,PrimeV2/PrimeLeaderboard ABI 还没进 `@venusprotocol` 包 / `generated/abis`。若 gap 要读合约,需先补 ABI;否则尽量靠后端路由避免接 v2 合约。
90+
91+
## 团队聊天补充的产品决策(影响 FE,与 API 无关)
92+
- **第一周期特殊形态**(Maxime 定):reward table 为空 → **隐藏 reward table,仅显示 rank table**;EndOfCycle 文案**去掉「See last cycle's Prime summary」CTA**(还没上一周期)。→ 用 #4 `/prime/cycles` 是否为空判断 first-cycle。
93+
- fresh start;测试网倍率压缩(0-1h/1-2h/2-3h/≥3h)。
94+
95+
## FE 接入计划(clients/api 规范)
96+
建 6 组 `getXxx + useGetXxx`(`FunctionKey.GET_PRIME_*`,queryKey 含 chainId+address+page+cycleIndex):
97+
1 leaderboard → RankTable;2 currentCycle → totalRewards/endDate;3 userPending → RankCard/userRewards;4 cycles → first-cycle 判断;5 pastCycle + 6 userCycleRewards → LastCycleSummaryModal。
98+
现有 3 个本地占位 hook 改为包装这些,组件 props 不变。
99+
100+
## 可先做(不阻塞于缺口/部署)
101+
- first-cycle 隐藏 reward table + 去 CTA(用 #4 判断,逻辑可先写、数据后接)。

apps/evm/src/clients/api/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,36 @@ export * from './queries/useGetPrimeEstimation';
202202
export * from './queries/getPrimeDistributionForMarket';
203203
export * from './queries/getPrimeDistributionForMarket/useGetPrimeDistributionForMarket';
204204

205+
export * from './queries/getPrimeLeaderboard';
206+
export * from './queries/getPrimeLeaderboard/useGetPrimeLeaderboard';
207+
208+
export * from './queries/getPrimeMinimumStake';
209+
export * from './queries/getPrimeMinimumStake/useGetPrimeMinimumStake';
210+
211+
export * from './queries/getPrimeCurrentCycle';
212+
export * from './queries/getPrimeCurrentCycle/useGetPrimeCurrentCycle';
213+
214+
export * from './queries/getPrimeUserPendingRewards';
215+
export * from './queries/getPrimeUserPendingRewards/useGetPrimeUserPendingRewards';
216+
217+
export * from './queries/getPrimeCycles';
218+
export * from './queries/getPrimeCycles/useGetPrimeCycles';
219+
220+
export * from './queries/getPrimePastCycle';
221+
export * from './queries/getPrimePastCycle/useGetPrimePastCycle';
222+
223+
export * from './queries/getPrimeUserCycleRewards';
224+
export * from './queries/getPrimeUserCycleRewards/useGetPrimeUserCycleRewards';
225+
226+
export * from './queries/getPrimeEffectiveStake';
227+
export * from './queries/getPrimeEffectiveStake/useGetPrimeEffectiveStake';
228+
229+
export * from './queries/getPrimeMultiplierTiers';
230+
export * from './queries/getPrimeMultiplierTiers/useGetPrimeMultiplierTiers';
231+
232+
export * from './queries/getPrimeOnChainPendingRewards';
233+
export * from './queries/getPrimeOnChainPendingRewards/useGetPrimeOnChainPendingRewards';
234+
205235
export * from './queries/getVaiVaultPaused';
206236
export * from './queries/getVaiVaultPaused/useGetVaiVaultPaused';
207237

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { VError } from 'libs/errors';
2+
import type { ChainId } from 'types';
3+
import { restService } from 'utilities';
4+
5+
export interface PrimeCurrentCycle {
6+
cycleIndex: number;
7+
status: string;
8+
startsAt: Date;
9+
endsAt: Date;
10+
anchorBlockNum: string | null;
11+
mintLimitUsed: number;
12+
}
13+
14+
export interface PrimePendingRewardPool {
15+
blockNumber: string;
16+
computedAt: Date;
17+
primeHolderCount: number;
18+
totalPendingUsdCents: string;
19+
}
20+
21+
export interface GetPrimeCurrentCycleInput {
22+
chainId: ChainId;
23+
}
24+
25+
export interface GetPrimeCurrentCycleOutput {
26+
cycle: PrimeCurrentCycle | null;
27+
pendingPool: PrimePendingRewardPool | null;
28+
}
29+
30+
interface PrimeCurrentCycleResponse {
31+
cycleIndex: number;
32+
status: string;
33+
startsAt: string;
34+
endsAt: string;
35+
anchorBlockNum: string | null;
36+
mintLimitUsed: number;
37+
}
38+
39+
interface PrimePendingRewardPoolResponse {
40+
blockNumber: string;
41+
computedAt: string;
42+
primeHolderCount: number;
43+
totalPendingUsdCents: string;
44+
}
45+
46+
interface GetPrimeCurrentCycleResponse {
47+
cycle: PrimeCurrentCycleResponse | null;
48+
pendingPool: PrimePendingRewardPoolResponse | null;
49+
}
50+
51+
export const getPrimeCurrentCycle = async ({
52+
chainId,
53+
}: GetPrimeCurrentCycleInput): Promise<GetPrimeCurrentCycleOutput> => {
54+
const response = await restService<GetPrimeCurrentCycleResponse>({
55+
endpoint: '/prime/cycle/current',
56+
method: 'GET',
57+
params: { chainId },
58+
});
59+
60+
const payload = response.data;
61+
62+
if (payload && 'error' in payload) {
63+
throw new VError({
64+
type: 'unexpected',
65+
code: 'somethingWentWrong',
66+
data: { exception: payload.error },
67+
});
68+
}
69+
70+
if (!payload) {
71+
throw new VError({ type: 'unexpected', code: 'somethingWentWrong' });
72+
}
73+
74+
return {
75+
cycle: payload.cycle
76+
? {
77+
...payload.cycle,
78+
startsAt: new Date(payload.cycle.startsAt),
79+
endsAt: new Date(payload.cycle.endsAt),
80+
}
81+
: null,
82+
pendingPool: payload.pendingPool
83+
? {
84+
...payload.pendingPool,
85+
computedAt: new Date(payload.pendingPool.computedAt),
86+
}
87+
: null,
88+
};
89+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type QueryObserverOptions, useQuery } from '@tanstack/react-query';
2+
3+
import FunctionKey from 'constants/functionKey';
4+
import { useChainId } from 'libs/wallet';
5+
6+
import {
7+
type GetPrimeCurrentCycleInput,
8+
type GetPrimeCurrentCycleOutput,
9+
getPrimeCurrentCycle,
10+
} from '.';
11+
12+
type Options = QueryObserverOptions<
13+
GetPrimeCurrentCycleOutput,
14+
Error,
15+
GetPrimeCurrentCycleOutput,
16+
GetPrimeCurrentCycleOutput,
17+
[FunctionKey.GET_PRIME_CURRENT_CYCLE, GetPrimeCurrentCycleInput]
18+
>;
19+
20+
export const useGetPrimeCurrentCycle = (options?: Partial<Options>) => {
21+
const { chainId } = useChainId();
22+
23+
return useQuery({
24+
queryKey: [FunctionKey.GET_PRIME_CURRENT_CYCLE, { chainId }],
25+
queryFn: () => getPrimeCurrentCycle({ chainId }),
26+
...options,
27+
});
28+
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { VError } from 'libs/errors';
2+
import type { ChainId } from 'types';
3+
import { restService } from 'utilities';
4+
5+
export interface PrimeFinalizedCycle {
6+
cycleIndex: number;
7+
startsAt: Date;
8+
endsAt: Date;
9+
mintLimitUsed: number;
10+
totalRewardPoolUsdCents: string | null;
11+
finalizedAt: Date | null;
12+
}
13+
14+
export interface GetPrimeCyclesInput {
15+
chainId: ChainId;
16+
page?: number;
17+
limit?: number;
18+
}
19+
20+
export interface GetPrimeCyclesOutput {
21+
page: number;
22+
limit: number;
23+
total: number;
24+
cycles: PrimeFinalizedCycle[];
25+
}
26+
27+
interface PrimeFinalizedCycleResponse {
28+
cycleIndex: number;
29+
startsAt: string;
30+
endsAt: string;
31+
mintLimitUsed: number;
32+
totalRewardPoolUsdCents: string | null;
33+
finalizedAt: string | null;
34+
}
35+
36+
interface GetPrimeCyclesResponse {
37+
page: number;
38+
limit: number;
39+
total: number;
40+
result: PrimeFinalizedCycleResponse[];
41+
}
42+
43+
export const getPrimeCycles = async ({
44+
chainId,
45+
page,
46+
limit,
47+
}: GetPrimeCyclesInput): Promise<GetPrimeCyclesOutput> => {
48+
const response = await restService<GetPrimeCyclesResponse>({
49+
endpoint: '/prime/cycles',
50+
method: 'GET',
51+
params: { chainId, page, limit },
52+
});
53+
54+
const payload = response.data;
55+
56+
if (payload && 'error' in payload) {
57+
throw new VError({
58+
type: 'unexpected',
59+
code: 'somethingWentWrong',
60+
data: { exception: payload.error },
61+
});
62+
}
63+
64+
if (!payload) {
65+
throw new VError({ type: 'unexpected', code: 'somethingWentWrong' });
66+
}
67+
68+
return {
69+
page: payload.page,
70+
limit: payload.limit,
71+
total: payload.total,
72+
cycles: payload.result.map(cycle => ({
73+
...cycle,
74+
startsAt: new Date(cycle.startsAt),
75+
endsAt: new Date(cycle.endsAt),
76+
finalizedAt: cycle.finalizedAt ? new Date(cycle.finalizedAt) : null,
77+
})),
78+
};
79+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type QueryObserverOptions, keepPreviousData, useQuery } from '@tanstack/react-query';
2+
3+
import FunctionKey from 'constants/functionKey';
4+
import { useChainId } from 'libs/wallet';
5+
6+
import { type GetPrimeCyclesInput, type GetPrimeCyclesOutput, getPrimeCycles } from '.';
7+
8+
type TrimmedInput = Omit<GetPrimeCyclesInput, 'chainId'>;
9+
10+
type Options = QueryObserverOptions<
11+
GetPrimeCyclesOutput,
12+
Error,
13+
GetPrimeCyclesOutput,
14+
GetPrimeCyclesOutput,
15+
[FunctionKey.GET_PRIME_CYCLES, GetPrimeCyclesInput]
16+
>;
17+
18+
export const useGetPrimeCycles = (input: TrimmedInput = {}, options?: Partial<Options>) => {
19+
const { chainId } = useChainId();
20+
const params = { ...input, chainId };
21+
22+
return useQuery({
23+
queryKey: [FunctionKey.GET_PRIME_CYCLES, params],
24+
queryFn: () => getPrimeCycles(params),
25+
placeholderData: keepPreviousData,
26+
...options,
27+
});
28+
};

0 commit comments

Comments
 (0)