Skip to content

Commit 1fd123f

Browse files
authored
Merge pull request #6 from oasisprotocol/ZigaMr/feat/bump-sapphirepy-version
Update for new sapphirepy version, add async/websocket tests
2 parents 409ae3a + 17bb2ae commit 1fd123f

File tree

7 files changed

+174
-85
lines changed

7 files changed

+174
-85
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ This is a skeleton for confidential Oasis dApps in Python.
44

55
## Prerequisites
66

7-
This project was tested on python 3.10, but should work with most
7+
This project was tested on python 3.12, but should work with most
88
python3 versions.
99
Use pyenv to handle multiple python installations.
1010

1111
## Installation
1212

1313
1. Initialize an environment using preferred environment manager
14-
(venv, pipx...) ```python3 -m venv my_env```.
15-
2. Install the ```oasis-sapphire-py``` client library and
16-
other dependencies from requirements.txt ```pip install -r requirements.txt```.
14+
(venv, pipx...) ```python3 -m venv my_env```.
15+
2. Install the [`oasis-sapphire-py`](https://pypi.org/project/oasis-sapphire-py/) client library and
16+
other dependencies from requirements.txt ```pip install -r requirements.txt```.
1717

1818
## Setup
1919

@@ -36,7 +36,7 @@ Open main.py which contains a simple starter example.
3636

3737
### Initialization
3838

39-
The ```ContractUtility``` class is used to compile and deploy the contracts,
39+
The `ContractUtility` class is used to compile and deploy the contracts,
4040
based on the network name (sapphire, sapphire-testnet, sapphire-localnet).
4141
The private key used to deploy the contract is fetched from the PRIVATE_KEY
4242
environment variable.
@@ -62,7 +62,7 @@ ContractUtility.setup_and_compile_contract("MessageBox")
6262
### Deploying the contract
6363

6464
```python
65-
contract_utility.deploy_contract("MessageBox")
65+
await contract_utility.deploy_contract("MessageBox)
6666
```
6767
Provide the contract name, in the starter example case
6868
we use the provided **MessageBox** without the .sol extension.

main.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,44 @@
11
#!/usr/bin/env python3
22

3+
import asyncio
34
from src.ContractUtility import ContractUtility
45
from src.MessageBox import set_message, get_message
56
import argparse
67

78

8-
def main():
9+
async def async_main():
910
"""
1011
Main method for the Python CLI tool.
1112
1213
:return: None
1314
"""
1415
parser = argparse.ArgumentParser(
15-
description="A Python CLI tool for compiling, deploying, and interacting with smart contracts."
16+
description="""A Python CLI tool for compiling,
17+
deploying, and interacting with smart contracts."""
1618
)
1719

1820
subparsers = parser.add_subparsers(dest="command", help="Subcommands")
1921

2022
# Subparser for compile
21-
compile_parser = subparsers.add_parser("compile", help="Compile the source code")
23+
compile_parser = subparsers.add_parser(
24+
"compile",
25+
help="Compile the source code"
26+
)
2227
compile_parser.add_argument(
23-
"--contract", help="Name of the contract to compile", default="MessageBox"
28+
"--contract",
29+
help="Name of the contract to compile",
30+
default="MessageBox"
2431
)
2532

2633
# Subparser for deploy
27-
deploy_parser = subparsers.add_parser("deploy", help="Deploy the smart contract")
34+
deploy_parser = subparsers.add_parser(
35+
"deploy",
36+
help="Deploy the smart contract"
37+
)
2838
deploy_parser.add_argument(
29-
"--contract", help="Name of the contract to deploy", default="MessageBox"
39+
"--contract",
40+
help="Name of the contract to deploy",
41+
default="MessageBox"
3042
)
3143
deploy_parser.add_argument(
3244
"--network",
@@ -66,24 +78,36 @@ def main():
6678
required=True,
6779
)
6880

69-
7081
arguments = parser.parse_args()
7182

7283
match arguments.command:
7384
case "compile":
74-
# Use class method which does not require an instance of ContractUtility.
75-
# This is to avoid setting up the Web3 instance which requires the PRIVATE_KEY.
85+
# Use class method which does not
86+
# require an instance of ContractUtility.
87+
# This is to avoid setting up the Web3 instance
88+
# which requires the PRIVATE_KEY.
7689
ContractUtility.setup_and_compile_contract(arguments.contract)
7790
case "deploy":
7891
contract_utility = ContractUtility(arguments.network)
79-
contract_utility.deploy_contract(arguments.contract)
92+
await contract_utility.deploy_contract(arguments.contract)
8093
case "setMessage":
81-
set_message(arguments.address, arguments.message, arguments.network)
94+
await set_message(
95+
arguments.address,
96+
arguments.message,
97+
arguments.network
98+
)
8299
case "message":
83-
get_message(arguments.address, arguments.network)
100+
await get_message(arguments.address, arguments.network)
84101
case _:
85102
parser.print_help()
86103

87104

105+
def main():
106+
"""
107+
Entry point that runs the async main function
108+
"""
109+
asyncio.run(async_main())
110+
111+
88112
if __name__ == "__main__":
89113
main()

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
flake8
22
py-solc-x
33
pytest
4-
oasis-sapphire-py
4+
pytest-asyncio
5+
oasis-sapphire-py==0.4.*
6+
web3==7.*

src/ContractUtility.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import os
22
from pathlib import Path
33
from solcx import compile_standard, install_solc
4-
from eth_account.signers.local import LocalAccount
5-
from eth_account import Account
64

7-
from src.utils import setup_web3_middleware, get_contract, process_json_file
5+
from src.utils import (
6+
setup_web3_middleware,
7+
get_contract,
8+
process_json_file,
9+
)
810

911

1012
class ContractUtility:
@@ -24,6 +26,7 @@ def __init__(self, network_name: str):
2426
def setup_and_compile_contract(
2527
cls, contract_name: str = "MessageBox", SOLIDITY_VERSION: str = "0.8.0"
2628
) -> str:
29+
# This remains synchronous as compilation doesn't need to be async.
2730
install_solc(SOLIDITY_VERSION)
2831
contract_dir = (Path(__file__).parent.parent / "contracts").resolve()
2932
contract_dir.mkdir(parents=True, exist_ok=True)
@@ -33,10 +36,15 @@ def setup_and_compile_contract(
3336
compiled_sol = compile_standard(
3437
{
3538
"language": "Solidity",
36-
"sources": {f"{contract_name}.sol": {"content": contract_source_code}},
39+
"sources": {f"{contract_name}.sol":
40+
{"content": contract_source_code}},
3741
"settings": {
3842
"outputSelection": {
39-
"*": {"*": ["abi", "metadata", "evm.bytecode", "evm.sourceMap"]}
43+
"*": {"*": ["abi",
44+
"metadata",
45+
"evm.bytecode",
46+
"evm.sourceMap"
47+
]}
4048
}
4149
},
4250
},
@@ -52,10 +60,13 @@ def setup_and_compile_contract(
5260
print(f"Compiled contract {contract_name} {output_path}")
5361
return compiled_sol
5462

55-
def deploy_contract(self, contract_name: str):
63+
async def deploy_contract(self, contract_name: str):
5664
abi, bytecode = get_contract(contract_name)
5765
contract = self.w3.eth.contract(abi=abi, bytecode=bytecode)
58-
tx_hash = contract.constructor().transact({"gasPrice": self.w3.eth.gas_price})
59-
tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
66+
gas_price = await self.w3.eth.gas_price
67+
tx_hash = await contract.constructor().transact({
68+
"gasPrice": gas_price
69+
})
70+
tx_receipt = await self.w3.eth.wait_for_transaction_receipt(tx_hash)
6071
print(f"Contract deployed at {tx_receipt.contractAddress}")
6172
return tx_receipt.contractAddress

src/MessageBox.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,47 @@
33
from src.utils import get_contract
44

55

6-
def set_message(
7-
address: str, message: str, network_name: Optional[str] = "sapphire-localnet"
6+
async def set_message(
7+
address: str,
8+
message: str,
9+
network_name: Optional[str] = "sapphire-localnet"
810
) -> None:
911
contract_utility = ContractUtility(network_name)
1012

1113
abi, bytecode = get_contract("MessageBox")
1214

13-
contract = contract_utility.w3.eth.contract(address=address, abi=abi)
15+
contract = contract_utility.w3.eth.contract(
16+
address=address,
17+
abi=abi
18+
)
1419

15-
# Set a message
16-
tx_hash = contract.functions.setMessage(message).transact(
17-
{"gasPrice": contract_utility.w3.eth.gas_price}
20+
gas_price = await contract_utility.w3.eth.gas_price
21+
tx_hash = await contract.functions.setMessage(message).transact(
22+
{"gasPrice": gas_price}
1823
)
19-
tx_receipt = contract_utility.w3.eth.wait_for_transaction_receipt(tx_hash)
20-
print(f"Message set. Transaction hash: {tx_receipt.transactionHash.hex()}")
21-
22-
23-
def get_message(address: str, network_name: Optional[str] = "sapphire-localnet") -> str:
24+
tx_receipt = await contract_utility.w3.eth.wait_for_transaction_receipt(
25+
tx_hash
26+
)
27+
print(f"""Message set.
28+
Transaction hash: {tx_receipt.transactionHash.hex()}"""
29+
)
30+
31+
32+
async def get_message(
33+
address: str,
34+
network_name: Optional[str] = "sapphire-localnet"
35+
) -> str:
2436
contract_utility = ContractUtility(network_name)
2537

2638
abi, bytecode = get_contract("MessageBox")
2739

28-
contract = contract_utility.w3.eth.contract(address=address, abi=abi)
40+
contract = contract_utility.w3.eth.contract(
41+
address=address,
42+
abi=abi
43+
)
2944
# Retrieve message from contract
30-
message = contract.functions.message().call()
31-
author = contract.functions.author().call()
45+
message = await contract.functions.message().call()
46+
author = await contract.functions.author().call()
3247

3348
print(f"Retrieved message: {message}")
3449
print(f"Author: {author}")

src/utils.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
1-
from web3 import Web3
2-
from web3.middleware import construct_sign_and_send_raw_middleware
1+
from web3 import Web3, AsyncWeb3
2+
from web3.middleware import SignAndSendRawMiddlewareBuilder
3+
34
from eth_account.signers.local import LocalAccount
45
from eth_account import Account
56
from sapphirepy import sapphire
67
import json
78
from pathlib import Path
9+
from typing import Union
810

911

10-
def setup_web3_middleware(network_name: str, PRIVATE_KEY: str) -> Web3:
12+
def setup_web3_middleware(
13+
network_name: str,
14+
PRIVATE_KEY: str,
15+
) -> Union[Web3, AsyncWeb3]:
1116
if not all(
1217
[
1318
PRIVATE_KEY,
1419
]
1520
):
16-
raise Warning("Missing required environment variables. Please set PRIVATE_KEY.")
21+
raise Warning("""Missing required environment variables.
22+
Please set PRIVATE_KEY.""")
1723

1824
account: LocalAccount = Account.from_key(PRIVATE_KEY)
19-
20-
w3 = Web3(Web3.HTTPProvider(sapphire.NETWORKS[network_name]))
21-
w3.middleware_onion.add(construct_sign_and_send_raw_middleware(account))
25+
w3 = AsyncWeb3(
26+
AsyncWeb3.AsyncHTTPProvider(
27+
sapphire.NETWORKS[network_name]
28+
)
29+
)
30+
31+
w3.middleware_onion.add(
32+
SignAndSendRawMiddlewareBuilder.build(account)
33+
)
2234
w3 = sapphire.wrap(w3, account)
2335
# w3.eth.set_gas_price_strategy(rpc_gas_price_strategy)
2436
w3.eth.default_account = account.address
@@ -44,5 +56,8 @@ def get_contract(contract_name: str):
4456
contract_data = compiled_contract["contracts"][f"{contract_name}.sol"][
4557
contract_name
4658
]
47-
abi, bytecode = contract_data["abi"], contract_data["evm"]["bytecode"]["object"]
59+
abi, bytecode = (
60+
contract_data["abi"],
61+
contract_data["evm"]["bytecode"]["object"]
62+
)
4863
return abi, bytecode

0 commit comments

Comments
 (0)