前言继上一篇ReactNativeDApp开发全栈实战·从0到1系列(永续合约交易-合约部分)本文进入“前端交互”环节,本文带你把「开仓-行情剧变-平仓/清算」完整踩一遍:10×杠杆做多,价格拉涨20%,落袋为安;同样仓位,价格反杀20%,忍痛割肉;极端暴跌75%,触
继上一篇React Native DApp 开发全栈实战·从 0 到 1 系列(永续合约交易-合约部分)本文进入“前端交互”环节,本文带你把「开仓-行情剧变-平仓/清算」完整踩一遍:
- 10×杠杆做多,价格拉涨 20%,落袋为安;
- 同样仓位,价格反杀 20%,忍痛割肉;
- 极端暴跌 75%,触发清算,系统自动接管。
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';
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)
}
}
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)
}
}
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/da2b1a5514f64d93a7912ac1982cb9datplv-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/a7f2142153f54951bd6072c2b1d2f7b9tplv-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/a493c356986a4858bc8e192282a92c79tplv-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/257baa513c26456787f500d654d719fatplv-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>
本文把「永续合约+前端交互」最后一环补齐:
至此,「合约-喂价-前端」全链路打通:
后续改造:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码