React Native DApp 开发全栈实战·从 0 到 1 系列(流动性挖矿-前端部分)

  • 木西
  • 发布于 20小时前
  • 阅读 91

前言继上一篇《ReactNativeDApp开发全栈实战·从0到1系列(流动性挖矿-合约部分)》,本文进入“前端交互”环节:把Hardhat测试脚本里那套「mint→approve→deposit→evm_increaseTime→harvest」的自动化流程,

前言

继上一篇《React Native DApp 开发全栈实战·从 0 到 1 系列(流动性挖矿-合约部分)》,本文进入“前端交互”环节:把 Hardhat 测试脚本里那套「mint → approve → deposit → evm_increaseTime → harvest」的自动化流程,
原封不动地搬到浏览器/移动端,并通过 MetaMask 实现「真钱包、真签名、真 Gas」的交互体验。
同时,我们还会解决 Web3Provider 无法时间快进、授权额度不足、奖励误差等 3 个常见踩坑点,让你真正做到「开发时秒级验证,上线后零改动」。

前置准备

  • hardhat启动网络节点npx hardhat node
  • hardhat启动网络节点npx hardhat node
  • 合约编译npx hardhat compile 生成对应的xxx.json用获取abi等相关信息
  • 合约部署npx hardhat deploy --tags token,token1,LiquidityMiningVault 获取合约地址(奖励代币、质押代币和流动性挖矿的合约地址)
  • 节点的私钥导入钱包用来与合约交互时支付对应的gas费

核心代码

注意事项

  • 解决测试时模拟时间快进使用new ethers.providers.Web3Provider(window.ethereum)没有快进的属性
    const url = 'http://localhost:8545';   // 以 Hardhat 启动日志为准
    const raw = new ethers.providers.JsonRpcProvider(url);
    const id = await raw.send('eth_chainId', []);
    console.log('chainId', parseInt(id, 16));
    await raw.send('evm_increaseTime', [10]);
    await raw.send('evm_mine');
    console.log('✅ 直连成功');
  • 获取不到预取结果区分质押代币和奖励代币合约的使用场景
  • 领取奖励后会出现预期误差主要是模拟时间快进原因

    代码

    说明把测试操作集中在一个函数中

  • 用 MetaMask 做签名器,实现了「预充 → mint → 授权 → 质押 → 调速度 → 时间快进 → 领取」
  • 详细步骤

      1. 双签名器:owner 管 mint / 设速,alice 管授权、质押、领取,符合真实权限模型。
      1. 授权必校验:approve 完立刻读 allowance,杜绝「额度不足」导致的 deposit 失败。
      1. 时间快进:必须另起 JsonRpcProvider 直连接口,调用 evm_increaseTime + evm_mineWeb3Provider 无此 API。
      1. 触发更新:链上时间只会在新块里生效,快进后必须再发一笔交易(这里用 deposit(0))让合约重新计算 earned
      1. 代币别混淆:StakeToken 用于质押,RewardToken 用于收益;地址一旦反了,就会出现「查不到余额」或「领不到钱」。
      1. 日志全打满:每一步都 console.log.wait() 回执,开发阶段一眼定位失败点;上线前把 try/catch 细化到业务层即可直接复用。
        
        import { abi as LiquidityMiningVaultABI } from "@/abi/LiquidityMiningVault.json";
        import { abi as StakeTokenABI } from "@/abi/MyToken.json";
        import { abi as REWARDTokenABI } from "@/abi/MyToken1.json"; //代币
        import * as ethers from 'ethers';
        const withdrawToken = async () => {
        try {
        const provider = new ethers.providers.Web3Provider(window.ethereum);

    / 0. 连接 MetaMask 并确保 Alice 在账户列表里 / await provider.send('eth_requestAccounts', []); const ALICE_ADDR = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'; const VAULT_ADDR = '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8'; const STAKE_ADDR = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; const REWARD_ADDR = '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853';//奖励币

    let accounts = await provider.listAccounts(); if (!accounts.map(a => a.toLowerCase()).includes(ALICE_ADDR.toLowerCase())) { await window.ethereum.request({ method: 'wallet_requestPermissions', params: [{ eth_accounts: {} }] }); accounts = await provider.send('eth_requestAccounts', []); }

    const aliceIndex = accounts.findIndex( a => a.toLowerCase() === ALICE_ADDR.toLowerCase() ); if (aliceIndex === -1) throw new Error('MetaMask 中未找到 Alice 地址'); const aliceSigner = provider.getSigner(aliceIndex); const ownerSigner = provider.getSigner(0);

    const DEPOSIT = ethers.utils.parseEther('1000'); const SPEED = ethers.utils.parseEther('10');

    const stakeToken = new ethers.Contract(STAKE_ADDR, StakeTokenABI, ownerSigner); const rewardToken = new ethers.Contract(REWARD_ADDR, REWARDTokenABI, ownerSigner); const vault = new ethers.Contract(VAULT_ADDR, LiquidityMiningVaultABI, ownerSigner);

    / 1. vault 预充奖励 / console.log('1. vault 预充奖励'); try{ await (await rewardToken.mint(VAULT_ADDR, ethers.utils.parseEther('2000'))).wait(); }catch(err){ console.error('❌ 预充奖励失败', err); }

    / 2. 给 alice 发币 / console.log('2. alice mint'); await (await stakeToken.mint(ALICE_ADDR, DEPOSIT)).wait();

    / 3. alice 授权 —— 用 alice 自己的签名器 / console.log('3. alice 授权'); const stakeForAlice = stakeToken.connect(aliceSigner); const approveTx = await stakeForAlice.approve(VAULT_ADDR, DEPOSIT); const approveRcpt = await approveTx.wait(); console.log('approve receipt status:', approveRcpt.status);

    / 4. 再读一次授权,确认额度足够 / const allowance = await stakeToken.allowance(ALICE_ADDR, VAULT_ADDR); console.log('allowance (枚)', ethers.utils.formatEther(allowance)); if (!allowance.gte(DEPOSIT)) throw new Error('授权额仍不足');

    / 5. alice 质押 / console.log('4. alice deposit'); try{ await (await vault.connect(aliceSigner).deposit(DEPOSIT, ALICE_ADDR)).wait(); }catch(err){ console.error('❌ 质押失败', err); } / 6. owner 设奖励速度 / console.log('5. 设奖励速度'); await (await vault.connect(ownerSigner).setRewardPerSecond(SPEED)).wait();

    / 7. 时间快进 / console.log('6. evm+100s'); try{ const url = 'http://localhost:8545'; // 以 Hardhat 启动日志为准 const raw = new ethers.providers.JsonRpcProvider(url); const id = await raw.send('eth_chainId', []); console.log('chainId', parseInt(id, 16));

    await raw.send('evm_increaseTime', [10]);
    await raw.send('evm_mine');
    console.log('✅ 直连成功');

    }catch(err){ console.error('❌ 时间快进失败', err); } / 8. 触发更新 / console.log('7. 再存 0 触发更新'); await (await vault.connect(aliceSigner).deposit(0, ALICE_ADDR)).wait();

    / 9. 查询收益 / const earned = await vault.earned(ALICE_ADDR); console.log('earned(枚)', ethers.utils.formatEther(earned)); // 10. 提取奖励 console.log('8. 提取奖励'); try{ await (await vault.connect(aliceSigner).harvest()).wait(); }catch(err){ console.error('❌ 提取奖励失败', err); } console.log('9. 提取奖励后查询余额'); const balance = await rewardToken.balanceOf(ALICE_ADDR); console.log('balance(枚)', ethers.utils.formatEther(balance)); } catch (err) { console.error('❌ 流程中断', err.message ?? err); } };

    
    # 效果图

<div style="display:flex; gap:8px;flex-wrap:wrap;"> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ff3987436a164152a75cd3e134ae479f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757298674&x-orig-sign=7XbaVS9YGwXo7Snxj3LEAtyAB9A%3D" alt="图1转存失败,建议直接上传图片文件" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/90995b1ab024457280d7c28406912767~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757298831&x-orig-sign=wz%2FbDNv8FD7hfaN43%2B6fb%2BWb74A%3D" alt="图1转存失败,建议直接上传图片文件" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/36b6261ad6764aeb95d64bd0643beb92~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757298885&x-orig-sign=473FBIKUOgpSvZ%2FkqrjMe2uFWNE%3D" alt="图1转存失败,建议直接上传图片文件" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/cb5887fc182d44bdb4513a4bec9e29d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757298926&x-orig-sign=O1Uo3pa4Cn3Lqs5OWo6%2FPJw61gw%3D" alt="图1转存失败,建议直接上传图片文件" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ae558f5582074e199510300e5175a3d9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757298955&x-orig-sign=6i1vzSVhiw6Y%2BbN%2FDt0IEJYMe%2FU%3D" alt="图1转存失败,建议直接上传图片文件" width="200"/> </div>

总结

  1. 环境打通:一条 npx hardhat node 本地链 + 合约地址 + 节点私钥导入钱包,即可完成「开发 ⇄ 钱包」的双向通信。
  2. 时间快进:浏览器端无法 time.increase(),但可以通过直连 JsonRpcProvider 调用 evm_increaseTimeevm_mine,在 UI 上实现「一键跳 100 秒」的测试快感。
  3. 权限拆分:mint/预充用 owner 签名器,approve/deposit/harvest 用 Alice 签名器,既符合真实业务,也避免「一个私钥走天下」的安全误区。
  4. 代币辨析:StakeToken 只负责「质押额度」,RewardToken 只负责「收益发放」,两者地址一旦混淆,就会出现「查询不到余额」或「领取失败」的假象。
  5. 误差可控:时间快进后立即调用 deposit(0) 触发 updateReward,能把区块时间误差压到 1 s 以内,肉眼可见「earned ≈ 1000 枚」。
  6. 无缝迁移:整套代码基于 ethers.js v5,与 React/React Native 100% 兼容;只要把 window.ethereum 换成 @walletconnect/web3walletWalletConnectModal,即可原地切换主网、测试网或移动端钱包,真正做到「开发即生产」。

至此,「合约开发 → 单元测试 → 前端交互」完整链路已跑通。

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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