React版Dapp开发模板(连接钱包、合约调用全流程和一个批量转账工具实战)

  • Verin
  • 更新于 2022-08-12 19:12
  • 阅读 5536

React版Dapp开发模板(连接钱包、合约调用全流程和一个批量转账工具实战)

框架版本

框架分为两个版本: 1.Nextjs版。几个大的Dapp(pancakeswap ,sushiswap,uniswap等等)用的都是这个技术栈,考虑到Dapp很多核心计算逻辑以及处理数据逻辑都在前端,建议使用这个Nextjs版本,在写业务的时候可以直接参考这三个大项目Uniswap前端源码Sushiswap前端源码PancakeSwap前端源码。 2.Vite版本。之前使用了CRA构建了一套,但是和很多Dapp开发的库不兼容,后来尝试了Vite,不仅速度快而且很多基本完美兼容,所以构建了一套基础逻辑和Nextjs一样为SPA页面服务的模板。

所使用的技术栈

两个框架技术栈主要是:TypeScript+React17+ethers+web3-react;

备注:这些技术栈也是根据其他项目使用情况来决定的,虽然有wagmi的web3 hooks库,可是由于没有其他项目使用,所以暂时未考虑,web3-react未使用beta版本也是基于这种考虑sushiswap已经使用了最新版本,后续再更新。整个模板参考了sushiswap和pancake以及网络上的部分教程。

项目源码导读

1.文件夹: image.png 主要文件夹就是components,config,hooks和pages下的_app.tsx文件。

2.代码导读:

(1).全局配置(_app.tsx):

import "styles/globals.css";
import type { AppProps } from "next/app";
import { Web3Provider } from "@ethersproject/providers";
import { Web3ReactProvider } from "@web3-react/core";
import Web3ReactManager from "components/Web3ReactManager/index";
import dynamic from "next/dynamic";

export function getLibrary(provider: any): Web3Provider {
  const library = new Web3Provider(provider);
  library.pollingInterval = 15000;
  return library;
}

const Web3ProviderNetwork = dynamic(() => import("../components/Web3ProviderNetwork/index"), { ssr: false });

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Web3ReactProvider getLibrary={getLibrary}>
      <Web3ProviderNetwork getLibrary={getLibrary}>
        <Web3ReactManager>
          <Component {...pageProps} />
        </Web3ReactManager>
      </Web3ProviderNetwork>
    </Web3ReactProvider>
  );
}

export default MyApp;

_app.tsx有关键的Web3ReactProvider,Web3ProviderNetwork和Web3ReactManager,后面所有的配置以及合约调用都会与他们有关,Web3ReactProvider是提供全局使用web3 hooks,Web3ProviderNetwork则是网络节点一系列的全局配置,Web3ReactManager则是用来操作钱包连接和钱包状态监听等;

(2).登陆钱包:

import React, { useState, useEffect } from "react";
import { useWeb3React } from "@web3-react/core";
import { network } from "config/constants/wallets";
import { NetworkContextName } from "config/index";

import useEagerConnect from "hooks/useEagerConnect";
import useInactiveListener from "hooks/useInactiveListener";

export default function Web3ReactManager({ children }: { children: JSX.Element }) {
  const { active } = useWeb3React();
  const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName);

  // try to eagerly connect to an injected provider, if it exists and has granted access already
  const triedEager = useEagerConnect();

  // after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itd
  useEffect(() => {
    if (triedEager && !networkActive && !networkError && !active) {
      activateNetwork(network);
    }
  }, [triedEager, networkActive, networkError, activateNetwork, active]);

  // when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists
  useInactiveListener(!triedEager);

  // handle delayed loader state
  const [showLoader, setShowLoader] = useState(false);
  useEffect(() => {
    const timeout = setTimeout(() => {
      setShowLoader(true);
    }, 600);

    return () => {
      clearTimeout(timeout);
    };
  }, []);

  // on page load, do nothing until we've tried to connect to the injected connector
  if (!triedEager) {
    return null;
  }

  // if the account context isn't active, and there's an error on the network context, it's an irrecoverable error
  if (!active && networkError) {
    return <div>unknownError</div>;
  }

  // if neither context is active, spin
  if (!active && !networkActive) {
    return showLoader ? <div>Loader</div> : null;
  }

  return children;
}

在Web3ReactManager里面写好了连接钱包与管理状态的逻辑,核心是两个hooks:

useEagerConnect

import { useWeb3React as useWeb3ReactCore } from "@web3-react/core";
import { injected } from "config/constants/wallets";
import { isMobile } from "web3modal";
import { connectorLocalStorageKey } from "config/connectors/index";
export function useEagerConnect() {
    const { activate, active } = useWeb3ReactCore(); // specifically using useWeb3ReactCore because of what this hook does
    const [tried, setTried] = useState(false);

    useEffect(() => {
        injected.isAuthorized().then((isAuthorized) => {
            const hasSignedIn = window.localStorage.getItem(connectorLocalStorageKey);
            if (isAuthorized && hasSignedIn) {
                activate(injected, undefined, true)
                    // .then(() => window.ethereum.removeAllListeners(['networkChanged']))
                    .catch(() => {
                        setTried(true);
                    });
                // @ts-ignore TYPE NEEDS FIXING
                window.ethereum.removeAllListeners(["networkChanged"]);
            } else {
                if (isMobile() && window.ethereum && hasSignedIn) {
                    activate(injected, undefined, true)
                        // .then(() => window.ethereum.removeAllListeners(['networkChanged']))
                        .catch(() => {
                            setTried(true);
                        });
                    // @ts-ignore TYPE NEEDS FIXING
                    window.ethereum.removeAllListeners(["networkChanged"]);
                } else {
                    setTried(true);
                }
            }
        });
    }, [activate]);

    useEffect(() => {
        if (active) {
            setTried(true);
        }
    }, [active]);

    return tried;
}

export default useEagerConnect;

连接主要是使用的useWeb3ReactCore里面的activate,这里存了localStorage主要作用是自动连接钱包与非自动,如果不需要自动则把hasSignedIn删除即可;

useInactiveListener

import { useWeb3React as useWeb3ReactCore } from "@web3-react/core";
import { useEffect } from "react";

import { injected } from "config/constants/wallets";

/**
 * Use for network and injected - logs user in
 * and out after checking what network theyre on
 */
function useInactiveListener(suppress = false) {
  const { active, error, activate } = useWeb3ReactCore(); // specifically using useWeb3React because of what this hook does

  useEffect(() => {
    const { ethereum } = window;

    if (ethereum && ethereum.on && !active && !error && !suppress) {
      const handleChainChanged = () => {
        // eat errors
        activate(injected, undefined, true).catch((error) => {
          console.error("Failed to activate after chain changed", error);
        });
      };

      const handleAccountsChanged = (accounts: string[]) => {
        if (accounts.length > 0) {
          // eat errors
          activate(injected, undefined, true).catch((error) => {
            console.error("Failed to activate after accounts changed", error);
          });
        }
      };

      ethereum.on("chainChanged", handleChainChanged);
      ethereum.on("accountsChanged", handleAccountsChanged);

      return () => {
        if (ethereum.removeListener) {
          ethereum.removeListener("chainChanged", handleChainChanged);
          ethereum.removeListener("accountsChanged", handleAccountsChanged);
        }
      };
    }
    return undefined;
  }, [active, error, suppress, activate]);
}

export default useInactiveListener;

这里主要是监听钱包状态,比如账号更换和链更换;

备注:在config里面是配置钱包以及网络节点的,主要是NetworkConnector和wallets,一个是获取节点信息一个是配置钱包连接方式(metamask,walletconnect等等)

(2).合约调用 合约调用逻辑主要是在hooks文件夹下面的useContract:

import { useMemo } from "react";
import { useActiveWeb3React } from "hooks/useActiveWeb3React";
import { JsonRpcSigner, Web3Provider } from "@ethersproject/providers";
import { AddressZero } from "@ethersproject/constants";
import { isAddress } from "utils/isAddress";
import { getProviderOrSigner } from "utils";
import { Contract } from "@ethersproject/contracts";
export function useContract(address: string | undefined, ABI: any, withSignerIfPossible = true): Contract | null {
  const { library, account } = useActiveWeb3React();
  return useMemo(() => {
    if (!address || address === AddressZero || !ABI || !library) return null;
    try {
      return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined);
    } catch (error) {
      console.error("Failed to get contract", error);
      return null;
    }
  }, [address, ABI, library, withSignerIfPossible, account]);
}

export function getContract(address: string, ABI: any, library: Web3Provider, account?: string): Contract {
  if (!isAddress(address) || address === AddressZero) {
    throw Error(`Invalid 'address' parameter '${address}'.`);
  }
  return new Contract(address, ABI, getProviderOrSigner(library, account));
}

合约调用useContract是主方法,此方法已经自动兼容了连接钱包和未连接钱包状态,未连接钱包状态的时候会使用你默认的provider和节点(配置在wallets里面),连接钱包后则会使用你自身的钱包节点与provider。

使用如下: (1).实例一个合约:

export const useERC20 = (address: string, withSignerIfPossible = true) => {
    return useContract(address, ERC20_ABI, withSignerIfPossible);
};

(1).在组件中使用合约:

import { useERC20 } from "@/hooks/useContract";
export default function Index() {
const ERC20Instarnce = useERC20(address);

const handleTransfer=async()=>{
   const symbol = await ERC20Instarnce.symbol();
}
return...
}

自此从连接钱包到合约调用的逻辑全部讲完,其他业务逻辑就是正常的写Ts。

Vite版本与此类似,就不多说了。 基于Vite版本,我自己写了一个批量转账工具,包括合约在内都在我自己的git上开源,欢迎大家来star。

git地址https://github.com/Verin1005 批量转账网站https://www.vtool.bio/#/ Nextjs版模板https://github.com/Verin1005/NextJs-Dapp-Template Vite版模板https://github.com/Verin1005/React-Vite-Dapp-Template

最后再备注一下:两个框架都写过好几十次项目,一些其他模板遇到的Dapp开发问题都已经优化解决,比如连接钱包无法监听到网络切换,未连接钱包但是调用合约查询功能报错,以及连接钱包登录账户后依然使用了默认节点等等问题。不知道是否存在其他未知问题,欢迎大家来反馈。

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

0 条评论

请先 登录 后评论
Verin
Verin
discord:Verin#2256 v: daqingchong-pro 备注来意