algokit-utils 2.4.0b1__py3-none-any.whl → 3.0.0__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.

Files changed (70) hide show
  1. algokit_utils/__init__.py +23 -181
  2. algokit_utils/_debugging.py +89 -45
  3. algokit_utils/_legacy_v2/__init__.py +177 -0
  4. algokit_utils/{_ensure_funded.py → _legacy_v2/_ensure_funded.py} +21 -24
  5. algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +26 -23
  6. algokit_utils/_legacy_v2/account.py +203 -0
  7. algokit_utils/_legacy_v2/application_client.py +1472 -0
  8. algokit_utils/_legacy_v2/application_specification.py +21 -0
  9. algokit_utils/_legacy_v2/asset.py +168 -0
  10. algokit_utils/_legacy_v2/common.py +28 -0
  11. algokit_utils/_legacy_v2/deploy.py +822 -0
  12. algokit_utils/_legacy_v2/logic_error.py +14 -0
  13. algokit_utils/{models.py → _legacy_v2/models.py} +16 -45
  14. algokit_utils/_legacy_v2/network_clients.py +144 -0
  15. algokit_utils/account.py +12 -183
  16. algokit_utils/accounts/__init__.py +2 -0
  17. algokit_utils/accounts/account_manager.py +912 -0
  18. algokit_utils/accounts/kmd_account_manager.py +161 -0
  19. algokit_utils/algorand.py +359 -0
  20. algokit_utils/application_client.py +9 -1447
  21. algokit_utils/application_specification.py +39 -197
  22. algokit_utils/applications/__init__.py +7 -0
  23. algokit_utils/applications/abi.py +275 -0
  24. algokit_utils/applications/app_client.py +2108 -0
  25. algokit_utils/applications/app_deployer.py +725 -0
  26. algokit_utils/applications/app_factory.py +1134 -0
  27. algokit_utils/applications/app_manager.py +578 -0
  28. algokit_utils/applications/app_spec/__init__.py +2 -0
  29. algokit_utils/applications/app_spec/arc32.py +207 -0
  30. algokit_utils/applications/app_spec/arc56.py +989 -0
  31. algokit_utils/applications/enums.py +40 -0
  32. algokit_utils/asset.py +32 -168
  33. algokit_utils/assets/__init__.py +1 -0
  34. algokit_utils/assets/asset_manager.py +336 -0
  35. algokit_utils/beta/_utils.py +36 -0
  36. algokit_utils/beta/account_manager.py +4 -195
  37. algokit_utils/beta/algorand_client.py +4 -314
  38. algokit_utils/beta/client_manager.py +5 -74
  39. algokit_utils/beta/composer.py +5 -712
  40. algokit_utils/clients/__init__.py +2 -0
  41. algokit_utils/clients/client_manager.py +738 -0
  42. algokit_utils/clients/dispenser_api_client.py +224 -0
  43. algokit_utils/common.py +8 -26
  44. algokit_utils/config.py +76 -29
  45. algokit_utils/deploy.py +7 -894
  46. algokit_utils/dispenser_api.py +8 -176
  47. algokit_utils/errors/__init__.py +1 -0
  48. algokit_utils/errors/logic_error.py +121 -0
  49. algokit_utils/logic_error.py +7 -82
  50. algokit_utils/models/__init__.py +8 -0
  51. algokit_utils/models/account.py +217 -0
  52. algokit_utils/models/amount.py +200 -0
  53. algokit_utils/models/application.py +91 -0
  54. algokit_utils/models/network.py +29 -0
  55. algokit_utils/models/simulate.py +11 -0
  56. algokit_utils/models/state.py +68 -0
  57. algokit_utils/models/transaction.py +100 -0
  58. algokit_utils/network_clients.py +7 -128
  59. algokit_utils/protocols/__init__.py +2 -0
  60. algokit_utils/protocols/account.py +22 -0
  61. algokit_utils/protocols/typed_clients.py +108 -0
  62. algokit_utils/transactions/__init__.py +3 -0
  63. algokit_utils/transactions/transaction_composer.py +2499 -0
  64. algokit_utils/transactions/transaction_creator.py +688 -0
  65. algokit_utils/transactions/transaction_sender.py +1219 -0
  66. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/METADATA +11 -7
  67. algokit_utils-3.0.0.dist-info/RECORD +70 -0
  68. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/WHEEL +1 -1
  69. algokit_utils-2.4.0b1.dist-info/RECORD +0 -24
  70. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,738 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Literal, TypeVar
6
+ from urllib import parse
7
+
8
+ import algosdk
9
+ from algosdk.atomic_transaction_composer import TransactionSigner
10
+ from algosdk.kmd import KMDClient
11
+ from algosdk.source_map import SourceMap
12
+ from algosdk.transaction import SuggestedParams
13
+ from algosdk.v2client.algod import AlgodClient
14
+ from algosdk.v2client.indexer import IndexerClient
15
+
16
+ from algokit_utils._legacy_v2.application_specification import ApplicationSpecification
17
+ from algokit_utils.applications.app_deployer import ApplicationLookup
18
+ from algokit_utils.applications.app_spec.arc56 import Arc56Contract
19
+ from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient
20
+ from algokit_utils.models.network import AlgoClientConfigs, AlgoClientNetworkConfig
21
+ from algokit_utils.protocols.typed_clients import TypedAppClientProtocol, TypedAppFactoryProtocol
22
+
23
+ if TYPE_CHECKING:
24
+ from algokit_utils.algorand import AlgorandClient
25
+ from algokit_utils.applications.app_client import AppClient, AppClientCompilationParams
26
+ from algokit_utils.applications.app_factory import AppFactory
27
+
28
+ __all__ = [
29
+ "AlgoSdkClients",
30
+ "ClientManager",
31
+ "NetworkDetail",
32
+ ]
33
+
34
+ TypedFactoryT = TypeVar("TypedFactoryT", bound=TypedAppFactoryProtocol)
35
+ TypedAppClientT = TypeVar("TypedAppClientT", bound=TypedAppClientProtocol)
36
+
37
+
38
+ class AlgoSdkClients:
39
+ """Container for Algorand SDK client instances.
40
+
41
+ Holds references to Algod, Indexer and KMD clients.
42
+
43
+ :param algod: Algod client instance
44
+ :param indexer: Optional Indexer client instance
45
+ :param kmd: Optional KMD client instance
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ algod: algosdk.v2client.algod.AlgodClient,
51
+ indexer: IndexerClient | None = None,
52
+ kmd: KMDClient | None = None,
53
+ ):
54
+ self.algod = algod
55
+ self.indexer = indexer
56
+ self.kmd = kmd
57
+
58
+
59
+ @dataclass(kw_only=True, frozen=True)
60
+ class NetworkDetail:
61
+ """Details about an Algorand network.
62
+
63
+ Contains network type flags and genesis information.
64
+ """
65
+
66
+ is_testnet: bool
67
+ """Whether the network is a testnet"""
68
+ is_mainnet: bool
69
+ """Whether the network is a mainnet"""
70
+ is_localnet: bool
71
+ """Whether the network is a localnet"""
72
+ genesis_id: str
73
+ """The genesis ID of the network"""
74
+ genesis_hash: str
75
+ """The genesis hash of the network"""
76
+
77
+
78
+ def _get_config_from_environment(environment_prefix: str) -> AlgoClientNetworkConfig:
79
+ server = os.getenv(f"{environment_prefix}_SERVER")
80
+ if server is None:
81
+ raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER")
82
+ port = os.getenv(f"{environment_prefix}_PORT")
83
+ if port:
84
+ parsed = parse.urlparse(server)
85
+ server = parsed._replace(netloc=f"{parsed.hostname}").geturl()
86
+ return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", ""), port=port)
87
+
88
+
89
+ class ClientManager:
90
+ """Manager for Algorand SDK clients.
91
+
92
+ Provides access to Algod, Indexer and KMD clients and helper methods for working with them.
93
+
94
+ :param clients_or_configs: Either client instances or client configurations
95
+ :param algorand_client: AlgorandClient instance
96
+
97
+ :example:
98
+ >>> # Algod only
99
+ >>> client_manager = ClientManager(algod_client)
100
+ >>> # Algod and Indexer
101
+ >>> client_manager = ClientManager(algod_client, indexer_client)
102
+ >>> # Algod config only
103
+ >>> client_manager = ClientManager(ClientManager.get_algod_config_from_environment())
104
+ >>> # Algod and Indexer config
105
+ >>> client_manager = ClientManager(ClientManager.get_algod_config_from_environment(),
106
+ ... ClientManager.get_indexer_config_from_environment())
107
+ """
108
+
109
+ def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: AlgorandClient):
110
+ if isinstance(clients_or_configs, AlgoSdkClients):
111
+ _clients = clients_or_configs
112
+ elif isinstance(clients_or_configs, AlgoClientConfigs):
113
+ _clients = AlgoSdkClients(
114
+ algod=ClientManager.get_algod_client(clients_or_configs.algod_config),
115
+ indexer=ClientManager.get_indexer_client(clients_or_configs.indexer_config)
116
+ if clients_or_configs.indexer_config
117
+ else None,
118
+ kmd=ClientManager.get_kmd_client(clients_or_configs.kmd_config)
119
+ if clients_or_configs.kmd_config
120
+ else None,
121
+ )
122
+ self._algod = _clients.algod
123
+ self._indexer = _clients.indexer
124
+ self._kmd = _clients.kmd
125
+ self._algorand = algorand_client
126
+ self._suggested_params: SuggestedParams | None = None
127
+
128
+ @property
129
+ def algod(self) -> AlgodClient:
130
+ """Returns an algosdk Algod API client.
131
+
132
+ :return: Algod client instance
133
+ """
134
+ return self._algod
135
+
136
+ @property
137
+ def indexer(self) -> IndexerClient:
138
+ """Returns an algosdk Indexer API client.
139
+
140
+ :raises ValueError: If no Indexer client is configured
141
+ :return: Indexer client instance
142
+ """
143
+ if not self._indexer:
144
+ raise ValueError("Attempt to use Indexer client in AlgoKit instance with no Indexer configured")
145
+ return self._indexer
146
+
147
+ @property
148
+ def indexer_if_present(self) -> IndexerClient | None:
149
+ """Returns the Indexer client if configured, otherwise None.
150
+
151
+ :return: Indexer client instance or None
152
+ """
153
+ return self._indexer
154
+
155
+ @property
156
+ def kmd(self) -> KMDClient:
157
+ """Returns an algosdk KMD API client.
158
+
159
+ :raises ValueError: If no KMD client is configured
160
+ :return: KMD client instance
161
+ """
162
+ if not self._kmd:
163
+ raise ValueError("Attempt to use Kmd client in AlgoKit instance with no Kmd configured")
164
+ return self._kmd
165
+
166
+ def network(self) -> NetworkDetail:
167
+ """Get details about the connected Algorand network.
168
+
169
+ :return: Network details including type and genesis information
170
+
171
+ :example:
172
+ >>> client_manager = ClientManager(algod_client)
173
+ >>> network_detail = client_manager.network()
174
+ """
175
+ if self._suggested_params is None:
176
+ self._suggested_params = self._algod.suggested_params()
177
+ sp = self._suggested_params
178
+ return NetworkDetail(
179
+ is_testnet=sp.gen in ["testnet-v1.0", "testnet-v1", "testnet"],
180
+ is_mainnet=sp.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"],
181
+ is_localnet=ClientManager.genesis_id_is_localnet(str(sp.gen)),
182
+ genesis_id=str(sp.gen),
183
+ genesis_hash=sp.gh,
184
+ )
185
+
186
+ def is_localnet(self) -> bool:
187
+ """Check if connected to a local network.
188
+
189
+ :return: True if connected to a local network
190
+ """
191
+ return self.network().is_localnet
192
+
193
+ def is_testnet(self) -> bool:
194
+ """Check if connected to TestNet.
195
+
196
+ :return: True if connected to TestNet
197
+ """
198
+ return self.network().is_testnet
199
+
200
+ def is_mainnet(self) -> bool:
201
+ """Check if connected to MainNet.
202
+
203
+ :return: True if connected to MainNet
204
+ """
205
+ return self.network().is_mainnet
206
+
207
+ def get_testnet_dispenser(
208
+ self, auth_token: str | None = None, request_timeout: int | None = None
209
+ ) -> TestNetDispenserApiClient:
210
+ """Get a TestNet dispenser API client.
211
+
212
+ :param auth_token: Optional authentication token
213
+ :param request_timeout: Optional request timeout in seconds
214
+ :return: TestNet dispenser client instance
215
+ """
216
+ if request_timeout:
217
+ return TestNetDispenserApiClient(auth_token=auth_token, request_timeout=request_timeout)
218
+
219
+ return TestNetDispenserApiClient(auth_token=auth_token)
220
+
221
+ def get_app_factory(
222
+ self,
223
+ app_spec: Arc56Contract | ApplicationSpecification | str,
224
+ app_name: str | None = None,
225
+ default_sender: str | None = None,
226
+ default_signer: TransactionSigner | None = None,
227
+ version: str | None = None,
228
+ compilation_params: AppClientCompilationParams | None = None,
229
+ ) -> AppFactory:
230
+ """Get an application factory for deploying smart contracts.
231
+
232
+ :param app_spec: Application specification
233
+ :param app_name: Optional application name
234
+ :param default_sender: Optional default sender address
235
+ :param default_signer: Optional default transaction signer
236
+ :param version: Optional version string
237
+ :param compilation_params: Optional compilation parameters
238
+ :raises ValueError: If no Algorand client is configured
239
+ :return: Application factory instance
240
+ """
241
+ from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams
242
+
243
+ if not self._algorand:
244
+ raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client")
245
+
246
+ return AppFactory(
247
+ AppFactoryParams(
248
+ algorand=self._algorand,
249
+ app_spec=app_spec,
250
+ app_name=app_name,
251
+ default_sender=default_sender,
252
+ default_signer=default_signer,
253
+ version=version,
254
+ compilation_params=compilation_params,
255
+ )
256
+ )
257
+
258
+ def get_app_client_by_id(
259
+ self,
260
+ app_spec: (Arc56Contract | ApplicationSpecification | str),
261
+ app_id: int,
262
+ app_name: str | None = None,
263
+ default_sender: str | None = None,
264
+ default_signer: TransactionSigner | None = None,
265
+ approval_source_map: SourceMap | None = None,
266
+ clear_source_map: SourceMap | None = None,
267
+ ) -> AppClient:
268
+ """Get an application client for an existing application by ID.
269
+
270
+ :param app_spec: Application specification
271
+ :param app_id: Application ID
272
+ :param app_name: Optional application name
273
+ :param default_sender: Optional default sender address
274
+ :param default_signer: Optional default transaction signer
275
+ :param approval_source_map: Optional approval program source map
276
+ :param clear_source_map: Optional clear program source map
277
+ :raises ValueError: If no Algorand client is configured
278
+ :return: Application client instance
279
+ """
280
+ from algokit_utils.applications.app_client import AppClient, AppClientParams
281
+
282
+ if not self._algorand:
283
+ raise ValueError("Attempt to get app client from a ClientManager without an Algorand client")
284
+
285
+ return AppClient(
286
+ AppClientParams(
287
+ app_spec=app_spec,
288
+ algorand=self._algorand,
289
+ app_id=app_id,
290
+ app_name=app_name,
291
+ default_sender=default_sender,
292
+ default_signer=default_signer,
293
+ approval_source_map=approval_source_map,
294
+ clear_source_map=clear_source_map,
295
+ )
296
+ )
297
+
298
+ def get_app_client_by_network(
299
+ self,
300
+ app_spec: (Arc56Contract | ApplicationSpecification | str),
301
+ app_name: str | None = None,
302
+ default_sender: str | None = None,
303
+ default_signer: TransactionSigner | None = None,
304
+ approval_source_map: SourceMap | None = None,
305
+ clear_source_map: SourceMap | None = None,
306
+ ) -> AppClient:
307
+ """Get an application client for an existing application by network.
308
+
309
+ :param app_spec: Application specification
310
+ :param app_name: Optional application name
311
+ :param default_sender: Optional default sender address
312
+ :param default_signer: Optional default transaction signer
313
+ :param approval_source_map: Optional approval program source map
314
+ :param clear_source_map: Optional clear program source map
315
+ :raises ValueError: If no Algorand client is configured
316
+ :return: Application client instance
317
+ """
318
+ from algokit_utils.applications.app_client import AppClient
319
+
320
+ if not self._algorand:
321
+ raise ValueError("Attempt to get app client from a ClientManager without an Algorand client")
322
+
323
+ return AppClient.from_network(
324
+ app_spec=app_spec,
325
+ app_name=app_name,
326
+ default_sender=default_sender,
327
+ default_signer=default_signer,
328
+ approval_source_map=approval_source_map,
329
+ clear_source_map=clear_source_map,
330
+ algorand=self._algorand,
331
+ )
332
+
333
+ def get_app_client_by_creator_and_name(
334
+ self,
335
+ creator_address: str,
336
+ app_name: str,
337
+ app_spec: Arc56Contract | ApplicationSpecification | str,
338
+ default_sender: str | None = None,
339
+ default_signer: TransactionSigner | None = None,
340
+ ignore_cache: bool | None = None,
341
+ app_lookup_cache: ApplicationLookup | None = None,
342
+ approval_source_map: SourceMap | None = None,
343
+ clear_source_map: SourceMap | None = None,
344
+ ) -> AppClient:
345
+ """Get an application client by creator address and name.
346
+
347
+ :param creator_address: Creator address
348
+ :param app_name: Application name
349
+ :param app_spec: Application specification
350
+ :param default_sender: Optional default sender address
351
+ :param default_signer: Optional default transaction signer
352
+ :param ignore_cache: Optional flag to ignore cache
353
+ :param app_lookup_cache: Optional app lookup cache
354
+ :param approval_source_map: Optional approval program source map
355
+ :param clear_source_map: Optional clear program source map
356
+ :return: Application client instance
357
+ """
358
+ from algokit_utils.applications.app_client import AppClient
359
+
360
+ return AppClient.from_creator_and_name(
361
+ creator_address=creator_address,
362
+ app_name=app_name,
363
+ default_sender=default_sender,
364
+ default_signer=default_signer,
365
+ ignore_cache=ignore_cache,
366
+ app_lookup_cache=app_lookup_cache,
367
+ app_spec=app_spec,
368
+ approval_source_map=approval_source_map,
369
+ clear_source_map=clear_source_map,
370
+ algorand=self._algorand,
371
+ )
372
+
373
+ @staticmethod
374
+ def get_algod_client(config: AlgoClientNetworkConfig) -> AlgodClient:
375
+ """Get an Algod client from config or environment.
376
+
377
+ :param config: Optional client configuration
378
+ :return: Algod client instance
379
+ """
380
+ headers = {"X-Algo-API-Token": config.token or ""}
381
+ return AlgodClient(
382
+ algod_token=config.token or "",
383
+ algod_address=config.full_url(),
384
+ headers=headers,
385
+ )
386
+
387
+ @staticmethod
388
+ def get_algod_client_from_environment() -> AlgodClient:
389
+ """Get an Algod client from environment variables.
390
+
391
+ :return: Algod client instance
392
+ """
393
+ return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment())
394
+
395
+ @staticmethod
396
+ def get_kmd_client(config: AlgoClientNetworkConfig) -> KMDClient:
397
+ """Get a KMD client from config or environment.
398
+
399
+ :param config: Optional client configuration
400
+ :return: KMD client instance
401
+ """
402
+ return KMDClient(config.token, config.full_url())
403
+
404
+ @staticmethod
405
+ def get_kmd_client_from_environment() -> KMDClient:
406
+ """Get a KMD client from environment variables.
407
+
408
+ :return: KMD client instance
409
+ """
410
+ return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment())
411
+
412
+ @staticmethod
413
+ def get_indexer_client(config: AlgoClientNetworkConfig) -> IndexerClient:
414
+ """Get an Indexer client from config or environment.
415
+
416
+ :param config: Optional client configuration
417
+ :return: Indexer client instance
418
+ """
419
+ headers = {"X-Indexer-API-Token": config.token}
420
+ return IndexerClient(
421
+ indexer_token=config.token,
422
+ indexer_address=config.full_url(),
423
+ headers=headers,
424
+ )
425
+
426
+ @staticmethod
427
+ def get_indexer_client_from_environment() -> IndexerClient:
428
+ """Get an Indexer client from environment variables.
429
+
430
+ :return: Indexer client instance
431
+ """
432
+ return ClientManager.get_indexer_client(ClientManager.get_indexer_config_from_environment())
433
+
434
+ @staticmethod
435
+ def genesis_id_is_localnet(genesis_id: str | None) -> bool:
436
+ """Check if a genesis ID indicates a local network.
437
+
438
+ :param genesis_id: Genesis ID to check
439
+ :return: True if genesis ID indicates a local network
440
+
441
+ :example:
442
+ >>> ClientManager.genesis_id_is_localnet("devnet-v1")
443
+ """
444
+ return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"]
445
+
446
+ def get_typed_app_client_by_creator_and_name(
447
+ self,
448
+ typed_client: type[TypedAppClientT],
449
+ *,
450
+ creator_address: str,
451
+ app_name: str,
452
+ default_sender: str | None = None,
453
+ default_signer: TransactionSigner | None = None,
454
+ ignore_cache: bool | None = None,
455
+ app_lookup_cache: ApplicationLookup | None = None,
456
+ ) -> TypedAppClientT:
457
+ """Get a typed application client by creator address and name.
458
+
459
+ :param typed_client: Typed client class
460
+ :param creator_address: Creator address
461
+ :param app_name: Application name
462
+ :param default_sender: Optional default sender address
463
+ :param default_signer: Optional default transaction signer
464
+ :param ignore_cache: Optional flag to ignore cache
465
+ :param app_lookup_cache: Optional app lookup cache
466
+ :raises ValueError: If no Algorand client is configured
467
+ :return: Typed application client instance
468
+
469
+ :example:
470
+ >>> client_manager = ClientManager(algod_client)
471
+ >>> typed_app_client = client_manager.get_typed_app_client_by_creator_and_name(
472
+ ... typed_client=MyAppClient,
473
+ ... creator_address="creator_address",
474
+ ... app_name="app_name",
475
+ ... )
476
+ """
477
+ if not self._algorand:
478
+ raise ValueError("Attempt to get app client from a ClientManager without an Algorand client")
479
+
480
+ return typed_client.from_creator_and_name(
481
+ creator_address=creator_address,
482
+ app_name=app_name,
483
+ default_sender=default_sender,
484
+ default_signer=default_signer,
485
+ ignore_cache=ignore_cache,
486
+ app_lookup_cache=app_lookup_cache,
487
+ algorand=self._algorand,
488
+ )
489
+
490
+ def get_typed_app_client_by_id(
491
+ self,
492
+ typed_client: type[TypedAppClientT],
493
+ *,
494
+ app_id: int,
495
+ app_name: str | None = None,
496
+ default_sender: str | None = None,
497
+ default_signer: TransactionSigner | None = None,
498
+ approval_source_map: SourceMap | None = None,
499
+ clear_source_map: SourceMap | None = None,
500
+ ) -> TypedAppClientT:
501
+ """Get a typed application client by ID.
502
+
503
+ :param typed_client: Typed client class
504
+ :param app_id: Application ID
505
+ :param app_name: Optional application name
506
+ :param default_sender: Optional default sender address
507
+ :param default_signer: Optional default transaction signer
508
+ :param approval_source_map: Optional approval program source map
509
+ :param clear_source_map: Optional clear program source map
510
+ :raises ValueError: If no Algorand client is configured
511
+ :return: Typed application client instance
512
+
513
+ :example:
514
+ >>> client_manager = ClientManager(algod_client)
515
+ >>> typed_app_client = client_manager.get_typed_app_client_by_id(
516
+ ... typed_client=MyAppClient,
517
+ ... app_id=1234567890,
518
+ ... )
519
+ """
520
+ if not self._algorand:
521
+ raise ValueError("Attempt to get app client from a ClientManager without an Algorand client")
522
+
523
+ return typed_client(
524
+ app_id=app_id,
525
+ app_name=app_name,
526
+ default_sender=default_sender,
527
+ default_signer=default_signer,
528
+ approval_source_map=approval_source_map,
529
+ clear_source_map=clear_source_map,
530
+ algorand=self._algorand,
531
+ )
532
+
533
+ def get_typed_app_client_by_network(
534
+ self,
535
+ typed_client: type[TypedAppClientT],
536
+ *,
537
+ app_name: str | None = None,
538
+ default_sender: str | None = None,
539
+ default_signer: TransactionSigner | None = None,
540
+ approval_source_map: SourceMap | None = None,
541
+ clear_source_map: SourceMap | None = None,
542
+ ) -> TypedAppClientT:
543
+ """Returns a new typed client, resolves the app ID for the current network.
544
+
545
+ Uses pre-determined network-specific app IDs specified in the ARC-56 app spec.
546
+ If no IDs are in the app spec or the network isn't recognised, an error is thrown.
547
+
548
+ :param typed_client: The typed client class to instantiate
549
+ :param app_name: Optional application name
550
+ :param default_sender: Optional default sender address
551
+ :param default_signer: Optional default transaction signer
552
+ :param approval_source_map: Optional approval program source map
553
+ :param clear_source_map: Optional clear program source map
554
+ :raises ValueError: If no Algorand client is configured
555
+ :return: The typed client instance
556
+
557
+ :example:
558
+ >>> client_manager = ClientManager(algod_client)
559
+ >>> typed_app_client = client_manager.get_typed_app_client_by_network(
560
+ ... typed_client=MyAppClient,
561
+ ... app_name="app_name",
562
+ ... )
563
+ """
564
+ if not self._algorand:
565
+ raise ValueError("Attempt to get app client from a ClientManager without an Algorand client")
566
+
567
+ return typed_client.from_network(
568
+ app_name=app_name,
569
+ default_sender=default_sender,
570
+ default_signer=default_signer,
571
+ approval_source_map=approval_source_map,
572
+ clear_source_map=clear_source_map,
573
+ algorand=self._algorand,
574
+ )
575
+
576
+ def get_typed_app_factory(
577
+ self,
578
+ typed_factory: type[TypedFactoryT],
579
+ *,
580
+ app_name: str | None = None,
581
+ default_sender: str | None = None,
582
+ default_signer: TransactionSigner | None = None,
583
+ version: str | None = None,
584
+ compilation_params: AppClientCompilationParams | None = None,
585
+ ) -> TypedFactoryT:
586
+ """Get a typed application factory.
587
+
588
+ :param typed_factory: Typed factory class
589
+ :param app_name: Optional application name
590
+ :param default_sender: Optional default sender address
591
+ :param default_signer: Optional default transaction signer
592
+ :param version: Optional version string
593
+ :param compilation_params: Optional compilation parameters
594
+ :raises ValueError: If no Algorand client is configured
595
+ :return: Typed application factory instance
596
+
597
+ :example:
598
+ >>> client_manager = ClientManager(algod_client)
599
+ >>> typed_app_factory = client_manager.get_typed_app_factory(
600
+ ... typed_factory=MyAppFactory,
601
+ ... app_name="app_name",
602
+ ... )
603
+ """
604
+ if not self._algorand:
605
+ raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client")
606
+
607
+ return typed_factory(
608
+ algorand=self._algorand,
609
+ app_name=app_name,
610
+ default_sender=default_sender,
611
+ default_signer=default_signer,
612
+ version=version,
613
+ compilation_params=compilation_params,
614
+ )
615
+
616
+ @staticmethod
617
+ def get_config_from_environment_or_localnet() -> AlgoClientConfigs:
618
+ """Retrieve client configuration from environment variables or fallback to localnet defaults.
619
+
620
+ If ALGOD_SERVER is set in environment variables, it will use environment configuration,
621
+ otherwise it will use default localnet configuration.
622
+
623
+ :return: Configuration for algod, indexer, and optionally kmd
624
+
625
+ :example:
626
+ >>> client_manager = ClientManager(algod_client)
627
+ >>> config = client_manager.get_config_from_environment_or_localnet()
628
+ """
629
+ algod_server = os.getenv("ALGOD_SERVER")
630
+
631
+ if algod_server:
632
+ # Use environment configuration
633
+ algod_config = ClientManager.get_algod_config_from_environment()
634
+
635
+ # Only include indexer if INDEXER_SERVER is set
636
+ indexer_config = (
637
+ ClientManager.get_indexer_config_from_environment() if os.getenv("INDEXER_SERVER") else None
638
+ )
639
+
640
+ # Include KMD config only for local networks (not mainnet/testnet)
641
+ kmd_config = (
642
+ AlgoClientNetworkConfig(
643
+ server=algod_config.server, token=algod_config.token, port=os.getenv("KMD_PORT", "4002")
644
+ )
645
+ if not any(net in algod_server.lower() for net in ["mainnet", "testnet"])
646
+ else None
647
+ )
648
+ else:
649
+ # Use localnet defaults
650
+ algod_config = ClientManager.get_default_localnet_config("algod")
651
+ indexer_config = ClientManager.get_default_localnet_config("indexer")
652
+ kmd_config = ClientManager.get_default_localnet_config("kmd")
653
+
654
+ return AlgoClientConfigs(
655
+ algod_config=algod_config,
656
+ indexer_config=indexer_config,
657
+ kmd_config=kmd_config,
658
+ )
659
+
660
+ @staticmethod
661
+ def get_default_localnet_config(
662
+ config_or_port: Literal["algod", "indexer", "kmd"] | int,
663
+ ) -> AlgoClientNetworkConfig:
664
+ """Get default configuration for local network services.
665
+
666
+ :param config_or_port: Service name or port number
667
+ :return: Client configuration for local network
668
+
669
+ :example:
670
+ >>> client_manager = ClientManager(algod_client)
671
+ >>> config = client_manager.get_default_localnet_config("algod")
672
+ """
673
+ port = (
674
+ config_or_port
675
+ if isinstance(config_or_port, int)
676
+ else {"algod": 4001, "indexer": 8980, "kmd": 4002}[config_or_port]
677
+ )
678
+
679
+ return AlgoClientNetworkConfig(server="http://localhost", token="a" * 64, port=port)
680
+
681
+ @staticmethod
682
+ def get_algod_config_from_environment() -> AlgoClientNetworkConfig:
683
+ """Retrieve the algod configuration from environment variables.
684
+ Will raise an error if ALGOD_SERVER environment variable is not set
685
+
686
+ :return: Algod client configuration
687
+
688
+ :example:
689
+ >>> client_manager = ClientManager(algod_client)
690
+ >>> config = client_manager.get_algod_config_from_environment()
691
+ """
692
+ return _get_config_from_environment("ALGOD")
693
+
694
+ @staticmethod
695
+ def get_indexer_config_from_environment() -> AlgoClientNetworkConfig:
696
+ """Retrieve the indexer configuration from environment variables.
697
+ Will raise an error if INDEXER_SERVER environment variable is not set
698
+
699
+ :return: Indexer client configuration
700
+
701
+ :example:
702
+ >>> client_manager = ClientManager(algod_client)
703
+ >>> config = client_manager.get_indexer_config_from_environment()
704
+ """
705
+ return _get_config_from_environment("INDEXER")
706
+
707
+ @staticmethod
708
+ def get_kmd_config_from_environment() -> AlgoClientNetworkConfig:
709
+ """Retrieve the kmd configuration from environment variables.
710
+
711
+ :return: KMD client configuration
712
+
713
+ :example:
714
+ >>> client_manager = ClientManager(algod_client)
715
+ >>> config = client_manager.get_kmd_config_from_environment()
716
+ """
717
+ return _get_config_from_environment("KMD")
718
+
719
+ @staticmethod
720
+ def get_algonode_config(
721
+ network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"]
722
+ ) -> AlgoClientNetworkConfig:
723
+ """Returns the Algorand configuration to point to the free tier of the AlgoNode service.
724
+
725
+ :param network: Which network to connect to - TestNet or MainNet
726
+ :param config: Which algod config to return - Algod or Indexer
727
+ :return: Configuration for the specified network and service
728
+
729
+ :example:
730
+ >>> client_manager = ClientManager(algod_client)
731
+ >>> config = client_manager.get_algonode_config("testnet", "algod")
732
+ """
733
+ service_type = "api" if config == "algod" else "idx"
734
+ return AlgoClientNetworkConfig(
735
+ server=f"https://{network}-{service_type}.algonode.cloud",
736
+ port=443,
737
+ token="",
738
+ )