Force Hero
Force Hero

Reputation: 3262

Contract event listener is not firing when running hardhat tests with ethers js

Here is a very small repo to show the issue: https://github.com/adamdry/ethers-event-issue

But I'll explain it here too. This is my contract:

//SPDX-License-Identifier: UNLICENSED;
pragma solidity 0.8.4;

contract ContractA {

    event TokensMinted(uint amount);

    function mint(uint amount) public {
        emit TokensMinted(amount);
    }

}

And this is my test code:

import * as chai from 'chai'
import { BigNumber, ContractTransaction } from 'ethers'
import { ethers } from 'hardhat'
import { ContractA, ContractAFactory } from '../typechain'

const expect = chai.expect

describe("Example test", function () {
    it("should fire the event", async function () {
        const [owner] = await ethers.getSigners();

        const contractAFactory = (await ethers.getContractFactory(
            'ContractA',
            owner,
        )) as ContractAFactory

        const contractA: ContractA = await contractAFactory.deploy()

        contractA.on('TokensMinted', (amount: BigNumber) => {
            // THIS LINE NEVER GETS HIT
            console.log('###########')
        })

        const contractTx: ContractTransaction = await contractA.mint(123)
        const contractReceipt: ContractReceipt = await contractTx.wait()

        for (const event of contractReceipt.events!) {
            console.log(JSON.stringify(event))
        }
    });
});

I was expecting the ########### to get printed to the console however it doesn't so the listener function isn't being executed for some reason.

If I dig into the ContractReceipt the correct event data is there:

{
  "transactionIndex": 0,
  "blockNumber": 2,
  "transactionHash": "0x55d118548c8200e5e6c19759d9aab56cb2e6a274186a92643de776d617d51e1a",
  "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
  "topics": [
    "0x772f66a00a405709c30e7f18feadcc8f123b20c09c7260165d3eec36c9f21372"
  ],
  "data": "0x000000000000000000000000000000000000000000000000000000000000007b",
  "logIndex": 0,
  "blockHash": "0x808e6949118509b5a9e482e84cf47921a2fcffbcd943ebbd8ce4f6671469ee01",
  "args": [
    {
      "type": "BigNumber",
      "hex": "0x7b"
    }
  ],
  "event": "TokensMinted",
  "eventSignature": "TokensMinted(uint256)"
}

Upvotes: 6

Views: 7227

Answers (3)

Alexey Kureev
Alexey Kureev

Reputation: 2077

I might be late to the party, but I faced a very similar challenged (getting return value from the function call). I tried subscribing, getting transaction receipt and running receipt.events but nothing proved to work for me (ethers v6.11.1). Hence, I'd like to contribute my 2 cents to the issue:

async function getEvent(
  contract: Contract,
  tx: TransactionResponse,
  eventName: string,
) {
  const receipt = await tx.wait();
  if (receipt?.logs) {
    for (const log of receipt.logs) {
      const event = contract.interface.parseLog(log);
      if (event?.name === eventName) {
        return event;
      }
    }
  }

  return null;
}

This function accepts a contract, transaction and event name that you want to listen to. It tries to get a receipt for the transaction (mind that it can fail, so we need to check if the object is there). Then, we get transaction logs and filter them by name of the event. If successful, it returns the log entry (event) which has args property:

const tx = await factory.mint(ownerAddress, tokenURI);
const event = await getEvent(factory, tx, "TokenMinted");
const tokenId: number = event?.args[1];

Hope this answer would prove itself helpful over time as more and more people face similar problems.

Upvotes: 0

dav1app
dav1app

Reputation: 2001

In case you need to do it only inside the tests, an alternative would be to use Hardhats Chai expect().to.emit() syntax. Docs here

import { expect } from "chai";
import { ethers } from "hardhat";

//...

expect(contractA.mint(123)).to.emit(contractA, "TokensMinted")
  .withArgs(123)

Keep in mind that you still want to use a timeout pooling if you need to fire another event in sequence in the same test. Something like this.

      expect(contractA.mint(123)).to.emit(contractA, "TokensMinted")
        .withArgs(123)

      // wait 5s due to hardhat pooling of 4s
      await new Promise(resolve => setTimeout(resolve, 5000));

      // if you remove the wait above, it will try to burn tokens with 0 balance
      await constractA.burn(123)
   
      // "wait" for the burn to happen
      await new Promise(resolve => setTimeout(resolve, 5000));
      
      // now we can check balances
      expect(contractA.getBalance()).to.be(0)

Upvotes: 0

Force Hero
Force Hero

Reputation: 3262

The full answer is here: https://github.com/nomiclabs/hardhat/issues/1692#issuecomment-905674692

But to summarise, the reason this doesn't work is that ethers.js, by default, uses polling to get events, and the polling interval is 4 seconds. If you add this at the end of your test:

await new Promise(res => setTimeout(() => res(null), 5000));

the event should fire.

However! You can also adjust the polling interval of a given contract like this:

// at the time of this writing, ethers' default polling interval is
// 4000 ms. here we turn it down in order to speed up this test.
// see also
// https://github.com/ethers-io/ethers.js/issues/615#issuecomment-848991047
const provider = greeter.provider as EthersProviderWrapper;
provider.pollingInterval = 100;

As seen here: https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-ethers/test/index.ts#L642

However! (again) If you want to get the results from an event, the below method requires no changes to the polling or any other "time" based solutions, which in my experience can cause flakey tests:

it('testcase', async() => {
  const tx = await contract.transfer(...args); // 100ms
  const rc = await tx.wait(); // 0ms, as tx is already confirmed
  const event = rc.events.find(event => event.event === 'Transfer');
  const [from, to, value] = event.args;
  console.log(from, to, value);
})

Here is my TypeScriptyfied version (on my own contract with a slightly different event and args):

const contractTx: ContractTransaction = await tokenA.mint(owner.address, 500)
const contractReceipt: ContractReceipt = await contractTx.wait()
const event = contractReceipt.events?.find(event => event.event === 'TokensMinted')
const amountMintedFromEvent: BigNumber = event?.args!['amount']

This is the event declaration in solidity that goes with the above:

event TokensMinted(uint amount);

Upvotes: 8

Related Questions