0

I'm a bit confused on how to handle a connection to a wallet such as Metamask in a decentralised web-app.

Let's consider this very simple contract:

pragma solidity ^0.8.0;

contract ToDo {

    struct Task {

        string content;
        bool completed;
    }

    mapping(address => Task[]) private tasks;

    function addTask(string memory content) public {

        tasks[msg.sender].push(Task(content, false));
    }

    function changeTaskState(uint256 taskId) public {

        tasks[msg.sender][taskId].completed = !tasks[msg.sender][taskId].completed;
    }

    function editTaskContent(uint256 taskId, string memory content) public {

        tasks[msg.sender][taskId].content = content;
    }

    function getTasks() public view returns(Task[] memory) {
        
        return tasks[msg.sender];
    }
}

I've made a simple React front-end application that allows the users to interact with this contract, and it works just fine, but I'm not sure that I'm handling the connection to the wallet in the right way.

function App() {

  const [web3] = useState(new Web3(Web3.givenProvider || "http://localhost:9545"));
  const [provider] = useState(web3.currentProvider);
  const [contract] = useState(new web3.eth.Contract(TODO_ABI, TODO_ADDRESS));
  const [tasks, setTasks] = useState([]);
  const [account, setAccount] = useState(undefined);
  const [chainId, setChainId] = useState(provider.chainId);

  provider.on("chainChanged", chainId => setChainId(chainId));
  
  provider.on("connect", () => {

    web3.eth.getAccounts()
      .then(accounts => setAccount(accounts[0]));
  });

  provider.on("disconnect", () => setAccount(undefined));

  provider.on("accountsChanged", accounts => setAccount(accounts[0]));

  useEffect(() => {

    contract.methods.getTasks().call({from: account})
      .then(tasks => setTasks(tasks))
      .catch(error => {

        setTasks([]);
        console.error(error);
      });

  }, [account, contract.methods, chainId]);

  const components = [<ConnectButton account={account} provider={provider} setAccount={setAccount}></ConnectButton>];

  if(account) {

    components.push(<TaskForm provider={provider} account={account} contract={contract} setTasks={setTasks} tasks={tasks}></TaskForm>);
    components.push(<TasksList account={account} tasks={tasks} contract={contract}></TasksList>);
  }

  return (
    <div>
      { components }
    </div>
  );
}

I'm using the Ethereum Provider API's events in order to change certain states of the app, such as the address currently in use and the id of the current network. The useEffect() callback gets executed each time that chainId or account change value. Problem is that I'm not sure that I'm using some of these events in the right way.

No problem regarding the "accountsChanged" and "chainChanged" events, which seem to be as straight forward as they actually appear. However, I'm not sure on when the "connect" event actually takes place.

As you can see, upon connecting I'm using the web3.eth.getAccounts() method call in order to retrieve the address currently in use, but I wanted to do initially was something like this.

  provider.on("connect", () => {

    setAccount(provider.selectedAddress);
  });

However, this always sets account to null. My guess is that "connect" doesn't get triggered by an address actually connecting to a dapp, but by simply the app detecting a wallet, or something like that. So when the "connect" event happens, provider.selectedAddress is always still null, because the actual connection between an app and an address happens after that. However, the call to web3.eth.getAccounts() is asynchronus, therefore delayed, and if the user upon loading the app has already a connected address, while the accounts get retrieved, the wallet finishes connecting to the app, and everything works as it should. If this is actually what's happening here, this seems to be a very bad hack for retrieving a previously connected address upon loading the app. So, if what I'm doing is actually wrong, what would be the right way of doing this? Also, why do getAccounts() and the trigger to the "accountsChanged" return an array of addresses which is always of length 1? Is there a case when the returned array could contain multiple elements and how should something like that be handled?

Finally, let's consider the ConnectButton component, which renders a button that when clicked makes a prompt from the wallet to appear so that the user can confirm the connection to the app.

function ConnectButton(props) {

  const connect = () => {

    props.provider.request({ method: 'eth_requestAccounts' })
      .then(accounts => props.setAccount(accounts[0]));
  };

  return (

    <button onClick={connect} disabled={props.account}>{props.account ? props.account : "Connect"}</button>
  )
}

Is checking that props.account (which is the currently used address) is set a good way of determining if an address is connected to the dapp? Is there a way of detecting if the wallet prompt to confirm the connection is currently open? Is it true that currently there's no way of coding a button to disconnect an address (which is then an operation that the user must do manually, if so desired)?

2
  • Regarding the detection of a previously established connection upon loading the app, I think I found a very simple solution. I've replaced the "connect" callback with "useEffect(() => setAccount(provider.selectedAddress), [provider.selectedAddress]);" Basically, as soon as "selectedAddress" gets assigned a value, that value is assigned to account.
    – user75473
    Commented Jun 23, 2021 at 8:59
  • Unfortunately, this still requires to use the "accountsChanged" event, because useEffect's listeners monitor only changes in references, not in values, and when "selectedAddress" changes, apparently it doesn't change its reference (or at least that's my guess). So, that useEffect's callback, gets called just once during the whole app lifetime. However, I don't this is much of a problem. Still looking for answers regarding the other questions I've asked, btw.
    – user75473
    Commented Jun 23, 2021 at 8:59

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.