FR-x / BR-x / NFR-x)均指向 PRD。
本 TDD 描述 GS DeFi DApp 的技术实现方案,聚焦「怎么做」。 业务需求、规则、验收标准统一在 PRD 中维护。本文档各章节通过需求 ID 回引 PRD, 实现时请同时打开两份文档对照。
回答「做什么、为什么、约束是什么」。包含功能需求 FR、业务规则 BR、 非功能需求 NFR、验收标准 AC。
回答「怎么做」。包含技术栈、项目结构、合约对接、页面/组件设计、 API 规范、环境变量、开发阶段计划。
FR-x.y:功能需求(Functional Requirement),如 FR-1.1 开新仓BR-x:业务规则(Business Rule),如 BR-1 质押率 70%NFR-x:非功能需求(Non-Functional Requirement),如 NFR-2 国际化| 层级 | 技术 / 库 | 版本 | 用途说明 |
|---|---|---|---|
| 框架 | Next.js (App Router) | ^14 | SSR + API Routes,服务端可安全持有私钥 |
| Web3 | wagmi v2 + viem | ^2 / ^2 | 合约读写 hooks,类型安全,内置 TanStack Query 缓存 |
| 钱包 | RainbowKit | ^2 | MetaMask + WalletConnect 一键集成,支持移动端 |
| UI | shadcn/ui + TailwindCSS | latest | 暗色现代组件库,可自定义主题色(紫蓝渐变) |
| 图标 | lucide-react | latest | 轻量矢量图标库 |
| 国际化 | next-intl | ^3 | 中文/英文切换,URL 路径前缀方案 /zh / /en |
| 水龙头 | ethers.js v6 | ^6 | 仅用于服务端 API Route 签名转账 |
| 类型 | TypeScript | ^5 | 全项目强类型 |
DApp 作为独立子目录放在现有合约项目根目录下:
gs-defi-protocol/
├── contracts/ # 现有合约(不修改)
├── test/ # 现有测试(不修改)
├── docs/ # 合约需求文档
└── dapp/ # ← 新建 DApp 目录
├── app/
│ ├── layout.tsx # 根布局(Providers 注入)
│ ├── page.tsx # 首页 / 仪表盘
│ ├── lending/
│ │ └── page.tsx # 质押借贷页
│ ├── swap/
│ │ └── page.tsx # Swap 兑换页
│ ├── faucet/
│ │ └── page.tsx # 水龙头页
│ └── api/
│ └── faucet/
│ └── route.ts # 服务端转账 API(持私钥)
├── components/
│ ├── layout/
│ │ ├── Navbar.tsx
│ │ ├── Footer.tsx
│ │ └── LanguageSwitcher.tsx
│ ├── lending/
│ │ ├── BorrowForm.tsx # 开仓表单
│ │ ├── OrderList.tsx # 我的订单
│ │ ├── RepayModal.tsx # 还款弹窗
│ │ └── LiquidatePanel.tsx # 清算面板
│ ├── swap/
│ │ ├── SwapCard.tsx
│ │ └── SlippageSettings.tsx
│ └── faucet/
│ └── FaucetCard.tsx
├── hooks/
│ ├── usePledgeLending.ts # openOrder / repay / liquidate
│ ├── useSwap.ts # swap / getAmountsOut
│ └── useTokenBalance.ts # GS/GB/GC 余额 + approve
├── config/
│ ├── chains.ts # gsc_v2_test 网络定义
│ ├── contracts.ts # 合约地址常量
│ └── abis/ # ABI JSON 文件(从 artifacts/ 导出)
│ ├── PledgeLending.json
│ ├── TriggerParams.json
│ ├── UniswapV2Router02.json
│ └── ERC20.json
├── lib/
│ ├── i18n/
│ │ ├── zh.json # 中文翻译
│ │ └── en.json # 英文翻译
│ └── faucet-cooldown.ts # 服务端冷却时间 Map
├── public/
│ └── logo.svg
├── .env.local # 私钥等敏感配置(不提交 git)
├── .env.example # 环境变量示例模板(提交 git,含所有变量占位符)
├── package.json
├── tailwind.config.ts
└── tsconfig.json
网络参数全部从环境变量读取,部署到不同环境(测试网 / 主网)时只需修改 .env,
无需重新编译代码。所有配置项使用 NEXT_PUBLIC_ 前缀以便客户端可读。
// config/chains.ts
export const appChain = {
id: Number(process.env.NEXT_PUBLIC_CHAIN_ID),
name: process.env.NEXT_PUBLIC_CHAIN_NAME!,
nativeCurrency: {
name: process.env.NEXT_PUBLIC_NATIVE_NAME!,
symbol: process.env.NEXT_PUBLIC_NATIVE_SYMBOL!,
decimals: Number(process.env.NEXT_PUBLIC_NATIVE_DECIMALS ?? 18),
},
rpcUrls: {
default: { http: [process.env.NEXT_PUBLIC_RPC_URL!] },
},
blockExplorers: {
default: {
name: process.env.NEXT_PUBLIC_EXPLORER_NAME!,
url: process.env.NEXT_PUBLIC_EXPLORER_URL!,
},
},
}
| 合约 | 变量名 | 地址(gsc_v2_test) |
|---|---|---|
| GS Token | GS_TOKEN | 待部署 |
| GB Token | GB_TOKEN | 待部署 |
| GC Token | GC_TOKEN | 待部署 |
| PledgeLending | PLEDGE_LENDING | 待部署 |
| UniswapV2Router02 | ROUTER | 待部署 |
| UniswapV2Factory | FACTORY | 待部署 |
| TriggerParams | TRIGGER_PARAMS | 待部署 |
config/contracts.ts。
ABI 直接从现有 artifacts/contracts/ 导出,无需重新编译。
| 功能 | 合约方法 | 类型 |
|---|---|---|
| PledgeLending(质押借贷) | ||
| 开仓 | openPosition(uint256 gsAmount, uint256 cycleDays, LoanType loanType) returns (bytes32 orderId)LoanType 枚举:GB=0 / GC=1。开仓前需先 approve GS | Write |
| 补仓 | addCollateral(bytes32 orderId, uint256 gsAmount)仅借款人可调,需先 approve GS | Write |
| 还款 | repay(bytes32 orderId)必须在 [repayDueDate, repayDeadline] 窗口内;需先 approve GB 或 GC | Write |
| 清算 | liquidate(bytes32 orderId)任何人可调;触发:担保率<100% 或 当前时间>repayDeadline | Write |
| 订单完整查询(推荐) | getOrder(bytes32 orderId) returns (OrderView)一次返回订单原始数据 + 担保率 + 实时利息 + 应还数量 + 还款状态 | Read |
| 用户订单 ID 列表 | getUserOrders(address user) returns (bytes32[]) | Read |
| 担保率 | getCollateralRate(bytes32 orderId) returns (uint256)1e18 = 100% | Read |
| GC 实时利息 | getAccruedInterest(bytes32 orderId) returns (uint256) | Read |
| 预估应还数量 | getEstimatedRepayment(bytes32 orderId) returns (uint256) | Read |
| 还款状态 | getRepayStatus(bytes32 orderId) returns (RepayStatus)枚举:NotDue / Repayable / Overdue / Repaid / Liquidated | Read |
| GB 质押参数 | getGBPledgeParams() returns (uint256[] cycleDays, uint256[] rates) | Read |
| GC 质押参数 | getGCPledgeParams() returns (uint256[] cycleDays, uint256[] rates)GB/GC 各自独立的周期 → 质押率映射 | Read |
| TriggerParams(价格 / 利率参数) | ||
| GS/GB 汇率 | getGSGB() returns (uint256)= EMA(GS/GC,5) ÷ EMA(GB/GC,5) | Read |
| GS/GC 汇率(EMA-5) | getGSGC5() returns (uint256) | Read |
| GC 日利率 | getGCDailyRate() returns (uint256)iL = EMA(Rt,180) + K × σr,1e18 = 100% | Read |
| GB 日利率 | getGBDailyRate() returns (uint256)= 最新 Rt | Read |
| 最新 GS/GC 价格 | latestGsGcPrice() returns (uint256) | Read |
| 最新 GB/GC 价格 | latestGbGcPrice() returns (uint256) | Read |
| UniswapV2Router02 + ERC20 | ||
| Swap 兑换 | swapExactTokensForTokens(amountIn, amountOutMin, path, to, deadline) | Write |
| 查询兑换量 | getAmountsOut(amountIn, path) returns (uint256[]) | Read |
| 授权代币 | ERC20.approve(spender, amount) | Write |
| 查询授权额度 | ERC20.allowance(owner, spender) | Read |
| 查询余额 | ERC20.balanceOf(address) | Read |
// 借出类型
enum LoanType { GB, GC } // GB=0, GC=1
// 订单状态(合约存储层)
enum OrderStatus { Active, Repaid, Liquidated }
// 还款状态(getRepayStatus 返回,前端展示用)
enum RepayStatus {
NotDue, // 未到期(block.timestamp < repayDueDate)
Repayable, // 可还款(repayDueDate ≤ now ≤ repayDeadline)
Overdue, // 已逾期(now > repayDeadline,可被清算)
Repaid, // 已结清
Liquidated // 已清算
}
bytes32(不是 uint256),TS 用 0x{...} 字符串处理;wagmi 用 Hex 类型BR-11):合约要求 block.timestamp ≥ repayDueDate。UI 在 RepayStatus.NotDue 时必须禁用还款按钮1e18 = 100%、7e17 = 70%、1e16 = 1%。前端展示时除以 1e18 再 ×100gcDailyRate 被固化到订单中,之后利率变化不影响已开仓订单(避免开仓后利率上升导致借款人亏损)从已编译的 artifacts/contracts/ 直接导出 ABI 到 dapp/config/abis/:
artifacts/contracts/PledgeLending.sol/PledgeLending.json → PledgeLending.json
artifacts/contracts/TriggerParams.sol/TriggerParams.json → TriggerParams.json
artifacts/contracts/interfaces/IPledgeLending.sol/... → IPledgeLending.json
artifacts/contracts/interfaces/ITriggerParams.sol/... → ITriggerParams.json
artifacts/contracts/uniswap-v2-periphery/.../Router02.json → UniswapV2Router02.json
artifacts/.../ERC20.json → ERC20.json
参考 Sky Protocol(MakerDAO 新版)的 DApp 设计语言,采用 左侧固定边栏 + 右侧主内容区的现代布局。首页作为"中央枢纽", 所有复杂操作下沉到子页面,卡片化按功能分组呈现。
深紫渐变主题(#0d1117 → #1a0533 → #0d1b3e),
高对比文字,半透明卡片边框,悬停微光效果。
桌面端:左 240px 固定边栏 + 右侧弹性主区(最大 1200px)。 移动端:边栏收起为底部 4 图标导航。
卡片即入口:图标 + 标题 + 一句话描述,点击进入子页面。 首页不嵌入复杂表单,避免认知过载。
未连接钱包时,左侧 Balances 卡片显示插画 + Connect Wallet 引导; 右侧功能卡片仍可浏览(只读模式)。
┌──────────────────────────────────────────────────────────────┐
│ ⬡ GS DeFi [🌐 GSC V2 ▼] [Connect Wallet]│ ← 顶栏
├────────────┬─────────────────────────────────────────────────┤
│ 📊 Dashboard│ │
│ 💰 Lending │ 主内容区(按功能分组的卡片网格) │
│ 🔄 Swap │ │
│ 🚰 Faucet │ │
│ │ │
│ ┌────────┐ │ │
│ │Balances│ │ │
│ │ 我的资产│ │ │
│ │ GS:.. │ │ │
│ │ GB:.. │ │ │
│ │ GC:.. │ │ │
│ └────────┘ │ │
│ │ │
│ 🌐 中/EN │ │
└────────────┴─────────────────────────────────────────────────┘
/ (实现 FR-4 + FR-4.2)用户进入后的"中央枢纽"——左侧持久 Balances 卡片显示用户资产, 右侧按功能语义分组(质押借贷 / 兑换交易 / 新手入口 / 协议数据)。 每个分组下面是 1-2 行入口卡片,点击进入子页面执行实际操作。
/lending/swap?pair=.../faucet/lending (实现 FR-1.1 ~ FR-1.5,约束 BR-1 ~ BR-4、BR-7、BR-10)参考 MakerDAO / Aave 风格,左侧操作面板,右侧订单列表。具体业务规则见 PRD BR-1(质押率 70%)、BR-3(周期)、BR-7(利率来源)。
TriggerParams.getGCDailyRate())getGBPledgeParams() / getGCPledgeParams() 动态读取(默认 360 / 720 / 1080,admin 可动态增减)gsAmount × pledgeRate × getGSGB()gsAmount × pledgeRate × getGSGC5()
repayDueDate)、宽限期截止(repayDeadline)、预计满周期利息(GC)ERC20.approve(PledgeLending, gsAmount)(检查 allowance 够则跳过),再 openPosition()PositionOpened 事件中拿到 orderId 跳转到「我的订单」| 订单ID | 类型 | 质押GS | 借出量 | 到期日 | 状态 | 操作 |
|---|---|---|---|---|---|---|
#0x1a2b… |
GB | 10,000 GS | 7,000 GB | 2027-05-15 | 正常 | 补仓 还款 |
#0x3c4d… |
GC | 5,000 GS | 3,500 GC | 2026-08-10 | 临近到期 | 补仓 还款 |
getUserOrders(address) → bytes32[] → 逐个 getOrder(orderId) 拿 OrderView(含担保率、利息、应还、还款状态)RepayStatus 枚举(合约返回)+ 前端衍生:
NotDue 且距到期 ≥ 30 天 → 正常NotDue 且距到期 < 30 天 → 临近到期Repayable → 可还款Overdue 或 担保率 < 100% → 可清算Repayable 状态可点击;点击弹窗显示 estimatedRepay,需 approve(GB/GC, estimatedRepay) 后调 repay(orderId)approve(GS, addAmount) 后调 addCollateral(orderId, gsAmount)Overdue 或担保率 < 100% 的订单,显示可得 1% GS 奖励,一键调 liquidate(orderId)/swap (实现 FR-2.1、FR-2.2,约束 BR-8、BR-9)参考 Uniswap V2 经典界面,居中卡片布局。默认滑点 BR-8,手续费 BR-9。
getAmountsOut 计算 To 数量/faucet (实现 FR-3.1、FR-3.2,约束 BR-5、BR-6)简洁卡片,降低新用户上手门槛。冷却时间 BR-5,领取量 BR-6。
POST /api/faucet → 服务端转账.env.local 中配置,方便调整。
NFR-2)使用 next-intl 实现双语切换,翻译文件存于 lib/i18n/。需求详情见 PRD NFR-2。
// lib/i18n/zh.json(示例)
{
"nav": {
"home": "首页",
"lending": "质押借贷",
"swap": "Swap 兑换",
"faucet": "水龙头"
},
"lending": {
"borrow": "开仓借款",
"repay": "还款",
"liquidate": "清算",
"pledgeRate": "质押率"
}
}
zh),浏览器语言自动检测localStorage,刷新后保留/zh/lending、/en/lendingFR-3.1、FR-3.2,约束 BR-5、BR-6、NFR-4)POST /api/faucet
Content-Type: application/json
// Request
{ "address": "0x1234...abcd" }
// Response 200
{
"success": true,
"txHashes": {
"GS": "0xabc...",
"GB": "0xdef...",
"GC": "0x789..."
},
"nextClaimAt": "2026-05-16T10:30:00Z"
}
// Response 429 (冷却中)
{ "success": false, "error": "COOLDOWN", "nextClaimAt": "..." }
// Response 400 (地址无效)
{ "success": false, "error": "INVALID_ADDRESS" }
FAUCET_PRIVATE_KEY 仅存在于 .env.local,
通过 process.env 在服务端 API Route 中读取,绝不注入到客户端 bundle。
Next.js 只有不带 NEXT_PUBLIC_ 前缀的变量才会留在服务端。
NFR-3)TailwindCSS 断点(sm/md/lg)实现自适应布局。 卡片在移动端变为单列,表格支持横向滚动。
移动端隐藏顶部菜单,固定底部导航 4 个图标: 首页 / 借贷 / Swap / 水龙头。
RainbowKit 内置 WalletConnect v2, 支持 MetaMask Mobile、imToken、TokenPocket 等主流移动端钱包扫码连接。
按钮最小高度 44px,输入框字体 16px(防 iOS 自动缩放), 滑动手势支持下拉刷新余额。
NFR-4)# dapp/.env.local(不提交 git,本地实际配置)
# dapp/.env.example(提交 git,作为模板供他人参考)
# ── 水龙头钱包(服务端专用,无 NEXT_PUBLIC_ 前缀)
FAUCET_PRIVATE_KEY=0x... # 水龙头钱包私钥
# ── 每次领取量
FAUCET_GS_AMOUNT=1000 # 单位:GS(整数)
FAUCET_GB_AMOUNT=500
FAUCET_GC_AMOUNT=500
# ── 网络参数(动态加载,部署不同链时修改这里即可)
NEXT_PUBLIC_CHAIN_ID=44
NEXT_PUBLIC_CHAIN_NAME=Genesis Chain V2 Testnet
NEXT_PUBLIC_NATIVE_NAME=GS
NEXT_PUBLIC_NATIVE_SYMBOL=GS
NEXT_PUBLIC_NATIVE_DECIMALS=18
NEXT_PUBLIC_RPC_URL=https://v2test.genesischain.io
NEXT_PUBLIC_EXPLORER_NAME=GSCScan
NEXT_PUBLIC_EXPLORER_URL=https://v2scan.genesischain.io
# ── 合约地址(部署后填入)
NEXT_PUBLIC_GS_TOKEN=0x...
NEXT_PUBLIC_GB_TOKEN=0x...
NEXT_PUBLIC_GC_TOKEN=0x...
NEXT_PUBLIC_PLEDGE_LENDING=0x...
NEXT_PUBLIC_ROUTER=0x...
NEXT_PUBLIC_FACTORY=0x...
NEXT_PUBLIC_TRIGGER_PARAMS=0x...
.env.example 示例文件(提交 git)
为方便团队协作与新环境部署,在仓库中提供一份 .env.example 模板。
该文件 仅包含变量名和占位符,不含任何真实私钥或敏感地址,可安全提交到 git。
新成员克隆项目后 cp .env.example .env.local 再填充实际值即可。
# dapp/.env.example(提交到 git,作为团队配置模板)
# ── 水龙头钱包(服务端专用)
FAUCET_PRIVATE_KEY= # 留空,本地填入实际私钥
# ── 每次领取量
FAUCET_GS_AMOUNT=1000
FAUCET_GB_AMOUNT=500
FAUCET_GC_AMOUNT=500
# ── 网络参数(按部署目标链修改)
NEXT_PUBLIC_CHAIN_ID=44
NEXT_PUBLIC_CHAIN_NAME=Genesis Chain V2 Testnet
NEXT_PUBLIC_NATIVE_NAME=GS
NEXT_PUBLIC_NATIVE_SYMBOL=GS
NEXT_PUBLIC_NATIVE_DECIMALS=18
NEXT_PUBLIC_RPC_URL=https://v2test.genesischain.io
NEXT_PUBLIC_EXPLORER_NAME=GSCScan
NEXT_PUBLIC_EXPLORER_URL=https://v2scan.genesischain.io
# ── 合约地址(部署后填入)
NEXT_PUBLIC_GS_TOKEN=
NEXT_PUBLIC_GB_TOKEN=
NEXT_PUBLIC_GC_TOKEN=
NEXT_PUBLIC_PLEDGE_LENDING=
NEXT_PUBLIC_ROUTER=
NEXT_PUBLIC_FACTORY=
NEXT_PUBLIC_TRIGGER_PARAMS=
FAUCET_PRIVATE_KEY 不加 NEXT_PUBLIC_ 前缀,确保只在服务端可见。.env.local 必须加入 .gitignore,永不提交。.env.example 只放占位符 / 默认值,不放任何私钥与生产地址。创建 Next.js 项目、配置 wagmi/RainbowKit、接入 gsc_v2_test 网络、搭建导航栏、语言切换骨架
实现 API Route 服务端转账、冷却逻辑、水龙头前端卡片 UI
对接 UniswapV2 Router,实现代币兑换、滑点设置、实时汇率展示
实现开仓、订单列表、还款弹窗、清算面板,对接 PledgeLending 合约全生命周期
汇聚协议统计数据、代币价格、用户资产概览
完善底部导航、触摸优化、补全中英文翻译、端到端测试