前言继上一篇ReactNativeDApp开发全栈实战·从0到1系列(永续合约交易-合约部分)本文进入“前端交互”环节,本文带你把「开仓-行情剧变-平仓/清算」完整踩一遍:10×杠杆做多,价格拉涨20%,落袋为安;同样仓位,价格反杀20%,忍痛割肉;极端暴跌75%,触
继上一篇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费
代码
公共代码
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%+平仓 盈利场景)
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%+平仓 亏损场景")
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% 清算场景")
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); } };
### 查看持仓信息
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" }
# 效果图
<div style="display:flex; gap:8px;flex-wrap:wrap;">
<img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/da2b1a5514f64d93a7912ac1982cb9da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757589499&x-orig-sign=IXxHySyQotZgRUEqXHOo75oDFKE%3D" alt="图1转存失败,建议直接上传图片文件" width="200">
<img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/a7f2142153f54951bd6072c2b1d2f7b9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757589524&x-orig-sign=PQjwMFURWIGkfa5oJdzjF7NusMU%3D" alt="图1转存失败,建议直接上传图片文件" width="200">
<img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/a493c356986a4858bc8e192282a92c79~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757589542&x-orig-sign=OVIL8FEMhryFKTPH%2BXZZP1ABLZI%3D" alt="图1转存失败,建议直接上传图片文件" width="200">
<img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/257baa513c26456787f500d654d719fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757596204&x-orig-sign=QOcg7%2BciIaIGuDKIo3prtGXUvso%3D" alt="图1转存失败,建议直接上传图片文件" width="200">
</div>
# 总结
本文把「永续合约+前端交互」最后一环补齐:
1. 环境一键启动——hardhat node + 编译 + 部署,三条命令本地链就绪;
1. 三大极端场景——盈利、亏损、清算,全部用 ethers.js 脚本跑通,可直接复制到 React-Native/Web 项目;
1. 通用工具函数——positionCount + positions 循环拉取,链上 struct 秒变前端 JSON,仓位卡片即插即用。
至此,「合约-喂价-前端」全链路打通:
- 本地能 10×杠杆做多;
- 能手动改价体验 20% 涨跌盈亏;
- 能一键清算看 75% 爆仓。
后续改造:
- 把 MockV3Aggregator 换成主网/测试网 Price Feed;
- 把 mint 换成真实 USDC 入口;
- 再加上止盈止损、部分平仓、资金费率——就是一个可上线的永续 DApp。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!