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

前言

继上一篇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费

核心代码

公共代码
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"
}
# 关于 做多,做空前端可以通过计算获取也可以通不过合约添加一个方法也可以

效果图

总结

本文把「永续合约+前端交互」最后一环补齐:

  1. 环境一键启动------hardhat node + 编译 + 部署,三条命令本地链就绪;
  2. 三大极端场景------盈利、亏损、清算,全部用 ethers.js 脚本跑通,可直接复制到 React-Native/Web 项目;
  3. 通用工具函数------positionCount + positions 循环拉取,链上 struct 秒变前端 JSON,仓位卡片即插即用。

至此,「合约-喂价-前端」全链路打通:

  • 本地能 10×杠杆做多;
  • 能手动改价体验 20% 涨跌盈亏;
  • 能一键清算看 75% 爆仓。

后续改造:

  • 把 MockV3Aggregator 换成主网/测试网 Price Feed;
  • 把 mint 换成真实 USDC 入口;
  • 再加上止盈止损、部分平仓、资金费率------就是一个可上线的永续 DApp。
相关推荐
WebGISer_白茶乌龙桃几秒前
Cesium实现“悬浮岛”式,三维立体的行政区划
javascript·vue.js·3d·web3·html5·webgl
哈__26 分钟前
React Native 鸿蒙跨平台开发:LayoutAnimation 实现鸿蒙端页面切换的淡入淡出过渡动画
javascript·react native·react.js
哈__28 分钟前
React Native 鸿蒙跨平台开发:Animated 实现鸿蒙端组件的上下滑动入场动画
react native·react.js·harmonyos
wszy180911 小时前
顶部标题栏的设计与实现:让用户知道自己在哪
java·python·react native·harmonyos
wincheshe1 天前
React Native inspector 点击组件跳转编辑器技术详解
react native·react.js·编辑器
墨狂之逸才2 天前
React Native Hooks 快速参考卡
react native
墨狂之逸才2 天前
useRefreshTrigger触发器模式工作流程图解
react native
墨狂之逸才2 天前
react native项目中使用React Hook 高级模式
react native
wayne2142 天前
React Native 状态管理方案全梳理:Redux、Zustand、React Query 如何选
javascript·react native·react.js
电报号dapp1193 天前
钱包开发:在虚无中为数字自我筑巢
游戏·去中心化·区块链·智能合约