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

@@ -177,10 +177,10 @@ def get_account(
177
177
  For LocalNet environments, loads or creates an account from a KMD wallet named {name}.
178
178
 
179
179
  :example:
180
- >>> # If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call:
181
- >>> account = get_account('ACCOUNT', algod)
182
- >>> # If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created
183
- >>> # with an account that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser.
180
+ >>> # If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call:
181
+ >>> account = get_account('ACCOUNT', algod)
182
+ >>> # If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created
183
+ >>> # with an account that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser.
184
184
 
185
185
  :param client: The Algorand client to use
186
186
  :param name: The name identifier to use for loading/creating the account
@@ -74,6 +74,7 @@ __all__ = [
74
74
  representing an ABI method name or signature"""
75
75
 
76
76
 
77
+ @deprecated("Use 'algokit_utils.calculate_extra_program_pages' instead.")
77
78
  def num_extra_program_pages(approval: bytes, clear: bytes) -> int:
78
79
  """Calculate minimum number of extra_pages required for provided approval and clear programs"""
79
80
 
@@ -148,7 +148,7 @@ class AccountManager:
148
148
  :param client_manager: The ClientManager client to use for algod and kmd clients
149
149
 
150
150
  :example:
151
- >>> account_manager = AccountManager(client_manager)
151
+ >>> account_manager = AccountManager(client_manager)
152
152
  """
153
153
 
154
154
  def __init__(self, client_manager: ClientManager):
@@ -172,11 +172,11 @@ class AccountManager:
172
172
  :returns: The `AccountManager` so method calls can be chained
173
173
 
174
174
  :example:
175
- >>> signer_account = account_manager.random()
176
- >>> account_manager.set_default_signer(signer_account.signer)
177
- >>> # When signing a transaction, if there is no signer registered for the sender
178
- >>> # then the default signer will be used
179
- >>> signer = account_manager.get_signer("{SENDERADDRESS}")
175
+ >>> signer_account = account_manager.random()
176
+ >>> account_manager.set_default_signer(signer_account.signer)
177
+ >>> # When signing a transaction, if there is no signer registered for the sender
178
+ >>> # then the default signer will be used
179
+ >>> signer = account_manager.get_signer("{SENDERADDRESS}")
180
180
  """
181
181
  self._default_signer = signer if isinstance(signer, TransactionSigner) else signer.signer
182
182
  return self
@@ -190,7 +190,7 @@ class AccountManager:
190
190
  :returns: The `AccountManager` instance for method chaining
191
191
 
192
192
  :example:
193
- >>> account_manager.set_signer("SENDERADDRESS", transaction_signer)
193
+ >>> account_manager.set_signer("SENDERADDRESS", transaction_signer)
194
194
  """
195
195
  self._accounts[sender] = TransactionSignerAccount(address=sender, signer=signer)
196
196
  return self
@@ -221,11 +221,11 @@ class AccountManager:
221
221
  :returns: The `AccountManager` instance for method chaining
222
222
 
223
223
  :example:
224
- >>> account_manager = AccountManager(client_manager)
225
- >>> account_manager.set_signer_from_account(SigningAccount(private_key=algosdk.account.generate_account()[0]))
226
- >>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args)))
227
- >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2]))
228
- """
224
+ >>> account_manager = AccountManager(client_manager)
225
+ >>> account_manager.set_signer_from_account(SigningAccount(private_key=algosdk.account.generate_account()[0]))
226
+ >>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args)))
227
+ >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2]))
228
+ """ # noqa: E501
229
229
  self._accounts[account.address] = account
230
230
  return self
231
231
 
@@ -240,7 +240,7 @@ class AccountManager:
240
240
  :raises ValueError: If no signer is found and no default signer is set
241
241
 
242
242
  :example:
243
- >>> signer = account_manager.get_signer("SENDERADDRESS")
243
+ >>> signer = account_manager.get_signer("SENDERADDRESS")
244
244
  """
245
245
  signer = self._accounts.get(self._get_address(sender)) or self._default_signer
246
246
  if not signer:
@@ -256,10 +256,10 @@ class AccountManager:
256
256
  :raises ValueError: If no account is found or if the account is not a regular account
257
257
 
258
258
  :example:
259
- >>> sender = account_manager.random().address
260
- >>> # ...
261
- >>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered
262
- >>> account = account_manager.get_account(sender)
259
+ >>> sender = account_manager.random().address
260
+ >>> # ...
261
+ >>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered
262
+ >>> account = account_manager.get_account(sender)
263
263
  """
264
264
  account = self._accounts.get(sender)
265
265
  if not account:
@@ -279,8 +279,8 @@ class AccountManager:
279
279
  :returns: The account information
280
280
 
281
281
  :example:
282
- >>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA"
283
- >>> account_info = account_manager.get_information(address)
282
+ >>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA"
283
+ >>> account_info = account_manager.get_information(address)
284
284
  """
285
285
  info = self._client_manager.algod.account_info(self._get_address(sender))
286
286
  assert isinstance(info, dict)
@@ -342,7 +342,7 @@ class AccountManager:
342
342
  from the environment (ideally via a secret storage service) rather than the file system.
343
343
 
344
344
  :example:
345
- >>> account = account_manager.from_mnemonic("mnemonic secret ...")
345
+ >>> account = account_manager.from_mnemonic("mnemonic secret ...")
346
346
  """
347
347
  return self._register_account(to_private_key(mnemonic), sender)
348
348
 
@@ -355,7 +355,7 @@ class AccountManager:
355
355
 
356
356
  :param name: The name identifier of the account
357
357
  :param fund_with: Optional amount to fund the account with when it gets created
358
- (when targeting LocalNet)
358
+ (when targeting LocalNet)
359
359
  :returns: The account
360
360
  :raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME}
361
361
 
@@ -368,10 +368,10 @@ class AccountManager:
368
368
  it will create it and fund the account for you
369
369
 
370
370
  :example:
371
- >>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call:
372
- >>> account = account_manager.from_environment('MY_ACCOUNT')
373
- >>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created
374
- >>> # with an account that is automatically funded with the specified amount from the default LocalNet dispenser
371
+ >>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call:
372
+ >>> account = account_manager.from_environment('MY_ACCOUNT')
373
+ >>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created
374
+ >>> # with an account that is automatically funded with the specified amount from the LocalNet dispenser
375
375
  """
376
376
  account_mnemonic = os.getenv(f"{name.upper()}_MNEMONIC")
377
377
 
@@ -398,10 +398,10 @@ class AccountManager:
398
398
  :raises ValueError: If unable to find KMD account with given name and predicate
399
399
 
400
400
  :example:
401
- >>> # Get default funded account in a LocalNet:
402
- >>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet',
403
- ... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000
404
- ... )
401
+ >>> # Get default funded account in a LocalNet:
402
+ >>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet',
403
+ ... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000
404
+ ... )
405
405
  """
406
406
  kmd_account = self._kmd_account_manager.get_wallet_account(name, predicate, sender)
407
407
  if not kmd_account:
@@ -418,7 +418,7 @@ class AccountManager:
418
418
  :returns: A logic signature account wrapper
419
419
 
420
420
  :example:
421
- >>> account = account.logic_sig(program, [new Uint8Array(3, ...)])
421
+ >>> account = account.logic_sig(program, [new Uint8Array(3, ...)])
422
422
  """
423
423
  return self._register_logicsig(program, args)
424
424
 
@@ -431,12 +431,12 @@ class AccountManager:
431
431
  :returns: A multisig account wrapper
432
432
 
433
433
  :example:
434
- >>> account = account_manager.multi_sig(
435
- ... version=1,
436
- ... threshold=1,
437
- ... addrs=["ADDRESS1...", "ADDRESS2..."],
438
- ... signing_accounts=[account1, account2]
439
- ... )
434
+ >>> account = account_manager.multi_sig(
435
+ ... version=1,
436
+ ... threshold=1,
437
+ ... addrs=["ADDRESS1...", "ADDRESS2..."],
438
+ ... signing_accounts=[account1, account2]
439
+ ... )
440
440
  """
441
441
  return self._register_multisig(metadata, signing_accounts)
442
442
 
@@ -447,7 +447,7 @@ class AccountManager:
447
447
  :returns: The account
448
448
 
449
449
  :example:
450
- >>> account = account_manager.random()
450
+ >>> account = account_manager.random()
451
451
  """
452
452
  private_key, _ = algosdk.account.generate_account()
453
453
  return self._register_account(private_key)
@@ -461,7 +461,7 @@ class AccountManager:
461
461
  :returns: The account
462
462
 
463
463
  :example:
464
- >>> account = account_manager.localnet_dispenser()
464
+ >>> account = account_manager.localnet_dispenser()
465
465
  """
466
466
  kmd_account = self._kmd_account_manager.get_localnet_dispenser_account()
467
467
  return self._register_account(kmd_account.private_key)
@@ -475,7 +475,7 @@ class AccountManager:
475
475
  :returns: The account
476
476
 
477
477
  :example:
478
- >>> account = account_manager.dispenser_from_environment()
478
+ >>> account = account_manager.dispenser_from_environment()
479
479
  """
480
480
  name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC")
481
481
  if name:
@@ -493,8 +493,8 @@ class AccountManager:
493
493
  :returns: The rekeyed account
494
494
 
495
495
  :example:
496
- >>> account = account.from_mnemonic("mnemonic secret ...")
497
- >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...")
496
+ >>> account = account.from_mnemonic("mnemonic secret ...")
497
+ >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...")
498
498
  """
499
499
  sender_address = sender.address if isinstance(sender, SigningAccount) else sender
500
500
  self._accounts[sender_address] = TransactionSignerAccount(address=sender_address, signer=account.signer)
@@ -540,23 +540,23 @@ class AccountManager:
540
540
  `official rekey guidance <https://developer.algorand.org/docs/get-details/accounts/rekey/>`_.
541
541
 
542
542
  :example:
543
- >>> # Basic example (with string addresses):
544
- >>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"})
545
- >>> # Basic example (with signer accounts):
546
- >>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount})
547
- >>> # Advanced example:
548
- >>> algorand.account.rekey_account({
549
- ... account: "ACCOUNTADDRESS",
550
- ... rekey_to: "NEWADDRESS",
551
- ... lease: 'lease',
552
- ... note: 'note',
553
- ... first_valid_round: 1000,
554
- ... validity_window: 10,
555
- ... extra_fee: AlgoAmount.from_micro_algo(1000),
556
- ... static_fee: AlgoAmount.from_micro_algo(1000),
557
- ... max_fee: AlgoAmount.from_micro_algo(3000),
558
- ... suppress_log: True,
559
- ... })
543
+ >>> # Basic example (with string addresses):
544
+ >>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"})
545
+ >>> # Basic example (with signer accounts):
546
+ >>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount})
547
+ >>> # Advanced example:
548
+ >>> algorand.account.rekey_account({
549
+ ... account: "ACCOUNTADDRESS",
550
+ ... rekey_to: "NEWADDRESS",
551
+ ... lease: 'lease',
552
+ ... note: 'note',
553
+ ... first_valid_round: 1000,
554
+ ... validity_window: 10,
555
+ ... extra_fee: AlgoAmount.from_micro_algo(1000),
556
+ ... static_fee: AlgoAmount.from_micro_algo(1000),
557
+ ... max_fee: AlgoAmount.from_micro_algo(3000),
558
+ ... suppress_log: True,
559
+ ... })
560
560
  """
561
561
  sender_address = self._get_address(account)
562
562
  rekey_address = self._get_address(rekey_to)
@@ -623,7 +623,7 @@ class AccountManager:
623
623
  :param account_to_fund: The account to fund
624
624
  :param dispenser_account: The account to use as a dispenser funding source
625
625
  :param min_spending_balance: The minimum balance of Algo that the account
626
- should have available to spend
626
+ should have available to spend
627
627
  :param min_funding_increment: Optional minimum funding increment
628
628
  :param send_params: Parameters for the send operation, defaults to None
629
629
  :param signer: Optional transaction signer
@@ -637,20 +637,20 @@ class AccountManager:
637
637
  :param first_valid_round: Optional first valid round
638
638
  :param last_valid_round: Optional last valid round
639
639
  :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed,
640
- or None if no funds were needed
640
+ or None if no funds were needed
641
641
 
642
642
  :example:
643
- >>> # Basic example:
644
- >>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1))
645
- >>> # With configuration:
646
- >>> algorand.account.ensure_funded(
647
- ... "ACCOUNTADDRESS",
648
- ... "DISPENSERADDRESS",
649
- ... algokit.algo(1),
650
- ... min_funding_increment=algokit.algo(2),
651
- ... fee=AlgoAmount.from_micro_algo(1000),
652
- ... suppress_log=True
653
- ... )
643
+ >>> # Basic example:
644
+ >>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1))
645
+ >>> # With configuration:
646
+ >>> algorand.account.ensure_funded(
647
+ ... "ACCOUNTADDRESS",
648
+ ... "DISPENSERADDRESS",
649
+ ... algokit.algo(1),
650
+ ... min_funding_increment=algokit.algo(2),
651
+ ... fee=AlgoAmount.from_micro_algo(1000),
652
+ ... suppress_log=True
653
+ ... )
654
654
  """
655
655
  account_to_fund = self._get_address(account_to_fund)
656
656
  dispenser_account = self._get_address(dispenser_account)
@@ -724,7 +724,7 @@ class AccountManager:
724
724
 
725
725
  :param account_to_fund: The account to fund
726
726
  :param min_spending_balance: The minimum balance of Algo that the account should have available to
727
- spend
727
+ spend
728
728
  :param min_funding_increment: Optional minimum funding increment
729
729
  :param send_params: Parameters for the send operation, defaults to None
730
730
  :param signer: Optional transaction signer
@@ -738,7 +738,7 @@ class AccountManager:
738
738
  :param first_valid_round: Optional first valid round
739
739
  :param last_valid_round: Optional last valid round
740
740
  :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or
741
- None if no funds were needed
741
+ None if no funds were needed
742
742
 
743
743
  .. note::
744
744
  The dispenser account is retrieved from the account mnemonic stored in
@@ -746,16 +746,16 @@ class AccountManager:
746
746
  if it's a rekeyed account, or against default LocalNet if no environment variables present.
747
747
 
748
748
  :example:
749
- >>> # Basic example:
750
- >>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1))
751
- >>> # With configuration:
752
- >>> algorand.account.ensure_funded_from_environment(
753
- ... "ACCOUNTADDRESS",
754
- ... algokit.algo(1),
755
- ... min_funding_increment=algokit.algo(2),
756
- ... fee=AlgoAmount.from_micro_algo(1000),
757
- ... suppress_log=True
758
- ... )
749
+ >>> # Basic example:
750
+ >>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1))
751
+ >>> # With configuration:
752
+ >>> algorand.account.ensure_funded_from_environment(
753
+ ... "ACCOUNTADDRESS",
754
+ ... algokit.algo(1),
755
+ ... min_funding_increment=algokit.algo(2),
756
+ ... fee=AlgoAmount.from_micro_algo(1000),
757
+ ... suppress_log=True
758
+ ... )
759
759
  """
760
760
  account_to_fund = self._get_address(account_to_fund)
761
761
  dispenser_account = self.dispenser_from_environment()
@@ -818,26 +818,26 @@ class AccountManager:
818
818
  :param account_to_fund: The account to fund
819
819
  :param dispenser_client: The TestNet dispenser funding client
820
820
  :param min_spending_balance: The minimum balance of Algo that the account should have
821
- available to spend
821
+ available to spend
822
822
  :param min_funding_increment: Optional minimum funding increment
823
823
  :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or
824
- None if no funds were needed
824
+ None if no funds were needed
825
825
  :raises ValueError: If attempting to fund on non-TestNet network
826
826
 
827
827
  :example:
828
- >>> # Basic example:
829
- >>> algorand.account.ensure_funded_from_testnet_dispenser_api(
830
- ... "ACCOUNTADDRESS",
831
- ... algorand.client.get_testnet_dispenser_from_environment(),
832
- ... algokit.algo(1)
833
- ... )
834
- >>> # With configuration:
835
- >>> algorand.account.ensure_funded_from_testnet_dispenser_api(
836
- ... "ACCOUNTADDRESS",
837
- ... algorand.client.get_testnet_dispenser_from_environment(),
838
- ... algokit.algo(1),
839
- ... min_funding_increment=algokit.algo(2)
840
- ... )
828
+ >>> # Basic example:
829
+ >>> algorand.account.ensure_funded_from_testnet_dispenser_api(
830
+ ... "ACCOUNTADDRESS",
831
+ ... algorand.client.get_testnet_dispenser_from_environment(),
832
+ ... algokit.algo(1)
833
+ ... )
834
+ >>> # With configuration:
835
+ >>> algorand.account.ensure_funded_from_testnet_dispenser_api(
836
+ ... "ACCOUNTADDRESS",
837
+ ... algorand.client.get_testnet_dispenser_from_environment(),
838
+ ... algokit.algo(1),
839
+ ... min_funding_increment=algokit.algo(2)
840
+ ... )
841
841
  """
842
842
  account_to_fund = self._get_address(account_to_fund)
843
843
 
@@ -48,6 +48,8 @@ class KmdAccountManager:
48
48
  if self._kmd is None:
49
49
  if self._client_manager.is_localnet():
50
50
  kmd_config = ClientManager.get_config_from_environment_or_localnet()
51
+ if not kmd_config.kmd_config:
52
+ raise Exception("Attempt to use KMD client with no KMD configured")
51
53
  self._kmd = ClientManager.get_kmd_client(kmd_config.kmd_config)
52
54
  return self._kmd
53
55
  raise Exception("Attempt to use KMD client with no KMD configured")
@@ -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,
@@ -1189,28 +1190,51 @@ class _TransactionSender:
1189
1190
  :param params: Parameters for the application call including method and transaction options
1190
1191
  :param send_params: Send parameters
1191
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
1192
1194
  """
1193
1195
  is_read_only_call = (
1194
1196
  params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None
1195
1197
  ) and self._app_spec.get_arc56_method(params.method).readonly
1196
1198
 
1197
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
+
1198
1214
  method_call_to_simulate = self._algorand.new_group().add_app_call_method_call(
1199
- self._client.params.call(params)
1200
- )
1201
- send_params = send_params or SendParams()
1202
- simulate_response = self._client._handle_call_errors(
1203
- lambda: method_call_to_simulate.simulate(
1204
- allow_unnamed_resources=send_params.get("populate_app_call_resources") or True,
1205
- skip_signatures=True,
1206
- allow_more_logs=True,
1207
- allow_empty_signatures=True,
1208
- extra_opcode_budget=None,
1209
- exec_trace_config=None,
1210
- simulation_round=None,
1211
- )
1215
+ self._client.params.call(readonly_params)
1212
1216
  )
1213
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
+
1214
1238
  return SendAppTransactionResult[Arc56ReturnValueType](
1215
1239
  tx_ids=simulate_response.tx_ids,
1216
1240
  transactions=simulate_response.transactions,
@@ -21,6 +21,7 @@ from algokit_utils.transactions.transaction_composer import (
21
21
  AppUpdateMethodCallParams,
22
22
  AppUpdateParams,
23
23
  TransactionComposer,
24
+ calculate_extra_program_pages,
24
25
  )
25
26
  from algokit_utils.transactions.transaction_sender import (
26
27
  AlgorandClientTransactionSender,
@@ -169,7 +170,7 @@ class AppDeployer:
169
170
  f"{'teal code' if isinstance(deployment.create_params.approval_program, str) else 'AVM bytecode'} and "
170
171
  f"{len(deployment.create_params.clear_state_program)} bytes of "
171
172
  f"{'teal code' if isinstance(deployment.create_params.clear_state_program, str) else 'AVM bytecode'}",
172
- suppress_log=suppress_log,
173
+ extra={"suppress_log": suppress_log},
173
174
  )
174
175
  note = TransactionComposer.arc2_note(
175
176
  {
@@ -242,9 +243,13 @@ class AppDeployer:
242
243
 
243
244
  existing_approval = base64.b64encode(existing_app_record.approval_program).decode()
244
245
  existing_clear = base64.b64encode(existing_app_record.clear_state_program).decode()
246
+ existing_extra_pages = calculate_extra_program_pages(
247
+ existing_app_record.approval_program, existing_app_record.clear_state_program
248
+ )
245
249
 
246
250
  new_approval = base64.b64encode(approval_program).decode()
247
251
  new_clear = base64.b64encode(clear_program).decode()
252
+ new_extra_pages = calculate_extra_program_pages(approval_program, clear_program)
248
253
 
249
254
  is_update = new_approval != existing_approval or new_clear != existing_clear
250
255
  is_schema_break = (
@@ -256,6 +261,7 @@ class AppDeployer:
256
261
  < (deployment.create_params.schema.get("local_byte_slices", 0) if deployment.create_params.schema else 0)
257
262
  or existing_app_record.global_byte_slices
258
263
  < (deployment.create_params.schema.get("global_byte_slices", 0) if deployment.create_params.schema else 0)
264
+ or existing_extra_pages < new_extra_pages
259
265
  )
260
266
 
261
267
  if is_schema_break:
@@ -269,8 +275,8 @@ class AppDeployer:
269
275
  "local_byte_slices": existing_app_record.local_byte_slices,
270
276
  },
271
277
  "to": deployment.create_params.schema,
278
+ "suppress_log": suppress_log,
272
279
  },
273
- suppress_log=suppress_log,
274
280
  )
275
281
 
276
282
  return self._handle_schema_break(
@@ -288,7 +294,7 @@ class AppDeployer:
288
294
  clear_program=clear_program,
289
295
  )
290
296
 
291
- logger.debug("No detected changes in app, nothing to do.", suppress_log=suppress_log)
297
+ logger.debug("No detected changes in app, nothing to do.", extra={"suppress_log": suppress_log})
292
298
  return AppDeployResult(
293
299
  app=existing_app,
294
300
  operation_performed=OperationPerformed.Nothing,
@@ -340,6 +346,12 @@ class AppDeployer:
340
346
  )
341
347
 
342
348
  self._update_app_lookup(deployment.create_params.sender, app_metadata)
349
+ logger.debug(
350
+ f"Sent transaction ID {create_result.app_id} (AppCreate) from {deployment.create_params.sender}",
351
+ extra={
352
+ "suppress_log": deployment.send_params.get("suppress_log") or False if deployment.send_params else False
353
+ },
354
+ )
343
355
 
344
356
  return AppDeployResult(
345
357
  app=app_metadata,
@@ -412,6 +424,13 @@ class AppDeployer:
412
424
  deleted=False,
413
425
  )
414
426
  self._update_app_lookup(deployment.create_params.sender, app_metadata)
427
+ logger.debug(
428
+ f"Group transaction sent: Replaced app {existing_app.app_id} with new app {app_id} from "
429
+ f"{deployment.create_params.sender} (Composer group count: {composer.count()})",
430
+ extra={
431
+ "suppress_log": deployment.send_params.get("suppress_log") or False if deployment.send_params else False
432
+ },
433
+ )
415
434
 
416
435
  return AppDeployResult(
417
436
  app=app_metadata,
@@ -464,6 +483,12 @@ class AppDeployer:
464
483
  )
465
484
 
466
485
  self._update_app_lookup(deployment.create_params.sender, app_metadata)
486
+ logger.debug(
487
+ f"Sent transaction ID {existing_app.app_id} (AppUpdate) from {deployment.create_params.sender}",
488
+ extra={
489
+ "suppress_log": deployment.send_params.get("suppress_log") or False if deployment.send_params else False
490
+ },
491
+ )
467
492
 
468
493
  return AppDeployResult(
469
494
  app=app_metadata,
@@ -491,7 +516,10 @@ class AppDeployer:
491
516
  if existing_app.deletable:
492
517
  return self._replace_app(deployment, existing_app, approval_program, clear_program)
493
518
  else:
494
- raise ValueError("App is not deletable but onSchemaBreak=ReplaceApp, " "cannot delete and recreate app")
519
+ raise ValueError(
520
+ f"App is {'not' if not existing_app.deletable else ''} deletable and onSchemaBreak=ReplaceApp, "
521
+ "cannot delete and recreate app"
522
+ )
495
523
 
496
524
  def _handle_update(
497
525
  self,
@@ -512,13 +540,19 @@ class AppDeployer:
512
540
  if existing_app.updatable:
513
541
  return self._update_app(deployment, existing_app, approval_program, clear_program)
514
542
  else:
515
- raise ValueError("App is not updatable but onUpdate=UpdateApp, cannot update app")
543
+ raise ValueError(
544
+ f"App is {'not' if not existing_app.updatable else ''} updatable and onUpdate=UpdateApp, "
545
+ "cannot update app"
546
+ )
516
547
 
517
548
  if deployment.on_update in (OnUpdate.ReplaceApp, "replace"):
518
549
  if existing_app.deletable:
519
550
  return self._replace_app(deployment, existing_app, approval_program, clear_program)
520
551
  else:
521
- raise ValueError("App is not deletable but onUpdate=ReplaceApp, " "cannot delete and recreate app")
552
+ raise ValueError(
553
+ f"App is {'not' if not existing_app.deletable else ''} deletable and onUpdate=ReplaceApp, "
554
+ "cannot delete and recreate app"
555
+ )
522
556
 
523
557
  raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}")
524
558
 
@@ -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