algokit-utils 1.4.0b1__py3-none-any.whl → 2.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of algokit-utils might be problematic. Click here for more details.

algokit_utils/__init__.py CHANGED
@@ -1,4 +1,8 @@
1
- from algokit_utils._ensure_funded import EnsureBalanceParameters, ensure_funded
1
+ from algokit_utils._ensure_funded import (
2
+ EnsureBalanceParameters,
3
+ EnsureFundedResponse,
4
+ ensure_funded,
5
+ )
2
6
  from algokit_utils._transfer import TransferAssetParameters, TransferParameters, transfer, transfer_asset
3
7
  from algokit_utils.account import (
4
8
  create_kmd_wallet_account,
@@ -54,6 +58,13 @@ from algokit_utils.deploy import (
54
58
  get_creator_apps,
55
59
  replace_template_variables,
56
60
  )
61
+ from algokit_utils.dispenser_api import (
62
+ DISPENSER_ACCESS_TOKEN_KEY,
63
+ DISPENSER_REQUEST_TIMEOUT,
64
+ DispenserApiTestnetClient,
65
+ DispenserFundResponse,
66
+ DispenserLimitResponse,
67
+ )
57
68
  from algokit_utils.logic_error import LogicError
58
69
  from algokit_utils.models import (
59
70
  ABIArgsDict,
@@ -153,7 +164,13 @@ __all__ = [
153
164
  "is_localnet",
154
165
  "is_mainnet",
155
166
  "is_testnet",
167
+ "DispenserApiTestnetClient",
168
+ "DispenserFundResponse",
169
+ "DispenserLimitResponse",
170
+ "DISPENSER_ACCESS_TOKEN_KEY",
171
+ "DISPENSER_REQUEST_TIMEOUT",
156
172
  "EnsureBalanceParameters",
173
+ "EnsureFundedResponse",
157
174
  "TransferParameters",
158
175
  "ensure_funded",
159
176
  "transfer",
@@ -1,23 +1,21 @@
1
- import dataclasses
2
- import logging
3
- from typing import TYPE_CHECKING
1
+ from dataclasses import dataclass
4
2
 
5
3
  from algosdk.account import address_from_private_key
6
4
  from algosdk.atomic_transaction_composer import AccountTransactionSigner
7
- from algosdk.transaction import PaymentTxn, SuggestedParams
5
+ from algosdk.transaction import SuggestedParams
6
+ from algosdk.v2client.algod import AlgodClient
8
7
 
9
8
  from algokit_utils._transfer import TransferParameters, transfer
10
9
  from algokit_utils.account import get_dispenser_account
10
+ from algokit_utils.dispenser_api import (
11
+ DispenserApiTestnetClient,
12
+ DispenserAssetName,
13
+ )
11
14
  from algokit_utils.models import Account
15
+ from algokit_utils.network_clients import is_testnet
12
16
 
13
- if TYPE_CHECKING:
14
- from algosdk.v2client.algod import AlgodClient
15
17
 
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- @dataclasses.dataclass(kw_only=True)
18
+ @dataclass(kw_only=True)
21
19
  class EnsureBalanceParameters:
22
20
  """Parameters for ensuring an account has a minimum number of µALGOs"""
23
21
 
@@ -32,9 +30,10 @@ class EnsureBalanceParameters:
32
30
  """When issuing a funding amount, the minimum amount to transfer (avoids many small transfers if this gets
33
31
  called often on an active account)"""
34
32
 
35
- funding_source: Account | AccountTransactionSigner | None = None
33
+ funding_source: Account | AccountTransactionSigner | DispenserApiTestnetClient | None = None
36
34
  """The account (with private key) or signer that will send the µALGOs,
37
- will use `get_dispenser_account` by default"""
35
+ will use `get_dispenser_account` by default. Alternatively you can pass an instance of [`DispenserApiTestnetClient`](https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/capabilities/dispenser-client.md)
36
+ which will allow you to interact with [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md)."""
38
37
 
39
38
  suggested_params: SuggestedParams | None = None
40
39
  """(optional) transaction parameters"""
@@ -50,51 +49,103 @@ class EnsureBalanceParameters:
50
49
  if this is set it's possible the transaction could get rejected during network congestion"""
51
50
 
52
51
 
52
+ @dataclass(kw_only=True)
53
+ class EnsureFundedResponse:
54
+ """Response for ensuring an account has a minimum number of µALGOs"""
55
+
56
+ """The transaction ID of the funding transaction"""
57
+ transaction_id: str
58
+ """The amount of µALGOs that were funded"""
59
+ amount: int
60
+
61
+
62
+ def _get_address_to_fund(parameters: EnsureBalanceParameters) -> str:
63
+ if isinstance(parameters.account_to_fund, str):
64
+ return parameters.account_to_fund
65
+ else:
66
+ return str(address_from_private_key(parameters.account_to_fund.private_key)) # type: ignore[no-untyped-call]
67
+
68
+
69
+ def _get_account_info(client: AlgodClient, address_to_fund: str) -> dict:
70
+ account_info = client.account_info(address_to_fund)
71
+ assert isinstance(account_info, dict)
72
+ return account_info
73
+
74
+
75
+ def _calculate_fund_amount(
76
+ parameters: EnsureBalanceParameters, current_spending_balance_micro_algos: int
77
+ ) -> int | None:
78
+ if parameters.min_spending_balance_micro_algos > current_spending_balance_micro_algos:
79
+ min_fund_amount_micro_algos = parameters.min_spending_balance_micro_algos - current_spending_balance_micro_algos
80
+ return max(min_fund_amount_micro_algos, parameters.min_funding_increment_micro_algos)
81
+ else:
82
+ return None
83
+
84
+
85
+ def _fund_using_dispenser_api(
86
+ dispenser_client: DispenserApiTestnetClient, address_to_fund: str, fund_amount_micro_algos: int
87
+ ) -> EnsureFundedResponse | None:
88
+ response = dispenser_client.fund(
89
+ address=address_to_fund, amount=fund_amount_micro_algos, asset_id=DispenserAssetName.ALGO
90
+ )
91
+
92
+ return EnsureFundedResponse(transaction_id=response.tx_id, amount=response.amount)
93
+
94
+
95
+ def _fund_using_transfer(
96
+ client: AlgodClient, parameters: EnsureBalanceParameters, address_to_fund: str, fund_amount_micro_algos: int
97
+ ) -> EnsureFundedResponse:
98
+ if isinstance(parameters.funding_source, DispenserApiTestnetClient):
99
+ raise Exception(f"Invalid funding source: {parameters.funding_source}")
100
+
101
+ funding_source = parameters.funding_source or get_dispenser_account(client)
102
+ response = transfer(
103
+ client,
104
+ TransferParameters(
105
+ from_account=funding_source,
106
+ to_address=address_to_fund,
107
+ micro_algos=fund_amount_micro_algos,
108
+ note=parameters.note or "Funding account to meet minimum requirement",
109
+ suggested_params=parameters.suggested_params,
110
+ max_fee_micro_algos=parameters.max_fee_micro_algos,
111
+ fee_micro_algos=parameters.fee_micro_algos,
112
+ ),
113
+ )
114
+ transaction_id = response.get_txid() # type: ignore[no-untyped-call]
115
+ return EnsureFundedResponse(transaction_id=transaction_id, amount=response.amt)
116
+
117
+
53
118
  def ensure_funded(
54
- client: "AlgodClient",
119
+ client: AlgodClient,
55
120
  parameters: EnsureBalanceParameters,
56
- ) -> None | PaymentTxn:
121
+ ) -> EnsureFundedResponse | None:
57
122
  """
58
123
  Funds a given account using a funding source such that it has a certain amount of algos free to spend
59
124
  (accounting for ALGOs locked in minimum balance requirement)
60
125
  see <https://developer.algorand.org/docs/get-details/accounts/#minimum-balance>
61
126
 
62
- :return None | PaymentTxn: None if balance was sufficient or the payment transaction used to increase the balance
63
- """
64
- address_to_fund = (
65
- parameters.account_to_fund
66
- if isinstance(parameters.account_to_fund, str)
67
- else address_from_private_key(parameters.account_to_fund.private_key) # type: ignore[no-untyped-call]
68
- )
69
127
 
70
- account_info = client.account_info(address_to_fund)
71
- assert isinstance(account_info, dict)
128
+ Args:
129
+ client (AlgodClient): An instance of the AlgodClient class from the AlgoSDK library.
130
+ parameters (EnsureBalanceParameters): An instance of the EnsureBalanceParameters class that
131
+ specifies the account to fund and the minimum spending balance.
72
132
 
133
+ Returns:
134
+ PaymentTxn | str | None: If funds are needed, the function returns a payment transaction or a
135
+ string indicating that the dispenser API was used. If no funds are needed, the function returns None.
136
+ """
137
+
138
+ address_to_fund = _get_address_to_fund(parameters)
139
+ account_info = _get_account_info(client, address_to_fund)
73
140
  balance_micro_algos = account_info.get("amount", 0)
74
- minimum_balance_micro_algos = account_info.get("min-balance")
141
+ minimum_balance_micro_algos = account_info.get("min-balance", 0)
75
142
  current_spending_balance_micro_algos = balance_micro_algos - minimum_balance_micro_algos
76
- if parameters.min_spending_balance_micro_algos > current_spending_balance_micro_algos:
77
- funding_source = parameters.funding_source or get_dispenser_account(client)
78
- sender_address = address_from_private_key(funding_source.private_key) # type: ignore[no-untyped-call]
143
+ fund_amount_micro_algos = _calculate_fund_amount(parameters, current_spending_balance_micro_algos)
79
144
 
80
- min_fund_amount_micro_algos = parameters.min_spending_balance_micro_algos - current_spending_balance_micro_algos
81
- fund_amount_micro_algos = max(min_fund_amount_micro_algos, parameters.min_funding_increment_micro_algos)
82
- logger.info(
83
- f"Funding {address_to_fund} {fund_amount_micro_algos}µ from {sender_address} to reach "
84
- f"minimum spend amount of {parameters.min_spending_balance_micro_algos}µ (balance = "
85
- f"{balance_micro_algos}µ, requirement = {minimum_balance_micro_algos}µ)"
86
- )
87
- return transfer(
88
- client,
89
- TransferParameters(
90
- from_account=funding_source,
91
- to_address=address_to_fund,
92
- micro_algos=fund_amount_micro_algos,
93
- note=parameters.note or "Funding account to meet minimum requirement",
94
- suggested_params=parameters.suggested_params,
95
- max_fee_micro_algos=parameters.max_fee_micro_algos,
96
- fee_micro_algos=parameters.fee_micro_algos,
97
- ),
98
- )
145
+ if fund_amount_micro_algos is not None:
146
+ if is_testnet(client) and isinstance(parameters.funding_source, DispenserApiTestnetClient):
147
+ return _fund_using_dispenser_api(parameters.funding_source, address_to_fund, fund_amount_micro_algos)
148
+ else:
149
+ return _fund_using_transfer(client, parameters, address_to_fund, fund_amount_micro_algos)
99
150
 
100
151
  return None
@@ -4,7 +4,6 @@ import json
4
4
  import logging
5
5
  import re
6
6
  import typing
7
- from http import HTTPStatus
8
7
  from math import ceil
9
8
  from pathlib import Path
10
9
  from typing import Any, Literal, cast, overload
@@ -19,6 +18,7 @@ from algosdk.atomic_transaction_composer import (
19
18
  AccountTransactionSigner,
20
19
  AtomicTransactionComposer,
21
20
  AtomicTransactionResponse,
21
+ EmptySigner,
22
22
  LogicSigTransactionSigner,
23
23
  MultisigTransactionSigner,
24
24
  SimulateAtomicTransactionResponse,
@@ -26,13 +26,13 @@ from algosdk.atomic_transaction_composer import (
26
26
  TransactionWithSigner,
27
27
  )
28
28
  from algosdk.constants import APP_PAGE_MAX_SIZE
29
- from algosdk.error import AlgodHTTPError
30
29
  from algosdk.logic import get_application_address
31
30
  from algosdk.source_map import SourceMap
31
+ from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig
32
32
 
33
33
  import algokit_utils.application_specification as au_spec
34
34
  import algokit_utils.deploy as au_deploy
35
- from algokit_utils._simulate_315_compat import simulate_atc_315
35
+ from algokit_utils.config import config
36
36
  from algokit_utils.logic_error import LogicError, parse_logic_error
37
37
  from algokit_utils.models import (
38
38
  ABIArgsDict,
@@ -56,6 +56,7 @@ if typing.TYPE_CHECKING:
56
56
 
57
57
  logger = logging.getLogger(__name__)
58
58
 
59
+
59
60
  """A dictionary `dict[str, Any]` representing ABI argument names and values"""
60
61
 
61
62
  __all__ = [
@@ -169,7 +170,6 @@ class ApplicationClient:
169
170
  self._approval_program: Program | None = None
170
171
  self._approval_source_map: SourceMap | None = None
171
172
  self._clear_program: Program | None = None
172
- self._use_simulate_315 = False # flag to determine if old simulate 3.15 encoding should be used
173
173
 
174
174
  self.template_values: au_deploy.TemplateValueMapping = template_values or {}
175
175
  self.existing_deployments = existing_deployments
@@ -870,36 +870,22 @@ class ApplicationClient:
870
870
  def _simulate_readonly_call(
871
871
  self, method: Method, atc: AtomicTransactionComposer
872
872
  ) -> ABITransactionResponse | TransactionResponse:
873
- simulate_response = self._simulate_atc(atc)
873
+ simulate_response = _simulate_response(atc, self.algod_client)
874
+ traces = None
875
+ if config.debug:
876
+ traces = _create_simulate_traces(simulate_response)
874
877
  if simulate_response.failure_message:
875
878
  raise _try_convert_to_logic_error(
876
879
  simulate_response.failure_message,
877
880
  self.app_spec.approval_program,
878
881
  self._get_approval_source_map,
882
+ traces,
879
883
  ) or Exception(
880
884
  f"Simulate failed for readonly method {method.get_signature()}: {simulate_response.failure_message}"
881
885
  )
882
886
 
883
887
  return TransactionResponse.from_atr(simulate_response)
884
888
 
885
- def _simulate_atc(self, atc: AtomicTransactionComposer) -> SimulateAtomicTransactionResponse:
886
- # TODO: remove this once 3.16 is in mainnet
887
- # there was a breaking change in algod 3.16 to the simulate endpoint
888
- # attempt to transparently handle this by calling the endpoint with the old behaviour if
889
- # 3.15 is detected
890
- if self._use_simulate_315:
891
- return simulate_atc_315(atc, self.algod_client)
892
- try:
893
- return atc.simulate(self.algod_client)
894
- except AlgodHTTPError as ex:
895
- if ex.code == HTTPStatus.BAD_REQUEST.value and (
896
- "msgpack decode error [pos 12]: no matching struct field found when decoding stream map with key "
897
- "txn-groups" in ex.args
898
- ):
899
- self._use_simulate_315 = True
900
- return simulate_atc_315(atc, self.algod_client)
901
- raise ex
902
-
903
889
  def _load_reference_and_check_app_id(self) -> None:
904
890
  self._load_app_reference()
905
891
  self._check_app_id()
@@ -1244,6 +1230,7 @@ def _try_convert_to_logic_error(
1244
1230
  source_ex: Exception | str,
1245
1231
  approval_program: str,
1246
1232
  approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None,
1233
+ simulate_traces: list | None = None,
1247
1234
  ) -> Exception | None:
1248
1235
  source_ex_str = str(source_ex)
1249
1236
  logic_error_data = parse_logic_error(source_ex_str)
@@ -1254,6 +1241,7 @@ def _try_convert_to_logic_error(
1254
1241
  program=approval_program,
1255
1242
  source_map=approval_source_map() if callable(approval_source_map) else approval_source_map,
1256
1243
  **logic_error_data,
1244
+ traces=simulate_traces,
1257
1245
  )
1258
1246
 
1259
1247
  return None
@@ -1277,12 +1265,56 @@ def execute_atc_with_logic_error(
1277
1265
  try:
1278
1266
  return atc.execute(algod_client, wait_rounds=wait_rounds)
1279
1267
  except Exception as ex:
1280
- logic_error = _try_convert_to_logic_error(ex, approval_program, approval_source_map)
1268
+ if config.debug:
1269
+ simulate = _simulate_response(atc, algod_client)
1270
+ traces = _create_simulate_traces(simulate)
1271
+ else:
1272
+ traces = None
1273
+ logger.info("An error occurred while executing the transaction.")
1274
+ logger.info("To see more details, enable debug mode by setting config.debug = True ")
1275
+
1276
+ logic_error = _try_convert_to_logic_error(ex, approval_program, approval_source_map, traces)
1281
1277
  if logic_error:
1282
1278
  raise logic_error from ex
1283
1279
  raise ex
1284
1280
 
1285
1281
 
1282
+ def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list[dict[str, Any]]:
1283
+ traces = []
1284
+ if hasattr(simulate, "simulate_response") and hasattr(simulate, "failed_at") and simulate.failed_at:
1285
+ for txn_group in simulate.simulate_response["txn-groups"]:
1286
+ app_budget_added = txn_group.get("app-budget-added", None)
1287
+ app_budget_consumed = txn_group.get("app-budget-consumed", None)
1288
+ failure_message = txn_group.get("failure-message", None)
1289
+ txn_result = txn_group.get("txn-results", [{}])[0]
1290
+ exec_trace = txn_result.get("exec-trace", {})
1291
+ traces.append(
1292
+ {
1293
+ "app-budget-added": app_budget_added,
1294
+ "app-budget-consumed": app_budget_consumed,
1295
+ "failure-message": failure_message,
1296
+ "exec-trace": exec_trace,
1297
+ }
1298
+ )
1299
+ return traces
1300
+
1301
+
1302
+ def _simulate_response(
1303
+ atc: AtomicTransactionComposer, algod_client: "AlgodClient"
1304
+ ) -> SimulateAtomicTransactionResponse:
1305
+ unsigned_txn_groups = atc.build_group()
1306
+ empty_signer = EmptySigner()
1307
+ txn_list = [txn_group.txn for txn_group in unsigned_txn_groups]
1308
+ fake_signed_transactions = empty_signer.sign_transactions(txn_list, [])
1309
+ txn_group = [SimulateRequestTransactionGroup(txns=fake_signed_transactions)]
1310
+ trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True)
1311
+
1312
+ simulate_request = SimulateRequest(
1313
+ txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config
1314
+ )
1315
+ return atc.simulate(algod_client, simulate_request)
1316
+
1317
+
1286
1318
  def _convert_transaction_parameters(
1287
1319
  args: TransactionParameters | TransactionParametersDict | None,
1288
1320
  ) -> CreateCallParameters:
@@ -0,0 +1,25 @@
1
+ from collections.abc import Callable
2
+
3
+
4
+ class UpdatableConfig:
5
+ def __init__(self) -> None:
6
+ self._debug: bool = False
7
+
8
+ @property
9
+ def debug(self) -> bool:
10
+ return self._debug
11
+
12
+ def with_debug(self, lambda_func: Callable[[], None | str]) -> None:
13
+ original = self._debug
14
+ try:
15
+ self._debug = True
16
+ lambda_func()
17
+ finally:
18
+ self._debug = original
19
+
20
+ def configure(self, *, debug: bool) -> None:
21
+ if debug is not None:
22
+ self._debug = debug
23
+
24
+
25
+ config = UpdatableConfig()
@@ -0,0 +1,178 @@
1
+ import contextlib
2
+ import enum
3
+ import logging
4
+ import os
5
+ from dataclasses import dataclass
6
+
7
+ import httpx
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class DispenserApiConfig:
13
+ BASE_URL = "https://api.dispenser.algorandfoundation.tools"
14
+
15
+
16
+ class DispenserAssetName(enum.IntEnum):
17
+ ALGO = 0
18
+
19
+
20
+ @dataclass
21
+ class DispenserAsset:
22
+ asset_id: int
23
+ decimals: int
24
+ description: str
25
+
26
+
27
+ @dataclass
28
+ class DispenserFundResponse:
29
+ tx_id: str
30
+ amount: int
31
+
32
+
33
+ @dataclass
34
+ class DispenserLimitResponse:
35
+ amount: int
36
+
37
+
38
+ DISPENSER_ASSETS = {
39
+ DispenserAssetName.ALGO: DispenserAsset(
40
+ asset_id=0,
41
+ decimals=6,
42
+ description="Algo",
43
+ ),
44
+ }
45
+ DISPENSER_REQUEST_TIMEOUT = 15
46
+ DISPENSER_ACCESS_TOKEN_KEY = "ALGOKIT_DISPENSER_ACCESS_TOKEN"
47
+
48
+
49
+ class DispenserApiTestnetClient:
50
+ """
51
+ Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md).
52
+ To get started create a new access token via `algokit dispenser login --ci`
53
+ and pass it to the client constructor as `auth_token`.
54
+ Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`,
55
+ and it will be auto loaded. If both are set, the constructor argument takes precedence.
56
+
57
+ Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor.
58
+ """
59
+
60
+ auth_token: str
61
+ request_timeout = DISPENSER_REQUEST_TIMEOUT
62
+
63
+ def __init__(self, auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT):
64
+ auth_token_from_env = os.getenv(DISPENSER_ACCESS_TOKEN_KEY)
65
+
66
+ if auth_token:
67
+ self.auth_token = auth_token
68
+ elif auth_token_from_env:
69
+ self.auth_token = auth_token_from_env
70
+ else:
71
+ raise Exception(
72
+ f"Can't init AlgoKit TestNet Dispenser API client "
73
+ f"because neither environment variable {DISPENSER_ACCESS_TOKEN_KEY} or "
74
+ "the auth_token were provided."
75
+ )
76
+
77
+ self.request_timeout = request_timeout
78
+
79
+ def _process_dispenser_request(
80
+ self, *, auth_token: str, url_suffix: str, data: dict | None = None, method: str = "POST"
81
+ ) -> httpx.Response:
82
+ """
83
+ Generalized method to process http requests to dispenser API
84
+ """
85
+
86
+ headers = {"Authorization": f"Bearer {(auth_token)}"}
87
+
88
+ # Set request arguments
89
+ request_args = {
90
+ "url": f"{DispenserApiConfig.BASE_URL}/{url_suffix}",
91
+ "headers": headers,
92
+ "timeout": self.request_timeout,
93
+ }
94
+
95
+ if method.upper() != "GET" and data is not None:
96
+ request_args["json"] = data
97
+
98
+ try:
99
+ response: httpx.Response = getattr(httpx, method.lower())(**request_args)
100
+ response.raise_for_status()
101
+ return response
102
+
103
+ except httpx.HTTPStatusError as err:
104
+ error_message = f"Error processing dispenser API request: {err.response.status_code}"
105
+ error_response = None
106
+ with contextlib.suppress(Exception):
107
+ error_response = err.response.json()
108
+
109
+ if error_response and error_response.get("code"):
110
+ error_message = error_response.get("code")
111
+
112
+ elif err.response.status_code == httpx.codes.BAD_REQUEST:
113
+ error_message = err.response.json()["message"]
114
+
115
+ raise Exception(error_message) from err
116
+
117
+ except Exception as err:
118
+ error_message = "Error processing dispenser API request"
119
+ logger.debug(f"{error_message}: {err}", exc_info=True)
120
+ raise err
121
+
122
+ def fund(self, address: str, amount: int, asset_id: int) -> DispenserFundResponse:
123
+ """
124
+ Fund an account with Algos from the dispenser API
125
+ """
126
+
127
+ try:
128
+ response = self._process_dispenser_request(
129
+ auth_token=self.auth_token,
130
+ url_suffix=f"fund/{asset_id}",
131
+ data={"receiver": address, "amount": amount, "assetID": asset_id},
132
+ method="POST",
133
+ )
134
+
135
+ content = response.json()
136
+ return DispenserFundResponse(tx_id=content["txID"], amount=content["amount"])
137
+
138
+ except Exception as err:
139
+ logger.exception(f"Error funding account {address}: {err}")
140
+ raise err
141
+
142
+ def refund(self, refund_txn_id: str) -> None:
143
+ """
144
+ Register a refund for a transaction with the dispenser API
145
+ """
146
+
147
+ try:
148
+ self._process_dispenser_request(
149
+ auth_token=self.auth_token,
150
+ url_suffix="refund",
151
+ data={"refundTransactionID": refund_txn_id},
152
+ method="POST",
153
+ )
154
+
155
+ except Exception as err:
156
+ logger.exception(f"Error issuing refund for txn_id {refund_txn_id}: {err}")
157
+ raise err
158
+
159
+ def limit(
160
+ self,
161
+ address: str,
162
+ ) -> DispenserLimitResponse:
163
+ """
164
+ Get current limit for an account with Algos from the dispenser API
165
+ """
166
+
167
+ try:
168
+ response = self._process_dispenser_request(
169
+ auth_token=self.auth_token,
170
+ url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}/limit",
171
+ method="GET",
172
+ )
173
+ content = response.json()
174
+
175
+ return DispenserLimitResponse(amount=content["amount"])
176
+ except Exception as err:
177
+ logger.exception(f"Error setting limit for account {address}: {err}")
178
+ raise err
@@ -47,6 +47,7 @@ class LogicError(Exception):
47
47
  message: str,
48
48
  pc: int,
49
49
  logic_error: Exception | None = None,
50
+ traces: list | None = None,
50
51
  ):
51
52
  self.logic_error = logic_error
52
53
  self.logic_error_str = logic_error_str
@@ -56,6 +57,7 @@ class LogicError(Exception):
56
57
  self.transaction_id = transaction_id
57
58
  self.message = message
58
59
  self.pc = pc
60
+ self.traces = traces
59
61
 
60
62
  self.line_no = self.source_map.get_line_for_pc(self.pc) if self.source_map else None
61
63
 
@@ -87,13 +87,13 @@ def is_localnet(client: AlgodClient) -> bool:
87
87
  def is_mainnet(client: AlgodClient) -> bool:
88
88
  """Returns True if client genesis is `mainnet-v1`"""
89
89
  params = client.suggested_params()
90
- return bool(params.gen == "mainnet-v1")
90
+ return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"]
91
91
 
92
92
 
93
93
  def is_testnet(client: AlgodClient) -> bool:
94
94
  """Returns True if client genesis is `testnet-v1`"""
95
95
  params = client.suggested_params()
96
- return bool(params.gen == "testnet-v1")
96
+ return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"]
97
97
 
98
98
 
99
99
  def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient:
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: algokit-utils
3
- Version: 1.4.0b1
3
+ Version: 2.0.0b1
4
4
  Summary: Utilities for Algorand development for use by AlgoKit
5
5
  License: MIT
6
6
  Author: Algorand Foundation
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Requires-Dist: deprecated (>=1.2.14,<2.0.0)
14
+ Requires-Dist: httpx (>=0.23.1,<0.24.0)
14
15
  Requires-Dist: py-algorand-sdk (>=2.4.0,<3.0.0)
15
16
  Description-Content-Type: text/markdown
16
17
 
@@ -0,0 +1,18 @@
1
+ algokit_utils/__init__.py,sha256=zmNphGblbOciVTNTuNuovvIl64L2ue2kAiU_umwrPIw,4633
2
+ algokit_utils/_ensure_funded.py,sha256=4KeLe76KGl01Z7Q8zpdy3d59lSPnEXmEZGKQceFDEB8,6786
3
+ algokit_utils/_transfer.py,sha256=CyXGOR_Zy-2crQhk-78uUbB8Sj_ZeTzxPwOAHU7wwno,5947
4
+ algokit_utils/account.py,sha256=UIuOQZe28pQxjEP9TzhtYlOU20tUdzzS-nIIZM9Bp6Y,7364
5
+ algokit_utils/application_client.py,sha256=9YH4ecHsn0aXmDeaApraT5WeHaUlN4FPa27QdtN9tew,57878
6
+ algokit_utils/application_specification.py,sha256=XusOe7VrGPun2UoNspC9Ei202NzPkxRNx5USXiABuXc,7466
7
+ algokit_utils/config.py,sha256=V8010eUkbfcoB0bHtxsGQOymq1cqNRc1lDWnwcumQpM,567
8
+ algokit_utils/deploy.py,sha256=sY6u0T39DuF6oLpal0eJAc76EmjPWdoCPk2OSKGccnM,34650
9
+ algokit_utils/dispenser_api.py,sha256=eU2R37aAF5w06GBJx7XrBjriFrDe63jyrEECCHOreHo,5580
10
+ algokit_utils/logic_error.py,sha256=vkLVdxv-XPnwRiIP4CWedgtIZcOPr_5wr7XVH02If9w,2615
11
+ algokit_utils/models.py,sha256=KynZnM2YbOyTgr2NCT8CA-cYrO0eiyK6u48eeAzj82I,8246
12
+ algokit_utils/network_clients.py,sha256=sj5y_g5uclddWCEyUCptA-KjWuAtLV06hZH4QIGM1yE,5313
13
+ algokit_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ algokit_utils/test_dispenser_api.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ algokit_utils-2.0.0b1.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
16
+ algokit_utils-2.0.0b1.dist-info/METADATA,sha256=k_uevqLR0z0cRHwyJf-qk3oasQyTyHDBPI2PSjFJDno,2156
17
+ algokit_utils-2.0.0b1.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
18
+ algokit_utils-2.0.0b1.dist-info/RECORD,,
@@ -1,73 +0,0 @@
1
- import base64
2
- from typing import Any
3
-
4
- from algosdk import encoding
5
- from algosdk.atomic_transaction_composer import (
6
- AtomicTransactionComposer,
7
- AtomicTransactionComposerStatus,
8
- SimulateABIResult,
9
- SimulateAtomicTransactionResponse,
10
- )
11
- from algosdk.error import AtomicTransactionComposerError
12
- from algosdk.v2client.algod import AlgodClient
13
-
14
-
15
- def simulate_atc_315(atc: AtomicTransactionComposer, client: AlgodClient) -> SimulateAtomicTransactionResponse:
16
- """
17
- Ported from algosdk 2.1.2
18
-
19
- Send the transaction group to the `simulate` endpoint and wait for results.
20
- An error will be thrown if submission or execution fails.
21
- The composer's status must be SUBMITTED or lower before calling this method,
22
- since execution is only allowed once.
23
-
24
- Returns:
25
- SimulateAtomicTransactionResponse: Object with simulation results for this
26
- transaction group, a list of txIDs of the simulated transactions,
27
- an array of results for each method call transaction in this group.
28
- If a method has no return value (void), then the method results array
29
- will contain None for that method's return value.
30
- """
31
-
32
- if atc.status > AtomicTransactionComposerStatus.SUBMITTED:
33
- raise AtomicTransactionComposerError( # type: ignore[no-untyped-call]
34
- "AtomicTransactionComposerStatus must be submitted or lower to simulate a group"
35
- )
36
-
37
- signed_txns = atc.gather_signatures()
38
- txn = b"".join(
39
- base64.b64decode(encoding.msgpack_encode(txn)) for txn in signed_txns # type: ignore[no-untyped-call]
40
- )
41
- simulation_result = client.algod_request(
42
- "POST", "/transactions/simulate", data=txn, headers={"Content-Type": "application/x-binary"}
43
- )
44
- assert isinstance(simulation_result, dict)
45
-
46
- # Only take the first group in the simulate response
47
- txn_group: dict[str, Any] = simulation_result["txn-groups"][0]
48
- txn_results = txn_group["txn-results"]
49
-
50
- # Parse out abi results
51
- results = []
52
- for method_index, method in atc.method_dict.items():
53
- tx_info = txn_results[method_index]["txn-result"]
54
-
55
- result = atc.parse_result(method, atc.tx_ids[method_index], tx_info)
56
- sim_result = SimulateABIResult(
57
- tx_id=result.tx_id,
58
- raw_value=result.raw_value,
59
- return_value=result.return_value,
60
- decode_error=result.decode_error,
61
- tx_info=result.tx_info,
62
- method=result.method,
63
- )
64
- results.append(sim_result)
65
-
66
- return SimulateAtomicTransactionResponse(
67
- version=simulation_result.get("version", 0),
68
- failure_message=txn_group.get("failure-message", ""),
69
- failed_at=txn_group.get("failed-at"),
70
- simulate_response=simulation_result,
71
- tx_ids=atc.tx_ids,
72
- results=results,
73
- )
@@ -1,16 +0,0 @@
1
- algokit_utils/__init__.py,sha256=KIqgpe_cj8Bc7QZIC2jNKSZj5nfKef4aZ8KPaiiq_Sw,4214
2
- algokit_utils/_ensure_funded.py,sha256=DjwGnCC_6USLQV5wIMJZRVQFlQ1uLrGDMxRF471atsQ,4410
3
- algokit_utils/_simulate_315_compat.py,sha256=9qCsNnKa1FXYfCccMFiE0mGEcZJiBuPmUy7ZRvvUSqU,2841
4
- algokit_utils/_transfer.py,sha256=CyXGOR_Zy-2crQhk-78uUbB8Sj_ZeTzxPwOAHU7wwno,5947
5
- algokit_utils/account.py,sha256=UIuOQZe28pQxjEP9TzhtYlOU20tUdzzS-nIIZM9Bp6Y,7364
6
- algokit_utils/application_client.py,sha256=4_NM-RJYMCRCS4_4xFeG68yYvgTJRC1_y9mSCgYHSR4,56581
7
- algokit_utils/application_specification.py,sha256=XusOe7VrGPun2UoNspC9Ei202NzPkxRNx5USXiABuXc,7466
8
- algokit_utils/deploy.py,sha256=sY6u0T39DuF6oLpal0eJAc76EmjPWdoCPk2OSKGccnM,34650
9
- algokit_utils/logic_error.py,sha256=8O_4rJ1t57JEG81ucRNih2ojc1-EOm2fVxW6m-1ZXI8,2550
10
- algokit_utils/models.py,sha256=KynZnM2YbOyTgr2NCT8CA-cYrO0eiyK6u48eeAzj82I,8246
11
- algokit_utils/network_clients.py,sha256=KmuSHG2kSdJfo9W4pIB_4RjnBL2yMQNGlF54lxXTnsQ,5267
12
- algokit_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- algokit_utils-1.4.0b1.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
14
- algokit_utils-1.4.0b1.dist-info/METADATA,sha256=e0MIdb_WsahZulVVal3VP1udooOqOZafTHND30_Ei58,2116
15
- algokit_utils-1.4.0b1.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
16
- algokit_utils-1.4.0b1.dist-info/RECORD,,