diff --git a/hmt_escrow/eth_bridge.py b/hmt_escrow/eth_bridge.py index 4d18527a..08309bd8 100644 --- a/hmt_escrow/eth_bridge.py +++ b/hmt_escrow/eth_bridge.py @@ -1,6 +1,7 @@ import logging import os import time +import unittest from solcx import compile_files from web3 import Web3, HTTPProvider, EthereumTesterProvider @@ -63,29 +64,6 @@ def handle_transaction(txn_func, *args, **kwargs) -> AttributeDict: signing, building, sending the transaction and returning a transaction receipt. - >>> credentials = { - ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", - ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" - ... } - >>> rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" - >>> job = Job(credentials=credentials, escrow_manifest=manifest) - >>> job.launch(rep_oracle_pub_key) - True - - >>> gas = 4712388 - >>> hmt_amount = int(job.amount * 10**18) - >>> hmtoken_contract = get_hmtoken() - >>> txn_func = hmtoken_contract.functions.transfer - >>> func_args = [job.job_contract.address, hmt_amount] - >>> txn_info = { - ... "gas_payer": job.gas_payer, - ... "gas_payer_priv": job.gas_payer_priv, - ... "gas": gas - ... } - >>> txn_receipt = handle_transaction(txn_func, *func_args, **txn_info) - >>> type(txn_receipt) - - Args: txn_func: the transaction function to be handled. @@ -340,9 +318,58 @@ def set_pub_key_at_addr(pub_key: str) -> Dict[str, Any]: return handle_transaction(txn_func, *func_args, **txn_info) -if __name__ == "__main__": - import doctest - from test_manifest import manifest - from job import Job +class EthBridgeTestCase(unittest.TestCase): + def setUp(self): + from job import Job + + self.credentials = { + "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", + "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5", + } + self.rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" + self.job = Job(credentials=self.credentials, escrow_manifest=manifest) + + def test_handle_transaction(self): + from web3.datastructures import AttributeDict as Web3AttributeDict + + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + gas = 4712388 + hmt_amount = int(self.job.amount * 10 ** 18) + hmtoken_contract = get_hmtoken() + txn_func = hmtoken_contract.functions.transfer + func_args = [self.job.job_contract.address, hmt_amount] + txn_info = { + "gas_payer": self.job.gas_payer, + "gas_payer_priv": self.job.gas_payer_priv, + "gas": gas, + } + txn_receipt = handle_transaction(txn_func, *func_args, **txn_info) + self.assertIs(type(txn_receipt), Web3AttributeDict) + + def test_get_escrow(self): + self.job.launch(self.rep_oracle_pub_key) + self.assertIsNotNone(get_escrow(self.job.job_contract.address)) + + def test_get_factory(self): + self.assertIsNotNone(get_factory(self.job.factory_contract.address)) + + def test_get_pub_key_from_address(self): + with self.assertRaises(ValueError): + get_pub_key_from_addr("badaddress") + os.environ["GAS_PAYER"] = self.credentials["gas_payer"] + os.environ["GAS_PAYER_PRIV"] = self.credentials["gas_payer_priv"] + set_pub_key_at_addr(self.rep_oracle_pub_key) + self.assertEqual( + get_pub_key_from_addr(os.environ["GAS_PAYER"]), self.rep_oracle_pub_key + ) - doctest.testmod(raise_on_error=True) + def test_set_pub_key_at_address(self): + os.environ["GAS_PAYER"] = self.credentials["gas_payer"] + os.environ["GAS_PAYER_PRIV"] = self.credentials["gas_payer_priv"] + self.assertIsNotNone( + set_pub_key_at_addr(self.rep_oracle_pub_key).transactionHash + ) + + +if __name__ == "__main__": + unittest.main(exit=False) diff --git a/hmt_escrow/job.py b/hmt_escrow/job.py index 4186a72b..18445572 100644 --- a/hmt_escrow/job.py +++ b/hmt_escrow/job.py @@ -2,6 +2,7 @@ import os import sys import logging +import unittest from decimal import Decimal from enum import Enum @@ -1249,7 +1250,6 @@ def _validate_credentials( bool: returns True if the calculated and the given address match. """ - addr_valid = False gas_payer_addr = credentials["gas_payer"] gas_payer_priv = credentials["gas_payer_priv"] @@ -1502,8 +1502,259 @@ def _raffle_txn( return txn_succeeded -if __name__ == "__main__": - import doctest +class JobTestCase(unittest.TestCase): + def setUp(self): + self.credentials = { + "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", + "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5", + } + self.rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" + self.job = Job(self.credentials, manifest) + + def test_status(self): + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertEqual(status(self.job.job_contract, self.job.gas_payer), Status(1)) + + def test_manifest_url(self): + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + self.assertEqual( + manifest_hash(self.job.job_contract, self.job.gas_payer), + self.job.manifest_hash, + ) + + def test_job_init(self): + + # Creating a new Job instance initializes the critical attributes correctly. + self.assertEqual(self.job.gas_payer, self.credentials["gas_payer"]) + self.assertEqual(self.job.gas_payer_priv, self.credentials["gas_payer_priv"]) + self.assertEqual(self.job.serialized_manifest["oracle_stake"], "0.05") + self.assertEqual(self.job.amount, Decimal("100.0")) + + # Initializing a new Job instance with a factory address succeeds. + factory_addr = deploy_factory(**(self.credentials)) + self.job = Job(self.credentials, manifest, factory_addr) + self.assertTrue(self.job.factory_contract.address, factory_addr) + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + self.assertTrue( + launcher(self.job.job_contract, self.credentials["gas_payer"]).lower(), + self.job.factory_contract.address.lower(), + ) + + # Initializing an existing Job instance with a factory and escrow address succeeds. + self.credentials[ + "rep_oracle_priv_key" + ] = b"28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" + escrow_addr = self.job.job_contract.address + factory_addr = self.job.factory_contract.address + manifest_url = self.job.manifest_url + new_job = Job( + credentials=self.credentials, + factory_addr=factory_addr, + escrow_addr=escrow_addr, + ) + self.assertEqual(new_job.manifest_url, manifest_url) + self.assertEqual(new_job.job_contract.address, escrow_addr) + self.assertEqual(new_job.factory_contract.address, factory_addr) + with self.assertRaises(AttributeError): + new_job.launch(self.rep_oracle_pub_key) + + def test_job_launch(self): + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertEqual(self.job.status(), Status(1)) + multi_credentials = [ + ( + "0x61F9F0B31eacB420553da8BCC59DC617279731Ac", + "486a0621e595dd7fcbe5608cbbeec8f5a8b5cabe7637f11eccfc7acd408c3a0e", + ), + ( + "0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", + "f22d4fc42da79aa5ba839998a0a9f2c2c45f5e55ee7f1504e464d2c71ca199e1", + ), + ] + self.job = Job(self.credentials, manifest, multi_credentials=multi_credentials) + + # Inject wrong credentials on purpose to test out raffling + + self.job.gas_payer_priv = ( + "657b6497a355a3982928d5515d48a84870f057c4d16923eb1d104c0afada9aa8" + ) + self.job.multi_credentials = [ + ( + "0x61F9F0B31eacB420553da8BCC59DC617279731Ac", + "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5", + ), + ( + "0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", + "f22d4fc42da79aa5ba839998a0a9f2c2c45f5e55ee7f1504e464d2c71ca199e1", + ), + ] + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertEqual(self.job.status(), Status(1)) + + # Make sure we launched with raffled credentials + + self.assertEqual( + self.job.gas_payer, "0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809" + ) + self.assertEqual( + self.job.gas_payer_priv, + "f22d4fc42da79aa5ba839998a0a9f2c2c45f5e55ee7f1504e464d2c71ca199e1", + ) + + def test_job_setup(self): + + # A Job can't be setup without deploying it first. + + self.assertFalse(self.job.setup()) + multi_credentials = [ + ( + "0x61F9F0B31eacB420553da8BCC59DC617279731Ac", + "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5", + ), + ( + "0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", + "f22d4fc42da79aa5ba839998a0a9f2c2c45f5e55ee7f1504e464d2c71ca199e1", + ), + ] + self.job = Job(self.credentials, manifest, multi_credentials=multi_credentials) + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + + def test_job_add_trusted_handlers(self): + + # Make sure we se set our gas payer as a trusted handler by default. + + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue( + is_trusted_handler( + self.job.job_contract, self.job.gas_payer, self.job.gas_payer + ) + ) + trusted_handlers = [ + "0x61F9F0B31eacB420553da8BCC59DC617279731Ac", + "0xD979105297fB0eee83F7433fC09279cb5B94fFC6", + ] + self.assertTrue(self.job.add_trusted_handlers(trusted_handlers)) + self.assertTrue( + is_trusted_handler( + self.job.job_contract, + "0x61F9F0B31eacB420553da8BCC59DC617279731Ac", + self.job.gas_payer, + ) + ) + self.assertTrue( + is_trusted_handler( + self.job.job_contract, + "0xD979105297fB0eee83F7433fC09279cb5B94fFC6", + self.job.gas_payer, + ) + ) + + def test_job_bulk_payout(self): + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + payouts = [ + ("0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", Decimal("20.0")), + ("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("50.0")), + ] + self.assertTrue(self.job.bulk_payout(payouts, {}, self.rep_oracle_pub_key)) + + # The escrow contract is still in Partial state as there's still balance left. + + self.assertEqual(self.job.balance(), 30000000000000000000) + self.assertEqual(self.job.status(), Status(3)) + + # Trying to pay more than the contract balance results in failure. + + payouts = [("0x9d689b8f50Fd2CAec716Cc5220bEd66E03F07B5f", Decimal("40.0"))] + self.assertFalse(self.job.bulk_payout(payouts, {}, self.rep_oracle_pub_key)) + + # Paying the remaining amount empties the escrow and updates the status correctly. - # IMPORTANT, don't modify this so CI catches the doctest errors. - doctest.testmod() + payouts = [("0x9d689b8f50Fd2CAec716Cc5220bEd66E03F07B5f", Decimal("30.0"))] + self.assertTrue(self.job.bulk_payout(payouts, {}, self.rep_oracle_pub_key)) + self.assertEqual(self.job.balance(), 0) + self.assertEqual(self.job.status(), Status(4)) + + multi_credentials = [ + ( + "0x61F9F0B31eacB420553da8BCC59DC617279731Ac", + "486a0621e595dd7fcbe5608cbbeec8f5a8b5cabe7637f11eccfc7acd408c3a0e", + ), + ( + "0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", + "f22d4fc42da79aa5ba839998a0a9f2c2c45f5e55ee7f1504e464d2c71ca199e1", + ), + ] + self.job = Job(self.credentials, manifest, multi_credentials=multi_credentials) + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + payouts = [ + ("0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", Decimal("20.0")), + ("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("50.0")), + ] + self.assertTrue(self.job.bulk_payout(payouts, {}, self.rep_oracle_pub_key)) + + def test_job_abort(self): + + # The escrow contract is in Paid state after the a full bulk payout and it can't be aborted. + + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("100.0"))] + self.assertTrue( + self.job.bulk_payout(payouts, {"results": 0}, self.rep_oracle_pub_key) + ) + self.assertFalse(self.job.abort()) + self.assertEqual(self.job.status(), Status(4)) + + # Trusted handler should be able to abort an existing contract + + self.job = Job(self.credentials, manifest) + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + trusted_handler = "0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809" + self.assertTrue(self.job.add_trusted_handlers([trusted_handler])) + + handler_credentials = { + "gas_payer": "0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", + "gas_payer_priv": "f22d4fc42da79aa5ba839998a0a9f2c2c45f5e55ee7f1504e464d2c71ca199e1", + "rep_oracle_priv_key": b"28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5", + } + access_job = Job( + credentials=handler_credentials, + factory_addr=self.job.factory_contract.address, + escrow_addr=self.job.job_contract.address, + ) + self.assertTrue(access_job.abort()) + + def test_job_cancel(self): + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + payouts = [("0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", Decimal("20.0"))] + self.assertTrue(self.job.bulk_payout(payouts, {}, self.rep_oracle_pub_key)) + self.assertEqual(self.job.status(), Status(3)) + + # The escrow contract is in Paid state after the second payout and it can't be cancelled. + + payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("80.0"))] + self.assertTrue( + self.job.bulk_payout(payouts, {"results": 0}, self.rep_oracle_pub_key) + ) + self.assertFalse(self.job.cancel()) + self.assertEqual(self.job.status(), Status(4)) + + def test_job_status(self): + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertEqual(self.job.status(), Status(1)) + + def test_job_balance(self): + self.assertTrue(self.job.launch(self.rep_oracle_pub_key)) + self.assertTrue(self.job.setup()) + self.assertEqual(self.job.balance(), 100000000000000000000) + + +if __name__ == "__main__": + unittest.main(exit=False) diff --git a/hmt_escrow/storage.py b/hmt_escrow/storage.py index 23ba62ca..acd1f0d0 100644 --- a/hmt_escrow/storage.py +++ b/hmt_escrow/storage.py @@ -3,6 +3,7 @@ import codecs import hashlib import json +import unittest from typing import Dict, Tuple from eth_keys import keys @@ -199,7 +200,58 @@ def _encrypt(public_key: bytes, msg: str) -> bytes: return ecies.encrypt(msg_bytes, pub_key, shared_mac_data=SHARED_MAC_DATA) -if __name__ == "__main__": - import doctest +class StorageTest(unittest.TestCase): + def test_download(self): + credentials = { + "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", + "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5", + } + pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" + job = Job(credentials=credentials, escrow_manifest=manifest) + (_, manifest_url) = upload(job.serialized_manifest, pub_key) + manifest_dict = download(manifest_url, job.gas_payer_priv) + self.assertEqual(manifest_dict, job.serialized_manifest) + + job = Job(credentials=credentials, escrow_manifest=manifest) + (_, manifest_url) = upload(job.serialized_manifest, pub_key) + manifest_dict = download(manifest_url, job.gas_payer_priv) + self.assertEqual(manifest_dict, job.serialized_manifest) + + def test_upload(self): + credentials = { + "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", + "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5", + } + pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" + job = Job(credentials=credentials, escrow_manifest=manifest) + (_, manifest_url) = upload(job.serialized_manifest, pub_key) + manifest_dict = download(manifest_url, job.gas_payer_priv) + self.assertEqual(manifest_dict, job.serialized_manifest) + + job = Job(credentials=credentials, escrow_manifest=manifest) + (_, manifest_url) = upload(job.serialized_manifest, pub_key) + self.assertTrue(manifest_url.startswith("s3")) + manifest_dict = download(manifest_url, job.gas_payer_priv) + self.assertEqual(manifest_dict, job.serialized_manifest) + + def test_decrypt(self): + from p2p.exceptions import DecryptionError + + priv_key = "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" + pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" + msg = "test" + self.assertEqual(_decrypt(priv_key, _encrypt(pub_key, msg)), msg) + # Using a wrong public key to decrypt a message results in failure. + false_pub_key = b"74c81fe41b30f741b31185052664a10c3256e2f08bcfb20c8f54e733bef58972adcf84e4f5d70a979681fd39d7f7847d2c0d3b5d4aead806c4fec4d8534be114" + with self.assertRaises(DecryptionError): + _decrypt(priv_key, _encrypt(false_pub_key, msg)) == msg + + def test_encrypt(self): + priv_key = "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" + pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" + msg = "test" + self.assertEqual(_decrypt(priv_key, _encrypt(pub_key, msg)), msg) + - doctest.testmod(raise_on_error=True) +if __name__ == "__main__": + unittest.main(exit=False) diff --git a/package-lock.json b/package-lock.json index bff60159..6c4ad0bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5208,9 +5208,9 @@ } }, "npm-registry-fetch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.4.tgz", - "integrity": "sha512-6jb34hX/iYNQebqWUHtU8YF6Cjb1H6ouTFPClYsyiW6lpFkljTpdeftm53rRojtja1rKAvKNIIiTS5Sjpw4wsA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.5.tgz", + "integrity": "sha512-yQ0/U4fYpCCqmueB2g8sc+89ckQ3eXpmU4+Yi2j5o/r0WkKvE2+Y0tK3DEILAtn2UaQTkjTHxIXe2/CSdit+/Q==", "dev": true, "requires": { "JSONStream": "^1.3.4",