diff --git a/tools/web3py-example/.env.example b/tools/web3py-example/.env.example new file mode 100644 index 0000000000..a980be1bc6 --- /dev/null +++ b/tools/web3py-example/.env.example @@ -0,0 +1,2 @@ +OPERATOR_PRIVATE_KEY= +RELAY_ENDPOINT= diff --git a/tools/web3py-example/.gitignore b/tools/web3py-example/.gitignore new file mode 100644 index 0000000000..0c2ad0902b --- /dev/null +++ b/tools/web3py-example/.gitignore @@ -0,0 +1 @@ +.env diff --git a/tools/web3py-example/README.md b/tools/web3py-example/README.md new file mode 100644 index 0000000000..3a2921fa4c --- /dev/null +++ b/tools/web3py-example/README.md @@ -0,0 +1,46 @@ +# Web3py example +Example scripts for basic operations + +### How to start +1. **Set up a clean environment (with virtual env)** + +```bash +# Install pip if it is not available: +$ which pip || curl https://bootstrap.pypa.io/get-pip.py | python + +# Install virtualenv if it is not available: +$ which virtualenv || pip install --upgrade virtualenv + +# *If* the above command displays an error, you can try installing as root: +$ sudo pip install virtualenv + +# Create a virtual environment: +$ virtualenv -p python3 ~/.venv-py3 + +# Activate your new virtual environment: +$ source ~/.venv-py3/bin/activate + +# With virtualenv active, make sure you have the latest packaging tools +$ pip install --upgrade pip setuptools + +# Now we can install web3.py... +$ pip install --upgrade web3 + +# Install python-dotenv +$ pip install python-dotenv + +# Install py-solc-x +$ pip install py-solc-x +``` + +Remember that each new terminal session requires you to reactivate your virtualenv, like: +```bash +$ source ~/.venv-py3/bin/activate +``` + +2. **Create and complete `.env` file from `.env.example`** + +3. **Run script** +```bash +python scripts/test.py +``` diff --git a/tools/web3py-example/contract/Greeter.sol b/tools/web3py-example/contract/Greeter.sol new file mode 100644 index 0000000000..235f168134 --- /dev/null +++ b/tools/web3py-example/contract/Greeter.sol @@ -0,0 +1,18 @@ +//SPDX-License-Identifier: Apache-2.0 +pragma solidity >0.5.0; + +contract Greeter { + string public greeting; + + constructor() public { + greeting = 'Hello'; + } + + function setGreeting(string memory _greeting) public { + greeting = _greeting; + } + + function greet() view public returns (string memory) { + return greeting; + } +} diff --git a/tools/web3py-example/scripts/test.py b/tools/web3py-example/scripts/test.py new file mode 100644 index 0000000000..93feccaa9f --- /dev/null +++ b/tools/web3py-example/scripts/test.py @@ -0,0 +1,171 @@ +# +# Hedera JSON RPC Relay +# +# Copyright (C) 2022-2024 Hedera Hashgraph, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import unittest +from dotenv import load_dotenv +from web3 import Web3 +from solcx import install_solc, compile_files + +def setup_environment(): + """ + Returns: + - w3: Initialized Web3 instance + - acc: Web3 account object + """ + # install solc + install_solc(version='0.8.24') + + # load values from our .env file + load_dotenv() + OPERATOR_PRIVATE_KEY = os.getenv('OPERATOR_PRIVATE_KEY') + RELAY_ENDPOINT = os.getenv('RELAY_ENDPOINT') + + # connect to chain + w3 = Web3(Web3.HTTPProvider(RELAY_ENDPOINT)) + + # get account from pk + acc = w3.eth.account.from_key(OPERATOR_PRIVATE_KEY) + + return w3, acc + + +def get_balance(w3, acc): + """ + Args: + - w3: Initialized Web3 instance + - acc: Web3 account object + + Returns: + - Account balance in wei + """ + balance = w3.eth.get_balance(acc.address) + return balance + + +def deploy_contract(w3, acc): + """ + Args: + - w3: Initialized Web3 instance + - acc: Web3 account object + + Returns: + - tuple: (Deployed contract instance, Contract address) + """ + # compile our Greeter contract + compiled_sol = compile_files(['contract/Greeter.sol'], output_values=['abi', 'bin'], optimize=True) + + # retrieve the contract interface + contract_id, contract_interface = compiled_sol.popitem() + + bytecode = contract_interface['bin'] + abi = contract_interface['abi'] + + # create web3.py contract instance + Greeter = w3.eth.contract(abi=abi, bytecode=bytecode) + + # build transaction + unsent_tx_hash = Greeter.constructor().build_transaction({ + "from": acc.address, + "nonce": w3.eth.get_transaction_count(acc.address), + }) + + # sign transaction + signed_tx = w3.eth.account.sign_transaction(unsent_tx_hash, private_key=acc.key) + + # send transaction + tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) + tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + # create instance of deployed contract + greeter = w3.eth.contract( + address=tx_receipt.contractAddress, + abi=abi + ) + + return greeter, tx_receipt.contractAddress + + +def contract_view_call(greeter): + """ + Args: + - greeter: Deployed Greeter contract instance + + Returns: + - Current greeting message + """ + greeting = greeter.functions.greet().call() + return greeting + + +def contract_call(w3, acc, greeter): + """ + Args: + - w3: Initialized Web3 instance + - acc: Web3 account object + - greeter: Deployed Greeter contract instance + + Returns: + - Updated greeting message + """ + # build contract call transaction + unsent_call_tx_hash = greeter.functions.setGreeting('Hello2').build_transaction({ + "from": acc.address, + "nonce": w3.eth.get_transaction_count(acc.address), + }) + + # sign transaction + signed_call_tx = w3.eth.account.sign_transaction(unsent_call_tx_hash, private_key=acc.key) + + # send transaction + call_tx_hash = w3.eth.send_raw_transaction(signed_call_tx.rawTransaction) + w3.eth.wait_for_transaction_receipt(call_tx_hash) + + # Verify the greeting has been updated + new_greeting = greeter.functions.greet().call() + return new_greeting + + +class TestGreeterContract(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.w3, cls.acc = setup_environment() + cls.greeter, cls.contract_address = deploy_contract(cls.w3, cls.acc) + + def test_get_balance(self): + balance = get_balance(self.w3, self.acc) + self.assertIsInstance(balance, int, "Account balance is an integer") + + def test_deploy_contract(self): + self.assertTrue(self.contract_address.startswith('0x'), "Contract address starts with '0x'") + + def test_call_view(self): + greeting = contract_view_call(self.greeter) + self.assertEqual(greeting, 'Hello', "Initial greeting matches expected value") + + def test_contract_call(self): + new_greeting = contract_call(self.w3, self.acc, self.greeter) + self.assertEqual(new_greeting, 'Hello2', "Updated greeting matches expected value") + + final_greeting = contract_view_call(self.greeter) + self.assertEqual(final_greeting, 'Hello2', "Final greeting matches expected value after update") + + +if __name__ == "__main__": + unittest.main()