Skip to content

Commit

Permalink
algod: Add endpoints for devmode timestamps, sync, and ready (#468)
Browse files Browse the repository at this point in the history
* Add endpoints for devmode timestamps

* Add cucumber step impls

* Add ready endpoint

* Simulate endpoint response changes
  • Loading branch information
algochoi authored Apr 27, 2023
1 parent c1120d5 commit e668c2c
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 5 deletions.
3 changes: 0 additions & 3 deletions algosdk/atomic_transaction_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,13 @@ class SimulateAtomicTransactionResponse:
def __init__(
self,
version: int,
would_succeed: bool,
failure_message: str,
failed_at: Optional[List[int]],
simulate_response: Dict[str, Any],
tx_ids: List[str],
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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion algosdk/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
77 changes: 77 additions & 0 deletions algosdk/v2client/algod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions tests/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 0 additions & 1 deletion tests/integration.tags
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,3 @@
@rekey_v1
@send
@send.keyregtxn
@simulate
40 changes: 40 additions & 0 deletions tests/steps/other_v2_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
5 changes: 5 additions & 0 deletions tests/unit.tags
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit e668c2c

Please sign in to comment.