3

I have my own private keys for an account and I'm using web3py to invoke a smart contract function. The way things are right now as per the docs, I first need to build the transaction, sign with my private keys and then send it as a raw transaction.

The problem is I can't build the transaction without it throwing a ContractLogicError. The function will be valid a few blocks down the line so I want to send it as soon as possible but even though the docs claim that buildTransaction doesn't actually send anything, it seems to validate the arguments with the current block for which it fails.

Is there a way for me to build the transaction without the smart contract validation, package it up and send it. This risks my contract failing on the chain and costing me gas fees, but it's a risk I'm willing to take.

Code looks something like this:

contract = [
    {"name":"send","outputs":[],"stateMutability":"nonpayable","type":"function","inputs":[{"internalType":"uint256","name":"gameId","type":"uint256"},{"internalType":"uint256","name":"resourceId","type":"uint256"}]},
]

action = contract.functions.send(game_id, resource_id)
try:
    txn = action.buildTransaction({
        'chainId': CHAIN_ID,
        'maxFeePerGas': w3.toWei(max_fee, 'gwei'),
        'maxPriorityFeePerGas': w3.toWei(max_priority_fee, 'gwei'),
        'from': from_address,
        'nonce': nonce_manager[from_address],
    })
    signed = w3.eth.account.sign_transaction(txn, PRIVATE_KEY)
    txhash = Web3.toHex(w3.eth.send_raw_transaction(signed.rawTransaction))
    receipt = w3.eth.wait_for_transaction_receipt(txhash, timeout=60)
    nonce_manager[from_address] += 1
    return receipt, None
except Exception as e:
    return None, e

I get a contract specific exception when I run this: INVALID RESOURCE_ID and never even gets to send_raw_transaction. Removing it makes no difference. However if I put this in a while loop it eventually works after a few blocks.

Tested on latest web3py and Ubuntu 20.01 as well as Mac 12.1 both using Python 3.9.7

7
  • "Is there a way for me to build the transaction without the smart contract validation." Yes, there is, just add some source code to your answer so we can provide a useful response. Jan 25, 2022 at 19:32
  • @GiuseppeBertone As I mentioned in the question, I'm doing this exactly as mentioned in the docs. My source code is pretty much a copy paste of web3py.readthedocs.io/en/stable/…. The specific contract is irrelevant.
    – userinev
    Jan 26, 2022 at 9:05
  • The buildTransaction method does not care about your node or the blockchain's current state, it cares about the ABI. ContractLogicError is fired if the transaction is sent and the contract's execution reverts. The example in the doc works, so that "pretty much" is where the problem lies. Maybe we can help if you share exactly what you are trying to do, differently than the base example. Jan 26, 2022 at 9:47
  • "The buildTransaction method does not care about your node or the blockchain's current state" That is verifiably false. It throws a contract logic error in the build transaction step. I don't even have a send_raw_transaction step and it throws the error. There is no point in be posting the source code because it will look identical to the example. The only way to replicate it is to run my entire program because it keeps monitoring the blockchain and quickly attempts to react to a transaction. Since I can working off of data from pending transactions I am able to see what's about to happen.
    – userinev
    Jan 26, 2022 at 11:26
  • I guess the issue here is I can't really replicate the error without sharing the full program (don't want to do that for a lot of reasons). I guess you'll have to take my at my word that it fails in that step. Not really sure what else I can do other than create some video of me running the program just to prove my question is based on a real premise.
    – userinev
    Jan 26, 2022 at 11:29

1 Answer 1

4
+100

Is there a way for me to build the transaction without the smart contract validation, package it up and send it. This risks my contract failing on the chain and costing me gas fees, but it's a risk I'm willing to take.

If I understand correctly, you are almost there, just remember that what you call "smart contract validation" is just the gas estimation in units of gas. Basically a dry run on the provider side with no change whatsoever on the chain, simply returning the amount of gas used or an error if any. But that's an execution nonetheless.

The gas parameter (and others) will be automatically set if it is not already present.

Since you define your tx with :

  txn = action.buildTransaction({
        'chainId': CHAIN_ID,
        'maxFeePerGas': w3.toWei(max_fee, 'gwei'),
        'maxPriorityFeePerGas': w3.toWei(max_priority_fee, 'gwei'),
        'from': from_address,
        'nonce': nonce_manager[from_address],
    })

There is no gas parameter and it will be set automatically through an internal call to estimateGas, executing your smart contract function which fails... Simply setting a value for the gas parameter will solve your issue as no gas estimate will be required since a value will already be specified.

You can see a similar behavior with the following contract :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Example {

  function fail() public {
    require(false);
  }
}

And this web3 py code to build a transaction :

from web3 import Web3

w3 = Web3(Web3.WebsocketProvider('ws://127.0.0.1:8545'))

abi = [
    {
        "inputs": [],
        "name": "fail",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

contract = w3.eth.contract(address="0x8CdaF0CD259887258Bc13a92C0a6dA92698644C0", abi=abi)
action = contract.functions.fail()

# This will fail as it forces a gas estimation
txn = action.buildTransaction({'from': "0x627306090abaB3A6e1400e9345bC60c78a8BEf57"})

# This will not
txn = action.buildTransaction({'from': "0x627306090abaB3A6e1400e9345bC60c78a8BEf57", 'gas': 100})

The first call to buildTransaction fails, since the transaction is bound to fail with require(false). You get ContractLogicError. The second call, specify a value for the gas parameter, no estimation is required and the tx can be sent. It will fail anyway in my example but not necessarily on yours. (comment out the first call to test the second one)

It might be better for you to poll the gas estimate to only send your tx when it has a chance to succeed, but that is your choice. You must also give enough gas for your function to run until completion.

I hope that answers your question.

2
  • Thank you so much, this is exactly what I wanted! For some reason I thought passing maxFeePerGas and maxPriorityFeePerGas was enough and that gas wasn't needed for EIP 1559. I guess this is one of those few times where automagical functions bite you because they try to intelligently fail. Would have preferred something like an explicit gas: 'estimate' || <int> but oh well. Thanks again!
    – userinev
    Jan 27, 2022 at 8:23
  • 2
    Well you are in a special case, the api accommodates the majority of cases which are fine with a standard implicit estimate. Gas always needs to be specified, EIP-1559 changes how gas price are defined / distributed, but it does nothing on the amount of gas itself.
    – hroussille
    Jan 27, 2022 at 8:52

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.