algokit-utils 3.0.0b1__py3-none-any.whl → 3.0.0b3__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 -183
  2. algokit_utils/_debugging.py +123 -97
  3. algokit_utils/_legacy_v2/__init__.py +177 -0
  4. algokit_utils/{_ensure_funded.py → _legacy_v2/_ensure_funded.py} +19 -18
  5. algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +24 -23
  6. algokit_utils/_legacy_v2/account.py +203 -0
  7. algokit_utils/_legacy_v2/application_client.py +1471 -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} +19 -142
  14. algokit_utils/_legacy_v2/network_clients.py +140 -0
  15. algokit_utils/account.py +12 -183
  16. algokit_utils/accounts/__init__.py +2 -0
  17. algokit_utils/accounts/account_manager.py +909 -0
  18. algokit_utils/accounts/kmd_account_manager.py +159 -0
  19. algokit_utils/algorand.py +265 -0
  20. algokit_utils/application_client.py +9 -1453
  21. algokit_utils/application_specification.py +39 -197
  22. algokit_utils/applications/__init__.py +7 -0
  23. algokit_utils/applications/abi.py +276 -0
  24. algokit_utils/applications/app_client.py +2054 -0
  25. algokit_utils/applications/app_deployer.py +600 -0
  26. algokit_utils/applications/app_factory.py +826 -0
  27. algokit_utils/applications/app_manager.py +470 -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 +1023 -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 +320 -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 +656 -0
  42. algokit_utils/clients/dispenser_api_client.py +192 -0
  43. algokit_utils/common.py +8 -26
  44. algokit_utils/config.py +71 -18
  45. algokit_utils/deploy.py +7 -892
  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 -80
  50. algokit_utils/models/__init__.py +8 -0
  51. algokit_utils/models/account.py +197 -0
  52. algokit_utils/models/amount.py +198 -0
  53. algokit_utils/models/application.py +61 -0
  54. algokit_utils/models/network.py +25 -0
  55. algokit_utils/models/simulate.py +11 -0
  56. algokit_utils/models/state.py +59 -0
  57. algokit_utils/models/transaction.py +100 -0
  58. algokit_utils/network_clients.py +7 -152
  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 +2287 -0
  64. algokit_utils/transactions/transaction_creator.py +156 -0
  65. algokit_utils/transactions/transaction_sender.py +574 -0
  66. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b3.dist-info}/METADATA +13 -8
  67. algokit_utils-3.0.0b3.dist-info/RECORD +70 -0
  68. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b3.dist-info}/WHEEL +1 -1
  69. algokit_utils-3.0.0b1.dist-info/RECORD +0 -24
  70. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b3.dist-info}/LICENSE +0 -0
@@ -0,0 +1,159 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, cast
3
+
4
+ from algosdk.kmd import KMDClient
5
+
6
+ from algokit_utils.clients.client_manager import ClientManager
7
+ from algokit_utils.config import config
8
+ from algokit_utils.models.account import SigningAccount
9
+ from algokit_utils.models.amount import AlgoAmount
10
+ from algokit_utils.transactions.transaction_composer import PaymentParams, TransactionComposer
11
+
12
+ __all__ = ["KmdAccount", "KmdAccountManager"]
13
+
14
+ logger = config.logger
15
+
16
+
17
+ class KmdAccount(SigningAccount):
18
+ """Account retrieved from KMD with signing capabilities, extending base Account.
19
+
20
+ Provides an account implementation that can be used to sign transactions using keys stored in KMD.
21
+
22
+ :param private_key: Base64 encoded private key
23
+ :param address: Optional address override for rekeyed accounts, defaults to None
24
+ """
25
+
26
+ def __init__(self, private_key: str, address: str | None = None) -> None:
27
+ super().__init__(private_key=private_key, address=address or "")
28
+
29
+
30
+ class KmdAccountManager:
31
+ """Provides abstractions over KMD that makes it easier to get and manage accounts."""
32
+
33
+ _kmd: KMDClient | None
34
+
35
+ def __init__(self, client_manager: ClientManager) -> None:
36
+ self._client_manager = client_manager
37
+ try:
38
+ self._kmd = client_manager.kmd
39
+ except ValueError:
40
+ self._kmd = None
41
+
42
+ def kmd(self) -> KMDClient:
43
+ """Returns the KMD client, initializing it if needed.
44
+
45
+ :raises Exception: If KMD client is not configured and not running against LocalNet
46
+ :return: The KMD client
47
+ """
48
+ if self._kmd is None:
49
+ if self._client_manager.is_localnet():
50
+ kmd_config = ClientManager.get_config_from_environment_or_localnet()
51
+ self._kmd = ClientManager.get_kmd_client(kmd_config.kmd_config)
52
+ return self._kmd
53
+ raise Exception("Attempt to use KMD client with no KMD configured")
54
+ return self._kmd
55
+
56
+ def get_wallet_account(
57
+ self,
58
+ wallet_name: str,
59
+ predicate: Callable[[dict[str, Any]], bool] | None = None,
60
+ sender: str | None = None,
61
+ ) -> KmdAccount | None:
62
+ """Returns an Algorand signing account with private key loaded from the given KMD wallet.
63
+
64
+ Retrieves an account from a KMD wallet that matches the given predicate, or a random account
65
+ if no predicate is provided.
66
+
67
+ :param wallet_name: The name of the wallet to retrieve an account from
68
+ :param predicate: Optional filter to use to find the account (otherwise gets a random account from the wallet)
69
+ :param sender: Optional sender address to use this signer for (aka a rekeyed account)
70
+ :return: The signing account or None if no matching wallet or account was found
71
+ """
72
+
73
+ kmd_client = self.kmd()
74
+ wallets = kmd_client.list_wallets()
75
+ wallet = next((w for w in wallets if w["name"] == wallet_name), None)
76
+ if not wallet:
77
+ return None
78
+
79
+ wallet_id = wallet["id"]
80
+ wallet_handle = kmd_client.init_wallet_handle(wallet_id, "")
81
+ addresses = kmd_client.list_keys(wallet_handle)
82
+
83
+ matched_address = None
84
+ if predicate:
85
+ for address in addresses:
86
+ account_info = self._client_manager.algod.account_info(address)
87
+ if predicate(cast(dict[str, Any], account_info)):
88
+ matched_address = address
89
+ break
90
+ else:
91
+ matched_address = next(iter(addresses), None)
92
+
93
+ if not matched_address:
94
+ return None
95
+
96
+ private_key = kmd_client.export_key(wallet_handle, "", matched_address)
97
+ return KmdAccount(private_key=private_key, address=sender)
98
+
99
+ def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = None) -> KmdAccount:
100
+ """Gets or creates a funded account in a KMD wallet of the given name.
101
+
102
+ Provides idempotent access to accounts from LocalNet without specifying the private key.
103
+
104
+ :param name: The name of the wallet to retrieve / create
105
+ :param fund_with: The number of Algos to fund the account with when created
106
+ :return: An Algorand account with private key loaded
107
+ """
108
+ fund_with = fund_with or AlgoAmount.from_algo(1000)
109
+
110
+ existing = self.get_wallet_account(name)
111
+ if existing:
112
+ return existing
113
+
114
+ kmd_client = self.kmd()
115
+ wallet_id = kmd_client.create_wallet(name, "")["id"]
116
+ wallet_handle = kmd_client.init_wallet_handle(wallet_id, "")
117
+ kmd_client.generate_key(wallet_handle)
118
+
119
+ account = self.get_wallet_account(name)
120
+ assert account is not None
121
+
122
+ logger.info(
123
+ f"LocalNet account '{name}' doesn't yet exist; created account {account.address} "
124
+ f"with keys stored in KMD and funding with {fund_with} ALGO"
125
+ )
126
+
127
+ dispenser = self.get_localnet_dispenser_account()
128
+ TransactionComposer(
129
+ algod=self._client_manager.algod,
130
+ get_signer=lambda _: dispenser.signer,
131
+ get_suggested_params=self._client_manager.algod.suggested_params,
132
+ ).add_payment(
133
+ PaymentParams(
134
+ sender=dispenser.address,
135
+ receiver=account.address,
136
+ amount=fund_with,
137
+ )
138
+ ).send()
139
+ return account
140
+
141
+ def get_localnet_dispenser_account(self) -> KmdAccount:
142
+ """Returns an Algorand account with private key loaded for the default LocalNet dispenser account.
143
+
144
+ Retrieves the default funded account from LocalNet that can be used to fund other accounts.
145
+
146
+ :raises Exception: If not running against LocalNet or dispenser account not found
147
+ :return: The default LocalNet dispenser account
148
+ """
149
+ if not self._client_manager.is_localnet():
150
+ raise Exception("Can't get LocalNet dispenser account from non LocalNet network")
151
+
152
+ dispenser = self.get_wallet_account(
153
+ "unencrypted-default-wallet",
154
+ lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000, # noqa: PLR2004
155
+ )
156
+ if not dispenser:
157
+ raise Exception("Error retrieving LocalNet dispenser account; couldn't find the default account in KMD")
158
+
159
+ return dispenser
@@ -0,0 +1,265 @@
1
+ import copy
2
+ import time
3
+
4
+ import typing_extensions
5
+ from algosdk.atomic_transaction_composer import TransactionSigner
6
+ from algosdk.kmd import KMDClient
7
+ from algosdk.transaction import SuggestedParams
8
+ from algosdk.v2client.algod import AlgodClient
9
+ from algosdk.v2client.indexer import IndexerClient
10
+
11
+ from algokit_utils.accounts.account_manager import AccountManager
12
+ from algokit_utils.applications.app_deployer import AppDeployer
13
+ from algokit_utils.applications.app_manager import AppManager
14
+ from algokit_utils.assets.asset_manager import AssetManager
15
+ from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager
16
+ from algokit_utils.models.network import AlgoClientConfigs, AlgoClientNetworkConfig
17
+ from algokit_utils.protocols.account import TransactionSignerAccountProtocol
18
+ from algokit_utils.transactions.transaction_composer import (
19
+ TransactionComposer,
20
+ )
21
+ from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator
22
+ from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender
23
+
24
+ __all__ = [
25
+ "AlgorandClient",
26
+ ]
27
+
28
+
29
+ class AlgorandClient:
30
+ """A client that brokers easy access to Algorand functionality."""
31
+
32
+ def __init__(self, config: AlgoClientConfigs | AlgoSdkClients):
33
+ self._client_manager: ClientManager = ClientManager(clients_or_configs=config, algorand_client=self)
34
+ self._account_manager: AccountManager = AccountManager(self._client_manager)
35
+ self._asset_manager: AssetManager = AssetManager(self._client_manager.algod, lambda: self.new_group())
36
+ self._app_manager: AppManager = AppManager(self._client_manager.algod)
37
+ self._transaction_sender = AlgorandClientTransactionSender(
38
+ new_group=lambda: self.new_group(),
39
+ asset_manager=self._asset_manager,
40
+ app_manager=self._app_manager,
41
+ algod_client=self._client_manager.algod,
42
+ )
43
+ self._app_deployer: AppDeployer = AppDeployer(
44
+ self._app_manager, self._transaction_sender, self._client_manager.indexer_if_present
45
+ )
46
+ self._transaction_creator = AlgorandClientTransactionCreator(
47
+ new_group=lambda: self.new_group(),
48
+ )
49
+
50
+ self._cached_suggested_params: SuggestedParams | None = None
51
+ self._cached_suggested_params_expiry: float | None = None
52
+ self._cached_suggested_params_timeout: int = 3_000 # three seconds
53
+ self._default_validity_window: int | None = None
54
+
55
+ def set_default_validity_window(self, validity_window: int) -> typing_extensions.Self:
56
+ """
57
+ Sets the default validity window for transactions.
58
+
59
+ :param validity_window: The number of rounds between the first and last valid rounds
60
+ :return: The `AlgorandClient` so method calls can be chained
61
+ """
62
+ self._default_validity_window = validity_window
63
+ return self
64
+
65
+ def set_default_signer(
66
+ self, signer: TransactionSigner | TransactionSignerAccountProtocol
67
+ ) -> typing_extensions.Self:
68
+ """
69
+ Sets the default signer to use if no other signer is specified.
70
+
71
+ :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccountProtocol`
72
+ :return: The `AlgorandClient` so method calls can be chained
73
+ """
74
+ self._account_manager.set_default_signer(signer)
75
+ return self
76
+
77
+ def set_signer(self, sender: str, signer: TransactionSigner) -> typing_extensions.Self:
78
+ """
79
+ Tracks the given account for later signing.
80
+
81
+ :param sender: The sender address to use this signer for
82
+ :param signer: The signer to sign transactions with for the given sender
83
+ :return: The `AlgorandClient` so method calls can be chained
84
+ """
85
+ self._account_manager.set_signer(sender, signer)
86
+ return self
87
+
88
+ def set_signer_account(self, signer: TransactionSignerAccountProtocol) -> typing_extensions.Self:
89
+ """
90
+ Sets the default signer to use if no other signer is specified.
91
+
92
+ :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccountProtocol`
93
+ :return: The `AlgorandClient` so method calls can be chained
94
+ """
95
+ self._account_manager.set_default_signer(signer)
96
+ return self
97
+
98
+ def set_suggested_params(
99
+ self, suggested_params: SuggestedParams, until: float | None = None
100
+ ) -> typing_extensions.Self:
101
+ """
102
+ Sets a cache value to use for suggested params.
103
+
104
+ :param suggested_params: The suggested params to use
105
+ :param until: A timestamp until which to cache, or if not specified then the timeout is used
106
+ :return: The `AlgorandClient` so method calls can be chained
107
+ """
108
+ self._cached_suggested_params = suggested_params
109
+ self._cached_suggested_params_expiry = until or time.time() + self._cached_suggested_params_timeout
110
+ return self
111
+
112
+ def set_suggested_params_timeout(self, timeout: int) -> typing_extensions.Self:
113
+ """
114
+ Sets the timeout for caching suggested params.
115
+
116
+ :param timeout: The timeout in milliseconds
117
+ :return: The `AlgorandClient` so method calls can be chained
118
+ """
119
+ self._cached_suggested_params_timeout = timeout
120
+ return self
121
+
122
+ def get_suggested_params(self) -> SuggestedParams:
123
+ """Get suggested params for a transaction (either cached or from algod if the cache is stale or empty)"""
124
+ if self._cached_suggested_params and (
125
+ self._cached_suggested_params_expiry is None or self._cached_suggested_params_expiry > time.time()
126
+ ):
127
+ return copy.deepcopy(self._cached_suggested_params)
128
+
129
+ self._cached_suggested_params = self._client_manager.algod.suggested_params()
130
+ self._cached_suggested_params_expiry = time.time() + self._cached_suggested_params_timeout
131
+
132
+ return copy.deepcopy(self._cached_suggested_params)
133
+
134
+ def new_group(self) -> TransactionComposer:
135
+ """Start a new `TransactionComposer` transaction group"""
136
+ return TransactionComposer(
137
+ algod=self.client.algod,
138
+ get_signer=lambda addr: self.account.get_signer(addr),
139
+ get_suggested_params=self.get_suggested_params,
140
+ default_validity_window=self._default_validity_window,
141
+ )
142
+
143
+ @property
144
+ def client(self) -> ClientManager:
145
+ """Get clients, including algosdk clients and app clients."""
146
+ return self._client_manager
147
+
148
+ @property
149
+ def account(self) -> AccountManager:
150
+ """Get or create accounts that can sign transactions."""
151
+ return self._account_manager
152
+
153
+ @property
154
+ def asset(self) -> AssetManager:
155
+ """Get or create assets."""
156
+ return self._asset_manager
157
+
158
+ @property
159
+ def app(self) -> AppManager:
160
+ return self._app_manager
161
+
162
+ @property
163
+ def app_deployer(self) -> AppDeployer:
164
+ """Get or create applications."""
165
+ return self._app_deployer
166
+
167
+ @property
168
+ def send(self) -> AlgorandClientTransactionSender:
169
+ """Methods for sending a transaction and waiting for confirmation"""
170
+ return self._transaction_sender
171
+
172
+ @property
173
+ def create_transaction(self) -> AlgorandClientTransactionCreator:
174
+ """Methods for building transactions"""
175
+ return self._transaction_creator
176
+
177
+ @staticmethod
178
+ def default_localnet() -> "AlgorandClient":
179
+ """
180
+ Returns an `AlgorandClient` pointing at default LocalNet ports and API token.
181
+
182
+ :return: The `AlgorandClient`
183
+ """
184
+ return AlgorandClient(
185
+ AlgoClientConfigs(
186
+ algod_config=ClientManager.get_default_localnet_config("algod"),
187
+ indexer_config=ClientManager.get_default_localnet_config("indexer"),
188
+ kmd_config=ClientManager.get_default_localnet_config("kmd"),
189
+ )
190
+ )
191
+
192
+ @staticmethod
193
+ def testnet() -> "AlgorandClient":
194
+ """
195
+ Returns an `AlgorandClient` pointing at TestNet using AlgoNode.
196
+
197
+ :return: The `AlgorandClient`
198
+ """
199
+ return AlgorandClient(
200
+ AlgoClientConfigs(
201
+ algod_config=ClientManager.get_algonode_config("testnet", "algod"),
202
+ indexer_config=ClientManager.get_algonode_config("testnet", "indexer"),
203
+ kmd_config=None,
204
+ )
205
+ )
206
+
207
+ @staticmethod
208
+ def mainnet() -> "AlgorandClient":
209
+ """
210
+ Returns an `AlgorandClient` pointing at MainNet using AlgoNode.
211
+
212
+ :return: The `AlgorandClient`
213
+ """
214
+ return AlgorandClient(
215
+ AlgoClientConfigs(
216
+ algod_config=ClientManager.get_algonode_config("mainnet", "algod"),
217
+ indexer_config=ClientManager.get_algonode_config("mainnet", "indexer"),
218
+ kmd_config=None,
219
+ )
220
+ )
221
+
222
+ @staticmethod
223
+ def from_clients(
224
+ algod: AlgodClient, indexer: IndexerClient | None = None, kmd: KMDClient | None = None
225
+ ) -> "AlgorandClient":
226
+ """
227
+ Returns an `AlgorandClient` pointing to the given client(s).
228
+
229
+ :param algod: The algod client to use
230
+ :param indexer: The indexer client to use
231
+ :param kmd: The kmd client to use
232
+ :return: The `AlgorandClient`
233
+ """
234
+ return AlgorandClient(AlgoSdkClients(algod=algod, indexer=indexer, kmd=kmd))
235
+
236
+ @staticmethod
237
+ def from_environment() -> "AlgorandClient":
238
+ """
239
+ Returns an `AlgorandClient` loading the configuration from environment variables.
240
+
241
+ Retrieve configurations from environment variables when defined or get defaults.
242
+
243
+ Expects to be called from a Python environment.
244
+
245
+ :return: The `AlgorandClient`
246
+ """
247
+ return AlgorandClient(ClientManager.get_config_from_environment_or_localnet())
248
+
249
+ @staticmethod
250
+ def from_config(
251
+ algod_config: AlgoClientNetworkConfig,
252
+ indexer_config: AlgoClientNetworkConfig | None = None,
253
+ kmd_config: AlgoClientNetworkConfig | None = None,
254
+ ) -> "AlgorandClient":
255
+ """
256
+ Returns an `AlgorandClient` from the given config.
257
+
258
+ :param algod_config: The config to use for the algod client
259
+ :param indexer_config: The config to use for the indexer client
260
+ :param kmd_config: The config to use for the kmd client
261
+ :return: The `AlgorandClient`
262
+ """
263
+ return AlgorandClient(
264
+ AlgoClientConfigs(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config)
265
+ )