1

I'm making a simple dapp using React, Hardhat, Ethers.js, and Solidity.

I've been following along a tutorial on YouTube but I got stuck at the part where you call the Solidity functions from the created contract object.

Whenever I try to call a function from my contract it keeps producing the following error:

"contract runner does not support calling"

Querying the contract works since I can get the balance of the contract but I can't find any resources on how to fix the contract runner error. Would appreciate the help. Here follows the code in React.

const providerInitializer = async() => {
        if (window.ethereum == null) {
            console.log("MetaMask not installed; using read-only defaults")
            provider = ethers.getDefaultProvider()
        
        } else {
            provider = new ethers.BrowserProvider(window.ethereum)
            signer = await provider.getSigner();
        }
    }
    
    const ABI = [
        {
          "inputs": [],
          "stateMutability": "nonpayable",
          "type": "constructor"
        },
        {
          "inputs": [],
          "name": "changeAvailability",
          "outputs": [],
          "stateMutability": "nonpayable",
          "type": "function"
        },
        {
          "inputs": [],
          "name": "getBikes",
          "outputs": [
            {
              "components": [
                {
                  "internalType": "string",
                  "name": "name",
                  "type": "string"
                },
                {
                  "internalType": "uint256",
                  "name": "rangePower",
                  "type": "uint256"
                },
                {
                  "internalType": "uint256",
                  "name": "maxSpeed",
                  "type": "uint256"
                },
                {
                  "internalType": "uint256",
                  "name": "batteryCapacity",
                  "type": "uint256"
                },
                {
                  "internalType": "uint256",
                  "name": "costPerHour",
                  "type": "uint256"
                },
                {
                  "internalType": "bool",
                  "name": "isAvailable",
                  "type": "bool"
                }
              ],
              "internalType": "struct Rental.Bike[]",
              "name": "",
              "type": "tuple[]"
            }
          ],
          "stateMutability": "view",
          "type": "function"
        },
        {
          "inputs": [],
          "name": "getNumOfBikes",
          "outputs": [
            {
              "internalType": "uint256",
              "name": "",
              "type": "uint256"
            }
          ],
          "stateMutability": "view",
          "type": "function"
        },
        {
          "inputs": [
            {
              "internalType": "uint256",
              "name": "_totalHours",
              "type": "uint256"
            }
          ],
          "name": "rent",
          "outputs": [],
          "stateMutability": "payable",
          "type": "function"
        }
      ]
    
    const contract = new ethers.Contract(contractAddress, ABI, provider);



    useEffect(()=>{
        providerInitializer()
        .catch(console.error);

        const getBalance = async() =>{
            const balance = await provider.getBalance(contractAddress);
            const formattedBalance = ethers.formatEther(balance);
            setRunningBalance(formattedBalance);
        }

        const getNumber = async() =>{
            const number = await contract.getNumOfBikes(); //Throws error here for me
            setNumberOfBikes(number);
        }

        getBalance().catch(console.error);

        getNumber().catch(console.error);
    });

Solidity code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract Rental {
    struct Bike{
        string name;
        uint rangePower;
        uint maxSpeed;
        uint batteryCapacity;
        uint costPerHour;
        bool isAvailable;
    }
    
    //admin variables
    address owner;
    uint totalHours=0;
    uint totalRents=0;
    uint totalEbikes;

    //array of bikes
    Bike[] bikes;
 
    constructor(){
        //contract deployer address
        owner = msg.sender;

        //initialization of bike values
        bikes.push(Bike("48v/12ah Electric Bicycle Electric Bike", 60, 50, 576, 70, true));
        bikes.push(Bike("51v/17ah Electric Bicycle Electric Bike", 60, 50, 867, 70, true));
        bikes.push(Bike("43v/11ah Electric Bicycle Electric Bike", 60, 50, 473, 70, true));
        bikes.push(Bike("60v/18ah Electric Bicycle Electric Bike", 60, 50, 1080, 70, true));

        totalEbikes=bikes.length;
    }
    
    function getBikes() public view returns (Bike[] memory){
        return bikes;
    }

    function getNumOfBikes() public view returns (uint){
        return bikes.length;
    }

    function changeAvailability() public {
        bikes[1].isAvailable=false;
    }

    function rent(uint _totalHours) payable public {
        totalHours+=_totalHours;
        totalRents++;
    }


}

The tutorial I was watching seems to be using Ethers v5 and I'm using Ethers v6 but based on the documentation the part where I get stuck seems to be the same on both.

NickS1
  • 496
  • 1
  • 6
  • 19
m-dango
  • 31
  • 4

2 Answers2

2

The problem is you are trying to use a contract instance created with a "provider". A provider only supports querying, not calling. You need to create a contract instance with a signer.

const contract = new ethers.Contract(contractAddress, ABI, signer);

thks173
  • 1,500
  • 1
  • 8
  • 6
1

Actually, I faced the same problem and after many hours of grind..finally, I got the thing what's really happening here

As per ethers.js v6, there's a thing we need to know, By the way that's just not for v6, the same concept kinda applies in v5 too but anyways..

We know there are two types of functions in solidity, one that doesn't change the state(or read-only functions) and others that do change the state of the contract..

// This doesn't change the state
function getBikes() public view returns (Bike[] memory){
    return bikes;
}

// This do change the state
function changeAvailability() public {
    bikes[1].isAvailable=false;
}

So, for the read-only functions, we need to use the provider and for state-changing transactions or functions we need to use the signer. Also make sure to implement that signer variable in the form const signer = await provider.getSigner()

Here's what I meant by my above statement:

// Yes we can write ABI in this form and it will perfectly work.you just need to write the function..that's it
const ABI = ["function greet() public view returns (string)"]
const contract = new ethers.Contract(contractAddress, ABI, provider);

// As you can see above ABI consists of just read-only function and that's why 
// `contract` variable got provider as its 3rd parameter..Now see this
const ABI = [
"function setGreeting(string _greeting) public",
"function deposit() public payable"
]
const contract = new ethers.Contract(contractAddress, ABI, signer)

Hope that really solves this issue..and do look upon this whole code for a better understanding..

import React, {useState, useEffect} from 'react'
const { ethers } = require("ethers");

function App1() {
  const [greet, setGreet] = useState('')
  const [balance, setBalance] = useState(0)
  const [depositValue, setDepositValue] = useState('')
  const [greetingValue, setGreetingValue] = useState('')
  const [isWalletConnected, setIsWalletConnected] = useState(false)
  const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
  // ABI
  const ABI = [
    "function greet() public view returns (string)",
    "function setGreeting(string _greeting) public",
    "function deposit() public payable"
  ]

  const checkIfWalletIsConnected = async () => {
    try {
      if (window.ethereum) {
        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
        const account = accounts[0];
        setIsWalletConnected(true);
        console.log("Account Connected: ", account);
      } else {
        console.log("No Metamask detected");
      }
    } catch (error) {
      console.log(error);
    }
  }

  const getBalance = async () => {
    try{
        if(window.ethereum){
            // read data
            const provider = new ethers.BrowserProvider(window.ethereum);
            // const contract = new ethers.Contract(contractAddress, ABI, provider);

            const balance = await provider.getBalance(contractAddress)
            const balanceFormatted = ethers.formatEther(balance)
            setBalance(balanceFormatted)
        } else{
            console.log("Ethereum object not found, install Metamask.");
        }
    } catch (error) {
        console.log(error)
    }
  }

  const getGreeting = async () => {
    try{
        if(window.ethereum){
            // read data
            const provider = new ethers.BrowserProvider(window.ethereum);
            const contract = new ethers.Contract(contractAddress, ABI, provider);

            const greeting = await contract.greet()
            setGreet(greeting)
        } else{
            console.log("Ethereum object not found, install Metamask.");
        }
    } catch (error) {
        console.log(error)
    }
  }

  const handleGreetingChange = (e) => {
    setGreetingValue(e.target.value)
  }
  
  const handleGreetingSubmit = async (e) => {
    try{
        e.preventDefault()
        if(window.ethereum){
            const provider = new ethers.BrowserProvider(window.ethereum);
            const signer = await provider.getSigner();
            const contract = new ethers.Contract(contractAddress, ABI, signer);

            const greetingUpdate = await contract.setGreeting(greetingValue)
            await greetingUpdate.wait()
            setGreet(greetingValue)
            setGreetingValue('')
        } else {
            console.log("Ethereum object not found, install Metamask.");
        }
    } catch (error) {
        console.log(error)
    }
  } 

  const handleDepositchange = (e) => {
    setDepositValue(e.target.value)
  }

  const handleDepositSubmit = async (e) => {
    try{
        e.preventDefault()
        if(window.ethereum){
            const provider = new ethers.BrowserProvider(window.ethereum);
            const signer = await provider.getSigner();
            const contract = new ethers.Contract(contractAddress, ABI, signer);

            const ethValue = ethers.parseEther(depositValue)
            const depositEth = await contract.deposit({value: ethValue})
            await depositEth.wait()
            const balance = await provider.getBalance(contractAddress)
            const balanceFormatted = ethers.formatEther(balance)
            setBalance(balanceFormatted)
        } else {
            console.log("Ethereum object not found, install Metamask.");
        }
    } catch (error) {
        console.log(error)
    }
  }

  useEffect(() => {
    checkIfWalletIsConnected();
    getBalance();
    getGreeting()
  }, [isWalletConnected])


  return (
    <div className="container">
      <div className="container">
        <div className="row mt-5">
          <div className="col">
            <h3>{greet}</h3>
            <p>Contract balance: {balance} ETH</p>
          </div>
          <div className="col">
            <form onSubmit={handleDepositSubmit}>
              <div className="mb-3">
                <input type="number" className="form-control" placeholder="0" onChange = {handleDepositchange} value={depositValue}/>
              </div>
              <button type="submit" className="btn btn-success">Deposit</button>
            </form>
            <form className="mt-5" onSubmit={handleGreetingSubmit}>
              <div className="mb-3">
                <input type="text" className="form-control" onChange={handleGreetingChange} value={greetingValue}/>
              </div>
              <button type="submit" className="btn btn-dark">Change</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  );
}

export default App1;