[Ethereum] How to call a contract function/method using ethersjs

ethers.js

I for the life of me cannot figure out how to simply call a function from a smart contract when a button is pressed on the front end. I have been reading over the docs and watching countless tutorials over the past week and still cannot figure out how to call a transaction.

It's merely a simple buyToken function, but I think the problem has something to do with the signer because when I console.log signer it returns null. Everything works fine when I comment out the contract call, but as soon I call it I get a nasty error: "Error: Transaction reverted without a reason string"

Heres the smart contract function

function buyToken() public payable {
        uint buyAmount = msg.value * rate;
        require (bartoken.balanceOf(address(this)) >= buyAmount, 'Insufficient balance');
        bartoken.transfer(msg.sender, buyAmount);

        emit TokenBought(msg.sender, address(bartoken), buyAmount);
    }

And here's the Front-end (Reactjs & ethersjs)

async function buyTokens(e) {
        e.preventDefault();
        if (typeof window.ethereum !== 'undefined'){
            const provider = new ethers.providers.Web3Provider(window.ethereum);
            const account = await window.ethereum.request({ method: 'eth_requestAccounts' })
            const signer = provider.getSigner();

            const exchange = new ethers.Contract('CONTRACT_ADDRESS', abi, signer);
        
    }
}

Best Answer

July 2022, ethers.js 5.6, TypeScript. An example that prints USDT token total supply on Mainnet.

Metamask:

    const ethereum = (window as any).ethereum;
    const accounts = await ethereum.request({
      method: "eth_requestAccounts",
    });

    const provider = new ethers.providers.Web3Provider(ethereum)
    const walletAddress = accounts[0]    // first account in MetaMask
    const signer = provider.getSigner(walletAddress)

In case of RPC provider like Alchemy:

    // Second parameter is chainId, 1 for Ethereum mainnet 
    const provider = new ethers.providers.JsonRpcProvider("API_URL", 1);
    const signer = new ethers.Wallet("WALLET_PRIVATE_KEY", provider);

This part calls methods name, symbol, decimals, totalSupply.

    const abi = [
      "function name() public view returns (string)",
      "function symbol() public view returns (string)",
      "function decimals() public view returns (uint8)",
      "function totalSupply() public view returns (uint256)",
      "function approve(address _spender, uint256 _value) public returns (bool success)"]

    const USDTContract = new ethers.Contract("0xdAC17F958D2ee523a2206206994597C13D831ec7", abi, signer)

    const name = await USDTContract.name()
    const symbol = await USDTContract.symbol()
    const decimals = await USDTContract.decimals()
    const totalSupply = await USDTContract.totalSupply()

    console.log(`${symbol} (${name}) total supply is ${ethers.utils.formatUnits(totalSupply, decimals)}`)

When you call state-changing contract methods (for example, method "approve" with two parameters "SOME_ADDRESS" and "1000000")), in case of external RPC provider like Alchemy you'd need to set chainId, nonce, gasLimit and gasPrice yourself like in the example below:

    const estimatedGasLimit = await USDTContract.estimateGas.approve("SOME_ADDRESS", "1000000"); // approves 1 USDT
    const approveTxUnsigned = await USDTContract.populateTransaction.approve("SOME_ADDRESS", "1000000");
    approveTxUnsigned.chainId = 1; // chainId 1 for Ethereum mainnet
    approveTxUnsigned.gasLimit = estimatedGasLimit;
    approveTxUnsigned.gasPrice = await provider.getGasPrice();
    approveTxUnsigned.nonce = await provider.getTransactionCount(walletAddress);

    const approveTxSigned = await signer.signTransaction(approveTxUnsigned);
    const submittedTx = await provider.sendTransaction(approveTxSigned);
    const approveReceipt = await submittedTx.wait();
    if (approveReceipt.status === 0)
        throw new Error("Approve transaction failed");

In case of call from MetaMask it would be just one line. Metamask sets all these fields.

await USDTContract.approve("SOME_ADDRESS", "1000000");
Related Topic