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

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

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

Related

How to call Openbrush contract from Front-end app

I implement smart contracts with ink!, substrate's WASM smart contract implementation language.
At that time, I decided to use the openbrush library. openbrush is like openzeppelin in EVM.
The smart contract was easy to implement according to the official website. [https://docs.openbrush.io/]
But I couldn't figure out how to call it from the front end.
I was able to solve it by looking at Telegram, but I will write this in the hope that it will help others.
A function defined in openbrush is declared like this:
psp34::transfer (to: TransferInput1, id: TransferInput2, data: TransferInput3)
psp34::ownerOf (id: OwnerOfInput1): Option<AccountId>
Below is a sample code that calls the above two contract functions.
const wsProvider = new WsProvider(blockchainUrl);
const api = await ApiPromise.create({ provider: wsProvider });
const contract = new ContractPromise(api, psp_abi, PSP_ADDRESS);
setApi(api);
const { gasConsumed, result, output } = await contract.query['psp34::ownerOf'](
actingAddress,
{ value: 0, gasLimit: -1 },
NFT_ID
);
const { web3FromSource } = await import("#polkadot/extension-dapp");
const wsProvider = new WsProvider(blockchainUrl);
const api = await ApiPromise.create({ provider: wsProvider });
setApi(api);
const contract = new ContractPromise(api, psp_abi, PSP_ADDRESS);
const performingAccount = accounts[0];
const injector = await web3FromSource(performingAccount.meta.source);
const flip = await contract.tx['psp34::transfer'](
{ value: 0, gasLimit: gasLimit },
"ajYMsCKsEAhEvHpeA4XqsfiA9v1CdzZPrCfS6pEfeGHW9j8",
NFT_ID,
Null
);
if (injector !== undefined) {
const unsub = await flip.signAndSend(
actingAddress,
{ signer: injector.signer },
({ events = [], status }) => {
-- snip --

How to test the Solidity fallback() function via Hardhat?

I have a Solidity smart contract Demo which I am developing in Hardhat and testing on the RSK Testnet.
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Demo {
event Error(string);
fallback() external {
emit Error("call of a non-existent function");
}
}
I want to make sure that the fallback function is called and the event Error is emitted. To this end, I am trying to call a nonExistentFunction on the smart contract:
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe('Demo', () => {
let deployer;
let demoContract;
before(async () => {
[deployer] = await ethers.getSigners();
const factory = await ethers.getContractFactory('Demo');
demoContract = await factory.deploy().then((res) => res.deployed());
});
it('should invoke the fallback function', async () => {
const tx = demoContract.nonExistentFunction();
await expect(tx)
.to.emit(demoContract, 'Error')
.withArgs('call of a non-existent function');
});
});
However Hardhat throws a TypeError even before it actually connects to the smart contract on RSK:
Demo
1) should invoke the fallback function
0 passing (555ms)
1 failing
1) Demo
should invoke the fallback function:
TypeError: demoContract.nonExistentFunction is not a function
at Context.<anonymous> (test/Demo.js:13:29)
at processImmediate (internal/timers.js:461:21)
How can I outsmart Hardhat/Ethers.js and finally be able to call non-existent function thus invoking the fallback function in the smart contract?
For reference, this is my hardhat.config.js
require('#nomiclabs/hardhat-waffle');
const { mnemonic } = require('./.secret.json');
module.exports = {
solidity: '0.8.4',
networks: {
hardhat: {},
rsktestnet: {
chainId: 31,
url: 'https://public-node.testnet.rsk.co/',
accounts: {
mnemonic,
path: "m/44'/60'/0'/0",
},
},
},
mocha: {
timeout: 600000,
},
};
You can use the approach of injecting a non-existent function signature
into the smart contract object (ethers.Contract).
Create a function signature for nonExistentFunction:
const nonExistentFuncSignature =
'nonExistentFunction(uint256,uint256)';
Note that the parameter list should contain no whitespace,
and consists of parameter types only (no parameter names).
Then instantiate a new smart contract object.
When you do this you need to modify the ABI for Demo
such that it includes this additional function signature.:
const fakeDemoContract = new ethers.Contract(
demoContract.address,
[
...demoContract.interface.fragments,
`function ${nonExistentFuncSignature}`,
],
deployer,
);
Note that the contract that is deployed is the same as before -
it does not contain this new function.
However the client interacting with this smart contract -
the tests in this case -
does think that the smart contract has this function now.
At this point, you'll be able to run your original test,
with a minor modification:
const tx = fakeDemoContract[nonExistentFuncSignature](8, 9);
await expect(tx)
.to.emit(demoContract, 'Error')
.withArgs('call of a non-existent function');
Full test:
it('should invoke the fallback function', async () => {
const nonExistentFuncSignature = 'nonExistentFunc(uint256,uint256)';
const fakeDemoContract = new ethers.Contract(
demoContract.address,
[
...demoContract.interface.fragments,
`function ${nonExistentFuncSignature}`,
],
deployer,
);
const tx = fakeDemoContract[nonExistentFuncSignature](8, 9);
await expect(tx)
.to.emit(demoContract, 'Error')
.withArgs('call of a non-existent function');
});
Test result:
Demo
✔ should invoke the fallback function (77933ms)
1 passing (2m)
A transaction executing a function contains the function selector following its (ABI-encoded) input params in the data field.
The fallback() function gets executed when the transaction data field starts with a selector that does not match any existing function. For example an empty selector.
So you can generate a transaction to the contract address, with empty data field, which invokes the fallback() function.
it('should invoke the fallback function', async () => {
const tx = deployer.sendTransaction({
to: demoContract.address,
data: "0x",
});
await expect(tx)
.to.emit(demoContract, 'Error')
.withArgs('call of a non-existent function');
});
Note: If you also declared the receive() function, it takes precedence over fallback() in case of empty data field. However, fallback() still gets executed for every non-empty mismatching selector, while receive() is only invoked when the selector is empty.

How to combine a loop of Axios calls with an await-ed function?

That's a tricky one I guess. I'm prompting user for a word which I validate with an Axios API call. Once validation clears, the main loop of my game - hangman - starts with a wait between each move (hence the use of await).
Problem: in current version, the main game loop (starting after "once validation clears, game starts below" comment) must starts after validation, when in fact it starts at the same time which messes everything up.
And I can't put my main loop inside the then() part of my Axios call because in that case the await-ed function call ceases to work.
Any idea to get out of this mess?
async startGameComputerGuesser () {
var wordIsValidated = false
const vm = this
const dispatcher = {
execute: function () {
const wordApiBaseUrl = 'https://www.dictionaryapi.com/api/v1/references/sd4/xml'
wordToGuess = prompt('Enter a word:').toLowerCase()
const dispatcher = this
vm.axios.get(`${wordApiBaseUrl}/${wordToGuess}?key=${wordApiKey}`).then(res => {
if (!res.data.includes('def')) {
dispatcher.execute()
} else {
wordIsValidated = true
}
})
}
}
dispatcher.execute()
// once validation clears, game starts below
if (wordIsValidated) {
while (!this.$store.state.gameIsOver) {
await this.resolveAfter2Seconds()
// main loop of the game goes here
}
}
}
use await inside the execute and return true/false then use while to check that condition like below
async startGameComputerGuesser() {
let wordIsValidated = false;
const vm = this;
const dispatcher = {
async execute() {
const wordApiBaseUrl = 'https://www.dictionaryapi.com/api/v1/references/sd4/xml'
const wordToGuess = prompt('Enter a word:').toLowerCase();
const res = await vm.axios.get(`${wordApiBaseUrl}/${wordToGuess}?key=${wordApiKey}`);
return res.data.includes('def');
}
}
// validation
while (!wordIsValidated) {
wordIsValidated = await dispatcher.execute();
}
// game starts below
while (!this.$store.state.gameIsOver) {
await this.resolveAfter2Seconds()
// main loop of the game goes here
}
}
Example code:
const startGameComputerGuesser = async function() {
let wordIsValidated = false;
const dispatcher = {
async execute() {
const res = await new Promise(res => setTimeout(() => res(Math.floor(Math.random() * 10)), 500));
console.log(res);
return res == 6;
}
}
// validation
while (!wordIsValidated) {
wordIsValidated = await dispatcher.execute();
}
// once validation clears, game starts below
console.log('started');
}
startGameComputerGuesser();

how to receive a take with runSaga / redux-saga

I created a recordSaga function, its target is to record what actions have been dispatched during the saga.
export const recordSaga = async (saga, initialAction, state) => {
const dispatched = [];
const done = await runSaga(
{
dispatch: action => dispatched.push(action),
getState: () => state,
},
saga,
initialAction,
).done;
return {
dispatched,
done,
};
};
so let's say my saga is this one
export function* mySaga() {
const needToSave = yield select(needToSaveDocument);
if (needToSave) {
yield put(saveDocument());
yield take(SAVE_DOCUMENT_SUCCESS);
}
yield put(doSomethingElse())
}
I want to write two tests, which I expect to be the following
describe('mySaga', async () => {
it('test 1: no need to save', async () => {
const state = { needToSave: false }
const { dispatched } = await recordSaga(mySaga, {}, state);
expect(dispatched).toEqual([
doSomethingElse()
])
})
it('test 2: need to save', async () => {
const state = { needToSave: true }
const { dispatched } = await recordSaga(mySaga, {}, state);
expect(dispatched).toEqual([
saveDocument(),
doSomethingElse()
])
})
})
However, for the test 2 where there is a take in between, and of course jest (or its girlfriend jasmine) is yelling at me: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
I know it is because runSaga is waiting for the take(SAVE_DOCUMENT_SUCCESS), but how can I mock that up ?
The answer stdChannel().put({type, payload})
Why ?
Using stdChannel you can dispatch after the first run.
How ?
import stdChannel;
add to the first param in runSaga;
call stdChannel().put(SAVE_DOCUMENT_SUCCESS);
Example
what worked for me
I left the first test as it is the expected final result, but the solution comes on the last 2.
import { runSaga, stdchannel } from 'redux-saga'
let dispatchedActions = [];
let channel;
let fakeStore;
beforeEach(() => {
channel = stdChannel(); // you have to declare the channel to have access to it later
fakeStore = {
channel, // add it to the store in runSaga
getState: () => "initial",
dispatch: (action) => dispatchedActions.push(action),
};
});
afterEach(() => {
global.fetch.mockClear();
});
it("executes getData correctly", async () => {
await runSaga(fakeStore, getData, getAsyncData("test")).toPromise();
expect(global.fetch.mock.calls.length).toEqual(1);
expect(dispatchedActions[0]).toEqual(setData(set_value));
});
it("triggers takeLatest and call getData(), but unfortunately doesn't resolve promise", async () => {
await runSaga(fakeStore, rootSaga)// .toPromise() cannot be used here, as will throw Timeout error
channel.put(getAsyncData("test")); // if remove this line, the next 2 expects() will fail
expect(global.fetch.mock.calls.length).toEqual(1);
// expect(dispatchedActions[1]).toEqual(setData(set_value)); // will fail here, but pass on the next it()
});
it("takes the promised data from test above", () => {
expect(dispatchedActions[1]).toEqual(setData(set_value));
});
this answer (about true code, not tests) helped me
By looking at recordSaga:
export const recordSaga = async (saga, initialAction, state) => {
It seems that you should pass {type: SAVE_DOCUMENT_SUCCESS} as a second argument (i.e initialAction). That should trigger the take effect.

relay subscription onNext not triggered on react-native

I am a subscription setup but onNext is not getting triggered I am not sure why since this is my first time implementing subscription and docs was not much help with the issue.
Here are the code implementations:
import {
graphql,
requestSubscription
} from 'react-relay'
import environment from '../network';
const subscription = graphql`
subscription chatCreatedSubscription{
chatCreated{
id
initiate_time
update_time
support_id
category_id
email
name
}
}
`;
function chatCreated(callback) {
const variables = {};
requestSubscription(environment, {
subscription,
variables,
onNext: () => {
console.log("onNext");
callback()
},
updater: () => {
console.log("updater");
}
});
}
module.exports = chatCreated;
and here is my network for the subscription
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import Expo from "expo";
import { SubscriptionClient } from "subscriptions-transport-ws";
import { WebSocketLink } from 'apollo-link-ws';
import { execute } from 'apollo-link';
import accessHelper from "../helper/accessToken";
const networkSubscriptions = async (operation, variables) => {
let token = await accessHelper();
if (token != null || token != undefined) {
const subscriptionClient = new SubscriptionClient("ws://localhost:3000/graphql",
{
reconnect: true,
connectionParams: {
Authorization: token,
},
});
execute(new WebSocketLink(subscriptionClient), {
query: operation.text,
variables,
});
}
}
const network = Network.create(fetchQuery, networkSubscriptions);
const store = new Store(new RecordSource());
const environment = new Environment({
network,
store
});
export default environment;
the subscription is called in a componentDidMount method on a component it executes but the onNext method inside the subscription is never triggered when new information is added to what the subscription is listening to.
so i figured out that my issue was the network js not being setup properly and the version of subscription-transport-ws. i added version 0.8.3 of the package and made the following changes to my network file:
const networkSubscriptions = async (config, variables, cacheConfig, observer) => {
const query = config.text;
let token = await accessHelper();
if (token != null || token != undefined) {
const subscriptionClient = new SubscriptionClient(`ws://${api}/graphql`,
{
reconnect: true,
connectionParams: {
Authorization: token,
},
});
subscriptionClient.subscribe({ query, variables }, (error, result) => {
observer.onNext({ data: result })
})
return {
dispose: subscriptionClient.unsubscribe
};
}
}
i hope this helps you if you get stuck with the same issue as mine.