reactjstypescriptmetamaskethers.js

Argument of type '(accounts: string[]) => void' is not assignable to parameter of type '(...args: unknown[]) => void'


I'm new to web3 development and am getting the following error in 2 places.

Error 1)

Argument of type '(accounts: string[]) => void' is not assignable to parameter of type '(...args: unknown[]) => void'.
  Types of parameters 'accounts' and 'args' are incompatible.
    Type 'unknown' is not assignable to type 'string[]'.

Error 2)

Argument of type '(network: string) => void' is not assignable to parameter of type '(...args: unknown[]) => void'.
  Types of parameters 'network' and 'args' are incompatible.
    Type 'unknown' is not assignable to type 'string'.ts(2345)

Code

import { BrowserProvider, ethers, JsonRpcSigner} from "ethers";
import { useToast } from "@chakra-ui/react";
import { useCallback, useEffect, useState } from "react";
import { MetaMaskInpageProvider } from "@metamask/providers";

declare global {
  interface Window{
    ethereum?:MetaMaskInpageProvider
  }
}


export interface IWeb3State {
  address: string | null;
  currentChain: number | null; 
  signer: JsonRpcSigner | null;
  provider: BrowserProvider | null;
  isAuthenticated: boolean;
}

const useWeb3Provider = () => {
  const initialWeb3State = {
    address: null,
    currentChain: null,
    signer: null,
    provider: null,
    isAuthenticated: false,
  };

  const toast = useToast();
  const [state, setState] = useState<IWeb3State>(initialWeb3State);

  const connectWallet = useCallback(async () => {
    if (state.isAuthenticated) return;

    try {
      const { ethereum } = window;

      if (!ethereum) {
        return toast({
          status: "error",
          position: "top-right",
          title: "Error",
          description: "No ethereum wallet found",
        });
      }
      const provider = new ethers.BrowserProvider(ethereum);

      const accounts: string[] = await provider.send("eth_requestAccounts", []) as string[];

      if (accounts.length > 0) {
        const signer = await provider.getSigner();
        const chain = Number(await (await provider.getNetwork()).chainId);
        //once we have the wallet, are we exporting the variables "provider" & accounts outside this try?
        setState({
          ...state,
          address: accounts[0],
          signer,
          currentChain: chain,
          provider,
          isAuthenticated: true,
        });

        localStorage.setItem("isAuthenticated", "true");
      }
    } catch {}
  }, [state, toast]);

  const disconnect = () => {
    setState(initialWeb3State);
    localStorage.removeItem("isAuthenticated");
  };

  useEffect(() => {
    if (window == null) return;

    if (localStorage.hasOwnProperty("isAuthenticated")) {
      connectWallet();
    }
  }, [connectWallet, state.isAuthenticated]);

  useEffect(() => {
    if (typeof window.ethereum === "undefined") return;

    window.ethereum.on("accountsChanged", (accounts: string[]) => {
      setState({ ...state, address: accounts[0] });
    });

    window.ethereum.on("networkChanged", (network: string) => {
      setState({ ...state, currentChain: network });
    });

    return () => {
      window.ethereum?.removeAllListeners();
    };
  }, [state]);

  return {
    connectWallet,
    disconnect,
    state,
  };
};

export default useWeb3Provider;

If it helps, I'm following this tutorial and have been trouble shooting for the past 3 days. Insert crying emoji

I'm not particularly sure how to even ask this question so if any further clarification is needed, just holler!


Solution

  • This doesn't have much to do with web3. It's more of a typescript question. See fixes below with comments:

    import { BrowserProvider, ethers, JsonRpcSigner } from "ethers";
    import { useToast } from "@chakra-ui/react";
    import { useCallback, useEffect, useState } from "react";
    import { MetaMaskInpageProvider } from "@metamask/providers";
    
    declare global {
      interface Window {
        ethereum?: MetaMaskInpageProvider;
      }
    }
    
    export interface IWeb3State {
      address: unknown; // Fix: Change from string | null; ❌
      currentChain: unknown; // Fix: Change from number | null; ❌
      signer: JsonRpcSigner | null;
      provider: BrowserProvider | null;
      isAuthenticated: boolean;
    }
    
    const useWeb3Provider = () => {
      const initialWeb3State = {
        address: null,
        currentChain: null,
        signer: null,
        provider: null,
        isAuthenticated: false,
      };
    
      const toast = useToast();
      const [state, setState] = useState<IWeb3State>(initialWeb3State);
    
      const connectWallet = useCallback(async () => {
        if (state.isAuthenticated) return;
    
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            return toast({
              status: "error",
              position: "top-right",
              title: "Error",
              description: "No ethereum wallet found",
            });
          }
          const provider = new ethers.BrowserProvider(ethereum);
    
          const accounts: string[] = (await provider.send("eth_requestAccounts", [])) as string[];
    
          if (accounts.length > 0) {
            const signer = await provider.getSigner();
               // unnecessary await here ❌
        const chain = Number((await provider.getNetwork()).chainId);
            //once we have the wallet, are we exporting the variables "provider" & accounts outside this try?
            setState({
              ...state,
              address: accounts[0],
              signer,
              currentChain: chain,
              provider,
              isAuthenticated: true,
            });
    
            localStorage.setItem("isAuthenticated", "true");
          }
          // Empty block statement is also an issue ❌
        } catch (e) {
          console.error(e);
        }
      }, [state, toast]);
    
      const disconnect = () => {
        setState(initialWeb3State);
        localStorage.removeItem("isAuthenticated");
      };
    
      useEffect(() => {
        if (window == null) return;
        // Do not access Object.prototype method 'hasOwnProperty' from target object. (Fix Below) ❌
        if (Object.prototype.hasOwnProperty.call(localStorage, "isAuthenticated")) {
          connectWallet();
        }
      }, [connectWallet, state.isAuthenticated]);
    
      useEffect(() => {
        if (typeof window.ethereum === "undefined") return;
    
        // '(...args: unknown[]) => void'. (Fix Below and in IWeb3State above) - literaaly telling you the expected type in the error message ❌
        window.ethereum.on("accountsChanged", (...accounts: unknown[]) => {
          setState({ ...state, address: accounts[0] });
        });
    
        // '(...args: unknown[]) => void'. (Fix Below and in IWeb3State above) - literaaly telling you the expected type in the error message ❌
        window.ethereum.on("networkChanged", (network: unknown) => {
          setState({ ...state, currentChain: network });
        });
    
        return () => {
          window.ethereum?.removeAllListeners();
        };
      }, [state]);
    
      return {
        connectWallet,
        disconnect,
        state,
      };
    };
    
    export default useWeb3Provider;
    

    An alternative fix would be to use typescript generics:

    Here:

    // Alternative fix with the use of typescript generics
    export interface IWeb3State<T> {
      address: T;
      currentChain: T;
      signer: JsonRpcSigner | null;
      provider: BrowserProvider | null;
      isAuthenticated: boolean;
    }
    

    And here:

     // pass unknown here as a type argument to IWeb3State
      const [state, setState] = useState<IWeb3State<unknown>>(initialWeb3State);