During my development process, I've spotted differences between calling function in my contract from script and from dApp. Whole thing is done on hardhat local network.

Script call:

  Contract call:       Contract#earningInfo
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  To:                  0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0

dApp call:

  Contract call:       Contract#earningInfo
  Transaction:         0xf58da19368c9aa20c9c33d4177af0efb8c08fd9bcbc59cdfd9c9abcb09742bc2
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  To:                  0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
  Value:               0 ETH
  Gas used:            23242 of 23242
  Block #6:            0x4389bde0717095ed768297b51425d805d99358f985592e3027f21d9ec1585280

This of course has some effects on the response itself. While first call (script) is returning plain response - second call(dApp) is returning me transactionInfo, which I probably can turn into receipt adn get Event from there, but... I think, that I miss something. Contract itself is a view, without nested calls:

function earningInfo(address account, uint256[] calldata _nftIds) external view returns (uint256[1] memory info) {
        uint256 earned = 0;

        for (uint i = 0; i < _nftIds.length; i++) {
            Stake memory staked = vault[_nftIds[i]];
            require(staked.owner == account, "not an owner");
            uint256 stakedAt = staked.timestamp;
            earned += tokensPerDay * 1 ether * (block.timestamp - stakedAt) / 86400;

        return [earned];

Call from dApp:

export const stakingEarningInfoQueryForTokens = async (userAddress: string, tokenIds: number[]): Promise<any> => {
  console.log('calling earning info', userAddress, tokenIds);
  const { contract } = await getProvider(chain, address, abi);
  return contract.earningInfo(userAddress, tokenIds);

And getProvider function:

export const getProvider = async (chain: string, address: string, abi: string[] ) => {
  const provider = new JsonRpcProvider(rpc[chain].rpcUrl);
  const signer = await provider.getSigner();
  const contract = new Contract(address, abi, signer);

  return {

I don't have to confirm this transaction in wallet (MetaMask).

Follow up question might be - is the only difference between eth_call and eth_sendTransaction is that the latter one is executed on the chain - therefore it cost money + different response type? :)

2 Answers 2


eth_call: This is a read-only operation that executes a contract function on the local node without creating a transaction on the blockchain. It is used for querying the contract state and doesn't modify anything on the blockchain. It returns the result of the function call directly as a response.

eth_sendTransaction: This is used to send a transaction to the Ethereum network, which can modify the contract state and interact with other contracts. When you call a contract function using eth_sendTransaction, you are creating a transaction on the blockchain, and it will be mined into a block. This operation incurs gas fees and returns a transaction receipt, which includes information about the transaction's status and gas used.

  • But my concern here is - why when I call it from dApp it turns into transaction, but when called from script its just a call? It looks, like it should be just a call. Right?
    – Lidjan
    Sep 21, 2023 at 21:26
  • @Lidjan indeed, you're doing something wrong on the dApp side. What's the code that calls this function? (side note about the differences between call and sendTransaction, eth_sendTransaction expects the tx data to be signed with a private key when eth_call doesn't)
    – Foxxxey
    Sep 21, 2023 at 21:53
  • @Foxxxey Added code from dApp - its really simple, dont see any problems there :( And side note - its working fine for other calls.
    – Lidjan
    Sep 22, 2023 at 7:28
  • @Lidjan I think you are using ethers.js? Looking at the code chunks you provided, ethers indeed should use eth_call as your contract is view. It's weird if it does an eth_sendTransaction and looks like it consumes all gas and fails since it's an invalid tx really. To troubleshoot, I'd cut out const signer = await provider.getSigner(); and see what it does (as you don't really need the signer for eth_call).
    – Ake
    Sep 22, 2023 at 9:21

It turns out, that Ive made a mistake inside ABI. I had:

  "function earningInfo(address account, uint256[] _nftIds) returns (uint256[1] memory info)",

instead of:

  "function earningInfo(address account, uint256[] _nftIds) view returns (uint256[1] memory info)",

Missing 'view' was the key. That was valuable lesson for me, that ABI means a lot more for ethers.js then I thought. Maybe it will also help someone else.

I will update this answer if I find a good, simple and fast way to keep consistency between my contracts and ABI's in my dApp.

Thank You @Mehran Ra, @Foxxxey and @Ake for Your input.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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