I’m playing around with ethereum and python and I’m running into some weird behavior I can’t make sense of. I’m having trouble understanding how return values work when calling a contract function with the python w3 client. Here’s a minimal example which is confusing me in several different ways:
Contract:
pragma solidity ^0.4.0; contract test { function test(){ } function return_true() public returns (bool) { return true; } function return_address() public returns (address) { return 0x111111111111111111111111111111111111111; } }
Python unittest code
from web3 import Web3, EthereumTesterProvider from solc import compile_source from web3.contract import ConciseContract import unittest import os def get_contract_source(file_name): with open(file_name) as f: return f.read() class TestContract(unittest.TestCase): CONTRACT_FILE_PATH = "test.sol" DEFAULT_PROPOSAL_ADDRESS = "0x1111111111111111111111111111111111111111" def setUp(self): # copied from https://github.com/ethereum/web3.py/tree/1802e0f6c7871d921e6c5f6e43db6bf2ef06d8d1 with MIT licence # has slight modifications to work with this unittest contract_source_code = get_contract_source(self.CONTRACT_FILE_PATH) compiled_sol = compile_source(contract_source_code) # Compiled source code contract_interface = compiled_sol[':test'] # web3.py instance self.w3 = Web3(EthereumTesterProvider()) # Instantiate and deploy contract self.contract = self.w3.eth.contract(abi=contract_interface['abi'], bytecode=contract_interface['bin']) # Get transaction hash from deployed contract tx_hash = self.contract.constructor().transact({'from': self.w3.eth.accounts[0]}) # Get tx receipt to get contract address tx_receipt = self.w3.eth.getTransactionReceipt(tx_hash) self.contract_address = tx_receipt['contractAddress'] # Contract instance in concise mode abi = contract_interface['abi'] self.contract_instance = self.w3.eth.contract(address=self.contract_address, abi=abi, ContractFactoryClass=ConciseContract) def test_return_true_with_gas(self): # Fails with HexBytes('0xd302f7841b5d7c1b6dcff6fca0cd039666dbd0cba6e8827e72edb4d06bbab38f') != True self.assertEqual(True, self.contract_instance.return_true(transact={"from": self.w3.eth.accounts[0]})) def test_return_true_no_gas(self): # passes self.assertEqual(True, self.contract_instance.return_true()) def test_return_address(self): # fails with AssertionError: '0x1111111111111111111111111111111111111111' != '0x0111111111111111111111111111111111111111' self.assertEqual(self.DEFAULT_PROPOSAL_ADDRESS, self.contract_instance.return_address())
I have three methods performing tests on the functions in the contract. In one of them, a non-True
value is returned and instead HexBytes
are returned. In another, the contract functions returns an address constant but python sees a different value from what’s expected. In yet another case I call the return_true
contract function without gas and the True
constant is seen by python.
- Why does calling
return_true
withtransact={"from": self.w3.eth.accounts[0]}
cause the return value of the function to beHexBytes(...)
? - Why does the address returned by
return_address
differ from what I expect?
I think I have some sort of fundamental misunderstanding of how gas affects function calls.
-
The returned value is the transaction hash on the blockchain. When transacting (i.e., when using “transact” rather than “call”) the blockchain gets modified, and the library you are using returns the transaction hash. During that process you must have paid ether in order to be able to modify the blockchain. However, operating in read-only mode costs no ether at all, so there is no need to specify gas.
-
Discounting the “0x” at the beginning, ethereum addresses have a length of 40, but in your test you are using a 39-character-long address, so there is a missing a “1” there. Meaning, tests are correct, you have an error in your input.
Offtopic, both return_true
and return_address
should be marked as view
in Solidity, since they are not actually modifying the state. I’m pretty sure you get a warning in remix. Once you do that, there is no need to access both methods using “transact” and paying ether, and you can do it using “call” for free.
EDIT
Forgot to mention: in case you need to access the transaction hash after using transact
you can do so calling the .hex()
method on the returned HexBytes
object. That’ll give you the transaction hash as a string, which is usually way more useful than as a HexBytes
.
I hope it helps!