Web3项目经验总结

1,407 阅读5分钟

1.构建项目

这里踩坑时间花得比较多,因为网上并没有能很快给出解决的方案

主要使用 Next.js 来进行开发,目的是节约搭建架构花费的时间

  • 创建项目建议使用 :TailwindcssEslintTypeScript、路由、路径别名
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false, // 开发过程不使用严格模式
  eslint: {
    ignoreDuringBuilds: true // 忽略构建时候的错误,防止错误打断构建
  },
  typescript: {
    ignoreBuildErrors: true // 忽略构建时候的错误,防止错误打断构建
  }
}

module.exports = nextConfig

1.1生产环境依赖

yarn add bignumber.js react-copy-to-clipboard ethers@5.7.0 wagmi@0.6.4 styled-components

1.2开发环境依赖

  • 主要是一些类型提示以及工具
yarn add @types/react-dom autoprefixer @types/react @types/node eslint-config-next eslint postcss sass @typechain/ethers-v5 @types/styled-components babel-plugin-styled-components typechain -D
  • typechain 用于编译 AIB 类型
"typechain": "typechain --out-dir src/contract/abi/types --target=ethers-v5 "src/contract/abi/*.json""

尽量不要使用pnpm安装包,可能出现意外

2.Wgmi和EtherJS的使用

  1. 初始化默认链

RPC节点可以到 Chainlist 中找到合适的节点

import { chain } from 'wagmi'
import { SupportedChains } from './types/chain'

export const supportedChains: SupportedChains = {
  mainnet: {
    ...chain.mainnet,
    rpcUrls: { // RPC节点,用于和对应的链进行交互,这里是以太坊主网节点的地址
      public: 'https://eth-mainnet.nodereal.io/v1/1659dfb40aa24bbb8153a677b98064d7',
      default: 'https://eth-mainnet.nodereal.io/v1/1659dfb40aa24bbb8153a677b98064d7'
    }
  },
  goerli: {
    ...chain.goerli,
    rpcUrls: {
      public: 'https://ethereum-goerli.publicnode.com',
      default: 'https://ethereum-goerli.publicnode.com'
    }
  }
}

export const defaultChain = process.env.NODE_ENV === 'production' ? supportedChains.mainnet : supportedChains.mainnet
  1. 初始化 wagmi,打开于区块链交互的大门
import { supportedChains } from '@/configs/chains'
import { configureChains, createClient } from 'wagmi'
import { InjectedConnector } from 'wagmi/connectors/injected'
import { mainnet } from '@wagmi/core/chains'
import { publicProvider } from 'wagmi/providers/public'

// 第一个参数传递支持的链,第二参数是
export const { chains, provider } = configureChains([mainnet], [publicProvider()])

export const client = createClient({
  autoConnect: true, // 自动连接
  connectors: [new InjectedConnector({ chains })], // 初始化连接器
  provider, // 提供一些和合约交互的方法
})
  1. 提供上下文,让其他后代组件可以使用wagmi的方法
import { AppProps } from 'next/app'
import { useEffect, useState } from 'react'
import Head from 'next/head'
import { WagmiConfig } from 'wagmi'
import { client } from '@/utils/wagmi'

const App = ({ Component, pageProps }: AppProps) => {
  const [mounted, setMounted] = useState(false)
  
  useEffect(() => setMounted(true), [])
  if (!mounted) return null // 只能在客户端中使用,所以服务端不渲染

  return (
    <>
      // 提供配置上下文给下级组件
      <WagmiConfig client={client}>
        <Component {...pageProps} />
      </WagmiConfig>
    </>
  )
}

export default App
  1. 基于EthersJS初始化合约连接,连接合约并且获取一些签名权限,这样进行敏感操作的时候就会弹出小狐狸窗口了
import { Contract } from 'ethers'
import { defaultChain } from '@/configs/chain'
import { provider } from '@/utils/wagmi'
import { Ido } from './abi/types'
import { useSigner } from 'wagmi'

import idoAbi from './abi/ido.json'

const contractAddresses =
  process.env.NODE_ENV === 'production'
    ? 'You contract address'
    : 'You contract address'

export const idoContract = () => {
  const { data: signer } = useSigner()
  // 获取签名或者客户端的公用注入者,如果注入者都没有那么连读合约都不行
  const signerOrProvider = signer ?? provider({ chainId: defaultChain.id })
  return new Contract(contractAddresses, idoAbi, signerOrProvider) as Ido
}
  1. 合约读写交互、钱包状态、前端的链信息
import { defaultChain } from '@/configs/chain'
import { useState } from 'react'
import { idoContract } from '@/contract'
import { useInterval, useMount } from 'ahooks'
import { waitForTransaction, getNetwork, switchNetwork } from '@wagmi/core'
import { useAccount } from 'wagmi'
import { formatEther, parseEther } from 'ethers/lib/utils'
import { Message } from '@arco-design/web-react'
import { minus } from '@/utils/bignumber'
import { checkWhitelist } from '@/api/chekcWhitelist'

export const useIDO = () => {
  // 钱包状态
  const { address, isConnected } = useAccount()
  const { chain } = getNetwork()

  // 加载状态
  const [loading, setLoading] = useState(false)
  const [pageLoading, setPageLoading] = useState(true)

  const [value, setValue] = useState('') // 文本框
  const [paidCount, setPaidCount] = useState(0) // 用户购买量
  const [isClaim, setIsClaim] = useState(false) // 用户是否购买
  const [userIsWhite, setUserIsWhite] = useState(false) // 用户是否白名单
  const [info, setInfo] = useState() // IDO 信息
  const { getInfo, paidEth, buy, isClaim: isClaimFn, claim: onClaimFn } = idoContract()

  const getMax = () => Number(formatEther(minus(info?.totalGatherEth, info?.totalPaidEth).toString()))

  const checkoutUserStatus = () => {
    if (!isConnected) {
      return false
    }

    if (chain?.id !== defaultChain.id) { // 检查链是否正确
      switchNetwork({ chainId: defaultChain.id })
      return false
    }

    return true
  }

  // 点击IDO参与
  const onBuy = async () => {
    if (!checkoutUserStatus()) return

    const pric = Number(value || 0)
    const max = Number(formatEther(info?.maxEth || 0))
    const min = Number(formatEther(info?.minEth || 0))
    const diff = getMax()

    // 最小参与量只能是差额
    if (diff < min) {
      if (pric !== diff) {
        return Message.error(`Can only participate in ${+diff.toFixed(4)} ETH`)
      }
    } else {
      if (pric > max) {
        return Message.error(`Maximum participation ${max} ETH`)
      } else if (pric < min) {
        return Message.error(`Minimum participation ${min} ETH`)
      }
    }

    try {
      setLoading(true)
      const { hash } = await buy({
        from: address,
        value: parseEther(value) // 需要转换成大数
      })

      await waitForTransaction({ hash, confirmations: 1 }) // 等待交易完成再更新状态
    } catch (e) {
      console.log(e)
    } finally {
      setLoading(false)
    }
  }

  const onClaim = async () => {
    if (!checkoutUserStatus()) return
    try {
      setLoading(true)
      const { hash } = await onClaimFn({ from: address })
      await waitForTransaction({ hash, confirmations: 1 }) // 等待交易完成再更新状态
    } catch (e) {
      console.log(e)
    } finally {
      setLoading(false)
    }
  }

  // 初始化获取数据
  const init = async () => {
    try {
      const [info, paidEthCount, isClaim, userIsWhite] = await Promise.all([
        getInfo(),
        address && paidEth(address),
        address && isClaimFn(address),
        address && checkWhitelist(address)
      ])

      setInfo(info)
      setIsClaim(!!isClaim)
      setUserIsWhite(!!userIsWhite)
      setPaidCount(Number(formatEther(paidEthCount || 0)))
      setPageLoading(false)
    } catch (e) {
      console.log(e)
    }
  }

  useMount(() => init())
  useInterval(() => init(), 3000)

  return {
    info,
    paidCount,
    value,
    isClaim,
    loading,
    pageLoading,
    userIsWhite,
    setValue,
    getMax,
    onBuy,
    onClaim
  }
}

3.钱包连接

主要分为两种情况:

  • 手机DAPP上的浏览器
  • PC端DAPP浏览器

两者的区别在于,手机端大部分浏览器都是可以直接使用 wagmInjectedConnector类连接钱包,因为手机浏览器会在window对象上提供ethereum对象,这样InjectedConnector可以直接连接钱包,PC端上的话可以使用一些React的第三方UII来实现更好看的交互效果,但本质上都差不多,只是为了兼容更多的钱包(但是PC市场上的用户用小狐狸的居多)

  • 可以通过一些第三方的媒体查询库来判断使用PC端钱包库还是移动端直连钱包
  • 这些钱包库一般都是搭配着wagmi一起来使用的

3.1针对PC端的React钱包库

Rainbowkit(推荐使用)

入手比较简单,但需要注意版本,如果你的wagmi是^1.0.0版本的则安装最新的rainbowkit,否则你得安装老版本

yarn add @rainbow-me/rainbowkit

官网:www.rainbowkit.com/

这个是版本迁移指南:www.rainbowkit.com/docs/migrat…

ThirdWeb

拥有一套完善的前端以及合约层面的工具生态,GitHub 209颗星

官网:portal.thirdweb.com/react/react…

npm i @thirdweb-dev/react @thirdweb-dev/sdk ethers@^5

Web3React

yarn add web3-react@unstable

NPM地址:www.npmjs.com/package/web…

如何加入比特鹰

目前我们在招聘的岗位有:投研分析师,Python 后端研发工程师,前端研发工程师,AI研发工程师。

可以将简历投递到邮箱 join@bitying.cn