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 +18 -1
- algokit_utils/_ensure_funded.py +98 -47
- algokit_utils/application_client.py +56 -24
- algokit_utils/config.py +25 -0
- algokit_utils/dispenser_api.py +178 -0
- algokit_utils/logic_error.py +2 -0
- algokit_utils/network_clients.py +2 -2
- algokit_utils/test_dispenser_api.py +0 -0
- {algokit_utils-1.4.0b1.dist-info → algokit_utils-2.0.0b1.dist-info}/METADATA +2 -1
- algokit_utils-2.0.0b1.dist-info/RECORD +18 -0
- algokit_utils/_simulate_315_compat.py +0 -73
- algokit_utils-1.4.0b1.dist-info/RECORD +0 -16
- {algokit_utils-1.4.0b1.dist-info → algokit_utils-2.0.0b1.dist-info}/LICENSE +0 -0
- {algokit_utils-1.4.0b1.dist-info → algokit_utils-2.0.0b1.dist-info}/WHEEL +0 -0
algokit_utils/__init__.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from algokit_utils._ensure_funded import
|
|
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",
|
algokit_utils/_ensure_funded.py
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
import
|
|
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
|
|
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:
|
|
119
|
+
client: AlgodClient,
|
|
55
120
|
parameters: EnsureBalanceParameters,
|
|
56
|
-
) ->
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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:
|
algokit_utils/config.py
ADDED
|
@@ -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
|
algokit_utils/logic_error.py
CHANGED
|
@@ -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
|
|
algokit_utils/network_clients.py
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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,,
|
|
File without changes
|
|
File without changes
|