React Native DApp 开发全栈实战·从 0 到 1 系列(永续合约交易-前端部分)

  • 木西
  • 发布于 1天前
  • 阅读 66

前言继上一篇ReactNativeDApp开发全栈实战·从0到1系列(永续合约交易-合约部分)本文进入“前端交互”环节,本文带你把「开仓-行情剧变-平仓/清算」完整踩一遍:10×杠杆做多,价格拉涨20%,落袋为安;同样仓位,价格反杀20%,忍痛割肉;极端暴跌75%,触

前言

继上一篇React Native DApp 开发全栈实战·从 0 到 1 系列(永续合约交易-合约部分)本文进入“前端交互”环节,本文带你把「开仓-行情剧变-平仓/清算」完整踩一遍:

  1. 10×杠杆做多,价格拉涨 20%,落袋为安;
  2. 同样仓位,价格反杀 20%,忍痛割肉;
  3. 极端暴跌 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。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。