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.文件夹: 主要文件夹就是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开发问题都已经优化解决,比如连接钱包无法监听到网络切换,未连接钱包但是调用合约查询功能报错,以及连接钱包登录账户后依然使用了默认节点等等问题。不知道是否存在其他未知问题,欢迎大家来反馈。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!