Reputation: 3262
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
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
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
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