algokit-utils 3.0.0b3__py3-none-any.whl → 3.0.0b5__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.

@@ -5,7 +5,7 @@ import copy
5
5
  import json
6
6
  import os
7
7
  from collections.abc import Sequence
8
- from dataclasses import asdict, dataclass, fields
8
+ from dataclasses import asdict, dataclass, fields, replace
9
9
  from typing import TYPE_CHECKING, Any, Generic, Literal, TypedDict, TypeVar
10
10
 
11
11
  import algosdk
@@ -54,6 +54,7 @@ from algokit_utils.transactions.transaction_composer import (
54
54
  AppUpdateParams,
55
55
  BuiltTransactions,
56
56
  PaymentParams,
57
+ SendAtomicTransactionComposerResults,
57
58
  )
58
59
  from algokit_utils.transactions.transaction_sender import (
59
60
  SendAppTransactionResult,
@@ -76,7 +77,6 @@ __all__ = [
76
77
  "AppClient",
77
78
  "AppClientBareCallCreateParams",
78
79
  "AppClientBareCallParams",
79
- "AppClientCallParams",
80
80
  "AppClientCompilationParams",
81
81
  "AppClientCompilationResult",
82
82
  "AppClientCreateSchema",
@@ -85,6 +85,8 @@ __all__ = [
85
85
  "AppClientParams",
86
86
  "AppSourceMaps",
87
87
  "BaseAppClientMethodCallParams",
88
+ "CommonAppCallCreateParams",
89
+ "CommonAppCallParams",
88
90
  "CreateOnComplete",
89
91
  "FundAppAccountParams",
90
92
  "get_constant_block_offset",
@@ -201,106 +203,35 @@ class AppClientCompilationParams(TypedDict, total=False):
201
203
  deletable: bool | None
202
204
 
203
205
 
204
- @dataclass(kw_only=True)
205
- class FundAppAccountParams:
206
- """Parameters for funding an application's account.
207
-
208
- :ivar sender: Optional sender address
209
- :ivar signer: Optional transaction signer
210
- :ivar rekey_to: Optional address to rekey to
211
- :ivar note: Optional transaction note
212
- :ivar lease: Optional lease
213
- :ivar static_fee: Optional static fee
214
- :ivar extra_fee: Optional extra fee
215
- :ivar max_fee: Optional maximum fee
216
- :ivar validity_window: Optional validity window in rounds
217
- :ivar first_valid_round: Optional first valid round
218
- :ivar last_valid_round: Optional last valid round
219
- :ivar amount: Amount to fund
220
- :ivar close_remainder_to: Optional address to close remainder to
221
- :ivar on_complete: Optional on complete action
222
- """
223
-
224
- sender: str | None = None
225
- signer: TransactionSigner | None = None
226
- rekey_to: str | None = None
227
- note: bytes | None = None
228
- lease: bytes | None = None
229
- static_fee: AlgoAmount | None = None
230
- extra_fee: AlgoAmount | None = None
231
- max_fee: AlgoAmount | None = None
232
- validity_window: int | None = None
233
- first_valid_round: int | None = None
234
- last_valid_round: int | None = None
235
- amount: AlgoAmount
236
- close_remainder_to: str | None = None
237
- on_complete: algosdk.transaction.OnComplete | None = None
238
-
239
-
240
- @dataclass(kw_only=True)
241
- class AppClientCallParams:
242
- """Parameters for calling an application.
243
-
244
- :ivar method: Optional ABI method name or signature
245
- :ivar args: Optional arguments to pass to method
246
- :ivar boxes: Optional box references to load
247
- :ivar accounts: Optional account addresses to load
248
- :ivar apps: Optional app IDs to load
249
- :ivar assets: Optional asset IDs to load
250
- :ivar lease: Optional lease
251
- :ivar sender: Optional sender address
252
- :ivar note: Optional transaction note
253
- :ivar send_params: Optional parameters to control transaction sending
254
- """
255
-
256
- method: str | None = None
257
- args: list | None = None
258
- boxes: list | None = None
259
- accounts: list[str] | None = None
260
- apps: list[int] | None = None
261
- assets: list[int] | None = None
262
- lease: (str | bytes) | None = None
263
- sender: str | None = None
264
- note: (bytes | dict | str) | None = None
265
- send_params: dict | None = None
266
-
267
-
268
206
  ArgsT = TypeVar("ArgsT")
269
207
  MethodT = TypeVar("MethodT")
270
208
 
271
209
 
272
210
  @dataclass(kw_only=True, frozen=True)
273
- class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT]):
274
- """Base parameters for application method calls.
211
+ class CommonAppCallParams:
212
+ """Common configuration for app call transaction parameters
213
+
214
+ :ivar account_references: List of account addresses to reference
215
+ :ivar app_references: List of app IDs to reference
216
+ :ivar asset_references: List of asset IDs to reference
217
+ :ivar box_references: List of box references to include
218
+ :ivar extra_fee: Additional fee to add to transaction
219
+ :ivar lease: Transaction lease value
220
+ :ivar max_fee: Maximum fee allowed for transaction
221
+ :ivar note: Arbitrary note for the transaction
222
+ :ivar rekey_to: Address to rekey account to
223
+ :ivar sender: Sender address override
224
+ :ivar signer: Custom transaction signer
225
+ :ivar static_fee: Fixed fee for transaction
226
+ :ivar validity_window: Number of rounds valid
227
+ :ivar first_valid_round: First valid round number
228
+ :ivar last_valid_round: Last valid round number"""
275
229
 
276
- :ivar method: Method to call
277
- :ivar args: Optional arguments to pass to method
278
- :ivar account_references: Optional account references
279
- :ivar app_references: Optional application references
280
- :ivar asset_references: Optional asset references
281
- :ivar box_references: Optional box references
282
- :ivar extra_fee: Optional extra fee
283
- :ivar first_valid_round: Optional first valid round
284
- :ivar lease: Optional lease
285
- :ivar max_fee: Optional maximum fee
286
- :ivar note: Optional note
287
- :ivar rekey_to: Optional rekey to address
288
- :ivar sender: Optional sender address
289
- :ivar signer: Optional transaction signer
290
- :ivar static_fee: Optional static fee
291
- :ivar validity_window: Optional validity window
292
- :ivar last_valid_round: Optional last valid round
293
- :ivar on_complete: Optional on complete action
294
- """
295
-
296
- method: MethodT
297
- args: ArgsT | None = None
298
230
  account_references: list[str] | None = None
299
231
  app_references: list[int] | None = None
300
232
  asset_references: list[int] | None = None
301
- box_references: Sequence[BoxReference | BoxIdentifier] | None = None
233
+ box_references: list[BoxReference | BoxIdentifier] | None = None
302
234
  extra_fee: AlgoAmount | None = None
303
- first_valid_round: int | None = None
304
235
  lease: bytes | None = None
305
236
  max_fee: AlgoAmount | None = None
306
237
  note: bytes | None = None
@@ -309,82 +240,85 @@ class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT]):
309
240
  signer: TransactionSigner | None = None
310
241
  static_fee: AlgoAmount | None = None
311
242
  validity_window: int | None = None
243
+ first_valid_round: int | None = None
312
244
  last_valid_round: int | None = None
313
- on_complete: algosdk.transaction.OnComplete | None = None
245
+ on_complete: OnComplete | None = None
246
+
247
+
248
+ @dataclass(frozen=True)
249
+ class AppClientCreateSchema:
250
+ """Schema for application creation.
251
+
252
+ :ivar extra_program_pages: Optional number of extra program pages
253
+ :ivar schema: Optional application creation schema
254
+ """
255
+
256
+ extra_program_pages: int | None = None
257
+ schema: AppCreateSchema | None = None
314
258
 
315
259
 
316
260
  @dataclass(kw_only=True, frozen=True)
317
- class AppClientMethodCallParams(
318
- BaseAppClientMethodCallParams[
319
- Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None],
320
- str,
321
- ]
322
- ):
323
- """Parameters for application method calls."""
261
+ class CommonAppCallCreateParams(AppClientCreateSchema, CommonAppCallParams):
262
+ """Common configuration for app create call transaction parameters."""
263
+
264
+ on_complete: CreateOnComplete | None = None
324
265
 
325
266
 
326
267
  @dataclass(kw_only=True, frozen=True)
327
- class AppClientBareCallParams:
268
+ class FundAppAccountParams(CommonAppCallParams):
269
+ """Parameters for funding an application's account.
270
+
271
+ :ivar amount: Amount to fund
272
+ :ivar close_remainder_to: Optional address to close remainder to
273
+ """
274
+
275
+ amount: AlgoAmount
276
+ close_remainder_to: str | None = None
277
+
278
+
279
+ @dataclass(kw_only=True, frozen=True)
280
+ class AppClientBareCallParams(CommonAppCallParams):
328
281
  """Parameters for bare application calls.
329
282
 
330
- :ivar signer: Optional transaction signer
331
- :ivar rekey_to: Optional rekey to address
332
- :ivar lease: Optional lease
333
- :ivar static_fee: Optional static fee
334
- :ivar extra_fee: Optional extra fee
335
- :ivar max_fee: Optional maximum fee
336
- :ivar validity_window: Optional validity window
337
- :ivar first_valid_round: Optional first valid round
338
- :ivar last_valid_round: Optional last valid round
339
- :ivar sender: Optional sender address
340
- :ivar note: Optional note
341
283
  :ivar args: Optional arguments
342
- :ivar account_references: Optional account references
343
- :ivar app_references: Optional application references
344
- :ivar asset_references: Optional asset references
345
- :ivar box_references: Optional box references
346
284
  """
347
285
 
348
- signer: TransactionSigner | None = None
349
- rekey_to: str | None = None
350
- lease: bytes | None = None
351
- static_fee: AlgoAmount | None = None
352
- extra_fee: AlgoAmount | None = None
353
- max_fee: AlgoAmount | None = None
354
- validity_window: int | None = None
355
- first_valid_round: int | None = None
356
- last_valid_round: int | None = None
357
- sender: str | None = None
358
- note: bytes | None = None
359
286
  args: list[bytes] | None = None
360
- account_references: list[str] | None = None
361
- app_references: list[int] | None = None
362
- asset_references: list[int] | None = None
363
- box_references: list[BoxReference | BoxIdentifier] | None = None
364
287
 
365
288
 
366
289
  @dataclass(frozen=True)
367
- class AppClientCreateSchema:
368
- """Schema for application creation.
290
+ class AppClientBareCallCreateParams(CommonAppCallCreateParams):
291
+ """Parameters for creating application with bare call."""
369
292
 
370
- :ivar extra_program_pages: Optional number of extra program pages
371
- :ivar schema: Optional application creation schema
372
- """
293
+ on_complete: CreateOnComplete | None = None
373
294
 
374
- extra_program_pages: int | None = None
375
- schema: AppCreateSchema | None = None
376
295
 
296
+ @dataclass(kw_only=True, frozen=True)
297
+ class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT], CommonAppCallParams):
298
+ """Base parameters for application method calls.
377
299
 
378
- @dataclass(frozen=True)
379
- class AppClientBareCallCreateParams(AppClientCreateSchema, AppClientBareCallParams):
380
- """Parameters for creating application with bare call."""
300
+ :ivar method: Method to call
301
+ :ivar args: Optional arguments to pass to method
302
+ :ivar on_complete: Optional on complete action
303
+ """
381
304
 
382
- on_complete: OnComplete | None = None
305
+ method: MethodT
306
+ args: ArgsT | None = None
307
+
308
+
309
+ @dataclass(kw_only=True, frozen=True)
310
+ class AppClientMethodCallParams(
311
+ BaseAppClientMethodCallParams[
312
+ Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None],
313
+ str,
314
+ ]
315
+ ):
316
+ """Parameters for application method calls."""
383
317
 
384
318
 
385
319
  @dataclass(frozen=True)
386
320
  class AppClientMethodCallCreateParams(AppClientCreateSchema, AppClientMethodCallParams):
387
- """Parameters for creating application with method call."""
321
+ """Parameters for creating application with method call"""
388
322
 
389
323
  on_complete: CreateOnComplete | None = None
390
324
 
@@ -1256,28 +1190,51 @@ class _TransactionSender:
1256
1190
  :param params: Parameters for the application call including method and transaction options
1257
1191
  :param send_params: Send parameters
1258
1192
  :return: The result of sending or simulating the transaction, including ABI return value if applicable
1193
+ :raises ValueError: If the transaction is read-only and `max_fee` is not provided
1259
1194
  """
1260
1195
  is_read_only_call = (
1261
1196
  params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None
1262
1197
  ) and self._app_spec.get_arc56_method(params.method).readonly
1263
1198
 
1264
1199
  if is_read_only_call:
1200
+ readonly_params = params
1201
+ readonly_send_params = send_params or SendParams()
1202
+
1203
+ # Read-only calls do not require fees to be paid, as they are only simulated on the network.
1204
+ # Therefore there is no value in calculating the minimum fee needed for a successful app call with inners.
1205
+ # As a a result we only need to send a single simulate call,
1206
+ # however to do this successfully we need to ensure fees for the transaction are fully covered using maxFee.
1207
+ if readonly_send_params.get("cover_app_call_inner_transaction_fees"):
1208
+ if params.max_fee is None:
1209
+ raise ValueError(
1210
+ "Please provide a `max_fee` for the transaction when `cover_app_call_inner_transaction_fees` is enabled." # noqa: E501
1211
+ )
1212
+ readonly_params = replace(readonly_params, static_fee=params.max_fee, extra_fee=None)
1213
+
1265
1214
  method_call_to_simulate = self._algorand.new_group().add_app_call_method_call(
1266
- self._client.params.call(params)
1267
- )
1268
- send_params = send_params or SendParams()
1269
- simulate_response = self._client._handle_call_errors(
1270
- lambda: method_call_to_simulate.simulate(
1271
- allow_unnamed_resources=send_params.get("populate_app_call_resources") or True,
1272
- skip_signatures=True,
1273
- allow_more_logs=True,
1274
- allow_empty_signatures=True,
1275
- extra_opcode_budget=None,
1276
- exec_trace_config=None,
1277
- simulation_round=None,
1278
- )
1215
+ self._client.params.call(readonly_params)
1279
1216
  )
1280
1217
 
1218
+ def run_simulate() -> SendAtomicTransactionComposerResults:
1219
+ try:
1220
+ return method_call_to_simulate.simulate(
1221
+ allow_unnamed_resources=readonly_send_params.get("populate_app_call_resources") or True,
1222
+ skip_signatures=True,
1223
+ allow_more_logs=True,
1224
+ allow_empty_signatures=True,
1225
+ extra_opcode_budget=None,
1226
+ exec_trace_config=None,
1227
+ simulation_round=None,
1228
+ )
1229
+ except Exception as e:
1230
+ if readonly_send_params.get("cover_app_call_inner_transaction_fees") and "fee too small" in str(e):
1231
+ raise ValueError(
1232
+ "Fees were too small. You may need to increase the transaction `maxFee`."
1233
+ ) from e
1234
+ raise
1235
+
1236
+ simulate_response = self._client._handle_call_errors(run_simulate)
1237
+
1281
1238
  return SendAppTransactionResult[Arc56ReturnValueType](
1282
1239
  tx_ids=simulate_response.tx_ids,
1283
1240
  transactions=simulate_response.transactions,
@@ -1616,7 +1573,7 @@ class AppClient:
1616
1573
  program: bytes | None = None,
1617
1574
  approval_source_info: ProgramSourceInfo | None = None,
1618
1575
  clear_source_info: ProgramSourceInfo | None = None,
1619
- ) -> Exception:
1576
+ ) -> LogicError | Exception:
1620
1577
  source_map = clear_source_map if is_clear_state_program else approval_source_map
1621
1578
 
1622
1579
  error_details = parse_logic_error(str(e))
@@ -1682,20 +1639,24 @@ class AppClient:
1682
1639
  get_line_for_pc=custom_get_line_for_pc,
1683
1640
  traces=None,
1684
1641
  )
1685
-
1686
1642
  if error_message:
1687
1643
  import re
1688
1644
 
1689
1645
  message = e.logic_error_str if isinstance(e, LogicError) else str(e)
1690
1646
  app_id = re.search(r"(?<=app=)\d+", message)
1691
1647
  tx_id = re.search(r"(?<=transaction )\S+(?=:)", message)
1692
- error = Exception(
1648
+ runtime_error_message = (
1693
1649
  f"Runtime error when executing {app_spec.name} "
1694
1650
  f"(appId: {app_id.group() if app_id else 'N/A'}) in transaction "
1695
1651
  f"{tx_id.group() if tx_id else 'N/A'}: {error_message}"
1696
1652
  )
1697
- error.__cause__ = e
1698
- return error
1653
+ if isinstance(e, LogicError):
1654
+ e.message = runtime_error_message
1655
+ return e
1656
+ else:
1657
+ error = Exception(runtime_error_message)
1658
+ error.__cause__ = e
1659
+ return error
1699
1660
 
1700
1661
  return e
1701
1662
 
@@ -23,7 +23,6 @@ from algokit_utils.applications.app_client import (
23
23
  AppClientBareCallParams,
24
24
  AppClientCompilationParams,
25
25
  AppClientCompilationResult,
26
- AppClientCreateSchema,
27
26
  AppClientMethodCallCreateParams,
28
27
  AppClientMethodCallParams,
29
28
  AppClientParams,
@@ -88,17 +87,12 @@ class AppFactoryParams:
88
87
 
89
88
 
90
89
  @dataclass(kw_only=True, frozen=True)
91
- class _AppFactoryCreateBaseParams(AppClientCreateSchema):
90
+ class AppFactoryCreateParams(AppClientBareCallCreateParams):
92
91
  on_complete: CreateOnComplete | None = None
93
92
 
94
93
 
95
94
  @dataclass(kw_only=True, frozen=True)
96
- class AppFactoryCreateParams(_AppFactoryCreateBaseParams, AppClientBareCallParams):
97
- pass
98
-
99
-
100
- @dataclass(kw_only=True, frozen=True)
101
- class AppFactoryCreateMethodCallParams(_AppFactoryCreateBaseParams, AppClientMethodCallParams):
95
+ class AppFactoryCreateMethodCallParams(AppClientMethodCallCreateParams):
102
96
  pass
103
97
 
104
98
 
@@ -43,13 +43,13 @@ class AssetInformation:
43
43
  :ivar decimals: The amount of decimal places the asset was created with
44
44
  :ivar default_frozen: Whether the asset was frozen by default for all accounts, defaults to None
45
45
  :ivar manager: The address of the optional account that can manage the configuration of the asset and destroy it,
46
- defaults to None
46
+ defaults to None
47
47
  :ivar reserve: The address of the optional account that holds the reserve (uncirculated supply) units of the asset,
48
- defaults to None
48
+ defaults to None
49
49
  :ivar freeze: The address of the optional account that can be used to freeze or unfreeze holdings of this asset,
50
- defaults to None
50
+ defaults to None
51
51
  :ivar clawback: The address of the optional account that can clawback holdings of this asset from any account,
52
- defaults to None
52
+ defaults to None
53
53
  :ivar unit_name: The optional name of the unit of this asset (e.g. ticker name), defaults to None
54
54
  :ivar unit_name_b64: The optional name of the unit of this asset as bytes, defaults to None
55
55
  :ivar asset_name: The optional name of the asset, defaults to None
@@ -57,7 +57,7 @@ class AssetInformation:
57
57
  :ivar url: Optional URL where more information about the asset can be retrieved, defaults to None
58
58
  :ivar url_b64: Optional URL where more information about the asset can be retrieved as bytes, defaults to None
59
59
  :ivar metadata_hash: 32-byte hash of some metadata that is relevant to the asset and/or asset holders,
60
- defaults to None
60
+ defaults to None
61
61
  """
62
62
 
63
63
  asset_id: int
@@ -77,8 +77,8 @@ def _get_config_from_environment(environment_prefix: str) -> AlgoClientNetworkCo
77
77
  port = os.getenv(f"{environment_prefix}_PORT")
78
78
  if port:
79
79
  parsed = parse.urlparse(server)
80
- server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl()
81
- return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", ""))
80
+ server = parsed._replace(netloc=f"{parsed.hostname}").geturl()
81
+ return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", ""), port=port)
82
82
 
83
83
 
84
84
  class ClientManager:
@@ -351,15 +351,18 @@ class ClientManager:
351
351
  )
352
352
 
353
353
  @staticmethod
354
- def get_algod_client(config: AlgoClientNetworkConfig | None = None) -> AlgodClient:
354
+ def get_algod_client(config: AlgoClientNetworkConfig) -> AlgodClient:
355
355
  """Get an Algod client from config or environment.
356
356
 
357
357
  :param config: Optional client configuration
358
358
  :return: Algod client instance
359
359
  """
360
- config = config or _get_config_from_environment("ALGOD")
361
360
  headers = {"X-Algo-API-Token": config.token or ""}
362
- return AlgodClient(algod_token=config.token or "", algod_address=config.server, headers=headers)
361
+ return AlgodClient(
362
+ algod_token=config.token or "",
363
+ algod_address=config.full_url(),
364
+ headers=headers,
365
+ )
363
366
 
364
367
  @staticmethod
365
368
  def get_algod_client_from_environment() -> AlgodClient:
@@ -370,14 +373,13 @@ class ClientManager:
370
373
  return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment())
371
374
 
372
375
  @staticmethod
373
- def get_kmd_client(config: AlgoClientNetworkConfig | None = None) -> KMDClient:
376
+ def get_kmd_client(config: AlgoClientNetworkConfig) -> KMDClient:
374
377
  """Get a KMD client from config or environment.
375
378
 
376
379
  :param config: Optional client configuration
377
380
  :return: KMD client instance
378
381
  """
379
- config = config or _get_config_from_environment("KMD")
380
- return KMDClient(config.token, config.server)
382
+ return KMDClient(config.token, config.full_url())
381
383
 
382
384
  @staticmethod
383
385
  def get_kmd_client_from_environment() -> KMDClient:
@@ -388,15 +390,18 @@ class ClientManager:
388
390
  return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment())
389
391
 
390
392
  @staticmethod
391
- def get_indexer_client(config: AlgoClientNetworkConfig | None = None) -> IndexerClient:
393
+ def get_indexer_client(config: AlgoClientNetworkConfig) -> IndexerClient:
392
394
  """Get an Indexer client from config or environment.
393
395
 
394
396
  :param config: Optional client configuration
395
397
  :return: Indexer client instance
396
398
  """
397
- config = config or _get_config_from_environment("INDEXER")
398
399
  headers = {"X-Indexer-API-Token": config.token}
399
- return IndexerClient(indexer_token=config.token, indexer_address=config.server, headers=headers)
400
+ return IndexerClient(
401
+ indexer_token=config.token,
402
+ indexer_address=config.full_url(),
403
+ headers=headers,
404
+ )
400
405
 
401
406
  @staticmethod
402
407
  def get_indexer_client_from_environment() -> IndexerClient:
@@ -611,7 +616,7 @@ class ClientManager:
611
616
  else {"algod": 4001, "indexer": 8980, "kmd": 4002}[config_or_port]
612
617
  )
613
618
 
614
- return AlgoClientNetworkConfig(server=f"http://localhost:{port}", token="a" * 64)
619
+ return AlgoClientNetworkConfig(server="http://localhost", token="a" * 64, port=port)
615
620
 
616
621
  @staticmethod
617
622
  def get_algod_config_from_environment() -> AlgoClientNetworkConfig:
@@ -71,6 +71,9 @@ class TestNetDispenserApiClient:
71
71
  Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor.
72
72
  """
73
73
 
74
+ # NOTE: ensures pytest does not think this is a test
75
+ # https://docs.pytest.org/en/stable/example/pythoncollection.html#customizing-test-collection
76
+ __test__ = False
74
77
  auth_token: str
75
78
  request_timeout = DISPENSER_REQUEST_TIMEOUT
76
79