0
votes

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.

  1. Why does calling return_true with transact={"from": self.w3.eth.accounts[0]} cause the return value of the function to be HexBytes(...)?
  2. 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.

1
Transactions don't have return values. If you send a transaction, the result is a transaction hash. You can then wait for the transaction to be mined and see the result, but no return value will be available. If you instead call the method, the result is computed locally without sending a transaction to the rest of the network, and the return value of the function is sent back to you in the response.user94559
Ah, I see. Thank you!Paymahn Moghadasian

1 Answers

1
votes
  1. 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.

  2. 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!