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) {
        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);
  • Signer should definitely return something other than null. Are you connecting with MetaMask? Have you tested your smart contract code out using Remix and all functions work as expected there? What is bartoken in bartoken.balanceOf.... ? Assuming you are replacing 'CONTRACT_ADDRESS' with a proper address, you'll also need to call the function with an amount, i.e. the msg.value. Something like: await exchange.buyToken({ value: ethers.utils.parseEther(_eth) })
    – Tom
    Feb 23 at 14:56
  • You don't have to figure it out yourself - just use create-eth-app. You get a boilerplate which includes code for how to call a smart contract when a button is clicked. Mar 8 at 12:38
  • You can't have empty signer, it is the account who "signs" your message that you send to blockchain. In case of Metamask it knows your private key, so signer is created "automatically". In case of using RPC API you need to create a signer with a private key Jul 18 at 9:59

2 Answers 2


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


    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");


I simply use the contract.method(args) to call it:

const provider = await this.web3Modal.connect();

const library = new Web3Provider(this.provider);

const network = await library.getNetwork();

const address = this.provider.selectedAddress ? this.provider.selectedAddress : this.provider?.accounts[0];

// get the contract
const contract = getContract(contract_address, abi, library, address);

const transaction = await contract.submitStateResult(dataArr);
const transactionReceipt = await transaction.wait();
if (transactionReceipt.status !== 1) {
   alert('error message');

You can refer to my code on - https://github.com/Vitomir2/dApp-web3-wallets/blob/master/src/App.tsx

P.S. In the the const address, you can put directly the wallet address, because here I am connecting with metamask and get it from there.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.