手撸一个连接web3的库(metamask)

1,291 阅读3分钟

手撸一个对标web3-react的库

目标:

动态监听walletAddress,chain, isconnect(可设置需要支持的chian,判断是否已连接钱包,自动保持登录,连接钱包,可构造合约交互,支持非钱包的只读模式。

为什么要自己写一个,这个原因是这样好方便自定义功能,以及方便移植到其他非eth项目的,互相保留多种链。

好了接下来实现,先写一个基本方法,这个要纯粹,不能和框架绑定,到时候好集成vue或者其他框架。

实现web3方法

实现

import Web3 from 'web3';
import detectProvider, { MetaMaskEthereumProvider } from './detectProvider';

const getRpc = (chain: number) =>
  chain === 1
    ? 'https://mainnet.infura.io/v3/'
    : 'https://rinkeby.infura.io/v3/';

const defaultChain = process.env.REACT_APP_BASE_ENV === 'test' ? 4 : 1;

let web3: Web3;
let provider: MetaMaskEthereumProvider;
const ethWeb3 = () => {
  // @ts-ignore
  const ethereum = window?.ethereum;
  return {
    isConnect(): boolean {
      return ethereum?.isConnected?.() ?? false;
    },
    connect() {
      ethereum?.enable();
    },
    notAccountWeb3(chain = defaultChain) {
      var web3 = new Web3();

      web3.setProvider(new Web3.providers.HttpProvider(getRpc(chain)));
      return web3;
    },
    async web3(): Promise<Web3 | undefined> {
      const _provider = await detectProvider();
      if (_provider) {
        provider = _provider;
        web3 = new Web3(provider as any);
        return web3;
      }
    },
    getProvider: () => provider,
  };
};

export { web3, ethWeb3, provider };

好了,可以用了,但是登录等操作,数据都是死的,我们需要做一个hooks。

在刚刚的eth web3的文件旁边加一个hook函数,然后新建一个叫useEth的函数。

import { useEffect, useState } from 'react';
import { ethWeb3, web3 } from '..';
import { BounceERC20 as erc20Abi } from 'modules/web3/contracts';

const supportChains = [1, 4];
export const useEth = () => {
  const [_web3, setWeb3] = useState<typeof web3>();
  const [rpcWeb3, setRpcWeb3] = useState<typeof web3>();
  const { notAccountWeb3, ...eth } = ethWeb3();
  const [walletAddress, setWalletAddress] = useState<string>();
  const [chain, setChain] = useState<number>();
  const [isConnect, setIsConnect] = useState<Boolean>(false);

  const fetchSetAccount = () => {
    if (_web3 ?? web3) {
      (_web3 ?? web3).eth.getAccounts().then(res => {
        setWalletAddress(res[0]);
      });
      fetchSetChain();
    }
  };
  const fetchSetChain = async () => {
    const res = await web3?.eth.getChainId();
    setChain(res);
  };
  const getTokenBalance = async ({
    tokenAddress,
    walletAddress,
    web3: _web3,
  }: {
    tokenAddress: string;
    walletAddress: string;
    web3?: typeof web3;
  }) => {
    const c_web3 = _web3 ?? web3;
    if (!c_web3) return console.log('not web3');
    try {
      var c = new c_web3.eth.Contract(erc20Abi, tokenAddress);
      const balance = await c.methods.balanceOf(walletAddress).call();
      return c_web3.utils.fromWei(balance);
    } catch (error) {
      console.log(
        'getTokenBalance error-->',
        error,
        tokenAddress,
        walletAddress,
      );
    }
  };

  useEffect(() => {
    setIsConnect(!!walletAddress && supportChains.includes(chain ?? -1));
  }, [walletAddress, chain]);

  useEffect(() => {
    setRpcWeb3(notAccountWeb3(chain));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [chain]);

  useEffect(() => {
    eth.web3().then(res => {
      setWeb3(res);
      eth.getProvider().on('connect', () => {
        console.log('connect');
        fetchSetAccount();
      });
      eth.getProvider().on('chainChanged', () => {
        console.log('chainChanged');
        fetchSetChain();
      });
      // run accountsChanged
      eth.getProvider().on('disconnect', () => {
        console.log('disconnect');
        fetchSetAccount();
      });
      eth.getProvider().on('accountsChanged', () => {
        console.log('accountsChanged');
        fetchSetAccount();
      });
    });
    // eslint-disable-next-line
  }, []);
  useEffect(() => {
    fetchSetAccount();
    // eslint-disable-next-line
  }, [_web3]);

  return {
    walletAddress,
    eth,
    address: walletAddress,
    chain,
    connect: eth.connect,
    web3: _web3,
    isConnect,
    getTokenBalance,
    rpcWeb3,
  };
};

可以啦。

对于上面一个导入大家可能比较好奇detectProvider是什么,这个是官方推荐的获取provider方法,我把内容直接下载了。

内容如下

export interface MetaMaskEthereumProvider {
  isMetaMask?: boolean;
  once(eventName: string | symbol, listener: (...args: any[]) => void): this;
  on(eventName: string | symbol, listener: (...args: any[]) => void): this;
  off(eventName: string | symbol, listener: (...args: any[]) => void): this;
  addListener(
    eventName: string | symbol,
    listener: (...args: any[]) => void,
  ): this;
  removeListener(
    eventName: string | symbol,
    listener: (...args: any[]) => void,
  ): this;
  removeAllListeners(event?: string | symbol): this;
}

interface Window {
  ethereum?: MetaMaskEthereumProvider;
}

export default detectEthereumProvider;

/**
 * Returns a Promise that resolves to the value of window.ethereum if it is
 * set within the given timeout, or null.
 * The Promise will not reject, but an error will be thrown if invalid options
 * are provided.
 *
 * @param options - Options bag.
 * @param options.mustBeMetaMask - Whether to only look for MetaMask providers.
 * Default: false
 * @param options.silent - Whether to silence console errors. Does not affect
 * thrown errors. Default: false
 * @param options.timeout - Milliseconds to wait for 'ethereum#initialized' to
 * be dispatched. Default: 3000
 * @returns A Promise that resolves with the Provider if it is detected within
 * given timeout, otherwise null.
 */
function detectEthereumProvider<T = MetaMaskEthereumProvider>({
  mustBeMetaMask = false,
  silent = false,
  timeout = 3000,
} = {}): Promise<T | null> {
  _validateInputs();

  let handled = false;

  return new Promise(resolve => {
    if ((window as Window).ethereum) {
      handleEthereum();
    } else {
      window.addEventListener('ethereum#initialized', handleEthereum, {
        once: true,
      });

      setTimeout(() => {
        handleEthereum();
      }, timeout);
    }

    function handleEthereum() {
      if (handled) {
        return;
      }
      handled = true;

      window.removeEventListener('ethereum#initialized', handleEthereum);

      const { ethereum } = window as Window;

      if (ethereum && (!mustBeMetaMask || ethereum.isMetaMask)) {
        resolve(ethereum as unknown as T);
      } else {
        const message =
          mustBeMetaMask && ethereum
            ? 'Non-MetaMask window.ethereum detected.'
            : 'Unable to detect window.ethereum.';

        !silent && console.error('@metamask/detect-provider:', message);
        resolve(null);
      }
    }
  });

  function _validateInputs() {
    if (typeof mustBeMetaMask !== 'boolean') {
      throw new Error(
        `@metamask/detect-provider: Expected option 'mustBeMetaMask' to be a boolean.`,
      );
    }
    if (typeof silent !== 'boolean') {
      throw new Error(
        `@metamask/detect-provider: Expected option 'silent' to be a boolean.`,
      );
    }
    if (typeof timeout !== 'number') {
      throw new Error(
        `@metamask/detect-provider: Expected option 'timeout' to be a number.`,
      );
    }
  }
}

如何使用

这样即可(需要多少方法自己暴露多少)

 const { isConnect, walletAddress, connect } = useEth();

其他

签名

这里我只针对metamask。另外也不会依赖上面的代码,我觉得这样比较纯粹。没有任何依赖~~

export const signMetaMask = (message: string) => {
    return new Promise((ok, reject) => {
        // @ts-ignore
        const ethereum = window.ethereum
        if (!ethereum) {
            return window.open('https://metamask.io/download/')
        }
        ethereum
            .request({ method: "eth_requestAccounts" })
            .then(async (accounts: string[]) => {
                console.log('accounts--->', accounts);
                try {
                    const sigature = await ethereum.request({
                        method: "personal_sign",
                        params: [
                            message,
                            ethereum.selectedAddress
                        ]
                    })
                    ok(sigature)
                } catch (error) {
                    reject(error)
                }
            }).catch(() => {
                reject('uninstall')
            })
    })
}

使用

   try {
        const sigature = await signMetaMask("massage")
        console.log('sigature---->', sigature)
        // your code 
    } catch (error) {
        console.log('sigature fail---->', error)
    }

--完--