前言
继上一篇React Native DApp 开发全栈实战·从 0 到 1 系列(永续合约交易-合约部分)本文进入"前端交互"环节,本文带你把「开仓-行情剧变-平仓/清算」完整踩一遍:
- 10×杠杆做多,价格拉涨 20%,落袋为安;
- 同样仓位,价格反杀 20%,忍痛割肉;
- 极端暴跌 75%,触发清算,系统自动接管。
前期准备
- hardhat启动网络节点:npx hardhat node
- 合约编译:npx hardhat compile 生成对应的xxx.json用获取abi等相关信息
- 合约部署:npx hardhat deploy --tags token2,MockV3Aggregator,PerpTrade 获取合约地址(代币、喂价和永续合约地址)
- 节点的私钥导入钱包:用来与合约交互时支付对应的gas费
核心代码
公共代码
javascript
import { abi as MockV3AggregatorAbi } from '@/abi/MockV3Aggregator.json';
import { abi as MyToken2Abi } from '@/abi/MyToken2.json';
import { abi as PerpTradeAbi } from '@/abi/PerpTrade.json';
import * as ethers from 'ethers';
场景1(开仓10x + 涨价 20%+平仓 盈利场景)
ini
const OpenPositionProfitFn=async()=>{
try{
const provider = new ethers.providers.Web3Provider(window.ethereum);
/* 0. 连接 MetaMask 并确保 Alice 在账户列表里 */
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const userAddr = await signer.getAddress();//用户地址
const PerpTradeAddress="0x998abeb3E57409262aE5b751f60747921B33613E";//永续合约
const MyToken2Address ="0xf5059a5D33d5853360D16C683c16e67980206f36";//保证金
const MockV3AggregatorAddress="0x95401dc811bb5740090279Ba06cfA8fcF6113778";//喂价格合约地址
const perpTradeContract = new ethers.Contract(PerpTradeAddress, PerpTradeAbi, signer);
const MyToken2Contract = new ethers.Contract(MyToken2Address, MyToken2Abi, signer);
const MockV3AggregatorContract = new ethers.Contract(MockV3AggregatorAddress, MockV3AggregatorAbi, signer);
console.log(perpTradeContract.address,MyToken2Contract,MockV3AggregatorContract)
/* 4. 常量 */
const collateral = ethers.utils.parseUnits("1000", 6); // 1000 USDC
const leverage = ethers.utils.parseEther("10"); // 10×
const extraUSDC = ethers.utils.parseUnits("10000", 6); // 额外打给合约做准备
/* 5. 给用户打钱(仅测试币) */
const mintTx = await MyToken2Contract.mint(userAddr, collateral.add(extraUSDC));
await mintTx.wait();
console.log("USDC 已 mint",ethers.utils.formatUnits(await MyToken2Contract.balanceOf(userAddr),6));
/* 6. 用户把 3000 USDC 打进永续合约(赔付准备金) */
const transferTx = await MyToken2Contract.transfer(PerpTradeAddress, extraUSDC);
await transferTx.wait();
console.log("赔付准备金已转入合约",ethers.utils.formatUnits(await MyToken2Contract.balanceOf(PerpTradeAddress),6));
/* 7. 授权永续合约可扣保证金 */
const allowTx = await MyToken2Contract.approve(PerpTradeAddress, collateral);
await allowTx.wait();
console.log("授权成功");
/* 8. 开仓:10×杠杆开多 */
const openTx = await perpTradeContract.open(true, collateral, leverage);
await openTx.wait();
console.log("开仓成功,当前价格:", ethers.utils.formatUnits(await perpTradeContract.getPrice(), 6));
/* 9. 把预言机价格拉涨 20 %(假设原 2200 → 2640) */
const newAnswer = 2640_0000_0000; // 根据你合约的 decimals 调整
const priceTx = await MockV3AggregatorContract.updateAnswer(newAnswer);
await priceTx.wait();
console.log("价格已拉升,最新价格:", ethers.utils.formatUnits(await perpTradeContract.getPrice(), 6));
/* 10. 平仓 */
try{
const count = await perpTradeContract.positionCount(userAddr);
console.log('仓位总数:', count.toString());
const pos=await perpTradeContract.positions(userAddr,count.sub(1))
const markPrice= await perpTradeContract.getPrice();
console.log('pos:',pos)
// 统一精度:价格 1e8 → 1e18
const entryPrice = pos.entryPrice.mul(ethers.BigNumber.from(10).pow(10));
const markPrice18= markPrice.mul(ethers.BigNumber.from(10).pow(10));
const delta = pos.isLong
? (markPrice18.gte(entryPrice)
? markPrice18.sub(entryPrice)
: entryPrice.sub(markPrice18))
: (markPrice18.lte(entryPrice)
? entryPrice.sub(markPrice18)
: markPrice18.sub(entryPrice));
const pnlValue = pos.size.mul(delta).div(entryPrice);
const profit = pos.isLong ? markPrice18.gte(entryPrice) : markPrice18.lte(entryPrice);
const unsafe = pnlValue.mul(100).gte(pos.collateral.mul(5)); // 5 % 维持保证金
console.log('盈利', profit, '金额', ethers.utils.formatUnits(pnlValue, 18), '可清算', unsafe);
const role = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("LIQUIDATOR_ROLE"));
console.log(role);
const admin = await perpTradeContract.getRoleAdmin(role);
console.log('owner:', admin);
console.log('当前 signer:', await signer.getAddress());
const closeTx = await perpTradeContract.close(count.sub(1)); // positionId 为 0
await closeTx.wait();
}catch(err){
console.log("平仓失败",err)
}
/* 11. 打印最终余额 */
const endBal = await MyToken2Contract.balanceOf(userAddr);
console.log("Alice 最终 USDC 余额:", ethers.utils.formatUnits(endBal, 6));
}catch(err){
console.log(err)
}
}
场景2("开仓10x + 降价 20%+平仓 亏损场景")
ini
const OpenPositionLossFn=async()=>{
try{
const provider = new ethers.providers.Web3Provider(window.ethereum);
/* 0. 连接 MetaMask 并确保 Alice 在账户列表里 */
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const userAddr = await signer.getAddress();//用户地址
const PerpTradeAddress="0x998abeb3E57409262aE5b751f60747921B33613E";//永续合约
const MyToken2Address ="0xf5059a5D33d5853360D16C683c16e67980206f36";//保证金
const MockV3AggregatorAddress="0x95401dc811bb5740090279Ba06cfA8fcF6113778";//喂价格合约地址
const perpTradeContract = new ethers.Contract(PerpTradeAddress, PerpTradeAbi, signer);
const MyToken2Contract = new ethers.Contract(MyToken2Address, MyToken2Abi, signer);
const MockV3AggregatorContract = new ethers.Contract(MockV3AggregatorAddress, MockV3AggregatorAbi, signer);
console.log(perpTradeContract.address,MyToken2Contract,MockV3AggregatorContract)
/* 4. 常量 */
const collateral = ethers.utils.parseUnits("1000", 6); // 1000 USDC
const leverage = ethers.utils.parseEther("10"); // 10×
const extraUSDC = ethers.utils.parseUnits("10000", 6); // 额外打给合约做准备
/* 5. 给用户打钱(仅测试币) */
const mintTx = await MyToken2Contract.mint(userAddr, collateral.add(extraUSDC));
await mintTx.wait();
console.log("USDC 已 mint",ethers.utils.formatUnits(await MyToken2Contract.balanceOf(userAddr),6));
/* 6. 用户把 3000 USDC 打进永续合约(赔付准备金) */
const transferTx = await MyToken2Contract.transfer(PerpTradeAddress, extraUSDC);
await transferTx.wait();
console.log("赔付准备金已转入合约",ethers.utils.formatUnits(await MyToken2Contract.balanceOf(PerpTradeAddress),6));
/* 7. 授权永续合约可扣保证金 */
const allowTx = await MyToken2Contract.approve(PerpTradeAddress, collateral);
await allowTx.wait();
console.log("授权成功");
/* 8. 开仓:10×杠杆开多 */
const openTx = await perpTradeContract.open(true, collateral, leverage);
await openTx.wait();
console.log("开仓成功,当前价格:", ethers.utils.formatUnits(await perpTradeContract.getPrice(), 6));
/* 9. 把预言机价格拉涨 20 %(假设原 2200 → 2640) */
const newAnswer = 1600_0000_0000; // 根据你合约的 decimals 调整
const priceTx = await MockV3AggregatorContract.updateAnswer(newAnswer);
await priceTx.wait();
console.log("价格已拉升,最新价格:", ethers.utils.formatUnits(await perpTradeContract.getPrice(), 6));
/* 10. 平仓 */
try{
const count = await perpTradeContract.positionCount(userAddr);
console.log('仓位总数:', count.toString());
const pos=await perpTradeContract.positions(userAddr,count.sub(1))
const markPrice= await perpTradeContract.getPrice();
console.log('pos:',pos)
// 统一精度:价格 1e8 → 1e18
const entryPrice = pos.entryPrice.mul(ethers.BigNumber.from(10).pow(10));
const markPrice18= markPrice.mul(ethers.BigNumber.from(10).pow(10));
const delta = pos.isLong
? (markPrice18.gte(entryPrice)
? markPrice18.sub(entryPrice)
: entryPrice.sub(markPrice18))
: (markPrice18.lte(entryPrice)
? entryPrice.sub(markPrice18)
: markPrice18.sub(entryPrice));
const pnlValue = pos.size.mul(delta).div(entryPrice);
const profit = pos.isLong ? markPrice18.gte(entryPrice) : markPrice18.lte(entryPrice);
const unsafe = pnlValue.mul(100).gte(pos.collateral.mul(5)); // 5 % 维持保证金
console.log('盈利', profit, '金额', ethers.utils.formatUnits(pnlValue, 18), '可清算', unsafe);
//
const role = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("LIQUIDATOR_ROLE"));
console.log(role);
const admin = await perpTradeContract.getRoleAdmin(role);
console.log('owner:', admin);
console.log('当前 signer:', await signer.getAddress());
const closeTx = await perpTradeContract.close(count.sub(1)); // positionId 为 0
await closeTx.wait();
}catch(err){
console.log("平仓失败",err)
}
/* 11. 打印最终余额 */
const endBal = await MyToken2Contract.balanceOf(userAddr);
console.log("Alice 最终 USDC 余额:", ethers.utils.formatUnits(endBal, 6));
}catch(err){
console.log(err)
}
}
场景3("开仓10x + 降价 75% 清算场景")
ini
const OpenPositionClearanceFn = async () => {
try {
/* 0. 连接钱包 */
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = provider.getSigner();
const user = await signer.getAddress();
/* 1. 合约地址(👈 换成你的) */
const PerpAddr = '0x998abeb3E57409262aE5b751f60747921B33613E';
const USDCAddr = '0xf5059a5D33d5853360D16C683c16e67980206f36';
const PriceAddr = '0x95401dc811bb5740090279Ba06cfA8fcF6113778';
/* 2. 合约实例 */
const perp = new ethers.Contract(PerpAddr, PerpTradeAbi, signer);
const usdc = new ethers.Contract(USDCAddr, MyToken2Abi, signer);
const price = new ethers.Contract(PriceAddr, MockV3AggregatorAbi, signer);
/* 3. 常量 */
const collateral = ethers.utils.parseUnits('1000', 6);
const leverage = ethers.utils.parseEther('10');
const reserve = ethers.utils.parseUnits('10000', 6); // 赔付金
/* 4. 管理员:一次性打开开关 & 授 LIQUIDATOR_ROLE 给当前钱包 */
const LIQUIDATOR_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes('LIQUIDATOR_ROLE'));
const isAdmin = await perp.hasRole(LIQUIDATOR_ROLE, user);
if (isAdmin) {
// 1. 激活合约(你的合约需要有一个 public view active() + setActive(bool))
// if (!(await perp.active())) {
// await (await perp.connect(signer).setActive(true)).wait();
// console.log('✅ 合约已激活');
// }
// 2. 授予清算角色
if (!(await perp.hasRole(LIQUIDATOR_ROLE, user))) {
await (await perp.connect(signer).grantRole(LIQUIDATOR_ROLE, user)).wait();
console.log('✅ 已授予自己 LIQUIDATOR_ROLE');
}
} else {
console.warn('当前地址不是 DEFAULT_ADMIN_ROLE,请用部署者地址执行');
}
/* 5. 给用户打钱 & 准备金 */
await (await usdc.mint(user, collateral.add(reserve))).wait();
await (await usdc.transfer(PerpAddr, reserve)).wait();
console.log('准备金已到位');
/* 6. 授权 & 开仓 */
await (await usdc.approve(PerpAddr, collateral)).wait();
await (await perp.connect(signer).open(true, collateral, leverage)).wait();
console.log('开仓成功,当前价格', ethers.utils.formatUnits(await perp.getPrice(), 6));
/* 7. 把价格打到 500(约 75% 跌幅) */
const newPrice = 500_0000_0000; // 按你合约 decimals 来
await (await price.updateAnswer(newPrice)).wait();
console.log('价格已暴跌 →', ethers.utils.formatUnits(await perp.getPrice(), 6));
/* 8. 自清算 */
const posId = (await perp.positionCount(user)).sub(1);
const balBefore = await usdc.balanceOf(user);
await (await perp.connect(signer).liquidate(user, posId)).wait();
const balAfter = await usdc.balanceOf(user);
console.log('清算完成,钱包余额增加:', ethers.utils.formatUnits(balAfter.sub(balBefore), 6), 'USDC');
/* 9. 最终余额 */
console.log('最终 USDC 余额', ethers.utils.formatUnits(balAfter, 6));
} catch (e) {
console.error(e);
}
};
查看持仓信息
csharp
# 获取仓位数组长度
const count = await perpTradeContract.positionCount(userAddr);
console.log('仓位总数:', count.toString());
# 获取指定仓位的详情
const pos=await perpTradeContract.positions(userAddr,count.sub(1))
# 通过循环返回 所有仓位的详情列表
# 返回的仓位数据结构 整理后
{
"positionId": 0,
"isLong": true,
"isActive": true,
"nominalValueUSD": 10000,
"collateralUSD": 1000,
"entryPriceUSD": 175,
"lastUpdate": "2024-07-20 12:15:02 UTC",
"leverage": 10,
"direction": "LONG",
"status": "OPEN"
}
# 关于 做多,做空前端可以通过计算获取也可以通不过合约添加一个方法也可以
效果图
总结
本文把「永续合约+前端交互」最后一环补齐:
- 环境一键启动------hardhat node + 编译 + 部署,三条命令本地链就绪;
- 三大极端场景------盈利、亏损、清算,全部用 ethers.js 脚本跑通,可直接复制到 React-Native/Web 项目;
- 通用工具函数------positionCount + positions 循环拉取,链上 struct 秒变前端 JSON,仓位卡片即插即用。
至此,「合约-喂价-前端」全链路打通:
- 本地能 10×杠杆做多;
- 能手动改价体验 20% 涨跌盈亏;
- 能一键清算看 75% 爆仓。
后续改造:
- 把 MockV3Aggregator 换成主网/测试网 Price Feed;
- 把 mint 换成真实 USDC 入口;
- 再加上止盈止损、部分平仓、资金费率------就是一个可上线的永续 DApp。