algokit-utils 1.4.0b2__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
@@ -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
@@ -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.0b2
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
 
@@ -1,16 +1,18 @@
1
- algokit_utils/__init__.py,sha256=KIqgpe_cj8Bc7QZIC2jNKSZj5nfKef4aZ8KPaiiq_Sw,4214
2
- algokit_utils/_ensure_funded.py,sha256=DjwGnCC_6USLQV5wIMJZRVQFlQ1uLrGDMxRF471atsQ,4410
1
+ algokit_utils/__init__.py,sha256=zmNphGblbOciVTNTuNuovvIl64L2ue2kAiU_umwrPIw,4633
2
+ algokit_utils/_ensure_funded.py,sha256=4KeLe76KGl01Z7Q8zpdy3d59lSPnEXmEZGKQceFDEB8,6786
3
3
  algokit_utils/_transfer.py,sha256=CyXGOR_Zy-2crQhk-78uUbB8Sj_ZeTzxPwOAHU7wwno,5947
4
4
  algokit_utils/account.py,sha256=UIuOQZe28pQxjEP9TzhtYlOU20tUdzzS-nIIZM9Bp6Y,7364
5
5
  algokit_utils/application_client.py,sha256=9YH4ecHsn0aXmDeaApraT5WeHaUlN4FPa27QdtN9tew,57878
6
6
  algokit_utils/application_specification.py,sha256=XusOe7VrGPun2UoNspC9Ei202NzPkxRNx5USXiABuXc,7466
7
7
  algokit_utils/config.py,sha256=V8010eUkbfcoB0bHtxsGQOymq1cqNRc1lDWnwcumQpM,567
8
8
  algokit_utils/deploy.py,sha256=sY6u0T39DuF6oLpal0eJAc76EmjPWdoCPk2OSKGccnM,34650
9
+ algokit_utils/dispenser_api.py,sha256=eU2R37aAF5w06GBJx7XrBjriFrDe63jyrEECCHOreHo,5580
9
10
  algokit_utils/logic_error.py,sha256=vkLVdxv-XPnwRiIP4CWedgtIZcOPr_5wr7XVH02If9w,2615
10
11
  algokit_utils/models.py,sha256=KynZnM2YbOyTgr2NCT8CA-cYrO0eiyK6u48eeAzj82I,8246
11
- algokit_utils/network_clients.py,sha256=KmuSHG2kSdJfo9W4pIB_4RjnBL2yMQNGlF54lxXTnsQ,5267
12
+ algokit_utils/network_clients.py,sha256=sj5y_g5uclddWCEyUCptA-KjWuAtLV06hZH4QIGM1yE,5313
12
13
  algokit_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- algokit_utils-1.4.0b2.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
14
- algokit_utils-1.4.0b2.dist-info/METADATA,sha256=SjLMFQaF5wK-8SFEWMx5LhfBHwY7snuPjluwKhBjdM4,2116
15
- algokit_utils-1.4.0b2.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
16
- algokit_utils-1.4.0b2.dist-info/RECORD,,
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,,