def _create_escrow(self, gas: int = GAS_LIMIT) -> bool: """Launches a new escrow contract to the ethereum network. >>> credentials = { ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" ... } >>> job = Job(credentials, manifest) >>> job._create_escrow() True Args: gas (int): maximum amount of gas the caller is ready to pay. Returns: bool: returns True if a new job was successfully launched to the network. Raises: TimeoutError: if wait_on_transaction times out. """ txn_func = self.factory_contract.functions.createEscrow txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *[], **txn_info) return True
def abort(self, gas: int = GAS_LIMIT) -> bool: """Kills the contract and returns the HMT back to the gas payer. The contract cannot be aborted if the contract is in Partial, Paid or Complete state. >>> credentials = { ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" ... } >>> rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" >>> job = Job(credentials, manifest) The escrow contract is in Pending state after setup so it can be aborted. >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True >>> job.abort() True The escrow contract is in Partial state after the first payout and it can't be aborted. >>> job = Job(credentials, manifest) >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True >>> payouts = [("0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", Decimal('20.0'))] >>> job.bulk_payout(payouts, {}, rep_oracle_pub_key) True >>> job.abort() False >>> job.status() <Status.Partial: 3> The escrow contract is in Paid state after the second payout and it can't be aborted. >>> payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal('80.0'))] >>> job.bulk_payout(payouts, {'results': 0}, rep_oracle_pub_key) True >>> job.abort() False >>> job.status() <Status.Paid: 4> Returns: bool: returns True if contract has been destroyed successfully. """ txn_func = self.job_contract.functions.abort txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *[], **txn_info) # After abort the contract should be destroyed w3 = get_w3() contract_code = w3.eth.getCode(self.job_contract.address) return contract_code == b"\x00"
def cancel(self, gas: int = GAS_LIMIT) -> bool: """Returns the HMT back to the gas payer. It's the softer version of abort as the contract is not destroyed. >>> credentials = { ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" ... } >>> rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" >>> job = Job(credentials, manifest) The escrow contract is in Pending state after setup so it can be cancelled. >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True >>> job.cancel() True Contract balance is zero and status is "Cancelled". >>> job.balance() 0 >>> job.status() <Status.Cancelled: 6> The escrow contract is in Partial state after the first payout and it can't be cancelled. >>> job = Job(credentials, manifest) >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True >>> payouts = [("0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", Decimal('20.0'))] >>> job.bulk_payout(payouts, {}, rep_oracle_pub_key) True >>> job.status() <Status.Partial: 3> The escrow contract is in Paid state after the second payout and it can't be cancelled. >>> payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal('80.0'))] >>> job.bulk_payout(payouts, {'results': 0}, rep_oracle_pub_key) True >>> job.cancel() False >>> job.status() <Status.Paid: 4> Returns: bool: returns True if gas payer has been paid back and contract is in "Cancelled" state. """ txn_func = self.job_contract.functions.cancel txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *[], **txn_info) return self.status() == Status.Cancelled
def complete(self, gas: int = GAS_LIMIT) -> bool: """Completes the Job if it has been paid. >>> credentials = { ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" ... } >>> rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" >>> job = Job(credentials, manifest) >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True >>> payouts = [("0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", Decimal('20.0'))] >>> job.bulk_payout(payouts, {}, rep_oracle_pub_key) True A Job can't be completed when it is still in partially paid state. >>> job.status() <Status.Partial: 3> >>> job.complete() False Job completes in paid state correctly. >>> payouts = [("0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", Decimal('80.0'))] >>> job.bulk_payout(payouts, {}, rep_oracle_pub_key) True >>> job.complete() True >>> job.status() <Status.Complete: 5> Returns: bool: returns True if the contract has been completed. """ txn_func = self.job_contract.functions.complete txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *[], **txn_info) return self.status() == Status.Complete
def store_intermediate_results(self, results: Dict, pub_key: bytes, gas: int = GAS_LIMIT) -> bool: """Recording Oracle stores intermediate results with Reputation Oracle's public key to IPFS and updates the contract's state. >>> credentials = { ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" ... } >>> rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" >>> job = Job(credentials, manifest) >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True Storing intermediate results uploads and updates results url correctly. >>> results = {"results": True} >>> job.store_intermediate_results(results, rep_oracle_pub_key) True >>> rep_oracle_priv_key = b"28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" >>> job.intermediate_results(rep_oracle_priv_key) {'results': True} Args: results (Dict): intermediate results of the Recording Oracle. pub_key (bytes): public key of the Reputation Oracle. Returns: returns True if contract's state is updated and IPFS upload succeeds. """ (hash_, url) = upload(results, pub_key) txn_func = self.job_contract.functions.storeResults func_args = [url, hash_] txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *func_args, **txn_info) return True
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 bulk_payout(self, payouts: List[Tuple[str, Decimal]], results: Dict, pub_key: bytes, gas: int = GAS_LIMIT) -> bool: """Performs a payout to multiple ethereum addresses. When the payout happens, final results are uploaded to IPFS and contract's state is updated to Partial or Paid depending on contract's balance. >>> credentials = { ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" ... } >>> rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" >>> job = Job(credentials, manifest) >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True >>> payouts = [("0x6b7E3C31F34cF38d1DFC1D9A8A59482028395809", Decimal('20.0')), ("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal('50.0'))] >>> job.bulk_payout(payouts, {}, rep_oracle_pub_key) True The escrow contract is still in Partial state as there's still balance left. >>> job.balance() 30000000000000000000 >>> job.status() <Status.Partial: 3> Trying to pay more than the contract balance results in failure. >>> payouts = [("0x9d689b8f50Fd2CAec716Cc5220bEd66E03F07B5f", Decimal('40.0'))] >>> job.bulk_payout(payouts, {}, rep_oracle_pub_key) False Paying the remaining amount empties the escrow and updates the status correctly. >>> payouts = [("0x9d689b8f50Fd2CAec716Cc5220bEd66E03F07B5f", Decimal('30.0'))] >>> job.bulk_payout(payouts, {}, rep_oracle_pub_key) True >>> job.balance() 0 >>> job.status() <Status.Paid: 4> Args: payouts (List[Tuple[str, int]]): a list of tuples with ethereum addresses and amounts. results (Dict): the final answer results stored by the Reputation Oracle. pub_key (bytes): the public key of the Reputation Oracle. Returns: bool: returns True if paying to ethereum addresses and oracles succeeds. """ (hash_, url) = upload(results, pub_key) eth_addrs = [eth_addr for eth_addr, amount in payouts] hmt_amounts = [int(amount * 10**18) for eth_addr, amount in payouts] txn_func = self.job_contract.functions.bulkPayOut func_args = [eth_addrs, hmt_amounts, url, hash_, 1] txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *func_args, **txn_info) return self._bulk_paid() == True
def setup(self, gas: int = GAS_LIMIT) -> bool: """Sets the escrow contract to be ready to receive answers from the Recording Oracle. The contract needs to be deployed and funded first. >>> credentials = { ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" ... } >>> rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" >>> job = Job(credentials, manifest) A Job can't be setup without deploying it first. >>> job.setup() Traceback (most recent call last): AttributeError: 'Job' object has no attribute 'job_contract' >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True Returns: bool: returns True if Job is in Pending state. Raises: AttributeError: if trying to setup the job before deploying it. """ # Prepare setup arguments for the escrow contract. reputation_oracle_stake = int( Decimal(self.serialized_manifest["oracle_stake"]) * 100) recording_oracle_stake = int( Decimal(self.serialized_manifest["oracle_stake"]) * 100) reputation_oracle = str( self.serialized_manifest["reputation_oracle_addr"]) recording_oracle = str( self.serialized_manifest["recording_oracle_addr"]) hmt_amount = int(self.amount * 10**18) # Fund the escrow contract with HMT. hmtoken_contract = get_hmtoken() txn_func = hmtoken_contract.functions.transfer func_args = [self.job_contract.address, hmt_amount] txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *func_args, **txn_info) # Setup the escrow contract with manifest and IPFS data. txn_func = self.job_contract.functions.setup func_args = [ reputation_oracle, recording_oracle, reputation_oracle_stake, recording_oracle_stake, self.manifest_url, self.manifest_hash ] txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *func_args, **txn_info) return self.status() == Status.Pending and self.balance() == hmt_amount
def store_intermediate_results(self, results: Dict, pub_key: bytes, gas: int = GAS_LIMIT, store_onchain: bool = True) -> bool: """Recording Oracle stores intermediate results with Reputation Oracle's public key to IPFS and updates the contract's state. Args: results (Dict): intermediate results of the Recording Oracle. pub_key (bytes): public key of the Reputation Oracle. gas (int): gas limit store_onchain (bool): false is don't run the blockchain fn. Returns: returns True if contract's state is updated and IPFS upload succeeds. >>> credentials = { ... "gas_payer": "0x1413862C2B7054CDbfdc181B83962CB0FC11fD92", ... "gas_payer_priv": "28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" ... } >>> rep_oracle_pub_key = b"2dbc2c2c86052702e7c219339514b2e8bd4687ba1236c478ad41b43330b08488c12c8c1797aa181f3a4596a1bd8a0c18344ea44d6655f61fa73e56e743f79e0d" >>> job = Job(credentials, manifest) >>> job.launch(rep_oracle_pub_key) True >>> job.setup() True >>> rep_oracle_priv_key = b"28e516f1e2f99e96a48a23cea1f94ee5f073403a1c68e818263f0eb898f1c8e5" Store intermediate results in IPNS >>> results = {"results": True} >>> job.store_intermediate_results(results, rep_oracle_pub_key) True >>> job.intermediate_results(rep_oracle_priv_key) {'results': True} Store intermediate results in IPNS. This time we see if IPNS link will update. >>> results = {"results": False} >>> job.store_intermediate_results(results, rep_oracle_pub_key) True >>> job.intermediate_results(rep_oracle_priv_key) {'results': False} """ (hash_, url) = upload( results, pub_key, ipns_keypair_name= f'intermediate-results-{self.job_contract.address}') if store_onchain: txn_func = self.job_contract.functions.storeResults func_args = [url, hash_] txn_info = { "gas_payer": self.gas_payer, "gas_payer_priv": self.gas_payer_priv, "gas": gas } handle_transaction(txn_func, *func_args, **txn_info) return True