From e668c2cebb4674b59fe1085970f5d127391acacc Mon Sep 17 00:00:00 2001 From: algochoi <86622919+algochoi@users.noreply.github.com> Date: Thu, 27 Apr 2023 13:08:50 -0400 Subject: [PATCH] algod: Add endpoints for devmode timestamps, sync, and ready (#468) * Add endpoints for devmode timestamps * Add cucumber step impls * Add ready endpoint * Simulate endpoint response changes --- algosdk/atomic_transaction_composer.py | 3 - algosdk/constants.py | 2 +- algosdk/v2client/algod.py | 77 ++++++++++++++++++++++++++ tests/environment.py | 8 +++ tests/integration.tags | 1 - tests/steps/other_v2_steps.py | 40 +++++++++++++ tests/unit.tags | 5 ++ 7 files changed, 131 insertions(+), 5 deletions(-) diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index 0ca9aaa3..e30b8a13 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -270,7 +270,6 @@ class SimulateAtomicTransactionResponse: def __init__( self, version: int, - would_succeed: bool, failure_message: str, failed_at: Optional[List[int]], simulate_response: Dict[str, Any], @@ -278,7 +277,6 @@ def __init__( results: List[SimulateABIResult], ) -> None: self.version = version - self.would_succeed = would_succeed self.failure_message = failure_message self.failed_at = failed_at self.simulate_response = simulate_response @@ -760,7 +758,6 @@ def simulate( return SimulateAtomicTransactionResponse( version=simulation_result.get("version", 0), - would_succeed=simulation_result.get("would-succeed", False), failure_message=txn_group.get("failure-message", ""), failed_at=txn_group.get("failed-at"), simulate_response=simulation_result, diff --git a/algosdk/constants.py b/algosdk/constants.py index cf3c8a21..d0466e7a 100644 --- a/algosdk/constants.py +++ b/algosdk/constants.py @@ -9,7 +9,7 @@ """str: header key for algod requests""" INDEXER_AUTH_HEADER = "X-Indexer-API-Token" """str: header key for indexer requests""" -UNVERSIONED_PATHS = ["/health", "/versions", "/metrics", "/genesis"] +UNVERSIONED_PATHS = ["/health", "/versions", "/metrics", "/genesis", "/ready"] """str[]: paths that don't use the version path prefix""" NO_AUTH: List[str] = [] """str[]: requests that don't require authentication""" diff --git a/algosdk/v2client/algod.py b/algosdk/v2client/algod.py index 586b780e..c07d3296 100644 --- a/algosdk/v2client/algod.py +++ b/algosdk/v2client/algod.py @@ -113,6 +113,11 @@ def algod_request( try: return json.load(resp) except Exception as e: + # Some algod responses currently return a 200 OK + # but have an empty response. + # Do not return an error, and just return an empty response. + if resp.status == 200 and resp.length == 0: + return {} raise error.AlgodResponseError( "Failed to parse JSON response from algod" ) from e @@ -641,6 +646,78 @@ def simulate_raw_transactions( ) return self.simulate_transactions(request, **kwargs) + def get_sync_round(self, **kwargs: Any) -> AlgodResponseType: + """ + Get the minimum sync round for the ledger. + + Returns: + Dict[str, Any]: Response from algod + """ + req = "/ledger/sync" + return self.algod_request("GET", req, **kwargs) + + def set_sync_round(self, round: int, **kwargs: Any) -> AlgodResponseType: + """ + Set the minimum sync round for the ledger. + + Args: + round (int): Sync round + + Returns: + Dict[str, Any]: Response from algod + """ + req = f"/ledger/sync/{round}" + return self.algod_request("POST", req, **kwargs) + + def unset_sync_round(self, **kwargs: Any) -> AlgodResponseType: + """ + Unset the minimum sync round for the ledger. + + Returns: + Dict[str, Any]: Response from algod + """ + req = "/ledger/sync" + return self.algod_request("DELETE", req, **kwargs) + + def ready(self, **kwargs: Any) -> AlgodResponseType: + """ + Returns OK if the node is healthy and fully caught up. + + Returns: + Dict[str, Any]: Response from algod + """ + req = "/ready" + return self.algod_request("GET", req, **kwargs) + + def get_timestamp_offset(self, **kwargs: Any) -> AlgodResponseType: + """ + Get the timestamp offset in block headers. + This feature is only available in dev mode networks. + + Returns: + Dict[str, Any]: Response from algod + """ + req = "/devmode/blocks/offset" + return self.algod_request("GET", req, **kwargs) + + def set_timestamp_offset( + self, + offset: int, + **kwargs: Any, + ) -> AlgodResponseType: + """ + Set the timestamp offset in block headers. + This feature is only available in dev mode networks. + + Args: + offset (int): Block timestamp offset + + Returns: + Dict[str, Any]: Response from algod + """ + req = f"/devmode/blocks/offset/{offset}" + return self.algod_request("POST", req, **kwargs) + def _specify_round_string( block: Union[int, None], round_num: Union[int, None] diff --git a/tests/environment.py b/tests/environment.py index 691c3aaf..32a7ecc3 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -53,6 +53,14 @@ def do_POST(self): m = bytes(m, "ascii") self.wfile.write(m) + def do_DELETE(self): + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + m = json.dumps({"path": self.path}) + m = bytes(m, "ascii") + self.wfile.write(m) + def get_status_to_use(): f = open("tests/features/resources/mock_response_status", "r") diff --git a/tests/integration.tags b/tests/integration.tags index 479c9b62..2ead9e95 100644 --- a/tests/integration.tags +++ b/tests/integration.tags @@ -14,4 +14,3 @@ @rekey_v1 @send @send.keyregtxn -@simulate diff --git a/tests/steps/other_v2_steps.py b/tests/steps/other_v2_steps.py index 63d42b09..27c9bbf4 100644 --- a/tests/steps/other_v2_steps.py +++ b/tests/steps/other_v2_steps.py @@ -905,6 +905,11 @@ def expect_path(context, path): assert exp_query == actual_query, f"{exp_query} != {actual_query}" +@then('expect the request to be "{method}" "{path}"') +def expect_request(context, method, path): + return expect_path(context, path) + + @then('expect error string to contain "{err:MaybeString}"') def expect_error(context, err): # TODO: this should actually do the claimed action @@ -1498,3 +1503,38 @@ def check_missing_signatures(context, group, path): "missing-signature" ] assert missing_sig is True + + +@when("we make a GetLedgerStateDelta call against round {round}") +def get_ledger_state_delta_call(context, round): + context.response = context.acl.get_ledger_state_delta(round) + + +@when("we make a SetSyncRound call against round {round}") +def set_sync_round_call(context, round): + context.response = context.acl.set_sync_round(round) + + +@when("we make a GetSyncRound call") +def get_sync_round_call(context): + context.response = context.acl.get_sync_round() + + +@when("we make a UnsetSyncRound call") +def unset_sync_round_call(context): + context.response = context.acl.unset_sync_round() + + +@when("we make a Ready call") +def ready_call(context): + context.response = context.acl.ready() + + +@when("we make a SetBlockTimeStampOffset call against offset {offset}") +def set_block_timestamp_offset(context, offset): + context.response = context.acl.set_timestamp_offset(offset) + + +@when("we make a GetBlockTimeStampOffset call") +def get_block_timestamp_offset(context): + context.response = context.acl.get_timestamp_offset() diff --git a/tests/unit.tags b/tests/unit.tags index 04be53d3..b6b057ce 100644 --- a/tests/unit.tags +++ b/tests/unit.tags @@ -15,14 +15,19 @@ @unit.indexer.logs @unit.offline @unit.program_sanity_check +@unit.ready @unit.rekey @unit.responses @unit.responses.231 @unit.responses.blocksummary @unit.responses.participationupdates +@unit.responses.sync +@unit.responses.timestamp @unit.responses.unlimited_assets @unit.sourcemap +@unit.sync @unit.tealsign +@unit.timestamp @unit.transactions @unit.transactions.keyreg @unit.transactions.payment