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

前言

继上一篇《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)没有快进的属性
javascript 复制代码
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 细化到业务层即可直接复用。
ini 复制代码
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);
  }
};

效果图

总结

  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,即可原地切换主网、测试网或移动端钱包,真正做到「开发即生产」。

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

相关推荐
wszy18094 小时前
顶部标题栏的设计与实现:让用户知道自己在哪
java·python·react native·harmonyos
wincheshe17 小时前
React Native inspector 点击组件跳转编辑器技术详解
react native·react.js·编辑器
墨狂之逸才1 天前
React Native Hooks 快速参考卡
react native
墨狂之逸才1 天前
useRefreshTrigger触发器模式工作流程图解
react native
墨狂之逸才1 天前
react native项目中使用React Hook 高级模式
react native
wayne2142 天前
React Native 状态管理方案全梳理:Redux、Zustand、React Query 如何选
javascript·react native·react.js
电报号dapp1192 天前
钱包开发:在虚无中为数字自我筑巢
游戏·去中心化·区块链·智能合约
Mintopia3 天前
🎙️ React Native(RN)语音输入场景全解析
android·react native·aigc
程序员Agions3 天前
React Native 邪修秘籍:在崩溃边缘疯狂试探的艺术
react native·react.js
Rockbean3 天前
3分钟Solidity: 11.1 重入攻击
web3·智能合约·solidity