diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index a6dd6a8f..b84f0f53 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -86,3 +86,46 @@ jobs: - name: Test JS client run: pnpm test:unit + + test-client-py: + name: test-client-py + runs-on: ubuntu-latest + services: + sapphire-localnet-ci: + image: ghcr.io/oasisprotocol/sapphire-localnet:latest + ports: + - 8545:8545 + options: >- + --rm + --health-cmd="test -f /CONTAINER_READY" + --health-start-period=90s + defaults: + run: + working-directory: ./clients/py + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Build sapphirepy .whl file + run: | + pip3 install --user -r requirements.txt + pip3 install --user -r requirements.dev.txt + make + + - name: Install sapphirepy .whl file + run: | + pip3 install --user -r requirements.txt + pip3 install --user dist/sapphire.py-0.3.0-py3-none-any.whl + + - name: Python client tests + working-directory: clients/py + run: | + python3 -mpip install --user -r requirements.txt + python3 -mpip install --user -r requirements.dev.txt + python3 -munittest discover + pytest sapphirepy/tests/ diff --git a/.github/workflows/contracts-test.yaml b/.github/workflows/contracts-test.yaml index 8676c288..85ebc790 100644 --- a/.github/workflows/contracts-test.yaml +++ b/.github/workflows/contracts-test.yaml @@ -28,11 +28,6 @@ jobs: uses: actions/checkout@v4 - name: Install jq run: sudo apt install -y jq - - name: Python client tests - working-directory: clients/py - run: | - python3 -mpip install --user -r requirements.txt - python3 -munittest discover - name: Install Node.js uses: actions/setup-node@v4 with: diff --git a/clients/py/requirements.dev.txt b/clients/py/requirements.dev.txt index dbe20b29..d09bd296 100644 --- a/clients/py/requirements.dev.txt +++ b/clients/py/requirements.dev.txt @@ -2,3 +2,5 @@ py-solc-x mypy pylint ruff +setuptools +pytest diff --git a/clients/py/sapphirepy/envelope.py b/clients/py/sapphirepy/envelope.py index 229940b7..5ef90bf1 100644 --- a/clients/py/sapphirepy/envelope.py +++ b/clients/py/sapphirepy/envelope.py @@ -97,6 +97,19 @@ def encrypt(self, plaintext: bytes): } return cbor2.dumps(envelope, canonical=True) + def make_envelope(self, plaintext: bytes): + ciphertext, nonce = self._encrypt_calldata(plaintext) + envelope = { + 'body': { + 'pk': self.ephemeral_pubkey, + 'data': ciphertext, + 'nonce': nonce, + 'epoch': self.epoch + }, + 'format': FORMAT_ENCRYPTED_X25519DEOXYSII + } + return envelope + def _decode_inner(self, plaintext:bytes) -> bytes: inner_result = cast(ResultInner, cbor2.loads(plaintext)) if inner_result.get('ok', None) is not None: diff --git a/clients/py/sapphirepy/sapphire.py b/clients/py/sapphirepy/sapphire.py index eaa0a5e6..b1c80bf1 100644 --- a/clients/py/sapphirepy/sapphire.py +++ b/clients/py/sapphirepy/sapphire.py @@ -1,9 +1,18 @@ -from typing import Any, Callable, cast, TypedDict, Optional from binascii import unhexlify, hexlify +from typing import ( + Any, + Callable, + cast, + Optional, + TypedDict, +) +import cbor2 from web3 import Web3 -from web3.types import RPCEndpoint, RPCResponse, TxParams +from web3.types import RPCEndpoint, RPCResponse, TxParams, Middleware from eth_typing import HexStr +from eth_account import Account +from eth_account.signers.local import LocalAccount from .envelope import TransactionCipher @@ -18,6 +27,13 @@ # Number of epochs to keep public keys for EPOCH_LIMIT = 5 +# Default gas price +DEFAULT_GAS_PRICE = 100_000_000_000 +# Default gas limit +DEFAULT_GAS_LIMIT = 30_000_000 +# Default block range +DEFAULT_BLOCK_RANGE = 15 + class CalldataPublicKey(TypedDict): epoch: int @@ -25,12 +41,14 @@ class CalldataPublicKey(TypedDict): signature: HexStr key: HexStr + class CalldataPublicKeyManager: _keys: list[CalldataPublicKey] + def __init__(self): self._keys = [] - def _trim_and_sort(self, newest_epoch:int): + def _trim_and_sort(self, newest_epoch: int): self._keys = sorted([v for v in self._keys if v['epoch'] >= newest_epoch - EPOCH_LIMIT], key=lambda o: o['epoch'])[-EPOCH_LIMIT:] @@ -41,7 +59,7 @@ def newest(self): return self._keys[-1] return None - def add(self, pk:CalldataPublicKey): + def add(self, pk: CalldataPublicKey): if self._keys: if self.newest['epoch'] < pk['epoch']: self._keys.append(pk) @@ -49,7 +67,8 @@ def add(self, pk:CalldataPublicKey): else: self._keys.append(pk) -def _should_intercept(method: RPCEndpoint, params:tuple[TxParams]): + +def _should_intercept(method: RPCEndpoint, params: tuple[TxParams]): if not ENCRYPT_DEPLOYS: if method in ('eth_sendTransaction', 'eth_estimateGas'): # When 'to' flag is missing, we assume it's a deployment @@ -57,7 +76,12 @@ def _should_intercept(method: RPCEndpoint, params:tuple[TxParams]): return False return method in ('eth_estimateGas', 'eth_sendTransaction', 'eth_call') -def _encrypt_tx_params(pk:CalldataPublicKey, params:tuple[TxParams]): + +def _encrypt_tx_params(pk: CalldataPublicKey, + params: tuple[TxParams], + method, + web3: Web3, + account: Optional[LocalAccount]=None) -> TransactionCipher: c = TransactionCipher(peer_pubkey=pk['key'], peer_epoch=pk['epoch']) data = params[0]['data'] if isinstance(data, bytes): @@ -69,73 +93,179 @@ def _encrypt_tx_params(pk:CalldataPublicKey, params:tuple[TxParams]): else: raise TypeError("Invalid 'data' type", type(data)) encrypted_data = c.encrypt(data_bytes) - params[0]['data'] = HexStr('0x' + hexlify(encrypted_data).decode('ascii')) + + if method == 'eth_call' and params[0]['from'] and account: + data_pack = _new_signed_call_data_pack(c.make_envelope(data_bytes), + data_bytes, + params, + web3, + account) + params[0]['data'] = cbor2.dumps(data_pack, canonical=True) + else: + params[0]['data'] = HexStr('0x' + hexlify(encrypted_data).decode('ascii')) return c -def sapphire_middleware( - make_request: Callable[[RPCEndpoint, Any], Any], _: "Web3" -) -> Callable[[RPCEndpoint, Any], RPCResponse]: - """ - Transparently encrypt the calldata for: - - eth_estimateGas - - eth_sendTransaction - - eth_call +def _new_signed_call_data_pack(encrypted_data: dict, + data_bytes: bytes, + params: tuple[TxParams], + web3: Web3, + account: LocalAccount) -> dict: + # Update params with default values, these get used outside the scope of this function + params[0]['gas'] = params[0].get('gas', DEFAULT_GAS_LIMIT) + params[0]['gasPrice'] = params[0].get('gasPrice', web3.to_wei(DEFAULT_GAS_PRICE, 'wei')) + + domain_data = { + "name": "oasis-runtime-sdk/evm: signed query", + "version": "1.0.0", + "chainId": web3.eth.chain_id, + # "verifyingContract": "", + # "salt": "", + } + msg_types = { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + ], + "Call": [ + {"name": "from", "type": "address"}, + {"name": "to", "type": "address"}, + {"name": "gasLimit", "type": "uint64"}, + {"name": "gasPrice", "type": "uint256"}, + {"name": "value", "type": "uint256"}, + {"name": "data", "type": "bytes"}, + {"name": "leash", "type": "Leash"}, + ], + "Leash": [ + {"name": "nonce", "type": "uint64"}, + {"name": "blockNumber", "type": "uint64"}, + {"name": "blockHash", "type": "bytes32"}, + {"name": "blockRange", "type": "uint64"}, + ], + } + nonce = web3.eth.get_transaction_count(web3.to_checksum_address(params[0]['from'])) + block_number = web3.eth.block_number + block_hash = web3.eth.get_block(block_number-1)['hash'].hex() + msg_data = { + "from": params[0].get('from'), + "to": params[0].get('to'), + "value": params[0].get('value', 0), + "gasLimit": params[0].get('gas', DEFAULT_GAS_LIMIT), + "gasPrice": params[0].get('gasPrice', DEFAULT_GAS_PRICE), + "data": data_bytes, + "leash": + { + "nonce": nonce, + "blockNumber": block_number - 1, + "blockHash": unhexlify(block_hash[2:]), + "blockRange": DEFAULT_BLOCK_RANGE, + } + } + + full_message = { + "types": msg_types, + "primaryType": "Call", + "domain": domain_data, + "message": msg_data, + } + + # sign the message with the private key: + signed_msg = Account().sign_typed_data(account.key, full_message=full_message) - The calldata public key, which used to derive a shared secret with an - ephemeral key, is retrieved upon the first request. This key is rotated by - Sapphire every epoch, and only transactions encrypted with keys from the - last 5 epochs are considered valid. + leash = { + "nonce": nonce, + "block_number": block_number - 1, + "block_hash": unhexlify(block_hash[2:]), + "block_range": DEFAULT_BLOCK_RANGE, + } - Deployment transactions will not be encrypted, unless the global - ENCRYPT_DEPLOYS flag is set. Encrypting deployments will prevent contracts - from being verified. + data_pack= { + 'data': encrypted_data, + 'leash': leash, + 'signature': bytes(signed_msg['signature']), + } + return data_pack - Pre-signed transactions can't be encrypted if submitted via this instance. + +def construct_sapphire_middleware( + account: Optional[LocalAccount] = None +) -> Middleware: + """ + Construct a Sapphire middleware for Web3.py. + :param account: Used to encrypt signed queries. + :return: A Sapphire middleware function. """ - manager = CalldataPublicKeyManager() - def middleware(method: RPCEndpoint, params: Any) -> RPCResponse: - if _should_intercept(method, params): - do_fetch = True - pk = manager.newest - while do_fetch: - if not pk: - # If no calldata public key exists, fetch one - cdpk = cast(RPCResponse, make_request(RPCEndpoint('oasis_callDataPublicKey'), [])) - pk = cast(Optional[CalldataPublicKey], cdpk.get('result', None)) - if pk: - manager.add(pk) - if not pk: - raise RuntimeError('Could not retrieve callDataPublicKey!') - do_fetch = False - - c = _encrypt_tx_params(pk, params) - - # We may encounter three errors here: - # 'core: invalid call format: epoch too far in the past' - # 'core: invalid call format: Tag verification failed' - # 'core: invalid call format: epoch in the future' - # We can only do something meaningful with the first! - result = cast(RPCResponse, make_request(method, params)) - if result.get('error', None) is not None: - error = result['error'] - if not isinstance(error, str) and error['code'] == -32000: - if error['message'] == 'core: invalid call format: epoch too far in the past': - # force the re-fetch, and encrypt with new key - do_fetch = True - pk = None - continue - - # Only eth_call is decrypted - if method == 'eth_call' and result.get('result', '0x') != '0x': - decrypted = c.decrypt(unhexlify(result['result'][2:])) - result['result'] = HexStr('0x' + hexlify(decrypted).decode('ascii')) - - return result - return make_request(method, params) - return middleware - -def wrap(w3: Web3): + + def sapphire_middleware( + make_request: Callable[[RPCEndpoint, Any], Any], w3: "Web3" + ) -> Callable[[RPCEndpoint, Any], RPCResponse]: + """ + Transparently encrypt the calldata for: + + - eth_estimateGas + - eth_sendTransaction + - eth_call + + The calldata public key, which used to derive a shared secret with an + ephemeral key, is retrieved upon the first request. This key is rotated by + Sapphire every epoch, and only transactions encrypted with keys from the + last 5 epochs are considered valid. + + Deployment transactions will not be encrypted, unless the global + ENCRYPT_DEPLOYS flag is set. Encrypting deployments will prevent contracts + from being verified. + + Pre-signed transactions can't be encrypted if submitted via this instance. + """ + manager = CalldataPublicKeyManager() + + def middleware(method: RPCEndpoint, params: Any) -> RPCResponse: + if _should_intercept(method, params): + do_fetch = True + pk = manager.newest + while do_fetch: + if not pk: + # If no calldata public key exists, fetch one + cdpk = cast(RPCResponse, make_request(RPCEndpoint('oasis_callDataPublicKey'), [])) + pk = cast(Optional[CalldataPublicKey], cdpk.get('result', None)) + if pk: + manager.add(pk) + if not pk: + raise RuntimeError('Could not retrieve callDataPublicKey!') + do_fetch = False + + c = _encrypt_tx_params(pk, params, method, w3, account) + + # We may encounter three errors here: + # 'core: invalid call format: epoch too far in the past' + # 'core: invalid call format: Tag verification failed' + # 'core: invalid call format: epoch in the future' + # We can only do something meaningful with the first! + result = cast(RPCResponse, make_request(method, params)) + if result.get('error', None) is not None: + error = result['error'] + if not isinstance(error, str) and error['code'] == -32000: + if error['message'] == 'core: invalid call format: epoch too far in the past': + # force the re-fetch, and encrypt with new key + do_fetch = True + pk = None + continue + + # Only eth_call is decrypted + if method == 'eth_call' and result.get('result', '0x') != '0x': + decrypted = c.decrypt(unhexlify(result['result'][2:])) + result['result'] = HexStr('0x' + hexlify(decrypted).decode('ascii')) + + return result + return make_request(method, params) + + return middleware + + return sapphire_middleware + + +def wrap(w3: Web3, account: Optional[LocalAccount] = None): """ Adds the Sapphire transaction encryption middleware to a Web3.py provider. @@ -143,5 +273,5 @@ def wrap(w3: Web3): added, otherwise pre-signed transactions will not be encrypted. """ if 'sapphire' not in w3.middleware_onion: - w3.middleware_onion.add(sapphire_middleware, "sapphire") + w3.middleware_onion.add(construct_sapphire_middleware(account), "sapphire") return w3 diff --git a/clients/py/sapphirepy/tests/test_e2e.py b/clients/py/sapphirepy/tests/test_e2e.py index 7946571e..8fed2066 100644 --- a/clients/py/sapphirepy/tests/test_e2e.py +++ b/clients/py/sapphirepy/tests/test_e2e.py @@ -20,9 +20,12 @@ def compiled_test_contract(): if not os.path.exists(GREETER_ABI): # pylint: disable=import-outside-toplevel - from solcx import compile_source # type: ignore + import solcx # type: ignore + with open(GREETER_SOL, 'r', encoding='utf-8') as handle: - compiled_sol = compile_source( + solcx.install_solc(version='0.8.9') + solcx.set_solc_version('0.8.9') + compiled_sol = solcx.compile_source( handle.read(), output_values=['abi', 'bin'] ) @@ -49,24 +52,28 @@ class TestEndToEnd(unittest.TestCase): def setUp(self): account: LocalAccount = Account.from_key("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") # pylint: disable=no-value-for-parameter + w3 = Web3(Web3.HTTPProvider('http://localhost:8545')) + # w3 = Web3(Web3.HTTPProvider('https://testnet.sapphire.oasis.io')) w3.middleware_onion.add(construct_sign_and_send_raw_middleware(account)) - self.w3 = w3 = sapphire.wrap(w3) + self.w3_no_signer = Web3(Web3.HTTPProvider('http://localhost:8545')) + self.w3 = w3 = sapphire.wrap(w3, account) w3.eth.default_account = account.address iface = compiled_test_contract() - contract = w3.eth.contract(abi=iface['abi'], bytecode=iface['bin']) + contract = w3.eth.contract(abi=iface['abi'], bytecode=iface['bin'], ) tx_hash = contract.constructor().transact({'gasPrice': w3.eth.gas_price}) tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) self.greeter = w3.eth.contract(address=tx_receipt['contractAddress'], abi=iface['abi']) + self.greeter_no_signer = self.w3_no_signer.eth.contract(address=tx_receipt['contractAddress'], abi=iface['abi']) def test_viewcall_revert_custom(self): with self.assertRaises(ContractCustomError) as cm: self.greeter.functions.revertWithCustomError().call() - data = self.greeter.encodeABI( + data = self.greeter.encode_abi( fn_name="MyCustomError", args=["thisIsCustom"] ) self.assertEqual(cm.exception.args[0], data) @@ -79,6 +86,14 @@ def test_viewcall_revert_reason(self): def test_viewcall(self): self.assertEqual(self.greeter.functions.greet().call(), 'Hello') + def test_viewcall_only_owner(self): + self.assertEqual(self.greeter.functions.greetOnlyOwner().call(), 'Hello') + + def test_viewcall_only_owner_no_signer(self): + with self.assertRaises(ContractLogicError) as cm: + self.greeter_no_signer.functions.greetOnlyOwner().call() + self.assertEqual(cm.exception.message, 'execution reverted: Only owner can call this function') + def test_transaction(self): w3 = self.w3 greeter = self.greeter diff --git a/clients/py/sapphirepy/tests/testdata/Greeter.abi b/clients/py/sapphirepy/tests/testdata/Greeter.abi index b7c3954a..cfbd2b24 100644 --- a/clients/py/sapphirepy/tests/testdata/Greeter.abi +++ b/clients/py/sapphirepy/tests/testdata/Greeter.abi @@ -1 +1 @@ -[{"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, {"inputs": [{"internalType": "string", "name": "blah", "type": "string"}], "name": "MyCustomError", "type": "error"}, {"anonymous": false, "inputs": [{"indexed": false, "internalType": "string", "name": "g", "type": "string"}], "name": "Greeting", "type": "event"}, {"inputs": [], "name": "blah", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "greet", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "greeting", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "revertWithCustomError", "outputs": [], "stateMutability": "pure", "type": "function"}, {"inputs": [], "name": "revertWithReason", "outputs": [], "stateMutability": "pure", "type": "function"}, {"inputs": [{"internalType": "string", "name": "_greeting", "type": "string"}], "name": "setGreeting", "outputs": [], "stateMutability": "nonpayable", "type": "function"}] \ No newline at end of file +[{"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, {"inputs": [{"internalType": "string", "name": "blah", "type": "string"}], "name": "MyCustomError", "type": "error"}, {"anonymous": false, "inputs": [{"indexed": false, "internalType": "string", "name": "g", "type": "string"}], "name": "Greeting", "type": "event"}, {"inputs": [], "name": "blah", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "greet", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "greetOnlyOwner", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "greeting", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "owner", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "revertWithCustomError", "outputs": [], "stateMutability": "pure", "type": "function"}, {"inputs": [], "name": "revertWithReason", "outputs": [], "stateMutability": "pure", "type": "function"}, {"inputs": [{"internalType": "string", "name": "_greeting", "type": "string"}], "name": "setGreeting", "outputs": [], "stateMutability": "nonpayable", "type": "function"}] \ No newline at end of file diff --git a/clients/py/sapphirepy/tests/testdata/Greeter.bin b/clients/py/sapphirepy/tests/testdata/Greeter.bin index ddf25029..cf9ef436 100644 --- a/clients/py/sapphirepy/tests/testdata/Greeter.bin +++ b/clients/py/sapphirepy/tests/testdata/Greeter.bin @@ -1 +1 @@ -"608060405234801562000010575f80fd5b506040518060400160405280600581526020017f48656c6c6f0000000000000000000000000000000000000000000000000000008152505f9081620000569190620002c1565b50620003a5565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680620000d957607f821691505b602082108103620000ef57620000ee62000094565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f60088302620001537fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000116565b6200015f868362000116565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001a9620001a36200019d8462000177565b62000180565b62000177565b9050919050565b5f819050919050565b620001c48362000189565b620001dc620001d382620001b0565b84845462000122565b825550505050565b5f90565b620001f2620001e4565b620001ff818484620001b9565b505050565b5b8181101562000226576200021a5f82620001e8565b60018101905062000205565b5050565b601f82111562000275576200023f81620000f5565b6200024a8462000107565b810160208510156200025a578190505b62000272620002698562000107565b83018262000204565b50505b505050565b5f82821c905092915050565b5f620002975f19846008026200027a565b1980831691505092915050565b5f620002b1838362000286565b9150826002028217905092915050565b620002cc826200005d565b67ffffffffffffffff811115620002e857620002e762000067565b5b620002f48254620000c1565b620003018282856200022a565b5f60209050601f83116001811462000337575f841562000322578287015190505b6200032e8582620002a4565b8655506200039d565b601f1984166200034786620000f5565b5f5b82811015620003705784890151825560018201915060208501945060208101905062000349565b868310156200039057848901516200038c601f89168262000286565b8355505b6001600288020188555050505b505050505050565b61096380620003b35f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c806346fc4bb1146100645780635b2dd1001461006e578063a413686214610078578063cfae321714610094578063e39dcc21146100b2578063ef690cc0146100bc575b5f80fd5b61006c6100da565b005b610076610115565b005b610092600480360381019061008d9190610409565b610157565b005b61009c610169565b6040516100a991906104ca565b60405180910390f35b6100ba6101f8565b005b6100c4610231565b6040516100d191906104ca565b60405180910390f35b6040517fb98c011300000000000000000000000000000000000000000000000000000000815260040161010c90610534565b60405180910390fd5b5f610155576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161014c9061059c565b60405180910390fd5b565b805f908161016591906107bd565b5050565b60605f8054610177906105e7565b80601f01602080910402602001604051908101604052809291908181526020018280546101a3906105e7565b80156101ee5780601f106101c5576101008083540402835291602001916101ee565b820191905f5260205f20905b8154815290600101906020018083116101d157829003601f168201915b5050505050905090565b7fa5263230ff6fc3abbbad333e24faf0f402b4e050b83a1d30ad4051f4e5d0f7275f604051610227919061090d565b60405180910390a1565b5f805461023d906105e7565b80601f0160208091040260200160405190810160405280929190818152602001828054610269906105e7565b80156102b45780601f1061028b576101008083540402835291602001916102b4565b820191905f5260205f20905b81548152906001019060200180831161029757829003601f168201915b505050505081565b5f604051905090565b5f80fd5b5f80fd5b5f80fd5b5f80fd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b61031b826102d5565b810181811067ffffffffffffffff8211171561033a576103396102e5565b5b80604052505050565b5f61034c6102bc565b90506103588282610312565b919050565b5f67ffffffffffffffff821115610377576103766102e5565b5b610380826102d5565b9050602081019050919050565b828183375f83830152505050565b5f6103ad6103a88461035d565b610343565b9050828152602081018484840111156103c9576103c86102d1565b5b6103d484828561038d565b509392505050565b5f82601f8301126103f0576103ef6102cd565b5b813561040084826020860161039b565b91505092915050565b5f6020828403121561041e5761041d6102c5565b5b5f82013567ffffffffffffffff81111561043b5761043a6102c9565b5b610447848285016103dc565b91505092915050565b5f81519050919050565b5f82825260208201905092915050565b5f5b8381101561048757808201518184015260208101905061046c565b5f8484015250505050565b5f61049c82610450565b6104a6818561045a565b93506104b681856020860161046a565b6104bf816102d5565b840191505092915050565b5f6020820190508181035f8301526104e28184610492565b905092915050565b7f746869734973437573746f6d00000000000000000000000000000000000000005f82015250565b5f61051e600c8361045a565b9150610529826104ea565b602082019050919050565b5f6020820190508181035f83015261054b81610512565b9050919050565b7f726561736f6e476f6573486572650000000000000000000000000000000000005f82015250565b5f610586600e8361045a565b915061059182610552565b602082019050919050565b5f6020820190508181035f8301526105b38161057a565b9050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806105fe57607f821691505b602082108103610611576106106105ba565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026106737fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82610638565b61067d8683610638565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f6106c16106bc6106b784610695565b61069e565b610695565b9050919050565b5f819050919050565b6106da836106a7565b6106ee6106e6826106c8565b848454610644565b825550505050565b5f90565b6107026106f6565b61070d8184846106d1565b505050565b5b81811015610730576107255f826106fa565b600181019050610713565b5050565b601f8211156107755761074681610617565b61074f84610629565b8101602085101561075e578190505b61077261076a85610629565b830182610712565b50505b505050565b5f82821c905092915050565b5f6107955f198460080261077a565b1980831691505092915050565b5f6107ad8383610786565b9150826002028217905092915050565b6107c682610450565b67ffffffffffffffff8111156107df576107de6102e5565b5b6107e982546105e7565b6107f4828285610734565b5f60209050601f831160018114610825575f8415610813578287015190505b61081d85826107a2565b865550610884565b601f19841661083386610617565b5f5b8281101561085a57848901518255600182019150602085019450602081019050610835565b868310156108775784890151610873601f891682610786565b8355505b6001600288020188555050505b505050505050565b5f8154610898816105e7565b6108a2818661045a565b9450600182165f81146108bc57600181146108d257610904565b60ff198316865281151560200286019350610904565b6108db85610617565b5f5b838110156108fc578154818901526001820191506020810190506108dd565b808801955050505b50505092915050565b5f6020820190508181035f830152610925818461088c565b90509291505056fea26469706673582212200cda85a3ddcbab6550677981bf967b48ec06f169c6d09f6149144e9eb0bb48bf64736f6c63430008160033" \ No newline at end of file +"608060405234801561001057600080fd5b506040518060400160405280600581526020017f48656c6c6f0000000000000000000000000000000000000000000000000000008152506000908051906020019061005c9291906100a3565b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101a7565b8280546100af90610175565b90600052602060002090601f0160209004810192826100d15760008555610118565b82601f106100ea57805160ff1916838001178555610118565b82800160010185558215610118579182015b828111156101175782518255916020019190600101906100fc565b5b5090506101259190610129565b5090565b5b8082111561014257600081600090555060010161012a565b5090565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061018d57607f821691505b602082108114156101a1576101a0610146565b5b50919050565b610a7d806101b66000396000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c8063a41368621161005b578063a4136862146100dd578063cfae3217146100f9578063e39dcc2114610117578063ef690cc01461012157610088565b806346fc4bb11461008d57806355f64074146100975780635b2dd100146100b55780638da5cb5b146100bf575b600080fd5b61009561013f565b005b61009f61017a565b6040516100ac91906105b5565b60405180910390f35b6100bd61029c565b005b6100c76102df565b6040516100d49190610618565b60405180910390f35b6100f760048036038101906100f2919061077c565b610305565b005b61010161031f565b60405161010e91906105b5565b60405180910390f35b61011f6103b1565b005b6101296103eb565b60405161013691906105b5565b60405180910390f35b6040517fb98c011300000000000000000000000000000000000000000000000000000000815260040161017190610811565b60405180910390fd5b6060600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461020c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610203906108a3565b60405180910390fd5b60008054610219906108f2565b80601f0160208091040260200160405190810160405280929190818152602001828054610245906108f2565b80156102925780601f1061026757610100808354040283529160200191610292565b820191906000526020600020905b81548152906001019060200180831161027557829003601f168201915b5050505050905090565b60006102dd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102d490610970565b60405180910390fd5b565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b806000908051906020019061031b929190610479565b5050565b60606000805461032e906108f2565b80601f016020809104026020016040519081016040528092919081815260200182805461035a906108f2565b80156103a75780601f1061037c576101008083540402835291602001916103a7565b820191906000526020600020905b81548152906001019060200180831161038a57829003601f168201915b5050505050905090565b7fa5263230ff6fc3abbbad333e24faf0f402b4e050b83a1d30ad4051f4e5d0f72760006040516103e19190610a25565b60405180910390a1565b600080546103f8906108f2565b80601f0160208091040260200160405190810160405280929190818152602001828054610424906108f2565b80156104715780601f1061044657610100808354040283529160200191610471565b820191906000526020600020905b81548152906001019060200180831161045457829003601f168201915b505050505081565b828054610485906108f2565b90600052602060002090601f0160209004810192826104a757600085556104ee565b82601f106104c057805160ff19168380011785556104ee565b828001600101855582156104ee579182015b828111156104ed5782518255916020019190600101906104d2565b5b5090506104fb91906104ff565b5090565b5b80821115610518576000816000905550600101610500565b5090565b600081519050919050565b600082825260208201905092915050565b60005b8381101561055657808201518184015260208101905061053b565b83811115610565576000848401525b50505050565b6000601f19601f8301169050919050565b60006105878261051c565b6105918185610527565b93506105a1818560208601610538565b6105aa8161056b565b840191505092915050565b600060208201905081810360008301526105cf818461057c565b905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610602826105d7565b9050919050565b610612816105f7565b82525050565b600060208201905061062d6000830184610609565b92915050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6106898261056b565b810181811067ffffffffffffffff821117156106a8576106a7610651565b5b80604052505050565b60006106bb610633565b90506106c78282610680565b919050565b600067ffffffffffffffff8211156106e7576106e6610651565b5b6106f08261056b565b9050602081019050919050565b82818337600083830152505050565b600061071f61071a846106cc565b6106b1565b90508281526020810184848401111561073b5761073a61064c565b5b6107468482856106fd565b509392505050565b600082601f83011261076357610762610647565b5b813561077384826020860161070c565b91505092915050565b6000602082840312156107925761079161063d565b5b600082013567ffffffffffffffff8111156107b0576107af610642565b5b6107bc8482850161074e565b91505092915050565b7f746869734973437573746f6d0000000000000000000000000000000000000000600082015250565b60006107fb600c83610527565b9150610806826107c5565b602082019050919050565b6000602082019050818103600083015261082a816107ee565b9050919050565b7f4f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f60008201527f6e00000000000000000000000000000000000000000000000000000000000000602082015250565b600061088d602183610527565b915061089882610831565b604082019050919050565b600060208201905081810360008301526108bc81610880565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061090a57607f821691505b6020821081141561091e5761091d6108c3565b5b50919050565b7f726561736f6e476f657348657265000000000000000000000000000000000000600082015250565b600061095a600e83610527565b915061096582610924565b602082019050919050565b600060208201905081810360008301526109898161094d565b9050919050565b60008190508160005260206000209050919050565b600081546109b2816108f2565b6109bc8186610527565b945060018216600081146109d757600181146109e957610a1c565b60ff1983168652602086019350610a1c565b6109f285610990565b60005b83811015610a14578154818901526001820191506020810190506109f5565b808801955050505b50505092915050565b60006020820190508181036000830152610a3f81846109a5565b90509291505056fea26469706673582212205e5a4ce57c30ee5012ef4f55f3e7c7d1e83602f3e823586ace8decb2703f7ebd64736f6c63430008090033" \ No newline at end of file diff --git a/clients/py/sapphirepy/tests/testdata/Greeter.sol b/clients/py/sapphirepy/tests/testdata/Greeter.sol index 17841318..8cd0619b 100644 --- a/clients/py/sapphirepy/tests/testdata/Greeter.sol +++ b/clients/py/sapphirepy/tests/testdata/Greeter.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; contract Greeter { string public greeting; + address public owner; constructor() { greeting = 'Hello'; + owner = msg.sender; } function setGreeting(string memory _greeting) public { @@ -17,6 +19,11 @@ contract Greeter { return greeting; } + function greetOnlyOwner() view public returns (string memory) { + require(msg.sender == owner, "Only owner can call this function"); + return greeting; + } + event Greeting(string g); function blah() external {