diff --git a/clients/py/sapphirepy/envelope.py b/clients/py/sapphirepy/envelope.py index 229940b7..b78ddd3a 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 encrypt_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 591f7d19..27f4babe 100644 --- a/clients/py/sapphirepy/sapphire.py +++ b/clients/py/sapphirepy/sapphire.py @@ -75,6 +75,7 @@ def _should_intercept(method: RPCEndpoint, params: tuple[TxParams]): def _encrypt_tx_params(pk: CalldataPublicKey, params: tuple[TxParams], + method, web3: Web3, account: LocalAccount) -> TransactionCipher: c = TransactionCipher(peer_pubkey=pk['key'], peer_epoch=pk['epoch']) @@ -84,26 +85,26 @@ def _encrypt_tx_params(pk: CalldataPublicKey, elif isinstance(data, str): if len(data) < 2 or data[:2] != '0x': raise ValueError('Data is not hex encoded!', data) - # Testing data_bytes = unhexlify(data[2:]) else: raise TypeError("Invalid 'data' type", type(data)) encrypted_data = c.encrypt(data_bytes) - if params[0]['from']: # and params[0]['from'] == account.address: - data_pack = _new_signed_call_data_pack(encrypted_data, + if params[0]['from'] and method == 'eth_call': + data_pack = _new_signed_call_data_pack(c.encrypt_envelope(data_bytes), data_bytes, params, web3, account) - params[0]['data'] = HexStr('0x' + hexlify(cbor2.dumps(data_pack)).decode('ascii')) - # params[0]['data'] = '0x' + params[0]['data'].hex() + data_pack['signature'] = bytes(data_pack['signature']) + params[0]['data'] = cbor2.dumps(data_pack, canonical=True) else: params[0]['data'] = HexStr('0x' + hexlify(encrypted_data).decode('ascii')) + return c -def _new_signed_call_data_pack(encrypted_data: bytes, +def _new_signed_call_data_pack(encrypted_data: dict, data_bytes: bytes, params: tuple[TxParams], web3: Web3, @@ -136,14 +137,14 @@ def _new_signed_call_data_pack(encrypted_data: bytes, ], "Leash": [ {"name": "nonce", "type": "uint64"}, - {"name": "block_number", "type": "uint64"}, - {"name": "block_hash", "type": "bytes32"}, - {"name": "block_range", "type": "uint64"}, + {"name": "blockNumber", "type": "uint64"}, + {"name": "blockHash", "type": "bytes32"}, + {"name": "blockRange", "type": "uint64"}, ], } nonce = web3.eth.get_transaction_count(params[0]['from']) block_number = web3.eth.block_number - block_hash = web3.eth.get_block(block_number)['hash'].hex() + block_hash = web3.eth.get_block(block_number-1)['hash'].hex() msg_data = { "from": params[0].get('from'), "to": params[0].get('to'), @@ -154,37 +155,12 @@ def _new_signed_call_data_pack(encrypted_data: bytes, "leash": { "nonce": nonce, - "block_number": block_number - 1, - "block_hash": unhexlify(block_hash[2:]), - "block_range": DEFAULT_BLOCK_RANGE, + "blockNumber": block_number - 1, + "blockHash": unhexlify(block_hash[2:]), + "blockRange": DEFAULT_BLOCK_RANGE, } } - # Testing - domain_data_test = { - "name": "oasis-runtime-sdk/evm: signed query", - "version": "1.0.0", - "chainId": 0x5aff, - # "verifyingContract": "", - # "salt": "", - } - - msg_data_test = { - "from": '0xDce075E1C39b1ae0b75D554558b6451A226ffe00', - # "to": params[0].get('to', '0x'), - "to": '0x595cce2312b7dfb068eb7dbb8c2b0b593b5c8883', - "value": params[0].get('value', 0), - "gasLimit": params[0].get('gas', DEFAULT_GAS_LIMIT), - "gasPrice": params[0].get('gasPrice', DEFAULT_GAS_PRICE), - "data": unhexlify('e21f37ce'), - "leash": - { - "nonce": 0x12, - "blockNumber": 0x1234, - "blockHash": unhexlify('2ec361fee28d09a3ad2c4d5f7f95d409ce2b68c39b5d647edf0ea651e069e4a8'), - "blockRange": 15, - } - } full_message = { "types": msg_types, "primaryType": "Call", @@ -196,26 +172,15 @@ def _new_signed_call_data_pack(encrypted_data: bytes, signed_msg = Account.sign_typed_data(account.key, full_message=full_message) - # Testing - # signed_msg = Account.sign_typed_data('c07b151fbc1e7a11dff926111188f8d872f62eba0396da97c0a24adb75161750', full_message=full_message) - # signed_msg = Account.sign_typed_data('c07b151fbc1e7a11dff926111188f8d872f62eba0396da97c0a24adb75161750', - # domain_data, msg_types, msg_data) - - # leash = { - # "nonce": nonce, - # "block_number": block_number - 1, - # "block_hash": unhexlify(block_hash[2:]), - # "block_range": DEFAULT_BLOCK_RANGE, - # } - leash = msg_data['leash'] - - class RequestPack(TypedDict): - Data: bytes - Leash: dict - Signature: HexStr - - data_pack: RequestPack = { - 'data': cbor2.loads(encrypted_data), + leash = { + "nonce": nonce, + "block_number": block_number - 1, + "block_hash": unhexlify(block_hash[2:]), + "block_range": DEFAULT_BLOCK_RANGE, + } + + data_pack= { + 'data': encrypted_data, 'leash': leash, 'signature': signed_msg['signature'], } @@ -269,7 +234,7 @@ def middleware(method: RPCEndpoint, params: Any) -> RPCResponse: raise RuntimeError('Could not retrieve callDataPublicKey!') do_fetch = False - c = _encrypt_tx_params(pk, params, w3, account) + 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' diff --git a/clients/py/sapphirepy/tests/test_e2e.py b/clients/py/sapphirepy/tests/test_e2e.py index 3738dc0b..fe27b43b 100644 --- a/clients/py/sapphirepy/tests/test_e2e.py +++ b/clients/py/sapphirepy/tests/test_e2e.py @@ -23,8 +23,8 @@ def compiled_test_contract(): import solcx # type: ignore with open(GREETER_SOL, 'r', encoding='utf-8') as handle: - - solcx.set_solc_version(solcx.install_solc()) + 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'] @@ -51,9 +51,10 @@ class TestEndToEnd(unittest.TestCase): def setUp(self): account: LocalAccount = Account.from_key("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") # pylint: disable=no-value-for-parameter - # account2: LocalAccount = Account.from_key("0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") # 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, account) @@ -61,24 +62,24 @@ def setUp(self): 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']) - # def test_viewcall_revert_custom(self): - # with self.assertRaises(ContractCustomError) as cm: - # self.greeter.functions.revertWithCustomError().call() - # data = self.greeter.encodeABI( - # fn_name="MyCustomError", args=["thisIsCustom"] - # ) - # self.assertEqual(cm.exception.args[0], data) - # - # def test_viewcall_revert_reason(self): - # with self.assertRaises(ContractLogicError) as cm: - # self.greeter.functions.revertWithReason().call() - # self.assertEqual(cm.exception.message, 'execution reverted: reasonGoesHere') + def test_viewcall_revert_custom(self): + with self.assertRaises(ContractCustomError) as cm: + self.greeter.functions.revertWithCustomError().call() + data = self.greeter.encodeABI( + fn_name="MyCustomError", args=["thisIsCustom"] + ) + self.assertEqual(cm.exception.args[0], data) + + def test_viewcall_revert_reason(self): + with self.assertRaises(ContractLogicError) as cm: + self.greeter.functions.revertWithReason().call() + self.assertEqual(cm.exception.message, 'execution reverted: reasonGoesHere') def test_viewcall(self): self.assertEqual(self.greeter.functions.greet().call(), 'Hello') @@ -86,14 +87,14 @@ def test_viewcall(self): def test_viewcall_only_owner(self): self.assertEqual(self.greeter.functions.greetOnlyOwner().call(), 'Hello') - # def test_transaction(self): - # w3 = self.w3 - # greeter = self.greeter - # - # x = self.greeter.functions.blah().transact({'gasPrice': w3.eth.gas_price}) - # y = w3.eth.wait_for_transaction_receipt(x) - # z = greeter.events.Greeting().process_receipt(y) - # self.assertEqual(z[0].args['g'], 'Hello') + def test_transaction(self): + w3 = self.w3 + greeter = self.greeter + + x = self.greeter.functions.blah().transact({'gasPrice': w3.eth.gas_price}) + y = w3.eth.wait_for_transaction_receipt(x) + z = greeter.events.Greeting().process_receipt(y) + self.assertEqual(z[0].args['g'], 'Hello') if __name__ == '__main__': unittest.main()