From b87f697785ef1bc74a2ee781b303b2602febed55 Mon Sep 17 00:00:00 2001 From: Gary Malouf <982483+gmalouf@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:57:54 -0500 Subject: [PATCH 1/4] Support for blockheaders call against Indexer API. (#553) --- algosdk/v2client/indexer.py | 58 +++++++++++++++++++++++++++++++++ tests/steps/other_v2_steps.py | 60 +++++++++++++++++++++++++++++++++++ tests/unit.tags | 1 + 3 files changed, 119 insertions(+) diff --git a/algosdk/v2client/indexer.py b/algosdk/v2client/indexer.py index e2cccb45..59d305bd 100644 --- a/algosdk/v2client/indexer.py +++ b/algosdk/v2client/indexer.py @@ -566,6 +566,64 @@ def search_transactions( return self.indexer_request("GET", req, query, **kwargs) + def search_block_headers( + self, + limit=None, + next_page=None, + min_round=None, + max_round=None, + start_time=None, + end_time=None, + proposers=None, + absent=None, + expired=None, + **kwargs + ): + """ + Return a list of block headers satisfying the conditions. + + Args: + limit (int, optional): maximum number of results to return + next_page (str, optional): the next page of results; use the next + token provided by the previous results + min_round (int, optional): include results at or after the + specified round + max_round (int, optional): include results at or before the + specified round + start_time (str, optional): include results after the given time; + must be an RFC 3339 formatted string + end_time (str, optional): include results before the given time; + must be an RFC 3339 formatted string + proposers (list, optional): include results with accounts marked as + proposers in the block header's participation updates + expired (list, optional): include results with accounts marked as + expired in the block header's participation updates + absent (list, optional): include results with accounts marked as + absent in the block header's participation updates + """ + req = "/block-headers" + query = dict() + if limit: + query["limit"] = limit + if next_page: + query["next"] = next_page + if min_round: + query["min-round"] = min_round + if max_round: + query["max-round"] = max_round + if end_time: + query["before-time"] = end_time + if start_time: + query["after-time"] = start_time + if proposers: + query["proposers"] = proposers + if expired: + query["expired"] = expired + if absent: + query["absent"] = absent + + return self.indexer_request("GET", req, query, **kwargs) + def search_transactions_by_address( self, address, diff --git a/tests/steps/other_v2_steps.py b/tests/steps/other_v2_steps.py index d36543e0..398ce7e0 100644 --- a/tests/steps/other_v2_steps.py +++ b/tests/steps/other_v2_steps.py @@ -831,6 +831,53 @@ def search_txns2( ) +@when( + 'we make a Search For BlockHeaders call with minRound {minRound} maxRound {maxRound} limit {limit} nextToken "{next:MaybeString}" beforeTime "{beforeTime:MaybeString}" afterTime "{afterTime:MaybeString}" proposers {proposers} expired {expired} absent {absent}' +) +def search_block_headers( + context, + minRound, + maxRound, + limit, + next, + beforeTime, + afterTime, + proposers, + expired, + absent, +): + if next == "none": + next = None + if beforeTime == "none": + beforeTime = None + if afterTime == "none": + afterTime = None + if not proposers or proposers == '""': + proposers = None + else: + proposers = eval(proposers) + if not expired or expired == '""': + expired = None + else: + expired = eval(expired) + if not absent or absent == '""': + absent = None + else: + absent = eval(absent) + + context.response = context.icl.search_block_headers( + limit=int(limit), + next_page=next, + min_round=int(minRound), + max_round=int(maxRound), + start_time=afterTime, + end_time=beforeTime, + proposers=proposers, + expired=expired, + absent=absent, + ) + + @when("we make any SearchForTransactions call") def search_txns_any(context): context.response = context.icl.search_transactions(asset_id=2) @@ -873,6 +920,19 @@ def parsed_search_for_hb_txns(context, roundNum, length, index, hb_address): ) +@when("we make any SearchForBlockHeaders call") +def search_bhs_any(context): + context.response = context.icl.search_block_headers() + + +@then( + 'the parsed SearchForBlockHeaders response should have a block array of len {length} and the element at index {index} should have round "{round}"' +) +def step_impl(context, length, index, round): + assert len(context.response["blocks"]) == int(length) + assert (context.response["blocks"][int(index)]["round"]) == int(round) + + @when( 'we make a SearchForAssets call with limit {limit} creator "{creator:MaybeString}" name "{name:MaybeString}" unit "{unit:MaybeString}" index {index}' ) diff --git a/tests/unit.tags b/tests/unit.tags index adc36a69..bf0ef6b3 100644 --- a/tests/unit.tags +++ b/tests/unit.tags @@ -14,6 +14,7 @@ @unit.dryrun.trace.application @unit.feetest @unit.indexer +@unit.indexer.blockheaders @unit.indexer.heartbeat @unit.indexer.ledger_refactoring @unit.indexer.logs From 71b02d5b215d40af31c85bcdcd7c7b37dfbf14d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 07:31:54 -0500 Subject: [PATCH 2/4] Dependencies: Bump black from 23.1.0 to 24.3.0 (#535) * Bump black from 23.1.0 to 24.3.0 Bumps [black](https://github.com/psf/black) from 23.1.0 to 24.3.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/23.1.0...24.3.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Apply updated black formatting rules. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gary Malouf <982483+gmalouf@users.noreply.github.com> --- algosdk/abi/tuple_type.py | 6 +- algosdk/atomic_transaction_composer.py | 6 +- algosdk/transaction.py | 67 ++++++++++--------- algosdk/v2client/models/account.py | 8 ++- .../v2client/models/account_participation.py | 8 ++- algosdk/v2client/models/application.py | 8 ++- .../models/application_local_state.py | 8 ++- algosdk/v2client/models/application_params.py | 8 ++- .../models/application_state_schema.py | 8 ++- algosdk/v2client/models/asset.py | 8 ++- algosdk/v2client/models/asset_holding.py | 8 ++- algosdk/v2client/models/asset_params.py | 8 ++- algosdk/v2client/models/dryrun_request.py | 8 ++- algosdk/v2client/models/dryrun_source.py | 8 ++- algosdk/v2client/models/teal_key_value.py | 8 ++- algosdk/v2client/models/teal_value.py | 8 ++- requirements.txt | 2 +- 17 files changed, 108 insertions(+), 77 deletions(-) diff --git a/algosdk/abi/tuple_type.py b/algosdk/abi/tuple_type.py index 62fecb46..c4e72efc 100644 --- a/algosdk/abi/tuple_type.py +++ b/algosdk/abi/tuple_type.py @@ -232,9 +232,9 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> list: "value string must be in bytes: {}".format(bytestring) ) tuple_elements = self.child_types - dynamic_segments: List[ - List[int] - ] = list() # Store the start and end of a dynamic element + dynamic_segments: List[List[int]] = ( + list() + ) # Store the start and end of a dynamic element value_partitions: List[Optional[Union[bytes, bytearray]]] = list() i = 0 array_index = 0 diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index c48a1e53..be5504ed 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -679,9 +679,9 @@ def gather_signatures(self) -> List[GenericSignedTransaction]: stxn_list: List[Optional[GenericSignedTransaction]] = [None] * len( self.txn_list ) - signer_indexes: Dict[ - TransactionSigner, List[int] - ] = {} # Map a signer to a list of indices to sign + signer_indexes: Dict[TransactionSigner, List[int]] = ( + {} + ) # Map a signer to a list of indices to sign txn_list = self.build_group() for i, txn_with_signer in enumerate(txn_list): if txn_with_signer.signer not in signer_indexes: diff --git a/algosdk/transaction.py b/algosdk/transaction.py index 0a338542..fe6e7f9a 100644 --- a/algosdk/transaction.py +++ b/algosdk/transaction.py @@ -220,9 +220,9 @@ def undictify(d): "sender": encoding.encode_address(d["snd"]), "note": d["note"] if "note" in d else None, "lease": d["lx"] if "lx" in d else None, - "rekey_to": encoding.encode_address(d["rekey"]) - if "rekey" in d - else None, + "rekey_to": ( + encoding.encode_address(d["rekey"]) if "rekey" in d else None + ), } txn_type = d["type"] if not isinstance(d["type"], str): @@ -394,13 +394,15 @@ def dictify(self): @staticmethod def _undictify(d): args = { - "close_remainder_to": encoding.encode_address(d["close"]) - if "close" in d - else None, + "close_remainder_to": ( + encoding.encode_address(d["close"]) if "close" in d else None + ), "amt": d["amt"] if "amt" in d else 0, - "receiver": encoding.encode_address(d["rcv"]) - if "rcv" in d - else constants.ZERO_ADDRESS, + "receiver": ( + encoding.encode_address(d["rcv"]) + if "rcv" in d + else constants.ZERO_ADDRESS + ), } return args @@ -1169,7 +1171,6 @@ def __init__( class AssetFreezeTxn(Transaction): - """ Represents a transaction for freezing or unfreezing an account's asset holdings. Must be issued by the asset's freeze manager. @@ -1367,17 +1368,19 @@ def dictify(self): @staticmethod def _undictify(d): args = { - "receiver": encoding.encode_address(d["arcv"]) - if "arcv" in d - else constants.ZERO_ADDRESS, + "receiver": ( + encoding.encode_address(d["arcv"]) + if "arcv" in d + else constants.ZERO_ADDRESS + ), "amt": d["aamt"] if "aamt" in d else 0, "index": d["xaid"] if "xaid" in d else None, - "close_assets_to": encoding.encode_address(d["aclose"]) - if "aclose" in d - else None, - "revocation_target": encoding.encode_address(d["asnd"]) - if "asnd" in d - else None, + "close_assets_to": ( + encoding.encode_address(d["aclose"]) if "aclose" in d else None + ), + "revocation_target": ( + encoding.encode_address(d["asnd"]) if "asnd" in d else None + ), } return args @@ -1685,12 +1688,12 @@ def _undictify(d): args = { "index": d["apid"] if "apid" in d else None, "on_complete": d["apan"] if "apan" in d else None, - "local_schema": StateSchema.undictify(d["apls"]) - if "apls" in d - else None, - "global_schema": StateSchema.undictify(d["apgs"]) - if "apgs" in d - else None, + "local_schema": ( + StateSchema.undictify(d["apls"]) if "apls" in d else None + ), + "global_schema": ( + StateSchema.undictify(d["apgs"]) if "apgs" in d else None + ), "approval_program": d["apap"] if "apap" in d else None, "clear_program": d["apsu"] if "apsu" in d else None, "app_args": d["apaa"] if "apaa" in d else None, @@ -1698,9 +1701,11 @@ def _undictify(d): "foreign_apps": d["apfa"] if "apfa" in d else None, "foreign_assets": d["apas"] if "apas" in d else None, "extra_pages": d["apep"] if "apep" in d else 0, - "boxes": [BoxReference.undictify(box) for box in d["apbx"]] - if "apbx" in d - else None, + "boxes": ( + [BoxReference.undictify(box) for box in d["apbx"]] + if "apbx" in d + else None + ), } if args["accounts"]: args["accounts"] = [ @@ -2303,9 +2308,9 @@ def merge( for s in range(len(stx.multisig.subsigs)): if stx.multisig.subsigs[s].signature: if not msigstx.multisig.subsigs[s].signature: - msigstx.multisig.subsigs[ - s - ].signature = stx.multisig.subsigs[s].signature + msigstx.multisig.subsigs[s].signature = ( + stx.multisig.subsigs[s].signature + ) elif ( not msigstx.multisig.subsigs[s].signature == stx.multisig.subsigs[s].signature diff --git a/algosdk/v2client/models/account.py b/algosdk/v2client/models/account.py index b84459ce..e1e15ab8 100644 --- a/algosdk/v2client/models/account.py +++ b/algosdk/v2client/models/account.py @@ -504,9 +504,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/account_participation.py b/algosdk/v2client/models/account_participation.py index 8944e0f8..86c5a744 100644 --- a/algosdk/v2client/models/account_participation.py +++ b/algosdk/v2client/models/account_participation.py @@ -212,9 +212,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/application.py b/algosdk/v2client/models/application.py index a4631fb7..5b1ec2ff 100644 --- a/algosdk/v2client/models/application.py +++ b/algosdk/v2client/models/application.py @@ -88,9 +88,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/application_local_state.py b/algosdk/v2client/models/application_local_state.py index 2bc4bf7f..89a7b992 100644 --- a/algosdk/v2client/models/application_local_state.py +++ b/algosdk/v2client/models/application_local_state.py @@ -117,9 +117,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/application_params.py b/algosdk/v2client/models/application_params.py index 587d6739..774c9802 100644 --- a/algosdk/v2client/models/application_params.py +++ b/algosdk/v2client/models/application_params.py @@ -211,9 +211,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/application_state_schema.py b/algosdk/v2client/models/application_state_schema.py index c9997987..75a45e78 100644 --- a/algosdk/v2client/models/application_state_schema.py +++ b/algosdk/v2client/models/application_state_schema.py @@ -93,9 +93,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/asset.py b/algosdk/v2client/models/asset.py index 160c6e6d..31bdbcc3 100644 --- a/algosdk/v2client/models/asset.py +++ b/algosdk/v2client/models/asset.py @@ -88,9 +88,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/asset_holding.py b/algosdk/v2client/models/asset_holding.py index 205a779b..39fb0fc7 100644 --- a/algosdk/v2client/models/asset_holding.py +++ b/algosdk/v2client/models/asset_holding.py @@ -152,9 +152,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/asset_params.py b/algosdk/v2client/models/asset_params.py index 1901dd47..92fcf99c 100644 --- a/algosdk/v2client/models/asset_params.py +++ b/algosdk/v2client/models/asset_params.py @@ -389,9 +389,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/dryrun_request.py b/algosdk/v2client/models/dryrun_request.py index 374663be..d627d0a7 100644 --- a/algosdk/v2client/models/dryrun_request.py +++ b/algosdk/v2client/models/dryrun_request.py @@ -232,9 +232,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/dryrun_source.py b/algosdk/v2client/models/dryrun_source.py index fc433f41..9663218c 100644 --- a/algosdk/v2client/models/dryrun_source.py +++ b/algosdk/v2client/models/dryrun_source.py @@ -146,9 +146,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/teal_key_value.py b/algosdk/v2client/models/teal_key_value.py index 87c0eac3..ca933527 100644 --- a/algosdk/v2client/models/teal_key_value.py +++ b/algosdk/v2client/models/teal_key_value.py @@ -86,9 +86,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/algosdk/v2client/models/teal_value.py b/algosdk/v2client/models/teal_value.py index 5535fe19..e8cc9661 100644 --- a/algosdk/v2client/models/teal_value.py +++ b/algosdk/v2client/models/teal_value.py @@ -115,9 +115,11 @@ def dictify(self): elif isinstance(value, dict): result[oas_attr] = dict( map( - lambda item: (item[0], item[1].dictify()) - if hasattr(item[1], "dictify") - else item, + lambda item: ( + (item[0], item[1].dictify()) + if hasattr(item[1], "dictify") + else item + ), value.items(), ) ) diff --git a/requirements.txt b/requirements.txt index a45e6cbf..c9568a07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ . -black==23.1.0 +black==24.3.0 glom==20.11.0 pytest==6.2.5 mypy==1.0 From 9a8e0f08ab0e05b62c2e9a489c98ed5bf08b7016 Mon Sep 17 00:00:00 2001 From: Gary Malouf <982483+gmalouf@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:36:51 -0500 Subject: [PATCH 3/4] Support for header-only flag on /v2/block algod endpoint. (#557) --- algosdk/v2client/algod.py | 8 ++++++++ tests/steps/other_v2_steps.py | 25 ++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/algosdk/v2client/algod.py b/algosdk/v2client/algod.py index f39aa77e..7f621057 100644 --- a/algosdk/v2client/algod.py +++ b/algosdk/v2client/algod.py @@ -270,6 +270,7 @@ def block_info( block: Optional[int] = None, response_format: str = "json", round_num: Optional[int] = None, + header_only: Optional[bool] = None, **kwargs: Any, ) -> AlgodResponseType: """ @@ -280,8 +281,15 @@ def block_info( response_format (str): the format in which the response is returned: either "json" or "msgpack" round_num (int, optional): alias for block; specify one of these + header_only (bool, optional): if set to true, only block header would be + present in the the response + """ query = {"format": response_format} + + if header_only: + query["header-only"] = "true" + req = "/blocks/" + _specify_round_string(block, round_num) res = self.algod_request( "GET", req, query, response_format=response_format, **kwargs diff --git a/tests/steps/other_v2_steps.py b/tests/steps/other_v2_steps.py index 398ce7e0..20f1a194 100644 --- a/tests/steps/other_v2_steps.py +++ b/tests/steps/other_v2_steps.py @@ -328,6 +328,19 @@ def block(context, block, response_format): ) +@when( + 'we make a Get Block call for round {round} with format "{response_format}" and header-only "{header_only}"' +) +def block(context, round, response_format, header_only): + bool_opt = None + if header_only == "true": + bool_opt = True + + context.response = context.acl.block_info( + int(round), response_format=response_format, header_only=bool_opt + ) + + @when("we make any Get Block call") def block_any(context): context.response = context.acl.block_info(3, response_format="msgpack") @@ -339,6 +352,17 @@ def parse_block(context, pool): assert context.response["block"]["rwd"] == pool +@then( + 'the parsed Get Block response should have rewards pool "{pool}" and no certificate or payset' +) +def parse_block_header(context, pool): + context.response = json.loads(context.response) + assert context.response["block"]["rwd"] == pool + assert ( + "cert" not in context.response + ), f"Key 'cert' unexpectedly found in dictionary" + + @then( 'the parsed Get Block response should have heartbeat address "{hb_address}"' ) @@ -654,7 +678,6 @@ def parse_txns_by_addr(context, roundNum, length, idx, sender): 'we make a Lookup Block call against round {block:d} and header "{headerOnly:MaybeBool}"' ) def lookup_block(context, block, headerOnly): - print("Header only = " + str(headerOnly)) context.response = context.icl.block_info( block=block, header_only=headerOnly ) From e34de3f1b9dcce2a07b8406295b8409f9eb84f0b Mon Sep 17 00:00:00 2001 From: gmalouf Date: Thu, 13 Feb 2025 20:16:48 +0000 Subject: [PATCH 4/4] bump up version to v2.8.0 --- CHANGELOG.md | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c41daba..e49af153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +# v2.8.0 + + + +## What's Changed +### Enhancements +* Blockheaders: Support for blockheaders call against Indexer API. by @gmalouf in https://github.com/algorand/py-algorand-sdk/pull/553 +* API: Support for header-only flag on /v2/block algod endpoint. by @gmalouf in https://github.com/algorand/py-algorand-sdk/pull/557 + +## New Contributors +* @dependabot made their first contribution in https://github.com/algorand/py-algorand-sdk/pull/535 + +**Full Changelog**: https://github.com/algorand/py-algorand-sdk/compare/v2.7.0...v2.8.0 + # v2.7.0 diff --git a/setup.py b/setup.py index 99ef7197..cf3d4f1f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ description="Algorand SDK in Python", author="Algorand", author_email="pypiservice@algorand.com", - version="2.7.0", + version="2.8.0", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/algorand/py-algorand-sdk",