algokit-utils 5.0.0a3__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.
Files changed (337) hide show
  1. algokit_abi/__init__.py +9 -0
  2. algokit_abi/_arc32_to_arc56.py +242 -0
  3. algokit_abi/_arc56_serde.py +161 -0
  4. algokit_abi/abi.py +667 -0
  5. algokit_abi/arc32.py +210 -0
  6. algokit_abi/arc56.py +821 -0
  7. algokit_abi/py.typed +0 -0
  8. algokit_algo25/__init__.py +38 -0
  9. algokit_algo25/_encoding.py +46 -0
  10. algokit_algo25/_wordlist.py +2065 -0
  11. algokit_algo25/exceptions.py +29 -0
  12. algokit_algo25/mnemonic.py +128 -0
  13. algokit_algo25/py.typed +0 -0
  14. algokit_algod_client/__init__.py +10 -0
  15. algokit_algod_client/client.py +1585 -0
  16. algokit_algod_client/config.py +36 -0
  17. algokit_algod_client/exceptions.py +59 -0
  18. algokit_algod_client/models/__init__.py +229 -0
  19. algokit_algod_client/models/_account.py +150 -0
  20. algokit_algod_client/models/_account_application_response.py +25 -0
  21. algokit_algod_client/models/_account_asset_response.py +25 -0
  22. algokit_algod_client/models/_account_participation.py +53 -0
  23. algokit_algod_client/models/_account_state_delta.py +30 -0
  24. algokit_algod_client/models/_allocations_for_genesis_file.py +23 -0
  25. algokit_algod_client/models/_allocations_for_genesis_file_state_model.py +42 -0
  26. algokit_algod_client/models/_application.py +23 -0
  27. algokit_algod_client/models/_application_initial_states.py +37 -0
  28. algokit_algod_client/models/_application_kvstorage.py +29 -0
  29. algokit_algod_client/models/_application_local_state.py +33 -0
  30. algokit_algod_client/models/_application_params.py +63 -0
  31. algokit_algod_client/models/_application_state_operation.py +41 -0
  32. algokit_algod_client/models/_application_state_schema.py +22 -0
  33. algokit_algod_client/models/_asset.py +23 -0
  34. algokit_algod_client/models/_asset_holding.py +29 -0
  35. algokit_algod_client/models/_asset_params.py +102 -0
  36. algokit_algod_client/models/_avm_key_value.py +28 -0
  37. algokit_algod_client/models/_avm_value.py +32 -0
  38. algokit_algod_client/models/_block.py +363 -0
  39. algokit_algod_client/models/_block_hash_response.py +14 -0
  40. algokit_algod_client/models/_block_txids_response.py +14 -0
  41. algokit_algod_client/models/_box.py +36 -0
  42. algokit_algod_client/models/_box_descriptor.py +24 -0
  43. algokit_algod_client/models/_boxes_response.py +21 -0
  44. algokit_algod_client/models/_build_version_contains_the_current_algod_build_version_information.py +34 -0
  45. algokit_algod_client/models/_compile_response.py +24 -0
  46. algokit_algod_client/models/_disassemble_response.py +14 -0
  47. algokit_algod_client/models/_error_response.py +22 -0
  48. algokit_algod_client/models/_eval_delta.py +32 -0
  49. algokit_algod_client/models/_eval_delta_key_value.py +28 -0
  50. algokit_algod_client/models/_genesis_file_in_json.py +53 -0
  51. algokit_algod_client/models/_get_block_time_stamp_offset_response.py +14 -0
  52. algokit_algod_client/models/_get_sync_round_response.py +14 -0
  53. algokit_algod_client/models/_ledger_state_delta.py +389 -0
  54. algokit_algod_client/models/_light_block_header_proof.py +32 -0
  55. algokit_algod_client/models/_node_status_response.py +118 -0
  56. algokit_algod_client/models/_pending_transaction_response.py +91 -0
  57. algokit_algod_client/models/_pending_transactions_response.py +29 -0
  58. algokit_algod_client/models/_post_transactions_response.py +14 -0
  59. algokit_algod_client/models/_scratch_change.py +23 -0
  60. algokit_algod_client/models/_serde_helpers.py +241 -0
  61. algokit_algod_client/models/_simulate_initial_states.py +25 -0
  62. algokit_algod_client/models/_simulate_request.py +54 -0
  63. algokit_algod_client/models/_simulate_request_transaction_group.py +25 -0
  64. algokit_algod_client/models/_simulate_response.py +44 -0
  65. algokit_algod_client/models/_simulate_trace_config.py +30 -0
  66. algokit_algod_client/models/_simulate_transaction_group_result.py +46 -0
  67. algokit_algod_client/models/_simulate_transaction_result.py +41 -0
  68. algokit_algod_client/models/_simulate_unnamed_resources_accessed.py +64 -0
  69. algokit_algod_client/models/_simulation_eval_overrides.py +40 -0
  70. algokit_algod_client/models/_simulation_opcode_trace_unit.py +55 -0
  71. algokit_algod_client/models/_simulation_transaction_exec_trace.py +82 -0
  72. algokit_algod_client/models/_source_map.py +30 -0
  73. algokit_algod_client/models/_state_delta.py +6 -0
  74. algokit_algod_client/models/_state_proof.py +28 -0
  75. algokit_algod_client/models/_state_proof_message.py +44 -0
  76. algokit_algod_client/models/_supply_response.py +26 -0
  77. algokit_algod_client/models/_teal_key_value.py +28 -0
  78. algokit_algod_client/models/_teal_key_value_store.py +6 -0
  79. algokit_algod_client/models/_teal_value.py +32 -0
  80. algokit_algod_client/models/_transaction_group_ledger_state_deltas_for_round_response.py +21 -0
  81. algokit_algod_client/models/_transaction_parameters_response.py +45 -0
  82. algokit_algod_client/models/_transaction_proof.py +44 -0
  83. algokit_algod_client/models/_version_contains_the_current_algod_version.py +38 -0
  84. algokit_algod_client/models/suggested_params.py +42 -0
  85. algokit_algod_client/py.typed +1 -0
  86. algokit_algod_client/types.py +7 -0
  87. algokit_algosdk/__init__.py +38 -0
  88. algokit_algosdk/account.py +32 -0
  89. algokit_algosdk/app_access.py +228 -0
  90. algokit_algosdk/box_reference.py +100 -0
  91. algokit_algosdk/constants.py +147 -0
  92. algokit_algosdk/encoding.py +89 -0
  93. algokit_algosdk/error.py +180 -0
  94. algokit_algosdk/logic.py +61 -0
  95. algokit_algosdk/logicsig.py +218 -0
  96. algokit_algosdk/mnemonic.py +216 -0
  97. algokit_algosdk/multisig.py +161 -0
  98. algokit_algosdk/py.typed +0 -0
  99. algokit_algosdk/transaction.py +596 -0
  100. algokit_algosdk/wordlist.py +2054 -0
  101. algokit_common/__init__.py +50 -0
  102. algokit_common/address.py +34 -0
  103. algokit_common/constants.py +47 -0
  104. algokit_common/hashing.py +25 -0
  105. algokit_common/py.typed +0 -0
  106. algokit_common/serde/__init__.py +40 -0
  107. algokit_common/serde/_core.py +610 -0
  108. algokit_common/serde/_primitives.py +135 -0
  109. algokit_common/source_map.py +158 -0
  110. algokit_indexer_client/__init__.py +10 -0
  111. algokit_indexer_client/client.py +1456 -0
  112. algokit_indexer_client/config.py +36 -0
  113. algokit_indexer_client/exceptions.py +59 -0
  114. algokit_indexer_client/models/__init__.py +148 -0
  115. algokit_indexer_client/models/_account.py +161 -0
  116. algokit_indexer_client/models/_account_participation.py +53 -0
  117. algokit_indexer_client/models/_account_response.py +19 -0
  118. algokit_indexer_client/models/_account_state_delta.py +29 -0
  119. algokit_indexer_client/models/_accounts_response.py +29 -0
  120. algokit_indexer_client/models/_application.py +35 -0
  121. algokit_indexer_client/models/_application_local_state.py +45 -0
  122. algokit_indexer_client/models/_application_local_states_response.py +29 -0
  123. algokit_indexer_client/models/_application_log_data.py +28 -0
  124. algokit_indexer_client/models/_application_logs_response.py +33 -0
  125. algokit_indexer_client/models/_application_params.py +62 -0
  126. algokit_indexer_client/models/_application_response.py +20 -0
  127. algokit_indexer_client/models/_application_state_schema.py +22 -0
  128. algokit_indexer_client/models/_applications_response.py +29 -0
  129. algokit_indexer_client/models/_asset.py +35 -0
  130. algokit_indexer_client/models/_asset_balances_response.py +29 -0
  131. algokit_indexer_client/models/_asset_holding.py +41 -0
  132. algokit_indexer_client/models/_asset_holdings_response.py +29 -0
  133. algokit_indexer_client/models/_asset_params.py +102 -0
  134. algokit_indexer_client/models/_asset_response.py +19 -0
  135. algokit_indexer_client/models/_assets_response.py +29 -0
  136. algokit_indexer_client/models/_block.py +150 -0
  137. algokit_indexer_client/models/_block_headers_response.py +29 -0
  138. algokit_indexer_client/models/_block_rewards.py +38 -0
  139. algokit_indexer_client/models/_block_upgrade_state.py +34 -0
  140. algokit_indexer_client/models/_block_upgrade_vote.py +26 -0
  141. algokit_indexer_client/models/_box.py +36 -0
  142. algokit_indexer_client/models/_box_descriptor.py +24 -0
  143. algokit_indexer_client/models/_box_reference.py +28 -0
  144. algokit_indexer_client/models/_boxes_response.py +29 -0
  145. algokit_indexer_client/models/_error_response.py +18 -0
  146. algokit_indexer_client/models/_eval_delta.py +32 -0
  147. algokit_indexer_client/models/_eval_delta_key_value.py +28 -0
  148. algokit_indexer_client/models/_hash_factory.py +14 -0
  149. algokit_indexer_client/models/_hb_proof_fields.py +57 -0
  150. algokit_indexer_client/models/_health_check.py +42 -0
  151. algokit_indexer_client/models/_holding_ref.py +23 -0
  152. algokit_indexer_client/models/_indexer_state_proof_message.py +40 -0
  153. algokit_indexer_client/models/_locals_ref.py +23 -0
  154. algokit_indexer_client/models/_merkle_array_proof.py +29 -0
  155. algokit_indexer_client/models/_mini_asset_holding.py +38 -0
  156. algokit_indexer_client/models/_on_completion.py +25 -0
  157. algokit_indexer_client/models/_participation_updates.py +22 -0
  158. algokit_indexer_client/models/_resource_ref.py +42 -0
  159. algokit_indexer_client/models/_serde_helpers.py +241 -0
  160. algokit_indexer_client/models/_state_delta.py +6 -0
  161. algokit_indexer_client/models/_state_proof_fields.py +57 -0
  162. algokit_indexer_client/models/_state_proof_participant.py +20 -0
  163. algokit_indexer_client/models/_state_proof_reveal.py +25 -0
  164. algokit_indexer_client/models/_state_proof_sig_slot.py +20 -0
  165. algokit_indexer_client/models/_state_proof_signature.py +37 -0
  166. algokit_indexer_client/models/_state_proof_tracking.py +32 -0
  167. algokit_indexer_client/models/_state_proof_verifier.py +24 -0
  168. algokit_indexer_client/models/_state_schema.py +25 -0
  169. algokit_indexer_client/models/_teal_key_value.py +28 -0
  170. algokit_indexer_client/models/_teal_key_value_store.py +6 -0
  171. algokit_indexer_client/models/_teal_value.py +32 -0
  172. algokit_indexer_client/models/_transaction.py +213 -0
  173. algokit_indexer_client/models/_transaction_application.py +105 -0
  174. algokit_indexer_client/models/_transaction_asset_config.py +31 -0
  175. algokit_indexer_client/models/_transaction_asset_freeze.py +29 -0
  176. algokit_indexer_client/models/_transaction_asset_transfer.py +41 -0
  177. algokit_indexer_client/models/_transaction_heartbeat.py +52 -0
  178. algokit_indexer_client/models/_transaction_keyreg.py +59 -0
  179. algokit_indexer_client/models/_transaction_payment.py +33 -0
  180. algokit_indexer_client/models/_transaction_response.py +19 -0
  181. algokit_indexer_client/models/_transaction_signature.py +35 -0
  182. algokit_indexer_client/models/_transaction_signature_logicsig.py +59 -0
  183. algokit_indexer_client/models/_transaction_signature_multisig.py +36 -0
  184. algokit_indexer_client/models/_transaction_signature_multisig_subsignature.py +28 -0
  185. algokit_indexer_client/models/_transaction_state_proof.py +32 -0
  186. algokit_indexer_client/models/_transactions_response.py +29 -0
  187. algokit_indexer_client/py.typed +1 -0
  188. algokit_indexer_client/types.py +7 -0
  189. algokit_kmd_client/__init__.py +10 -0
  190. algokit_kmd_client/client.py +1240 -0
  191. algokit_kmd_client/config.py +36 -0
  192. algokit_kmd_client/exceptions.py +59 -0
  193. algokit_kmd_client/models/__init__.py +112 -0
  194. algokit_kmd_client/models/_classical_signatures.py +4 -0
  195. algokit_kmd_client/models/_create_wallet_request.py +30 -0
  196. algokit_kmd_client/models/_create_wallet_response.py +19 -0
  197. algokit_kmd_client/models/_delete_key_request.py +27 -0
  198. algokit_kmd_client/models/_delete_multisig_request.py +27 -0
  199. algokit_kmd_client/models/_digest_represents_a32_byte_value_holding_the256_bit_hash_digest.py +4 -0
  200. algokit_kmd_client/models/_ed25519_public_key.py +4 -0
  201. algokit_kmd_client/models/_export_key_request.py +27 -0
  202. algokit_kmd_client/models/_export_key_response.py +24 -0
  203. algokit_kmd_client/models/_export_master_key_request.py +22 -0
  204. algokit_kmd_client/models/_export_master_key_response.py +18 -0
  205. algokit_kmd_client/models/_export_multisig_request.py +23 -0
  206. algokit_kmd_client/models/_export_multisig_response.py +26 -0
  207. algokit_kmd_client/models/_generate_key_request.py +18 -0
  208. algokit_kmd_client/models/_generate_key_response.py +19 -0
  209. algokit_kmd_client/models/_import_key_request.py +28 -0
  210. algokit_kmd_client/models/_import_key_response.py +19 -0
  211. algokit_kmd_client/models/_import_multisig_request.py +30 -0
  212. algokit_kmd_client/models/_import_multisig_response.py +19 -0
  213. algokit_kmd_client/models/_init_wallet_handle_token_request.py +22 -0
  214. algokit_kmd_client/models/_init_wallet_handle_token_response.py +18 -0
  215. algokit_kmd_client/models/_list_keys_request.py +18 -0
  216. algokit_kmd_client/models/_list_keys_response.py +18 -0
  217. algokit_kmd_client/models/_list_multisig_request.py +18 -0
  218. algokit_kmd_client/models/_list_multisig_response.py +18 -0
  219. algokit_kmd_client/models/_list_wallets_request.py +11 -0
  220. algokit_kmd_client/models/_list_wallets_response.py +25 -0
  221. algokit_kmd_client/models/_master_derivation_key.py +4 -0
  222. algokit_kmd_client/models/_multisig_sig.py +33 -0
  223. algokit_kmd_client/models/_multisig_subsig.py +23 -0
  224. algokit_kmd_client/models/_public_key.py +4 -0
  225. algokit_kmd_client/models/_release_wallet_handle_token_request.py +18 -0
  226. algokit_kmd_client/models/_rename_wallet_request.py +26 -0
  227. algokit_kmd_client/models/_rename_wallet_response.py +19 -0
  228. algokit_kmd_client/models/_renew_wallet_handle_token_request.py +18 -0
  229. algokit_kmd_client/models/_renew_wallet_handle_token_response.py +19 -0
  230. algokit_kmd_client/models/_serde_helpers.py +241 -0
  231. algokit_kmd_client/models/_sign_multisig_response.py +24 -0
  232. algokit_kmd_client/models/_sign_multisig_txn_request.py +45 -0
  233. algokit_kmd_client/models/_sign_program_multisig_request.py +50 -0
  234. algokit_kmd_client/models/_sign_program_multisig_response.py +24 -0
  235. algokit_kmd_client/models/_sign_program_request.py +37 -0
  236. algokit_kmd_client/models/_sign_program_response.py +24 -0
  237. algokit_kmd_client/models/_sign_transaction_response.py +24 -0
  238. algokit_kmd_client/models/_sign_txn_request.py +36 -0
  239. algokit_kmd_client/models/_signature.py +4 -0
  240. algokit_kmd_client/models/_tx_type.py +4 -0
  241. algokit_kmd_client/models/_versions_request.py +11 -0
  242. algokit_kmd_client/models/_versions_response.py +19 -0
  243. algokit_kmd_client/models/_wallet.py +38 -0
  244. algokit_kmd_client/models/_wallet_handle.py +24 -0
  245. algokit_kmd_client/models/_wallet_info_request.py +18 -0
  246. algokit_kmd_client/models/_wallet_info_response.py +19 -0
  247. algokit_kmd_client/py.typed +1 -0
  248. algokit_kmd_client/types.py +7 -0
  249. algokit_transact/__init__.py +190 -0
  250. algokit_transact/codec/__init__.py +0 -0
  251. algokit_transact/codec/msgpack.py +11 -0
  252. algokit_transact/codec/serde.py +7 -0
  253. algokit_transact/codec/signed.py +57 -0
  254. algokit_transact/codec/transaction.py +65 -0
  255. algokit_transact/exceptions.py +17 -0
  256. algokit_transact/logicsig.py +220 -0
  257. algokit_transact/models/__init__.py +0 -0
  258. algokit_transact/models/app_call.py +447 -0
  259. algokit_transact/models/asset_config.py +19 -0
  260. algokit_transact/models/asset_freeze.py +11 -0
  261. algokit_transact/models/asset_transfer.py +13 -0
  262. algokit_transact/models/common.py +17 -0
  263. algokit_transact/models/heartbeat.py +21 -0
  264. algokit_transact/models/key_registration.py +14 -0
  265. algokit_transact/models/payment.py +14 -0
  266. algokit_transact/models/signed_transaction.py +21 -0
  267. algokit_transact/models/state_proof.py +150 -0
  268. algokit_transact/models/transaction.py +88 -0
  269. algokit_transact/multisig.py +93 -0
  270. algokit_transact/ops/__init__.py +0 -0
  271. algokit_transact/ops/fees.py +47 -0
  272. algokit_transact/ops/group.py +28 -0
  273. algokit_transact/ops/ids.py +14 -0
  274. algokit_transact/ops/validate.py +503 -0
  275. algokit_transact/py.typed +0 -0
  276. algokit_transact/signer.py +195 -0
  277. algokit_transact/signing/__init__.py +0 -0
  278. algokit_transact/signing/logic_signature.py +19 -0
  279. algokit_transact/signing/multisig.py +84 -0
  280. algokit_transact/signing/types.py +39 -0
  281. algokit_transact/signing/validation.py +63 -0
  282. algokit_utils/__init__.py +23 -0
  283. algokit_utils/_debugging.py +304 -0
  284. algokit_utils/accounts/__init__.py +2 -0
  285. algokit_utils/accounts/account_manager.py +1051 -0
  286. algokit_utils/accounts/kmd_account_manager.py +206 -0
  287. algokit_utils/algo25.py +46 -0
  288. algokit_utils/algorand.py +383 -0
  289. algokit_utils/applications/__init__.py +7 -0
  290. algokit_utils/applications/abi.py +280 -0
  291. algokit_utils/applications/app_client.py +2193 -0
  292. algokit_utils/applications/app_deployer.py +788 -0
  293. algokit_utils/applications/app_factory.py +1140 -0
  294. algokit_utils/applications/app_manager.py +575 -0
  295. algokit_utils/applications/app_spec/__init__.py +6 -0
  296. algokit_utils/applications/enums.py +40 -0
  297. algokit_utils/assets/__init__.py +1 -0
  298. algokit_utils/assets/asset_manager.py +344 -0
  299. algokit_utils/clients/__init__.py +41 -0
  300. algokit_utils/clients/client_manager.py +756 -0
  301. algokit_utils/clients/dispenser_api_client.py +212 -0
  302. algokit_utils/common.py +40 -0
  303. algokit_utils/config.py +159 -0
  304. algokit_utils/errors/__init__.py +1 -0
  305. algokit_utils/errors/logic_error.py +160 -0
  306. algokit_utils/models/__init__.py +7 -0
  307. algokit_utils/models/account.py +12 -0
  308. algokit_utils/models/amount.py +198 -0
  309. algokit_utils/models/application.py +90 -0
  310. algokit_utils/models/network.py +29 -0
  311. algokit_utils/models/simulate.py +7 -0
  312. algokit_utils/models/state.py +53 -0
  313. algokit_utils/models/transaction.py +49 -0
  314. algokit_utils/protocols/__init__.py +3 -0
  315. algokit_utils/protocols/account.py +11 -0
  316. algokit_utils/protocols/signer.py +17 -0
  317. algokit_utils/protocols/typed_clients.py +110 -0
  318. algokit_utils/py.typed +0 -0
  319. algokit_utils/transact.py +195 -0
  320. algokit_utils/transactions/__init__.py +3 -0
  321. algokit_utils/transactions/builders/__init__.py +67 -0
  322. algokit_utils/transactions/builders/app.py +248 -0
  323. algokit_utils/transactions/builders/asset.py +256 -0
  324. algokit_utils/transactions/builders/common.py +263 -0
  325. algokit_utils/transactions/builders/keyreg.py +103 -0
  326. algokit_utils/transactions/builders/method_call.py +380 -0
  327. algokit_utils/transactions/builders/payment.py +43 -0
  328. algokit_utils/transactions/composer_resources.py +409 -0
  329. algokit_utils/transactions/fee_coverage.py +79 -0
  330. algokit_utils/transactions/helpers.py +9 -0
  331. algokit_utils/transactions/transaction_composer.py +1574 -0
  332. algokit_utils/transactions/transaction_creator.py +699 -0
  333. algokit_utils/transactions/transaction_sender.py +1240 -0
  334. algokit_utils/transactions/types.py +262 -0
  335. algokit_utils-5.0.0a3.dist-info/METADATA +105 -0
  336. algokit_utils-5.0.0a3.dist-info/RECORD +337 -0
  337. algokit_utils-5.0.0a3.dist-info/WHEEL +4 -0
@@ -0,0 +1,1574 @@
1
+ import base64
2
+ import json
3
+ import re
4
+ from collections.abc import Callable, Sequence
5
+ from dataclasses import dataclass, replace
6
+ from typing import Any, TypeAlias, TypedDict, cast
7
+
8
+ from algokit_abi import arc56
9
+ from algokit_algod_client import AlgodClient
10
+ from algokit_algod_client import models as algod_models
11
+ from algokit_algod_client.exceptions import UnexpectedStatusError
12
+ from algokit_algod_client.models import SimulateTransactionResult
13
+ from algokit_common.constants import MAX_TRANSACTION_GROUP_SIZE
14
+ from algokit_transact import decode_signed_transaction, encode_signed_transactions, make_empty_transaction_signer
15
+ from algokit_transact.models.signed_transaction import SignedTransaction
16
+ from algokit_transact.models.transaction import Transaction, TransactionType
17
+ from algokit_transact.ops.fees import calculate_fee
18
+ from algokit_transact.ops.group import group_transactions
19
+ from algokit_transact.ops.ids import get_transaction_id
20
+ from algokit_transact.signer import AddressWithTransactionSigner, TransactionSigner
21
+ from algokit_utils.applications.abi import ABIReturn
22
+ from algokit_utils.applications.app_manager import AppManager
23
+ from algokit_utils.clients.client_manager import ClientManager
24
+ from algokit_utils.config import config
25
+ from algokit_utils.models.amount import AlgoAmount
26
+ from algokit_utils.models.transaction import Arc2TransactionNote, SendParams
27
+ from algokit_utils.transactions.builders import (
28
+ build_app_call_method_call_transaction,
29
+ build_app_call_transaction,
30
+ build_app_create_method_call_transaction,
31
+ build_app_create_transaction,
32
+ build_app_delete_method_call_transaction,
33
+ build_app_delete_transaction,
34
+ build_app_update_method_call_transaction,
35
+ build_app_update_transaction,
36
+ build_asset_config_transaction,
37
+ build_asset_create_transaction,
38
+ build_asset_destroy_transaction,
39
+ build_asset_freeze_transaction,
40
+ build_asset_opt_in_transaction,
41
+ build_asset_opt_out_transaction,
42
+ build_asset_transfer_transaction,
43
+ build_offline_key_registration_transaction,
44
+ build_online_key_registration_transaction,
45
+ build_payment_transaction,
46
+ )
47
+ from algokit_utils.transactions.builders.common import calculate_inner_fee_delta
48
+ from algokit_utils.transactions.composer_resources import populate_group_resources, populate_transaction_resources
49
+ from algokit_utils.transactions.fee_coverage import FeeDelta, FeePriority
50
+ from algokit_utils.transactions.helpers import calculate_extra_program_pages
51
+ from algokit_utils.transactions.types import (
52
+ AppCallMethodCallParams,
53
+ AppCallParams,
54
+ AppCreateMethodCallParams,
55
+ AppCreateParams,
56
+ AppCreateSchema,
57
+ AppDeleteMethodCallParams,
58
+ AppDeleteParams,
59
+ AppUpdateMethodCallParams,
60
+ AppUpdateParams,
61
+ AssetConfigParams,
62
+ AssetCreateParams,
63
+ AssetDestroyParams,
64
+ AssetFreezeParams,
65
+ AssetOptInParams,
66
+ AssetOptOutParams,
67
+ AssetTransferParams,
68
+ OfflineKeyRegistrationParams,
69
+ OnlineKeyRegistrationParams,
70
+ PaymentParams,
71
+ TxnParams,
72
+ )
73
+
74
+ ABIMethod: TypeAlias = arc56.Method
75
+
76
+ __all__ = [
77
+ "MAX_TRANSACTION_GROUP_SIZE",
78
+ "AppCallMethodCallParams",
79
+ "AppCallParams",
80
+ "AppCreateMethodCallParams",
81
+ "AppCreateParams",
82
+ "AppCreateSchema",
83
+ "AppDeleteMethodCallParams",
84
+ "AppDeleteParams",
85
+ "AppMethodCallTransactionArgument",
86
+ "AppUpdateMethodCallParams",
87
+ "AppUpdateParams",
88
+ "AssetConfigParams",
89
+ "AssetCreateParams",
90
+ "AssetDestroyParams",
91
+ "AssetFreezeParams",
92
+ "AssetOptInParams",
93
+ "AssetOptOutParams",
94
+ "AssetTransferParams",
95
+ "BuiltTransactions",
96
+ "ErrorTransformer",
97
+ "ErrorTransformerError",
98
+ "InvalidErrorTransformerValueError",
99
+ "OfflineKeyRegistrationParams",
100
+ "OnlineKeyRegistrationParams",
101
+ "PaymentParams",
102
+ "SendParams",
103
+ "SendTransactionComposerResults",
104
+ "TransactionComposer",
105
+ "TransactionComposerConfig",
106
+ "TransactionComposerError",
107
+ "TransactionComposerParams",
108
+ "TransactionWithSigner",
109
+ "TxnParams",
110
+ "calculate_extra_program_pages",
111
+ ]
112
+
113
+ AppMethodCallTransactionArgument = Any
114
+ TxnParamTypes = (
115
+ PaymentParams
116
+ | AssetCreateParams
117
+ | AssetConfigParams
118
+ | AssetFreezeParams
119
+ | AssetDestroyParams
120
+ | AssetTransferParams
121
+ | AssetOptInParams
122
+ | AssetOptOutParams
123
+ | AppCreateParams
124
+ | AppUpdateParams
125
+ | AppDeleteParams
126
+ | AppCallParams
127
+ | OnlineKeyRegistrationParams
128
+ | OfflineKeyRegistrationParams
129
+ )
130
+ MethodCallTxnParamTypes = (
131
+ AppCreateMethodCallParams | AppUpdateMethodCallParams | AppDeleteMethodCallParams | AppCallMethodCallParams
132
+ )
133
+
134
+
135
+ class ErrorTransformerError(RuntimeError):
136
+ """Raised when an error transformer throws."""
137
+
138
+
139
+ ErrorTransformer = Callable[[Exception], Exception]
140
+
141
+
142
+ class InvalidErrorTransformerValueError(RuntimeError):
143
+ """Raised when an error transformer returns a non-error value."""
144
+
145
+ def __init__(self, original_error: Exception, value: object) -> None:
146
+ super().__init__(
147
+ f"An error transformer returned a non-error value: {value}. "
148
+ f"The original error before any transformation: {original_error}"
149
+ )
150
+
151
+
152
+ class TransactionComposerError(RuntimeError):
153
+ """Error raised when transaction composer fails to send transactions.
154
+
155
+ Contains detailed debugging information including simulation traces and sent transactions.
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ message: str,
161
+ *,
162
+ cause: Exception | None = None,
163
+ traces: list[SimulateTransactionResult] | None = None,
164
+ sent_transactions: list[Transaction] | None = None,
165
+ simulate_response: algod_models.SimulateResponse | None = None,
166
+ ) -> None:
167
+ super().__init__(message)
168
+ self.__cause__ = cause
169
+ self.traces = traces
170
+ self.sent_transactions = sent_transactions
171
+ self.simulate_response = simulate_response
172
+
173
+
174
+ @dataclass(slots=True)
175
+ class TransactionComposerConfig:
176
+ cover_app_call_inner_transaction_fees: bool = False
177
+ populate_app_call_resources: bool = True
178
+
179
+
180
+ @dataclass(slots=True)
181
+ class TransactionComposerParams:
182
+ algod: AlgodClient
183
+ get_signer: Callable[[str], TransactionSigner]
184
+ get_suggested_params: Callable[[], algod_models.SuggestedParams] | None = None
185
+ default_validity_window: int | None = None
186
+ app_manager: AppManager | None = None
187
+ error_transformers: list[ErrorTransformer] | None = None
188
+ composer_config: TransactionComposerConfig | None = None
189
+
190
+
191
+ class _BuilderKwargs(TypedDict):
192
+ suggested_params: algod_models.SuggestedParams
193
+ default_validity_window: int
194
+ default_validity_window_is_explicit: bool
195
+ is_localnet: bool
196
+
197
+
198
+ @dataclass(slots=True, frozen=True)
199
+ class TransactionWithSigner:
200
+ txn: Transaction
201
+ signer: TransactionSigner
202
+ method: ABIMethod | None = None
203
+
204
+
205
+ @dataclass(slots=True, frozen=True)
206
+ class BuiltTransactions:
207
+ transactions: list[Transaction]
208
+ method_calls: dict[int, ABIMethod]
209
+ signers: dict[int, TransactionSigner]
210
+
211
+
212
+ @dataclass(slots=True, frozen=True)
213
+ class SendTransactionComposerResults:
214
+ tx_ids: list[str]
215
+ transactions: list[Transaction]
216
+ confirmations: list[algod_models.PendingTransactionResponse]
217
+ returns: list[ABIReturn]
218
+ group_id: str | None = None
219
+ simulate_response: algod_models.SimulateResponse | None = None
220
+
221
+
222
+ @dataclass(slots=True)
223
+ class _QueuedTransaction:
224
+ txn: Transaction | TxnParams
225
+ signer: TransactionSigner | AddressWithTransactionSigner | None
226
+ max_fee: AlgoAmount | None = None
227
+
228
+
229
+ @dataclass(slots=True)
230
+ class _BuiltTxnSpec:
231
+ txn: Transaction
232
+ signer: TransactionSigner | None
233
+ logical_max_fee: AlgoAmount | None
234
+ method: ABIMethod | None = None
235
+
236
+
237
+ @dataclass(slots=True)
238
+ class _TransactionAnalysis:
239
+ required_fee_delta: FeeDelta | None
240
+ unnamed_resources_accessed: algod_models.SimulateUnnamedResourcesAccessed | None
241
+
242
+
243
+ @dataclass(slots=True)
244
+ class _GroupAnalysis:
245
+ transactions: list[_TransactionAnalysis]
246
+ unnamed_resources_accessed: algod_models.SimulateUnnamedResourcesAccessed | None
247
+
248
+
249
+ class TransactionComposer:
250
+ """Light-weight transaction composer built on top of algokit_transact."""
251
+
252
+ def __init__(self, params: TransactionComposerParams) -> None:
253
+ self._algod = params.algod
254
+ self._get_signer = params.get_signer
255
+ self._get_suggested_params = params.get_suggested_params or self._algod.suggested_params
256
+ self._config = params.composer_config or TransactionComposerConfig()
257
+ self._error_transformers = params.error_transformers or []
258
+ self._default_validity_window = params.default_validity_window or 10
259
+ self._default_validity_window_is_explicit = params.default_validity_window is not None
260
+ self._app_manager = params.app_manager or AppManager(params.algod)
261
+
262
+ self._queued: list[_QueuedTransaction] = []
263
+ self._transactions_with_signers: list[TransactionWithSigner] | None = None
264
+ self._signed_transactions: list[SignedTransaction] | None = None
265
+ self._raw_built_transactions: list[Transaction] | None = None
266
+
267
+ def clone(self, composer_config: TransactionComposerConfig | None = None) -> "TransactionComposer":
268
+ """Create a shallow copy of this composer, optionally overriding config flags."""
269
+ config_override = composer_config or self._config
270
+ cloned = TransactionComposer(
271
+ TransactionComposerParams(
272
+ algod=self._algod,
273
+ get_signer=self._get_signer,
274
+ get_suggested_params=self._get_suggested_params,
275
+ default_validity_window=self._default_validity_window
276
+ if self._default_validity_window_is_explicit
277
+ else None,
278
+ app_manager=self._app_manager,
279
+ error_transformers=list(self._error_transformers),
280
+ composer_config=TransactionComposerConfig(
281
+ cover_app_call_inner_transaction_fees=config_override.cover_app_call_inner_transaction_fees,
282
+ populate_app_call_resources=config_override.populate_app_call_resources,
283
+ ),
284
+ )
285
+ )
286
+ cloned._queued = [self._clone_entry(entry) for entry in self._queued]
287
+ cloned._raw_built_transactions = list(self._raw_built_transactions) if self._raw_built_transactions else None
288
+ return cloned
289
+
290
+ def register_error_transformer(self, transformer: ErrorTransformer) -> "TransactionComposer":
291
+ self._error_transformers.append(transformer)
292
+ return self
293
+
294
+ def add_transaction(self, txn: Transaction, signer: TransactionSigner | None = None) -> "TransactionComposer":
295
+ self._ensure_not_built()
296
+ self._queued.append(_QueuedTransaction(txn=self._sanitize_transaction(txn), signer=signer))
297
+ return self
298
+
299
+ def add_payment(self, params: PaymentParams) -> "TransactionComposer":
300
+ self._ensure_not_built()
301
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
302
+ return self
303
+
304
+ def add_asset_create(self, params: AssetCreateParams) -> "TransactionComposer":
305
+ self._ensure_not_built()
306
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
307
+ return self
308
+
309
+ def add_asset_config(self, params: AssetConfigParams) -> "TransactionComposer":
310
+ self._ensure_not_built()
311
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
312
+ return self
313
+
314
+ def add_asset_freeze(self, params: AssetFreezeParams) -> "TransactionComposer":
315
+ self._ensure_not_built()
316
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
317
+ return self
318
+
319
+ def add_asset_destroy(self, params: AssetDestroyParams) -> "TransactionComposer":
320
+ self._ensure_not_built()
321
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
322
+ return self
323
+
324
+ def add_asset_transfer(self, params: AssetTransferParams) -> "TransactionComposer":
325
+ self._ensure_not_built()
326
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
327
+ return self
328
+
329
+ def add_asset_opt_in(self, params: AssetOptInParams) -> "TransactionComposer":
330
+ self._ensure_not_built()
331
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
332
+ return self
333
+
334
+ def add_asset_opt_out(self, params: AssetOptOutParams) -> "TransactionComposer":
335
+ self._ensure_not_built()
336
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
337
+ return self
338
+
339
+ def add_app_create(self, params: AppCreateParams) -> "TransactionComposer":
340
+ self._ensure_not_built()
341
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
342
+ return self
343
+
344
+ def add_app_update(self, params: AppUpdateParams) -> "TransactionComposer":
345
+ self._ensure_not_built()
346
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
347
+ return self
348
+
349
+ def add_app_delete(self, params: AppDeleteParams) -> "TransactionComposer":
350
+ self._ensure_not_built()
351
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
352
+ return self
353
+
354
+ def add_app_call(self, params: AppCallParams) -> "TransactionComposer":
355
+ self._ensure_not_built()
356
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
357
+ return self
358
+
359
+ def add_app_create_method_call(self, params: AppCreateMethodCallParams) -> "TransactionComposer":
360
+ self._ensure_not_built()
361
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
362
+ return self
363
+
364
+ def add_app_update_method_call(self, params: AppUpdateMethodCallParams) -> "TransactionComposer":
365
+ self._ensure_not_built()
366
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
367
+ return self
368
+
369
+ def add_app_delete_method_call(self, params: AppDeleteMethodCallParams) -> "TransactionComposer":
370
+ self._ensure_not_built()
371
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
372
+ return self
373
+
374
+ def add_app_call_method_call(self, params: AppCallMethodCallParams) -> "TransactionComposer":
375
+ self._ensure_not_built()
376
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
377
+ return self
378
+
379
+ def add_online_key_registration(self, params: OnlineKeyRegistrationParams) -> "TransactionComposer":
380
+ self._ensure_not_built()
381
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
382
+ return self
383
+
384
+ def add_offline_key_registration(self, params: OfflineKeyRegistrationParams) -> "TransactionComposer":
385
+ self._ensure_not_built()
386
+ self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
387
+ return self
388
+
389
+ def count(self) -> int:
390
+ return len(self._queued)
391
+
392
+ def rebuild(self) -> BuiltTransactions:
393
+ self._transactions_with_signers = None
394
+ self._signed_transactions = None
395
+ self._raw_built_transactions = None
396
+ return self.build()
397
+
398
+ @staticmethod
399
+ def arc2_note(note: Arc2TransactionNote) -> bytes:
400
+ pattern = r"^[a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}$"
401
+ if not re.match(pattern, note["dapp_name"]):
402
+ raise ValueError(
403
+ "dapp_name must be 5-32 chars, start with alphanumeric, and contain only alphanumeric, _, /, @, ., or -"
404
+ )
405
+
406
+ data = note["data"]
407
+ if note["format"] == "j" and isinstance(data, (dict | list)):
408
+ data = json.dumps(data)
409
+
410
+ arc2_payload = f"{note['dapp_name']}:{note['format']}{data}"
411
+ return arc2_payload.encode("utf-8")
412
+
413
+ def add_transaction_composer(self, composer: "TransactionComposer") -> "TransactionComposer":
414
+ self._ensure_not_built()
415
+ current_size = len(self._queued)
416
+ composer_size = len(composer._queued) # noqa: SLF001
417
+ new_size = current_size + composer_size
418
+ if new_size > MAX_TRANSACTION_GROUP_SIZE:
419
+ raise ValueError(
420
+ "Adding transactions from composer would exceed the maximum group size. "
421
+ f"Current: {current_size}, Adding: {composer_size}, "
422
+ f"Maximum: {MAX_TRANSACTION_GROUP_SIZE}"
423
+ )
424
+ for entry in composer._queued: # noqa: SLF001
425
+ self._queued.append(self._clone_entry(entry))
426
+ return self
427
+
428
+ def build(self) -> BuiltTransactions:
429
+ """Build transactions with grouping, resource population, and fee adjustments applied."""
430
+ self._ensure_built()
431
+ assert self._transactions_with_signers is not None
432
+ transactions = [entry.txn for entry in self._transactions_with_signers]
433
+ signers = {index: entry.signer for index, entry in enumerate(self._transactions_with_signers)}
434
+ method_calls = {
435
+ index: entry.method for index, entry in enumerate(self._transactions_with_signers) if entry.method
436
+ }
437
+ return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers)
438
+
439
+ def build_transactions(self) -> BuiltTransactions:
440
+ """Build queued transactions without resource population or grouping.
441
+
442
+ Returns raw transactions, method call metadata, and any explicit signers. This does not
443
+ populate unnamed resources or adjust fees, and it leaves grouping unchanged.
444
+ """
445
+ if not self._queued:
446
+ raise ValueError("Cannot build an empty transaction group")
447
+
448
+ suggested_params = self._get_suggested_params()
449
+ genesis_id = getattr(suggested_params, "genesis_id", None)
450
+ if genesis_id is None:
451
+ genesis_id = getattr(suggested_params, "gen", "")
452
+ is_localnet = ClientManager.genesis_id_is_localnet(genesis_id or "")
453
+
454
+ built_entries, method_calls = self._build_txn_specs(suggested_params, is_localnet=is_localnet)
455
+ transactions = [entry.txn for entry in built_entries]
456
+ self._raw_built_transactions = list(transactions)
457
+ signers = {index: entry.signer for index, entry in enumerate(built_entries) if entry.signer is not None}
458
+ return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers)
459
+
460
+ def gather_signatures(self) -> list[SignedTransaction]:
461
+ self._ensure_built()
462
+ if self._signed_transactions is None:
463
+ self._signed_transactions = self._sign_transactions(self._transactions_with_signers or [])
464
+ return self._signed_transactions
465
+
466
+ def send(self, params: SendParams | None = None) -> SendTransactionComposerResults:
467
+ """Compose the transaction group and send it to the network."""
468
+ params = params or SendParams()
469
+
470
+ # Update config from params if provided
471
+ cover_flag = params.get("cover_app_call_inner_transaction_fees")
472
+ populate_flag = params.get("populate_app_call_resources")
473
+ effective_cover = bool(cover_flag)
474
+ effective_populate = bool(populate_flag) if populate_flag is not None else True
475
+ if (
476
+ effective_cover != self._config.cover_app_call_inner_transaction_fees
477
+ or effective_populate != self._config.populate_app_call_resources
478
+ ):
479
+ self._config = TransactionComposerConfig(
480
+ cover_app_call_inner_transaction_fees=effective_cover,
481
+ populate_app_call_resources=effective_populate,
482
+ )
483
+ # Reset built state to force rebuild with new config
484
+ self._transactions_with_signers = None
485
+ self._signed_transactions = None
486
+ self._raw_built_transactions = None
487
+
488
+ # Build and sign transactions - let validation errors bubble up as-is
489
+ signed_transactions = self.gather_signatures()
490
+
491
+ if config.debug and config.trace_all and config.project_root:
492
+ try:
493
+ self.simulate(result_on_failure=True)
494
+ except Exception:
495
+ config.logger.debug(
496
+ "Failed to simulate and persist trace for debugging",
497
+ exc_info=True,
498
+ extra={"suppress_log": params.get("suppress_log")},
499
+ )
500
+
501
+ # Send transactions and handle network errors
502
+ try:
503
+ blobs = encode_signed_transactions(signed_transactions)
504
+ self._algod.send_raw_transaction(blobs)
505
+
506
+ tx_ids = [get_transaction_id(entry.txn) for entry in self._transactions_with_signers or []]
507
+ group_id = self._group_id()
508
+ if not params.get("suppress_log") and tx_ids:
509
+ if len(tx_ids) > 1:
510
+ config.logger.info(
511
+ "Sent group of %s transactions (%s)",
512
+ len(tx_ids),
513
+ group_id or "no-group",
514
+ extra={"suppress_log": params.get("suppress_log")},
515
+ )
516
+ config.logger.debug(
517
+ "Transaction IDs (%s): %s",
518
+ group_id or "no-group",
519
+ tx_ids,
520
+ extra={"suppress_log": params.get("suppress_log")},
521
+ )
522
+ else:
523
+ txn = (self._transactions_with_signers or [])[0].txn
524
+ config.logger.info(
525
+ "Sent transaction ID %s %s from %s",
526
+ tx_ids[0],
527
+ txn.transaction_type,
528
+ txn.sender,
529
+ extra={"suppress_log": params.get("suppress_log")},
530
+ )
531
+ confirmations = self._wait_for_confirmations(tx_ids, params)
532
+ abi_returns = self._parse_abi_return_values(confirmations)
533
+ return SendTransactionComposerResults(
534
+ tx_ids=tx_ids,
535
+ transactions=[entry.txn for entry in self._transactions_with_signers or []],
536
+ confirmations=confirmations,
537
+ returns=abi_returns,
538
+ group_id=group_id,
539
+ )
540
+ except Exception as err:
541
+ sent_transactions = self._resolve_error_transactions()
542
+ simulate_response: algod_models.SimulateResponse | None = None
543
+ traces: list[SimulateTransactionResult] = []
544
+
545
+ if config.debug and sent_transactions:
546
+ simulate_response, traces = self._simulate_error_context(
547
+ sent_transactions,
548
+ suppress_log=params.get("suppress_log"),
549
+ )
550
+
551
+ if config.debug and config.project_root and not config.trace_all and simulate_response is None:
552
+ from algokit_utils._debugging import simulate_and_persist_response
553
+
554
+ try:
555
+ simulate_and_persist_response(
556
+ self,
557
+ config.project_root,
558
+ self._algod,
559
+ buffer_size_mb=config.trace_buffer_size_mb,
560
+ )
561
+ except Exception:
562
+ config.logger.debug(
563
+ "Failed to simulate and persist trace for debugging",
564
+ exc_info=True,
565
+ extra={"suppress_log": params.get("suppress_log")},
566
+ )
567
+
568
+ interpreted = self._interpret_error(err)
569
+ composer_error = self._create_composer_error(interpreted, sent_transactions, simulate_response, traces)
570
+ raise self._transform_error(composer_error) from err
571
+
572
+ def simulate(
573
+ self,
574
+ *,
575
+ skip_signatures: bool = False,
576
+ result_on_failure: bool = False,
577
+ **raw_options: Any,
578
+ ) -> SendTransactionComposerResults:
579
+ """Compose the transaction group and simulate execution without submitting to the network.
580
+
581
+ Args:
582
+ skip_signatures: Whether to skip signatures for all built transactions and use an empty signer instead.
583
+ This will set `allow_empty_signatures` and `fix_signers` when sending the request to algod.
584
+ result_on_failure: Whether to return the result on simulation failure instead of throwing an error.
585
+ Defaults to False (throws on failure).
586
+ **raw_options: Additional options to pass to the simulate request.
587
+
588
+ Returns:
589
+ SendTransactionComposerResults containing simulation results.
590
+ """
591
+ try:
592
+ persist_trace = bool(raw_options.pop("_persist_trace", True))
593
+ txns_with_signers: list[TransactionWithSigner]
594
+ if "throw_on_failure" in raw_options:
595
+ raw_options.pop("throw_on_failure")
596
+ effective_throw_on_failure = not result_on_failure
597
+ if skip_signatures:
598
+ raw_options.setdefault("allow_empty_signatures", True)
599
+ raw_options.setdefault("fix_signers", True)
600
+ if "allow_more_logs" in raw_options:
601
+ raw_options["allow_more_logging"] = raw_options.pop("allow_more_logs")
602
+ if "simulation_round" in raw_options:
603
+ raw_options["round_"] = raw_options.pop("simulation_round")
604
+
605
+ txns_with_signers = self._build_transactions_for_simulation()
606
+
607
+ if config.debug:
608
+ raw_options.setdefault("allow_more_logging", True)
609
+ raw_options.setdefault(
610
+ "exec_trace_config",
611
+ algod_models.SimulateTraceConfig(
612
+ enable=True,
613
+ scratch_change=True,
614
+ stack_change=True,
615
+ state_change=True,
616
+ ),
617
+ )
618
+
619
+ empty_signer: TransactionSigner = make_empty_transaction_signer()
620
+ signing_entries = [
621
+ TransactionWithSigner(
622
+ txn=entry.txn,
623
+ signer=empty_signer if skip_signatures else entry.signer,
624
+ )
625
+ for entry in txns_with_signers
626
+ ]
627
+ signed_transactions = self._sign_transactions(signing_entries)
628
+
629
+ request = algod_models.SimulateRequest(
630
+ txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
631
+ **raw_options,
632
+ )
633
+ response = self._algod.simulate_transactions(request)
634
+
635
+ if response.txn_groups and response.txn_groups[0].failure_message and effective_throw_on_failure:
636
+ raise RuntimeError(response.txn_groups[0].failure_message)
637
+
638
+ tx_ids = [get_transaction_id(entry.txn) for entry in txns_with_signers]
639
+ group = response.txn_groups[0] if response.txn_groups else None
640
+ confirmations = [result.txn_result for result in (group.txn_results if group else [])]
641
+ method_calls = {index: entry.method for index, entry in enumerate(txns_with_signers) if entry.method}
642
+ abi_returns = self._parse_abi_return_values(confirmations, method_calls)
643
+ result = SendTransactionComposerResults(
644
+ tx_ids=tx_ids,
645
+ transactions=[entry.txn for entry in txns_with_signers],
646
+ confirmations=confirmations,
647
+ returns=abi_returns,
648
+ group_id=(
649
+ base64.b64encode(txns_with_signers[0].txn.group).decode()
650
+ if txns_with_signers and txns_with_signers[0].txn.group
651
+ else None
652
+ ),
653
+ simulate_response=response,
654
+ )
655
+
656
+ if config.debug and config.project_root and config.trace_all and persist_trace:
657
+ from algokit_utils._debugging import simulate_and_persist_response
658
+
659
+ try:
660
+ simulate_and_persist_response(
661
+ self,
662
+ config.project_root,
663
+ self._algod,
664
+ buffer_size_mb=config.trace_buffer_size_mb,
665
+ result=result,
666
+ )
667
+ except Exception:
668
+ config.logger.debug("Failed to persist simulation trace", exc_info=True)
669
+
670
+ return result
671
+ except Exception as err:
672
+ interpreted = self._interpret_error(err)
673
+ raise self._transform_error(interpreted) from err
674
+
675
+ def _ensure_not_built(self) -> None:
676
+ if self._transactions_with_signers is not None:
677
+ raise RuntimeError("Transactions have already been built")
678
+ if len(self._queued) >= MAX_TRANSACTION_GROUP_SIZE:
679
+ raise ValueError("Transaction group size exceeds maximum limit")
680
+
681
+ def _build_txn_specs(
682
+ self,
683
+ suggested_params: algod_models.SuggestedParams,
684
+ *,
685
+ is_localnet: bool,
686
+ ) -> tuple[list[_BuiltTxnSpec], dict[int, ABIMethod]]:
687
+ if not self._queued:
688
+ raise ValueError("Cannot build an empty transaction group")
689
+
690
+ built_entries: list[_BuiltTxnSpec] = []
691
+ method_calls: dict[int, ABIMethod] = {}
692
+
693
+ for entry in self._queued:
694
+ sender = entry.txn.sender if hasattr(entry.txn, "sender") else None
695
+ override_signer = self._resolve_param_signer(entry.signer, sender)
696
+ if isinstance(entry.txn, Transaction):
697
+ txn = self._sanitize_transaction(entry.txn)
698
+ built_entries.append(
699
+ _BuiltTxnSpec(txn=txn, signer=override_signer, logical_max_fee=entry.max_fee, method=None)
700
+ )
701
+ continue
702
+
703
+ specs = self._build_txn_from_params(entry.txn, suggested_params, is_localnet=is_localnet)
704
+ for spec in specs:
705
+ resolved_signer = spec.signer or override_signer
706
+ if resolved_signer is None:
707
+ raise ValueError("Signer is required for transaction in composer queue")
708
+ index = len(built_entries)
709
+ built_entries.append(
710
+ _BuiltTxnSpec(
711
+ txn=self._sanitize_transaction(spec.txn),
712
+ signer=resolved_signer,
713
+ logical_max_fee=spec.logical_max_fee,
714
+ method=spec.method,
715
+ )
716
+ )
717
+ if spec.method:
718
+ method_calls[index] = spec.method
719
+
720
+ return built_entries, method_calls
721
+
722
+ def _ensure_built(self) -> None:
723
+ if self._transactions_with_signers is not None:
724
+ return
725
+
726
+ suggested_params = self._get_suggested_params()
727
+ genesis_id = getattr(suggested_params, "genesis_id", None)
728
+ if genesis_id is None:
729
+ genesis_id = getattr(suggested_params, "gen", "")
730
+ is_localnet = ClientManager.genesis_id_is_localnet(genesis_id or "")
731
+
732
+ built_entries, method_calls = self._build_txn_specs(suggested_params, is_localnet=is_localnet)
733
+ transactions = [entry.txn for entry in built_entries]
734
+ self._raw_built_transactions = list(transactions)
735
+ logical_max_fees = [entry.logical_max_fee for entry in built_entries]
736
+
737
+ needs_analysis = (
738
+ self._config.cover_app_call_inner_transaction_fees or self._config.populate_app_call_resources
739
+ ) and any(txn.transaction_type == TransactionType.AppCall for txn in transactions)
740
+ if needs_analysis:
741
+ group_analysis = self._analyze_group_requirements(
742
+ transactions,
743
+ logical_max_fees,
744
+ suggested_params,
745
+ self._config,
746
+ )
747
+ self._populate_transaction_and_group_resources(transactions, group_analysis, logical_max_fees)
748
+
749
+ grouped = group_transactions(transactions)
750
+ self._transactions_with_signers = [
751
+ TransactionWithSigner(
752
+ txn=grouped[index],
753
+ signer=cast(TransactionSigner, entry.signer),
754
+ method=method_calls.get(index),
755
+ )
756
+ for index, entry in enumerate(built_entries)
757
+ ]
758
+
759
+ def _build_transactions_for_simulation(self) -> list[TransactionWithSigner]:
760
+ if self._transactions_with_signers is None:
761
+ suggested_params = self._get_suggested_params()
762
+ genesis_id = getattr(suggested_params, "genesis_id", None)
763
+ if genesis_id is None:
764
+ genesis_id = getattr(suggested_params, "gen", "")
765
+ is_localnet = ClientManager.genesis_id_is_localnet(genesis_id or "")
766
+
767
+ built_entries, method_calls = self._build_txn_specs(suggested_params, is_localnet=is_localnet)
768
+ transactions = [entry.txn for entry in built_entries]
769
+ if len(transactions) > 1:
770
+ transactions = group_transactions(transactions)
771
+ return [
772
+ TransactionWithSigner(
773
+ txn=transactions[index],
774
+ signer=cast(TransactionSigner, entry.signer),
775
+ method=method_calls.get(index),
776
+ )
777
+ for index, entry in enumerate(built_entries)
778
+ ]
779
+
780
+ return self._transactions_with_signers
781
+
782
+ def _analyze_group_requirements( # noqa: C901, PLR0912
783
+ self,
784
+ transactions: list[Transaction],
785
+ logical_max_fees: Sequence[AlgoAmount | None],
786
+ suggested_params: algod_models.SuggestedParams,
787
+ config: TransactionComposerConfig,
788
+ ) -> _GroupAnalysis:
789
+ app_call_indexes_without_max_fees: list[int] = []
790
+ transactions_to_simulate: list[Transaction] = []
791
+ for index, txn in enumerate(transactions):
792
+ txn_to_simulate = replace(txn, group=None)
793
+ if config.cover_app_call_inner_transaction_fees and txn.transaction_type == TransactionType.AppCall:
794
+ logical_max_fee = logical_max_fees[index]
795
+ if logical_max_fee is None:
796
+ app_call_indexes_without_max_fees.append(index)
797
+ else:
798
+ txn_to_simulate = replace(txn_to_simulate, fee=logical_max_fee.micro_algo)
799
+ transactions_to_simulate.append(txn_to_simulate)
800
+
801
+ if config.cover_app_call_inner_transaction_fees and app_call_indexes_without_max_fees:
802
+ indexes = ", ".join(str(index) for index in app_call_indexes_without_max_fees)
803
+ raise ValueError(
804
+ "Please provide a `max_fee` for each app call transaction when "
805
+ "cover_app_call_inner_transaction_fees is enabled. "
806
+ f"Required for transaction {indexes}"
807
+ )
808
+
809
+ if len(transactions_to_simulate) > 1:
810
+ transactions_to_simulate = group_transactions(transactions_to_simulate)
811
+
812
+ empty_signer: TransactionSigner = make_empty_transaction_signer()
813
+ signed_transactions = self._sign_transactions(
814
+ [TransactionWithSigner(txn=txn, signer=empty_signer) for txn in transactions_to_simulate]
815
+ )
816
+
817
+ simulate_request = algod_models.SimulateRequest(
818
+ txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
819
+ allow_unnamed_resources=True,
820
+ allow_empty_signatures=True,
821
+ fix_signers=True,
822
+ allow_more_logging=True,
823
+ exec_trace_config=algod_models.SimulateTraceConfig(
824
+ enable=True,
825
+ scratch_change=True,
826
+ stack_change=True,
827
+ state_change=True,
828
+ ),
829
+ )
830
+
831
+ response = self._algod.simulate_transactions(simulate_request)
832
+ group_response = response.txn_groups[0]
833
+
834
+ if group_response.failure_message:
835
+ if config.cover_app_call_inner_transaction_fees and "fee too small" in group_response.failure_message:
836
+ raise ValueError(
837
+ "Fees were too small to resolve execution info via simulate. "
838
+ "You may need to increase an app call transaction maxFee."
839
+ )
840
+ raise ValueError(
841
+ "Error resolving execution info via simulate in transaction "
842
+ f"{group_response.failed_at or []}: {group_response.failure_message}"
843
+ )
844
+
845
+ txn_analysis_results: list[_TransactionAnalysis] = []
846
+ for index, simulate_txn_result in enumerate(group_response.txn_results):
847
+ txn = transactions[index]
848
+ required_fee_delta: FeeDelta | None = None
849
+ if config.cover_app_call_inner_transaction_fees:
850
+ min_txn_fee = calculate_fee(txn, fee_per_byte=suggested_params.fee, min_fee=suggested_params.min_fee)
851
+ txn_fee = txn.fee or 0
852
+ txn_fee_delta = FeeDelta.from_int(min_txn_fee - txn_fee)
853
+ if txn.transaction_type == TransactionType.AppCall:
854
+ inner_delta = calculate_inner_fee_delta(
855
+ simulate_txn_result.txn_result.inner_txns, suggested_params.min_fee
856
+ )
857
+ required_fee_delta = FeeDelta.add(inner_delta, txn_fee_delta)
858
+ else:
859
+ required_fee_delta = txn_fee_delta
860
+
861
+ unnamed_resources = (
862
+ simulate_txn_result.unnamed_resources_accessed if config.populate_app_call_resources else None
863
+ )
864
+ txn_analysis_results.append(
865
+ _TransactionAnalysis(
866
+ required_fee_delta=required_fee_delta,
867
+ unnamed_resources_accessed=unnamed_resources,
868
+ )
869
+ )
870
+
871
+ group_resources = group_response.unnamed_resources_accessed if config.populate_app_call_resources else None
872
+ if group_resources:
873
+ group_resources.accounts = sorted(group_resources.accounts or [])
874
+ group_resources.assets = sorted(group_resources.assets or [])
875
+ group_resources.apps = sorted(group_resources.apps or [])
876
+ group_resources.boxes = sorted(
877
+ group_resources.boxes or [],
878
+ key=lambda box: (box.app_id, box.name),
879
+ )
880
+ group_resources.app_locals = sorted(
881
+ group_resources.app_locals or [],
882
+ key=lambda entry: (entry.app_id, entry.address),
883
+ )
884
+ group_resources.asset_holdings = sorted(
885
+ group_resources.asset_holdings or [],
886
+ key=lambda entry: (entry.asset_id, entry.address),
887
+ )
888
+
889
+ return _GroupAnalysis(transactions=txn_analysis_results, unnamed_resources_accessed=group_resources)
890
+
891
+ def _populate_transaction_and_group_resources( # noqa: C901, PLR0912, PLR0915
892
+ self,
893
+ transactions: list[Transaction],
894
+ group_analysis: _GroupAnalysis,
895
+ logical_max_fees: Sequence[AlgoAmount | None],
896
+ ) -> None:
897
+ if not group_analysis:
898
+ return
899
+
900
+ surplus_group_fees = 0
901
+ transaction_analysis_list: list[dict[str, Any]] = []
902
+
903
+ for group_index, txn_analysis in enumerate(group_analysis.transactions):
904
+ fee_delta = txn_analysis.required_fee_delta
905
+ if fee_delta and FeeDelta.is_surplus(fee_delta):
906
+ surplus_group_fees += FeeDelta.amount(fee_delta)
907
+
908
+ txn = transactions[group_index]
909
+ max_fee_source = logical_max_fees[group_index]
910
+ max_fee_amount: int | None
911
+ if max_fee_source is not None:
912
+ max_fee_amount = max_fee_source.micro_algo
913
+ elif not self._config.cover_app_call_inner_transaction_fees:
914
+ txn_fee = txn.fee or 0
915
+ max_fee_amount = txn_fee if txn_fee > 0 else None
916
+ else:
917
+ max_fee_amount = None
918
+ is_immutable_fee = max_fee_amount is not None and max_fee_amount == (txn.fee or 0)
919
+
920
+ priority = FeePriority.Covered
921
+ if fee_delta and FeeDelta.is_deficit(fee_delta):
922
+ deficit_amount = FeeDelta.amount(fee_delta)
923
+ if is_immutable_fee or txn.transaction_type != TransactionType.AppCall:
924
+ priority = FeePriority.ImmutableDeficit(deficit_amount)
925
+ else:
926
+ priority = FeePriority.ModifiableDeficit(deficit_amount)
927
+
928
+ transaction_analysis_list.append(
929
+ {
930
+ "group_index": group_index,
931
+ "required_fee_delta": fee_delta,
932
+ "priority": priority,
933
+ "unnamed_resources_accessed": txn_analysis.unnamed_resources_accessed,
934
+ "logical_max_fee": max_fee_amount,
935
+ }
936
+ )
937
+
938
+ transaction_analysis_list.sort(key=lambda item: item["priority"], reverse=True)
939
+ indexes_with_access_references: list[int] = []
940
+
941
+ for item in transaction_analysis_list:
942
+ group_index = item["group_index"]
943
+ logical_max_fee = item["logical_max_fee"]
944
+ required_fee_delta: FeeDelta | None = item["required_fee_delta"]
945
+ unnamed_resources_accessed = item["unnamed_resources_accessed"]
946
+
947
+ if required_fee_delta and FeeDelta.is_deficit(required_fee_delta):
948
+ deficit_amount = FeeDelta.amount(required_fee_delta)
949
+ additional_fee_delta: FeeDelta | None
950
+
951
+ if surplus_group_fees == 0:
952
+ additional_fee_delta = required_fee_delta
953
+ elif surplus_group_fees >= deficit_amount:
954
+ surplus_group_fees -= deficit_amount
955
+ additional_fee_delta = None
956
+ else:
957
+ additional_fee_delta = FeeDelta.from_int(deficit_amount - surplus_group_fees)
958
+ surplus_group_fees = 0
959
+
960
+ if additional_fee_delta and FeeDelta.is_deficit(additional_fee_delta):
961
+ additional_deficit_amount = FeeDelta.amount(additional_fee_delta)
962
+ txn = transactions[group_index]
963
+
964
+ if txn.transaction_type != TransactionType.AppCall:
965
+ raise ValueError(
966
+ "An additional fee of "
967
+ f"{additional_deficit_amount} µALGO is required for non app call transaction {group_index}",
968
+ )
969
+
970
+ current_fee = txn.fee or 0
971
+ transaction_fee = current_fee + additional_deficit_amount
972
+ if logical_max_fee is not None and transaction_fee > logical_max_fee:
973
+ raise ValueError(
974
+ "Calculated transaction fee "
975
+ f"{transaction_fee} µALGO is greater than max of {logical_max_fee} "
976
+ f"for transaction {group_index}"
977
+ )
978
+
979
+ transactions[group_index] = replace(txn, fee=transaction_fee)
980
+
981
+ if unnamed_resources_accessed and transactions[group_index].transaction_type == TransactionType.AppCall:
982
+ has_access_references = bool(transactions[group_index].application_call.access_references)
983
+ if not has_access_references:
984
+ transactions[group_index] = populate_transaction_resources(
985
+ transactions[group_index], unnamed_resources_accessed, group_index
986
+ )
987
+ else:
988
+ indexes_with_access_references.append(group_index)
989
+
990
+ if indexes_with_access_references:
991
+ config.logger.warning(
992
+ "Resource population will be skipped for transaction indexes %s as they use access references.",
993
+ indexes_with_access_references,
994
+ )
995
+
996
+ if group_analysis.unnamed_resources_accessed:
997
+ populate_group_resources(transactions, group_analysis.unnamed_resources_accessed)
998
+
999
+ def _build_txn_from_params( # noqa: C901, PLR0911, PLR0912, PLR0915
1000
+ self,
1001
+ params: TxnParams,
1002
+ suggested_params: algod_models.SuggestedParams,
1003
+ *,
1004
+ is_localnet: bool,
1005
+ ) -> list[_BuiltTxnSpec]:
1006
+ builder_kwargs: _BuilderKwargs = {
1007
+ "suggested_params": suggested_params,
1008
+ "default_validity_window": self._default_validity_window,
1009
+ "default_validity_window_is_explicit": self._default_validity_window_is_explicit,
1010
+ "is_localnet": is_localnet,
1011
+ }
1012
+
1013
+ if isinstance(params, PaymentParams):
1014
+ signer = self._resolve_param_signer(params.signer, params.sender)
1015
+ built = build_payment_transaction(params, **builder_kwargs)
1016
+ return [
1017
+ _BuiltTxnSpec(
1018
+ txn=built.txn,
1019
+ signer=signer,
1020
+ logical_max_fee=built.logical_max_fee,
1021
+ )
1022
+ ]
1023
+ elif isinstance(params, AssetCreateParams):
1024
+ signer = self._resolve_param_signer(params.signer, params.sender)
1025
+ built = build_asset_create_transaction(params, **builder_kwargs)
1026
+ return [
1027
+ _BuiltTxnSpec(
1028
+ txn=built.txn,
1029
+ signer=signer,
1030
+ logical_max_fee=built.logical_max_fee,
1031
+ )
1032
+ ]
1033
+ elif isinstance(params, AssetConfigParams):
1034
+ signer = self._resolve_param_signer(params.signer, params.sender)
1035
+ built = build_asset_config_transaction(params, **builder_kwargs)
1036
+ return [
1037
+ _BuiltTxnSpec(
1038
+ txn=built.txn,
1039
+ signer=signer,
1040
+ logical_max_fee=built.logical_max_fee,
1041
+ )
1042
+ ]
1043
+ elif isinstance(params, AssetFreezeParams):
1044
+ signer = self._resolve_param_signer(params.signer, params.sender)
1045
+ built = build_asset_freeze_transaction(params, **builder_kwargs)
1046
+ return [
1047
+ _BuiltTxnSpec(
1048
+ txn=built.txn,
1049
+ signer=signer,
1050
+ logical_max_fee=built.logical_max_fee,
1051
+ )
1052
+ ]
1053
+ elif isinstance(params, AssetDestroyParams):
1054
+ signer = self._resolve_param_signer(params.signer, params.sender)
1055
+ built = build_asset_destroy_transaction(params, **builder_kwargs)
1056
+ return [
1057
+ _BuiltTxnSpec(
1058
+ txn=built.txn,
1059
+ signer=signer,
1060
+ logical_max_fee=built.logical_max_fee,
1061
+ )
1062
+ ]
1063
+ elif isinstance(params, AssetTransferParams):
1064
+ signer = self._resolve_param_signer(params.signer, params.sender)
1065
+ built = build_asset_transfer_transaction(params, **builder_kwargs)
1066
+ return [
1067
+ _BuiltTxnSpec(
1068
+ txn=built.txn,
1069
+ signer=signer,
1070
+ logical_max_fee=built.logical_max_fee,
1071
+ )
1072
+ ]
1073
+ elif isinstance(params, AssetOptInParams):
1074
+ signer = self._resolve_param_signer(params.signer, params.sender)
1075
+ built = build_asset_opt_in_transaction(params, **builder_kwargs)
1076
+ return [
1077
+ _BuiltTxnSpec(
1078
+ txn=built.txn,
1079
+ signer=signer,
1080
+ logical_max_fee=built.logical_max_fee,
1081
+ )
1082
+ ]
1083
+ elif isinstance(params, AssetOptOutParams):
1084
+ signer = self._resolve_param_signer(params.signer, params.sender)
1085
+ built = build_asset_opt_out_transaction(params, **builder_kwargs)
1086
+ return [
1087
+ _BuiltTxnSpec(
1088
+ txn=built.txn,
1089
+ signer=signer,
1090
+ logical_max_fee=built.logical_max_fee,
1091
+ )
1092
+ ]
1093
+ elif isinstance(params, AppCreateParams):
1094
+ signer = self._resolve_param_signer(params.signer, params.sender)
1095
+ built = build_app_create_transaction(params, app_manager=self._app_manager, **builder_kwargs)
1096
+ return [
1097
+ _BuiltTxnSpec(
1098
+ txn=built.txn,
1099
+ signer=signer,
1100
+ logical_max_fee=built.logical_max_fee,
1101
+ )
1102
+ ]
1103
+ elif isinstance(params, AppUpdateParams):
1104
+ signer = self._resolve_param_signer(params.signer, params.sender)
1105
+ built = build_app_update_transaction(params, app_manager=self._app_manager, **builder_kwargs)
1106
+ return [
1107
+ _BuiltTxnSpec(
1108
+ txn=built.txn,
1109
+ signer=signer,
1110
+ logical_max_fee=built.logical_max_fee,
1111
+ )
1112
+ ]
1113
+ elif isinstance(params, AppDeleteParams):
1114
+ signer = self._resolve_param_signer(params.signer, params.sender)
1115
+ built = build_app_delete_transaction(params, app_manager=self._app_manager, **builder_kwargs)
1116
+ return [
1117
+ _BuiltTxnSpec(
1118
+ txn=built.txn,
1119
+ signer=signer,
1120
+ logical_max_fee=built.logical_max_fee,
1121
+ )
1122
+ ]
1123
+ elif isinstance(params, AppCallParams):
1124
+ signer = self._resolve_param_signer(params.signer, params.sender)
1125
+ built = build_app_call_transaction(params, app_manager=self._app_manager, **builder_kwargs)
1126
+ return [
1127
+ _BuiltTxnSpec(
1128
+ txn=built.txn,
1129
+ signer=signer,
1130
+ logical_max_fee=built.logical_max_fee,
1131
+ )
1132
+ ]
1133
+ elif isinstance(params, OnlineKeyRegistrationParams):
1134
+ signer = self._resolve_param_signer(params.signer, params.sender)
1135
+ built = build_online_key_registration_transaction(params, **builder_kwargs)
1136
+ return [
1137
+ _BuiltTxnSpec(
1138
+ txn=built.txn,
1139
+ signer=signer,
1140
+ logical_max_fee=built.logical_max_fee,
1141
+ )
1142
+ ]
1143
+ elif isinstance(params, OfflineKeyRegistrationParams):
1144
+ signer = self._resolve_param_signer(params.signer, params.sender)
1145
+ built = build_offline_key_registration_transaction(params, **builder_kwargs)
1146
+ return [
1147
+ _BuiltTxnSpec(
1148
+ txn=built.txn,
1149
+ signer=signer,
1150
+ logical_max_fee=built.logical_max_fee,
1151
+ )
1152
+ ]
1153
+ elif isinstance(params, MethodCallTxnParamTypes):
1154
+ extra_specs, flattened_params = self._extract_method_call_transactions(
1155
+ params, suggested_params, is_localnet=is_localnet
1156
+ )
1157
+ if isinstance(params, AppCreateMethodCallParams):
1158
+ create_params = cast(AppCreateMethodCallParams, flattened_params)
1159
+ built = build_app_create_method_call_transaction(
1160
+ create_params,
1161
+ suggested_params=suggested_params,
1162
+ method_args=create_params.args,
1163
+ app_manager=self._app_manager,
1164
+ default_validity_window=self._default_validity_window,
1165
+ default_validity_window_is_explicit=self._default_validity_window_is_explicit,
1166
+ is_localnet=is_localnet,
1167
+ )
1168
+ return [
1169
+ *extra_specs,
1170
+ _BuiltTxnSpec(
1171
+ txn=built.txn,
1172
+ signer=self._resolve_param_signer(create_params.signer, create_params.sender),
1173
+ logical_max_fee=built.logical_max_fee,
1174
+ method=create_params.method,
1175
+ ),
1176
+ ]
1177
+ if isinstance(params, AppUpdateMethodCallParams):
1178
+ update_params = cast(AppUpdateMethodCallParams, flattened_params)
1179
+ built = build_app_update_method_call_transaction(
1180
+ update_params,
1181
+ suggested_params=suggested_params,
1182
+ method_args=update_params.args,
1183
+ app_manager=self._app_manager,
1184
+ default_validity_window=self._default_validity_window,
1185
+ default_validity_window_is_explicit=self._default_validity_window_is_explicit,
1186
+ is_localnet=is_localnet,
1187
+ )
1188
+ return [
1189
+ *extra_specs,
1190
+ _BuiltTxnSpec(
1191
+ txn=built.txn,
1192
+ signer=self._resolve_param_signer(update_params.signer, update_params.sender),
1193
+ logical_max_fee=built.logical_max_fee,
1194
+ method=update_params.method,
1195
+ ),
1196
+ ]
1197
+ if isinstance(params, AppDeleteMethodCallParams):
1198
+ delete_params = cast(AppDeleteMethodCallParams, flattened_params)
1199
+ built = build_app_delete_method_call_transaction(
1200
+ delete_params,
1201
+ suggested_params=suggested_params,
1202
+ method_args=delete_params.args,
1203
+ app_manager=self._app_manager,
1204
+ default_validity_window=self._default_validity_window,
1205
+ default_validity_window_is_explicit=self._default_validity_window_is_explicit,
1206
+ is_localnet=is_localnet,
1207
+ )
1208
+ return [
1209
+ *extra_specs,
1210
+ _BuiltTxnSpec(
1211
+ txn=built.txn,
1212
+ signer=self._resolve_param_signer(delete_params.signer, delete_params.sender),
1213
+ logical_max_fee=built.logical_max_fee,
1214
+ method=delete_params.method,
1215
+ ),
1216
+ ]
1217
+
1218
+ call_params = cast(AppCallMethodCallParams, flattened_params)
1219
+ built = build_app_call_method_call_transaction(
1220
+ call_params,
1221
+ suggested_params=suggested_params,
1222
+ method_args=call_params.args,
1223
+ app_manager=self._app_manager,
1224
+ default_validity_window=self._default_validity_window,
1225
+ default_validity_window_is_explicit=self._default_validity_window_is_explicit,
1226
+ is_localnet=is_localnet,
1227
+ )
1228
+
1229
+ return [
1230
+ *extra_specs,
1231
+ _BuiltTxnSpec(
1232
+ txn=built.txn,
1233
+ signer=self._resolve_param_signer(call_params.signer, call_params.sender),
1234
+ logical_max_fee=built.logical_max_fee,
1235
+ method=call_params.method,
1236
+ ),
1237
+ ]
1238
+
1239
+ raise ValueError(f"Unsupported transaction params type: {type(params)}")
1240
+
1241
+ def _clone_entry(self, entry: _QueuedTransaction) -> _QueuedTransaction:
1242
+ if isinstance(entry.txn, Transaction):
1243
+ return _QueuedTransaction(txn=self._sanitize_transaction(entry.txn), signer=entry.signer)
1244
+ # TxnParams are immutable (frozen dataclasses) so we can share them
1245
+ return _QueuedTransaction(txn=entry.txn, signer=entry.signer)
1246
+
1247
+ def _process_method_call_arg(
1248
+ self,
1249
+ arg: object | None,
1250
+ current_signer: TransactionSigner | None,
1251
+ suggested_params: algod_models.SuggestedParams,
1252
+ *,
1253
+ is_localnet: bool,
1254
+ ) -> tuple[list[_BuiltTxnSpec], object | None]:
1255
+ if arg is None:
1256
+ return [], None
1257
+
1258
+ if isinstance(arg, TransactionWithSigner):
1259
+ return [
1260
+ _BuiltTxnSpec(
1261
+ txn=self._sanitize_transaction(arg.txn),
1262
+ signer=arg.signer,
1263
+ logical_max_fee=None,
1264
+ )
1265
+ ], None
1266
+
1267
+ if isinstance(arg, MethodCallTxnParamTypes):
1268
+ nested_params = arg
1269
+ if arg.signer is None and current_signer is not None:
1270
+ nested_params = replace(arg, signer=current_signer)
1271
+ return (
1272
+ self._build_txn_from_params(
1273
+ nested_params,
1274
+ suggested_params,
1275
+ is_localnet=is_localnet,
1276
+ ),
1277
+ None,
1278
+ )
1279
+
1280
+ if isinstance(arg, Transaction):
1281
+ return [
1282
+ _BuiltTxnSpec(
1283
+ txn=self._sanitize_transaction(arg),
1284
+ signer=current_signer,
1285
+ logical_max_fee=None,
1286
+ )
1287
+ ], None
1288
+
1289
+ if isinstance(arg, TxnParamTypes):
1290
+ return (
1291
+ self._build_txn_from_params(arg, suggested_params, is_localnet=is_localnet),
1292
+ None,
1293
+ )
1294
+
1295
+ return [], arg
1296
+
1297
+ def _extract_method_call_transactions(
1298
+ self,
1299
+ params: MethodCallTxnParamTypes,
1300
+ suggested_params: algod_models.SuggestedParams,
1301
+ *,
1302
+ is_localnet: bool,
1303
+ ) -> tuple[list[_BuiltTxnSpec], MethodCallTxnParamTypes]:
1304
+ """Flatten transaction arguments inside ABI method calls into queued specs."""
1305
+ if not params.args:
1306
+ return [], params
1307
+
1308
+ def _to_signer(value: TransactionSigner | AddressWithTransactionSigner | None) -> TransactionSigner | None:
1309
+ if isinstance(value, AddressWithTransactionSigner):
1310
+ return value.signer
1311
+ return value
1312
+
1313
+ current_signer = _to_signer(params.signer)
1314
+ extra_specs: list[_BuiltTxnSpec] = []
1315
+ processed_args: list[Any] = []
1316
+
1317
+ for arg in params.args:
1318
+ specs, processed = self._process_method_call_arg(
1319
+ arg,
1320
+ current_signer,
1321
+ suggested_params,
1322
+ is_localnet=is_localnet,
1323
+ )
1324
+ extra_specs.extend(specs)
1325
+ processed_args.append(processed)
1326
+
1327
+ return extra_specs, replace(params, args=processed_args)
1328
+
1329
+ def _sanitize_transaction(self, txn: Transaction) -> Transaction:
1330
+ return replace(txn, group=None)
1331
+
1332
+ def _resolve_param_signer(
1333
+ self,
1334
+ signer: TransactionSigner | AddressWithTransactionSigner | None,
1335
+ sender: str | None = None,
1336
+ ) -> TransactionSigner:
1337
+ if isinstance(signer, AddressWithTransactionSigner):
1338
+ return signer.signer
1339
+ if signer is None:
1340
+ if sender is None:
1341
+ raise ValueError("Sender is required to resolve signer")
1342
+ resolved = self._get_signer(sender)
1343
+ if resolved is None:
1344
+ raise ValueError(f"No signer found for address {sender}")
1345
+ return resolved
1346
+ return signer
1347
+
1348
+ def _sign_transactions(self, txns_with_signers: Sequence[TransactionWithSigner]) -> list[SignedTransaction]:
1349
+ if not txns_with_signers:
1350
+ raise ValueError("No transactions available to sign")
1351
+
1352
+ transactions = [entry.txn for entry in txns_with_signers]
1353
+ signer_groups: dict[int, tuple[TransactionSigner, list[int]]] = {}
1354
+ for index, entry in enumerate(txns_with_signers):
1355
+ key = id(entry.signer)
1356
+ if key not in signer_groups:
1357
+ signer_groups[key] = (entry.signer, [])
1358
+ signer_groups[key][1].append(index)
1359
+
1360
+ signed_blobs: dict[int, list[bytes]] = {}
1361
+ for key, (signer, indexes) in signer_groups.items():
1362
+ blobs = signer(transactions, indexes)
1363
+ signed_blobs[key] = list(blobs)
1364
+ if len(blobs) != len(indexes):
1365
+ raise ValueError("Signer returned unexpected number of transactions")
1366
+
1367
+ ordered: list[SignedTransaction | None] = [None] * len(transactions)
1368
+ for key, (_, indexes) in signer_groups.items():
1369
+ blobs = signed_blobs[key]
1370
+ for blob_index, txn_index in enumerate(indexes):
1371
+ ordered[txn_index] = decode_signed_transaction(blobs[blob_index])
1372
+
1373
+ if any(item is None for item in ordered):
1374
+ raise ValueError("One or more transactions were not signed")
1375
+
1376
+ return [item for item in ordered if item is not None]
1377
+
1378
+ def _group_id(self) -> str | None:
1379
+ txns = self._transactions_with_signers or []
1380
+ if not txns:
1381
+ return None
1382
+ group = txns[0].txn.group
1383
+ if group is None:
1384
+ return None
1385
+ return base64.b64encode(group).decode()
1386
+
1387
+ def _wait_for_confirmations(
1388
+ self, tx_ids: Sequence[str], params: SendParams
1389
+ ) -> list[algod_models.PendingTransactionResponse]:
1390
+ confirmations: list[algod_models.PendingTransactionResponse] = []
1391
+ max_rounds = params.get("max_rounds_to_wait")
1392
+
1393
+ if max_rounds is None:
1394
+ suggested = self._get_suggested_params()
1395
+ first = int(getattr(suggested, "first_valid", getattr(suggested, "first", 0)))
1396
+ last = max(entry.txn.last_valid for entry in self._transactions_with_signers or [])
1397
+ max_rounds = int(max(last - first + 1, 0))
1398
+ for tx_id in tx_ids:
1399
+ confirmations.append(_wait_for_confirmation(self._algod, tx_id, max_rounds))
1400
+ return confirmations
1401
+
1402
+ def _transform_error(self, err: Exception) -> Exception:
1403
+ original_error = err
1404
+ transformed = err
1405
+ for transformer in self._error_transformers:
1406
+ try:
1407
+ transformed = transformer(transformed)
1408
+ except Exception as transformer_error:
1409
+ raise ErrorTransformerError("Error transformer raised an exception") from transformer_error
1410
+ if not isinstance(transformed, Exception):
1411
+ raise InvalidErrorTransformerValueError(original_error, transformed)
1412
+ return transformed
1413
+
1414
+ def _parse_abi_return_values(
1415
+ self,
1416
+ confirmations: Sequence[algod_models.PendingTransactionResponse],
1417
+ method_calls: dict[int, ABIMethod] | None = None,
1418
+ ) -> list[ABIReturn]:
1419
+ abi_returns: list[ABIReturn] = []
1420
+ method_calls = method_calls or {
1421
+ index: entry.method for index, entry in enumerate(self._transactions_with_signers or []) if entry.method
1422
+ }
1423
+ for index, confirmation in enumerate(confirmations):
1424
+ method = method_calls.get(index)
1425
+ if not method:
1426
+ continue
1427
+ abi_return = self._app_manager.get_abi_return(confirmation, method)
1428
+ if abi_return is not None:
1429
+ abi_returns.append(abi_return)
1430
+ return abi_returns
1431
+
1432
+ def _resolve_error_transactions(self) -> list[Transaction] | None:
1433
+ if self._transactions_with_signers is not None:
1434
+ return [entry.txn for entry in self._transactions_with_signers]
1435
+ if self._raw_built_transactions:
1436
+ transactions = list(self._raw_built_transactions)
1437
+ return group_transactions(transactions) if len(transactions) > 1 else transactions
1438
+ return None
1439
+
1440
+ def _simulate_error_context(
1441
+ self,
1442
+ sent_transactions: Sequence[Transaction],
1443
+ *,
1444
+ suppress_log: bool | None,
1445
+ ) -> tuple[algod_models.SimulateResponse | None, list[SimulateTransactionResult]]:
1446
+ """Simulate transactions to get error context including traces.
1447
+
1448
+ Returns:
1449
+ A tuple of (simulate_response, traces).
1450
+ """
1451
+ try:
1452
+ empty_signer: TransactionSigner = make_empty_transaction_signer()
1453
+ signed_transactions = self._sign_transactions(
1454
+ [TransactionWithSigner(txn=txn, signer=empty_signer) for txn in sent_transactions]
1455
+ )
1456
+ request = algod_models.SimulateRequest(
1457
+ txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
1458
+ allow_empty_signatures=True,
1459
+ fix_signers=True,
1460
+ allow_more_logging=True,
1461
+ exec_trace_config=algod_models.SimulateTraceConfig(
1462
+ enable=True,
1463
+ scratch_change=True,
1464
+ stack_change=True,
1465
+ state_change=True,
1466
+ ),
1467
+ )
1468
+ response = self._algod.simulate_transactions(request)
1469
+
1470
+ # Extract traces from the response - use SimulateTransactionResult directly
1471
+ # aligned with TypeScript which uses algod client types directly
1472
+ traces: list[SimulateTransactionResult] = []
1473
+ if response.txn_groups and response.txn_groups[0].failed_at:
1474
+ traces = list(response.txn_groups[0].txn_results)
1475
+
1476
+ return response, traces
1477
+ except Exception:
1478
+ config.logger.debug(
1479
+ "Failed to simulate transaction group after send error",
1480
+ exc_info=True,
1481
+ extra={"suppress_log": suppress_log},
1482
+ )
1483
+ return None, []
1484
+
1485
+ def _create_composer_error(
1486
+ self,
1487
+ err: Exception,
1488
+ sent_transactions: Sequence[Transaction] | None,
1489
+ simulate_response: algod_models.SimulateResponse | None,
1490
+ traces: list[SimulateTransactionResult],
1491
+ ) -> TransactionComposerError:
1492
+ """Create a TransactionComposerError with full context."""
1493
+ return TransactionComposerError(
1494
+ str(err),
1495
+ cause=err,
1496
+ traces=traces if traces else None,
1497
+ sent_transactions=list(sent_transactions) if sent_transactions else None,
1498
+ simulate_response=simulate_response,
1499
+ )
1500
+
1501
+ def set_max_fees(self, max_fees: dict[int, AlgoAmount]) -> "TransactionComposer":
1502
+ """Override max_fee for queued transactions by index before building."""
1503
+ if self._transactions_with_signers is not None:
1504
+ raise RuntimeError("Transactions have already been built")
1505
+
1506
+ for index in max_fees:
1507
+ if index < 0 or index >= len(self._queued):
1508
+ raise ValueError(
1509
+ f"Index {index} is out of range. The composer only contains {len(self._queued)} transactions"
1510
+ )
1511
+
1512
+ for index, max_fee in max_fees.items():
1513
+ entry = self._queued[index]
1514
+ if isinstance(entry.txn, Transaction):
1515
+ self._queued[index] = replace(entry, max_fee=max_fee)
1516
+ elif hasattr(entry.txn, "max_fee"):
1517
+ self._queued[index] = replace(entry, txn=replace(entry.txn, max_fee=max_fee))
1518
+ else:
1519
+ raise ValueError(f"Transaction at index {index} does not support max_fee overrides")
1520
+
1521
+ return self
1522
+
1523
+ def _interpret_error(self, err: Exception) -> Exception:
1524
+ if isinstance(err, UnexpectedStatusError):
1525
+ payload_message = self._extract_algod_error_message(err.payload)
1526
+ if payload_message:
1527
+ return RuntimeError(payload_message)
1528
+ return err
1529
+
1530
+ @staticmethod
1531
+ def _extract_algod_error_message(payload: object) -> str | None: # noqa: PLR0911
1532
+ if payload is None:
1533
+ return None
1534
+ if isinstance(payload, bytes):
1535
+ text = payload.decode("utf-8", errors="ignore")
1536
+ else:
1537
+ text = str(payload)
1538
+ text = text.strip()
1539
+ if not text:
1540
+ return None
1541
+ try:
1542
+ decoded = json.loads(text)
1543
+ except Exception:
1544
+ return text
1545
+ if isinstance(decoded, dict):
1546
+ for key in ("message", "msg", "error", "detail", "description"):
1547
+ value = decoded.get(key)
1548
+ if isinstance(value, str) and value.strip():
1549
+ return value
1550
+ return text
1551
+ if isinstance(decoded, list) and decoded:
1552
+ first = decoded[0]
1553
+ if isinstance(first, str) and first.strip():
1554
+ return first
1555
+ return text
1556
+
1557
+
1558
+ def _wait_for_confirmation(
1559
+ algod: AlgodClient,
1560
+ tx_id: str,
1561
+ max_rounds: int,
1562
+ ) -> algod_models.PendingTransactionResponse:
1563
+ remaining = max_rounds
1564
+ status = algod.get_status()
1565
+ current_round = getattr(status, "last_round", 0)
1566
+ while remaining > 0:
1567
+ pending = algod.pending_transaction_information(tx_id)
1568
+ confirmed_round = getattr(pending, "confirmed_round", None)
1569
+ if confirmed_round is not None and confirmed_round > 0:
1570
+ return pending
1571
+ current_round += 1
1572
+ algod.status_after_block(current_round)
1573
+ remaining -= 1
1574
+ raise TimeoutError(f"Transaction {tx_id} not confirmed after {max_rounds} rounds")