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,1051 @@
1
+ import os
2
+ from collections.abc import Callable, Sequence
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ import nacl.signing
7
+ from typing_extensions import Self
8
+
9
+ from algokit_algo25 import seed_from_mnemonic
10
+ from algokit_algod_client import models as algod_models
11
+ from algokit_common.serde import to_wire
12
+ from algokit_transact.signer import (
13
+ AddressWithSigners,
14
+ AddressWithTransactionSigner,
15
+ TransactionSigner,
16
+ generate_address_with_signers,
17
+ )
18
+ from algokit_utils.accounts.kmd_account_manager import KmdAccountManager
19
+ from algokit_utils.clients.client_manager import ClientManager
20
+ from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient
21
+ from algokit_utils.config import config
22
+ from algokit_utils.models.account import (
23
+ DISPENSER_ACCOUNT_NAME,
24
+ LogicSigAccount,
25
+ MultisigAccount,
26
+ MultisigMetadata,
27
+ )
28
+ from algokit_utils.models.amount import AlgoAmount
29
+ from algokit_utils.models.transaction import SendParams
30
+ from algokit_utils.transactions.transaction_composer import (
31
+ PaymentParams,
32
+ SendTransactionComposerResults,
33
+ TransactionComposer,
34
+ TransactionComposerParams,
35
+ )
36
+ from algokit_utils.transactions.transaction_sender import SendSingleTransactionResult
37
+
38
+ __all__ = [
39
+ "AccountInformation",
40
+ "AccountManager",
41
+ "EnsureFundedFromTestnetDispenserApiResult",
42
+ "EnsureFundedResult",
43
+ ]
44
+
45
+
46
+ @dataclass(frozen=True, kw_only=True)
47
+ class _CommonEnsureFundedParams:
48
+ """
49
+ Common parameters for ensure funded responses.
50
+ """
51
+
52
+ transaction_id: str
53
+ """The transaction ID of the funded transaction"""
54
+ amount_funded: AlgoAmount
55
+ """The amount of Algos funded"""
56
+
57
+
58
+ @dataclass(frozen=True, kw_only=True)
59
+ class EnsureFundedResult(SendSingleTransactionResult, _CommonEnsureFundedParams):
60
+ """
61
+ Result from performing an ensure funded call.
62
+ """
63
+
64
+
65
+ @dataclass(frozen=True, kw_only=True)
66
+ class EnsureFundedFromTestnetDispenserApiResult(_CommonEnsureFundedParams):
67
+ """
68
+ Result from performing an ensure funded call using TestNet dispenser API.
69
+ """
70
+
71
+
72
+ @dataclass(frozen=True, kw_only=True)
73
+ class AccountInformation:
74
+ """
75
+ Information about an Algorand account's current status, balance and other properties.
76
+
77
+ See `https://dev.algorand.co/reference/rest-apis/algod/#account` for detailed field descriptions.
78
+ """
79
+
80
+ address: str
81
+ """The account's address"""
82
+ amount: AlgoAmount
83
+ """The account's current balance"""
84
+ amount_without_pending_rewards: AlgoAmount
85
+ """The account's balance without the pending rewards"""
86
+ min_balance: AlgoAmount
87
+ """The account's minimum required balance"""
88
+ pending_rewards: AlgoAmount
89
+ """The amount of pending rewards"""
90
+ rewards: AlgoAmount
91
+ """The amount of rewards earned"""
92
+ round: int
93
+ """The round for which this information is relevant"""
94
+ status: str
95
+ """The account's status (e.g., 'Offline', 'Online')"""
96
+ total_apps_opted_in: int | None = None
97
+ """Number of applications this account has opted into"""
98
+ total_assets_opted_in: int | None = None
99
+ """Number of assets this account has opted into"""
100
+ total_box_bytes: int | None = None
101
+ """Total number of box bytes used by this account"""
102
+ total_boxes: int | None = None
103
+ """Total number of boxes used by this account"""
104
+ total_created_apps: int | None = None
105
+ """Number of applications created by this account"""
106
+ total_created_assets: int | None = None
107
+ """Number of assets created by this account"""
108
+ apps_local_state: list[dict] | None = None
109
+ """Local state of applications this account has opted into"""
110
+ apps_total_extra_pages: int | None = None
111
+ """Number of extra pages allocated to applications"""
112
+ apps_total_schema: dict | None = None
113
+ """Total schema for all applications"""
114
+ assets: list[dict] | None = None
115
+ """Assets held by this account"""
116
+ auth_addr: str | None = None
117
+ """If rekeyed, the authorized address"""
118
+ closed_at_round: int | None = None
119
+ """Round when this account was closed"""
120
+ created_apps: list[dict] | None = None
121
+ """Applications created by this account"""
122
+ created_assets: list[dict] | None = None
123
+ """Assets created by this account"""
124
+ created_at_round: int | None = None
125
+ """Round when this account was created"""
126
+ deleted: bool | None = None
127
+ """Whether this account is deleted"""
128
+ incentive_eligible: bool | None = None
129
+ """Whether this account is eligible for incentives"""
130
+ last_heartbeat: int | None = None
131
+ """Last heartbeat round for this account"""
132
+ last_proposed: int | None = None
133
+ """Last round this account proposed a block"""
134
+ participation: dict | None = None
135
+ """Participation information for this account"""
136
+ reward_base: int | None = None
137
+ """Base reward for this account"""
138
+ sig_type: str | None = None
139
+ """Signature type for this account"""
140
+
141
+
142
+ # Type alias for accounts that can be stored in the account manager
143
+ StoredAccountType = AddressWithSigners | LogicSigAccount | MultisigAccount
144
+
145
+
146
+ class AccountManager:
147
+ """
148
+ Creates and keeps track of signing accounts that can sign transactions for a sending address.
149
+
150
+ This class provides functionality to create, track, and manage various types of accounts including
151
+ mnemonic-based, rekeyed, multisig, and logic signature accounts.
152
+
153
+ :param client_manager: The ClientManager client to use for algod and kmd clients
154
+
155
+ :example:
156
+ >>> account_manager = AccountManager(client_manager)
157
+ """
158
+
159
+ def __init__(self, client_manager: ClientManager):
160
+ self._client_manager = client_manager
161
+ self._kmd_account_manager = KmdAccountManager(client_manager)
162
+ self._accounts: dict[str, StoredAccountType | AddressWithTransactionSigner] = {}
163
+ self._default_signer: TransactionSigner | None = None
164
+
165
+ @property
166
+ def kmd(self) -> KmdAccountManager:
167
+ """
168
+ KMD account manager that allows you to easily get and create accounts using KMD.
169
+
170
+ :return KmdAccountManager: The 'KmdAccountManager' instance
171
+ :example:
172
+ >>> kmd_manager = account_manager.kmd
173
+ """
174
+ return self._kmd_account_manager
175
+
176
+ def _signer_account(self, account: StoredAccountType) -> AddressWithSigners:
177
+ """
178
+ Register account and return AddressWithSigners.
179
+
180
+ Records the given account against its address for later retrieval and returns
181
+ an AddressWithSigners object.
182
+
183
+ :param account: The account to register (AddressWithSigners, LogicSigAccount, or MultisigAccount)
184
+ :returns: AddressWithSigners for the account
185
+ """
186
+ # Get the address from the account
187
+ if isinstance(account, AddressWithSigners):
188
+ addr = account.addr
189
+ else:
190
+ # LogicSigAccount and MultisigAccount have .addr property
191
+ addr = account.addr
192
+
193
+ # Store the account
194
+ self._accounts[addr] = account
195
+
196
+ # If it's already AddressWithSigners, return it directly
197
+ if isinstance(account, AddressWithSigners):
198
+ return account
199
+
200
+ # For LogicSigAccount and MultisigAccount, create an AddressWithSigners wrapper
201
+ # These accounts have a .signer property but may not have all the other signers
202
+ # We create placeholder signers for the other capabilities
203
+ def _placeholder_bytes_signer(data: bytes) -> bytes:
204
+ raise NotImplementedError("bytes_signer not available for this account type")
205
+
206
+ def _placeholder_lsig_signer(program: bytes, msig_address: bytes | None = None) -> bytes:
207
+ raise NotImplementedError("delegated_lsig_signer not available for this account type")
208
+
209
+ def _placeholder_program_data_signer(data: bytes, program_address: bytes) -> bytes:
210
+ raise NotImplementedError("program_data_signer not available for this account type")
211
+
212
+ def _placeholder_mx_bytes_signer(data: bytes) -> bytes:
213
+ raise NotImplementedError("mx_bytes_signer not available for this account type")
214
+
215
+ return AddressWithSigners(
216
+ addr=addr,
217
+ signer=account.signer,
218
+ delegated_lsig_signer=_placeholder_lsig_signer,
219
+ program_data_signer=_placeholder_program_data_signer,
220
+ bytes_signer=_placeholder_bytes_signer,
221
+ mx_bytes_signer=_placeholder_mx_bytes_signer,
222
+ )
223
+
224
+ def set_default_signer(self, signer: TransactionSigner | AddressWithTransactionSigner) -> Self:
225
+ """
226
+ Sets the default signer to use if no other signer is specified.
227
+
228
+ If this isn't set and a transaction needs signing for a given sender
229
+ then an error will be thrown from `get_signer` / `get_account`.
230
+
231
+ :param signer: A `TransactionSigner` signer to use.
232
+ :returns: The `AccountManager` so method calls can be chained
233
+
234
+ :example:
235
+ >>> signer_account = account_manager.random()
236
+ >>> account_manager.set_default_signer(signer_account)
237
+ """
238
+ # Check if signer is an AddressWithTransactionSigner (has .signer property) or is just a callable
239
+ if isinstance(signer, AddressWithTransactionSigner):
240
+ self._default_signer = signer.signer
241
+ else:
242
+ self._default_signer = signer
243
+ return self
244
+
245
+ def set_signer(self, sender: str, signer: TransactionSigner) -> Self:
246
+ """
247
+ Tracks the given `TransactionSigner` against the given sender address for later signing.
248
+
249
+ :param sender: The sender address to use this signer for
250
+ :param signer: The `TransactionSigner` to sign transactions with for the given sender
251
+ :returns: The `AccountManager` instance for method chaining
252
+
253
+ :example:
254
+ >>> account_manager.set_signer("SENDERADDRESS", transaction_signer)
255
+ """
256
+
257
+ # Create a minimal AddressWithSigners with placeholder signers for non-transaction operations
258
+ def _placeholder_bytes_signer(data: bytes) -> bytes:
259
+ raise NotImplementedError("bytes_signer not available for signer-only accounts")
260
+
261
+ def _placeholder_lsig_signer(program: bytes, msig_address: bytes | None = None) -> bytes:
262
+ raise NotImplementedError("delegated_lsig_signer not available for signer-only accounts")
263
+
264
+ def _placeholder_program_data_signer(data: bytes, program_address: bytes) -> bytes:
265
+ raise NotImplementedError("program_data_signer not available for signer-only accounts")
266
+
267
+ def _placeholder_mx_bytes_signer(data: bytes) -> bytes:
268
+ raise NotImplementedError("mx_bytes_signer not available for signer-only accounts")
269
+
270
+ self._accounts[sender] = AddressWithSigners(
271
+ addr=sender,
272
+ signer=signer,
273
+ delegated_lsig_signer=_placeholder_lsig_signer,
274
+ program_data_signer=_placeholder_program_data_signer,
275
+ bytes_signer=_placeholder_bytes_signer,
276
+ mx_bytes_signer=_placeholder_mx_bytes_signer,
277
+ )
278
+ return self
279
+
280
+ def set_signers(self, *, another_account_manager: "AccountManager", overwrite_existing: bool = True) -> Self:
281
+ """
282
+ Merges the given `AccountManager` into this one.
283
+
284
+ :param another_account_manager: The `AccountManager` to merge into this one
285
+ :param overwrite_existing: Whether to overwrite existing signers in this manager
286
+ :returns: The `AccountManager` instance for method chaining
287
+
288
+ :example:
289
+ >>> accountManager2.set_signers(accountManager1)
290
+ """
291
+ self._accounts = (
292
+ {**self._accounts, **another_account_manager._accounts} # noqa: SLF001
293
+ if overwrite_existing
294
+ else {**another_account_manager._accounts, **self._accounts} # noqa: SLF001
295
+ )
296
+ return self
297
+
298
+ def set_signer_from_account(
299
+ self,
300
+ *args: AddressWithTransactionSigner,
301
+ **kwargs: AddressWithTransactionSigner,
302
+ ) -> Self:
303
+ """
304
+ Tracks the given account for later signing.
305
+
306
+ Note: If you are generating accounts via the various methods on `AccountManager`
307
+ (like `random`, `from_mnemonic`, `logic_sig`, etc.) then they automatically get tracked.
308
+
309
+ The method accepts either a positional argument or a keyword argument named 'account' or 'signer'.
310
+ The 'signer' parameter is deprecated and will show a warning when used.
311
+
312
+ :param *args: Variable positional arguments. The first argument should be a AddressWithTransactionSigner.
313
+ :param **kwargs: Variable keyword arguments. Can include 'account' or 'signer' (deprecated) as
314
+ AddressWithTransactionSigner.
315
+ :returns: The `AccountManager` instance for method chaining
316
+ :raises ValueError: If no account or signer argument is provided
317
+
318
+ :example:
319
+ >>> account_manager = AccountManager(client_manager)
320
+ >>> # Using positional argument
321
+ >>> account_manager.set_signer_from_account(
322
+ ... AddressWithSigners(...)
323
+ ... )
324
+ >>> # Using keyword argument 'account'
325
+ >>> account_manager.set_signer_from_account(
326
+ ... account=LogicSigAccount(AlgosdkLogicSigAccount(program, args))
327
+ ... )
328
+ >>> # Using deprecated keyword argument 'signer'
329
+ >>> account_manager.set_signer_from_account(
330
+ ... signer=MultisigAccount(multisig_params, [account1, account2])
331
+ ... )
332
+ """
333
+ # Extract the account from either positional args or keyword args
334
+ if args:
335
+ account_obj = args[0]
336
+ elif "account" in kwargs:
337
+ account_obj = kwargs["account"]
338
+ elif "signer" in kwargs:
339
+ account_obj = kwargs["signer"]
340
+ else:
341
+ raise ValueError("Missing required argument: either 'account' or 'signer'")
342
+
343
+ self._accounts[account_obj.addr] = account_obj
344
+ return self
345
+
346
+ def get_signer(self, sender: str | AddressWithTransactionSigner) -> TransactionSigner:
347
+ """
348
+ Returns the `TransactionSigner` for the given sender address.
349
+
350
+ If no signer has been registered for that address then the default signer is used if registered.
351
+
352
+ :param sender: The sender address or account
353
+ :returns: The `TransactionSigner`
354
+ :raises ValueError: If no signer is found and no default signer is set
355
+ :raises TypeError: If a registered signer has an unexpected type
356
+
357
+ :example:
358
+ >>> signer = account_manager.get_signer("SENDERADDRESS")
359
+ """
360
+ signer_or_account = self._accounts.get(self._get_address(sender)) or self._default_signer
361
+ if not signer_or_account:
362
+ raise ValueError(f"No signer found for address {sender}")
363
+ # Check for AddressWithSigners first (uses .addr and .signer, not .address)
364
+ if isinstance(signer_or_account, AddressWithSigners):
365
+ return signer_or_account.signer
366
+ if isinstance(signer_or_account, AddressWithTransactionSigner):
367
+ return signer_or_account.signer
368
+ # Assume it's a TransactionSigner callable
369
+ if callable(signer_or_account):
370
+ return signer_or_account
371
+ raise TypeError(f"Unexpected signer type {type(signer_or_account)}")
372
+
373
+ def get_account(self, sender: str) -> StoredAccountType | AddressWithTransactionSigner:
374
+ """
375
+ Returns the registered account for the given sender address.
376
+
377
+ :param sender: The sender address
378
+ :returns: The registered account (AddressWithSigners, LogicSigAccount, MultisigAccount,
379
+ or AddressWithTransactionSigner)
380
+ :raises ValueError: If no account is found for the address
381
+
382
+ :example:
383
+ >>> sender = account_manager.random().addr
384
+ >>> # ...
385
+ >>> # Returns the account for `sender` that has previously been registered
386
+ >>> account = account_manager.get_account(sender)
387
+ """
388
+ account = self._accounts.get(sender)
389
+ if not account:
390
+ raise ValueError(f"No account found for address {sender}")
391
+ return account
392
+
393
+ def get_information(self, sender: str | AddressWithTransactionSigner) -> AccountInformation:
394
+ """
395
+ Returns the given sender account's current status, balance and spendable amounts.
396
+
397
+ See `<https://dev.algorand.co/reference/rest-apis/algod/#account>`_
398
+ for response data schema details.
399
+
400
+ :param sender: The address or account compliant with `AddressWithTransactionSigner` protocol to look up
401
+ :returns: The account information
402
+
403
+ :example:
404
+ >>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA"
405
+ >>> account_info = account_manager.get_information(address)
406
+ """
407
+ account_info = self._client_manager.algod.account_information(self._get_address(sender))
408
+ return self._build_account_information(account_info)
409
+
410
+ def _build_account_information(self, account_info: algod_models.Account) -> AccountInformation:
411
+ """Convert a typed algod account model into an AccountInformation dataclass."""
412
+ return AccountInformation(
413
+ address=account_info.address,
414
+ amount=AlgoAmount.from_micro_algo(account_info.amount),
415
+ amount_without_pending_rewards=AlgoAmount.from_micro_algo(account_info.amount_without_pending_rewards),
416
+ min_balance=AlgoAmount.from_micro_algo(account_info.min_balance),
417
+ pending_rewards=AlgoAmount.from_micro_algo(account_info.pending_rewards),
418
+ rewards=AlgoAmount.from_micro_algo(account_info.rewards),
419
+ round=account_info.round_,
420
+ status=account_info.status,
421
+ total_apps_opted_in=account_info.total_apps_opted_in,
422
+ total_assets_opted_in=account_info.total_assets_opted_in,
423
+ total_box_bytes=getattr(account_info, "total_box_bytes", None),
424
+ total_boxes=getattr(account_info, "total_boxes", None),
425
+ total_created_apps=account_info.total_created_apps,
426
+ total_created_assets=account_info.total_created_assets,
427
+ apps_local_state=[to_wire(app) for app in account_info.apps_local_state]
428
+ if account_info.apps_local_state
429
+ else None,
430
+ apps_total_extra_pages=account_info.apps_total_extra_pages,
431
+ apps_total_schema=to_wire(account_info.apps_total_schema) if account_info.apps_total_schema else None,
432
+ assets=[to_wire(asset) for asset in account_info.assets] if account_info.assets else None,
433
+ auth_addr=account_info.auth_addr,
434
+ closed_at_round=getattr(account_info, "closed_at_round", None),
435
+ created_apps=[to_wire(app) for app in account_info.created_apps] if account_info.created_apps else None,
436
+ created_assets=[to_wire(asset) for asset in account_info.created_assets]
437
+ if account_info.created_assets
438
+ else None,
439
+ )
440
+
441
+ def from_mnemonic(self, *, mnemonic: str, sender: str | None = None) -> AddressWithSigners:
442
+ """
443
+ Tracks and returns an Algorand account with secret key loaded by taking the mnemonic secret.
444
+
445
+ :param mnemonic: The mnemonic secret representing the private key of an account
446
+ :param sender: Optional address to use as the sender (for rekeyed accounts)
447
+ :returns: The account as AddressWithSigners
448
+
449
+ .. warning::
450
+ Be careful how the mnemonic is handled. Never commit it into source control and ideally load it
451
+ from the environment (ideally via a secret storage service) rather than the file system.
452
+
453
+ :example:
454
+ >>> account = account_manager.from_mnemonic("mnemonic secret ...")
455
+ """
456
+ seed = seed_from_mnemonic(mnemonic)
457
+ signing_key = nacl.signing.SigningKey(seed)
458
+ public_key = signing_key.verify_key.encode()
459
+
460
+ def raw_signer(bytes_to_sign: bytes) -> bytes:
461
+ return signing_key.sign(bytes_to_sign).signature
462
+
463
+ account = generate_address_with_signers(
464
+ ed25519_pubkey=public_key,
465
+ raw_ed25519_signer=raw_signer,
466
+ sending_address=sender,
467
+ )
468
+ return self._signer_account(account)
469
+
470
+ def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> AddressWithSigners:
471
+ """
472
+ Tracks and returns an Algorand account with private key loaded by convention from environment variables.
473
+
474
+ This allows you to write code that will work seamlessly in production and local development (LocalNet)
475
+ without manual config locally (including when you reset the LocalNet).
476
+
477
+ :param name: The name identifier of the account
478
+ :param fund_with: Optional amount to fund the account with when it gets created
479
+ (when targeting LocalNet)
480
+ :returns: The account as AddressWithSigners
481
+ :raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME}
482
+
483
+ .. note::
484
+ Convention:
485
+ * **Non-LocalNet:** will load `{NAME}_MNEMONIC` as a mnemonic secret.
486
+ If `{NAME}_SENDER` is defined then it will use that for the sender address
487
+ (i.e. to support rekeyed accounts)
488
+ * **LocalNet:** will load the account from a KMD wallet called {NAME} and if that wallet doesn't exist
489
+ it will create it and fund the account for you
490
+
491
+ :example:
492
+ >>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call:
493
+ >>> account = account_manager.from_environment('MY_ACCOUNT')
494
+ >>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created
495
+ >>> # with an account that is automatically funded with the specified amount from the LocalNet dispenser
496
+ """
497
+ account_mnemonic = os.getenv(f"{name.upper()}_MNEMONIC")
498
+ sender = os.getenv(f"{name.upper()}_SENDER")
499
+
500
+ if account_mnemonic:
501
+ return self.from_mnemonic(mnemonic=account_mnemonic, sender=sender)
502
+
503
+ if self._client_manager.is_localnet():
504
+ kmd_account = self._kmd_account_manager.get_or_create_wallet_account(name, fund_with)
505
+ return self._signer_account(kmd_account)
506
+
507
+ raise ValueError(f"Missing environment variable {name.upper()}_MNEMONIC when looking for account {name}")
508
+
509
+ def from_kmd(
510
+ self, name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None
511
+ ) -> AddressWithSigners:
512
+ """
513
+ Tracks and returns an Algorand account with private key loaded from the given KMD wallet.
514
+
515
+ :param name: The name of the wallet to retrieve an account from
516
+ :param predicate: Optional filter to use to find the account
517
+ :param sender: Optional sender address to use this signer for (aka a rekeyed account)
518
+ :returns: The account as AddressWithSigners
519
+ :raises ValueError: If unable to find KMD account with given name and predicate
520
+
521
+ :example:
522
+ >>> # Get default funded account in a LocalNet:
523
+ >>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet',
524
+ ... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000
525
+ ... )
526
+ """
527
+ kmd_account = self._kmd_account_manager.get_wallet_account(name, predicate, sender)
528
+ if not kmd_account:
529
+ raise ValueError(f"Unable to find KMD account {name}{' with predicate' if predicate else ''}")
530
+
531
+ return self._signer_account(kmd_account)
532
+
533
+ def logicsig(self, program: bytes, args: list[bytes] | None = None) -> AddressWithSigners:
534
+ """
535
+ Tracks and returns an account that represents a logic signature.
536
+
537
+ :param program: The bytes that make up the compiled logic signature
538
+ :param args: Optional (binary) arguments to pass into the logic signature
539
+ :returns: An AddressWithSigners wrapper for the logic signature account
540
+
541
+ :example:
542
+ >>> account = account.logicsig(program, [new Uint8Array(3, ...)])
543
+ """
544
+ logic_sig = LogicSigAccount(program, args)
545
+ return self._signer_account(logic_sig)
546
+
547
+ def multisig(self, metadata: MultisigMetadata, sub_signers: Sequence[AddressWithSigners]) -> AddressWithSigners:
548
+ """
549
+ Tracks and returns an account that supports partial or full multisig signing.
550
+
551
+ :param metadata: The metadata for the multisig account
552
+ :param sub_signers: The signers that are currently present
553
+ :returns: An AddressWithSigners wrapper for the multisig account
554
+
555
+ :example:
556
+ >>> account = account_manager.multi_sig(
557
+ ... version=1,
558
+ ... threshold=1,
559
+ ... addrs=["ADDRESS1...", "ADDRESS2..."],
560
+ ... sub_signers=[account1, account2]
561
+ ... )
562
+ """
563
+ msig_account = MultisigAccount(metadata, sub_signers)
564
+ return self._signer_account(msig_account)
565
+
566
+ def random(self) -> AddressWithSigners:
567
+ """
568
+ Tracks and returns a new, random Algorand account.
569
+
570
+ :returns: The account as AddressWithSigners
571
+
572
+ :example:
573
+ >>> account = account_manager.random()
574
+ """
575
+ # Generate random keypair using nacl
576
+ keypair = nacl.signing.SigningKey.generate()
577
+ public_key = keypair.verify_key.encode()
578
+
579
+ def raw_signer(bytes_to_sign: bytes) -> bytes:
580
+ return keypair.sign(bytes_to_sign).signature
581
+
582
+ account = generate_address_with_signers(
583
+ ed25519_pubkey=public_key,
584
+ raw_ed25519_signer=raw_signer,
585
+ )
586
+ return self._signer_account(account)
587
+
588
+ def localnet_dispenser(self) -> AddressWithSigners:
589
+ """
590
+ Returns an Algorand account with private key loaded for the default LocalNet dispenser account.
591
+
592
+ This account can be used to fund other accounts.
593
+
594
+ :returns: The account as AddressWithSigners
595
+
596
+ :example:
597
+ >>> account = account_manager.localnet_dispenser()
598
+ """
599
+ kmd_account = self._kmd_account_manager.get_localnet_dispenser_account()
600
+ return self._signer_account(kmd_account)
601
+
602
+ def dispenser_from_environment(self) -> AddressWithSigners:
603
+ """
604
+ Returns an account (with private key loaded) that can act as a dispenser from environment variables.
605
+
606
+ If environment variables are not present, returns the default LocalNet dispenser account.
607
+
608
+ :returns: The account as AddressWithSigners
609
+
610
+ :example:
611
+ >>> account = account_manager.dispenser_from_environment()
612
+ """
613
+ name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC")
614
+ if name:
615
+ return self.from_environment(DISPENSER_ACCOUNT_NAME)
616
+ return self.localnet_dispenser()
617
+
618
+ def rekeyed(self, *, sender: str, account: AddressWithTransactionSigner | AddressWithSigners) -> AddressWithSigners:
619
+ """
620
+ Tracks and returns an Algorand account that is a rekeyed version of the given account to a new sender.
621
+
622
+ :param sender: The address to use as the sender
623
+ :param account: The account to use as the signer for this new rekeyed account
624
+ :returns: The rekeyed account as AddressWithSigners
625
+
626
+ :example:
627
+ >>> account = account.from_mnemonic("mnemonic secret ...")
628
+ >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...")
629
+ """
630
+
631
+ # Create AddressWithSigners with the new sender address but using the original account's signer
632
+ def _placeholder_bytes_signer(data: bytes) -> bytes:
633
+ raise NotImplementedError("bytes_signer not available for rekeyed accounts")
634
+
635
+ def _placeholder_lsig_signer(program: bytes, msig_address: bytes | None = None) -> bytes:
636
+ raise NotImplementedError("delegated_lsig_signer not available for rekeyed accounts")
637
+
638
+ def _placeholder_program_data_signer(data: bytes, program_address: bytes) -> bytes:
639
+ raise NotImplementedError("program_data_signer not available for rekeyed accounts")
640
+
641
+ def _placeholder_mx_bytes_signer(data: bytes) -> bytes:
642
+ raise NotImplementedError("mx_bytes_signer not available for rekeyed accounts")
643
+
644
+ rekeyed_account = AddressWithSigners(
645
+ addr=sender,
646
+ signer=account.signer,
647
+ delegated_lsig_signer=_placeholder_lsig_signer,
648
+ program_data_signer=_placeholder_program_data_signer,
649
+ bytes_signer=_placeholder_bytes_signer,
650
+ mx_bytes_signer=_placeholder_mx_bytes_signer,
651
+ )
652
+
653
+ self._accounts[sender] = rekeyed_account
654
+ return rekeyed_account
655
+
656
+ def rekey_account( # noqa: PLR0913
657
+ self,
658
+ account: str,
659
+ rekey_to: str | AddressWithTransactionSigner,
660
+ *, # Common transaction parameters
661
+ signer: TransactionSigner | None = None,
662
+ note: bytes | None = None,
663
+ lease: bytes | None = None,
664
+ static_fee: AlgoAmount | None = None,
665
+ extra_fee: AlgoAmount | None = None,
666
+ max_fee: AlgoAmount | None = None,
667
+ validity_window: int | None = None,
668
+ first_valid_round: int | None = None,
669
+ last_valid_round: int | None = None,
670
+ suppress_log: bool | None = None,
671
+ ) -> SendTransactionComposerResults:
672
+ """
673
+ Rekey an account to a new address.
674
+
675
+ :param account: The account to rekey
676
+ :param rekey_to: The address or account to rekey to
677
+ :param signer: Optional transaction signer
678
+ :param note: Optional transaction note
679
+ :param lease: Optional transaction lease
680
+ :param static_fee: Optional static fee
681
+ :param extra_fee: Optional extra fee
682
+ :param max_fee: Optional max fee
683
+ :param validity_window: Optional validity window
684
+ :param first_valid_round: Optional first valid round
685
+ :param last_valid_round: Optional last valid round
686
+ :param suppress_log: Optional flag to suppress logging
687
+ :returns: The result of the transaction and the transaction that was sent
688
+
689
+ .. warning::
690
+ Please be careful with this function and be sure to read the
691
+ `official rekey guidance <https://dev.algorand.co/concepts/accounts/rekeying>`_.
692
+
693
+ :example:
694
+ >>> # Basic example (with string addresses):
695
+ >>> algorand.account.rekey_account("ACCOUNTADDRESS", "NEWADDRESS")
696
+ >>> # Basic example (with signer accounts):
697
+ >>> algorand.account.rekey_account(account1, newSignerAccount)
698
+ >>> # Advanced example:
699
+ >>> algorand.account.rekey_account(
700
+ ... account="ACCOUNTADDRESS",
701
+ ... rekey_to="NEWADDRESS",
702
+ ... lease='lease',
703
+ ... note='note',
704
+ ... first_valid_round=1000,
705
+ ... validity_window=10,
706
+ ... extra_fee=AlgoAmount.from_micro_algo(1000),
707
+ ... static_fee=AlgoAmount.from_micro_algo(1000),
708
+ ... max_fee=AlgoAmount.from_micro_algo(3000),
709
+ ... suppress_log=True,
710
+ ... )
711
+ """
712
+ sender_address = self._get_address(account)
713
+ rekey_address = self._get_address(rekey_to)
714
+
715
+ result = (
716
+ self._get_composer()
717
+ .add_payment(
718
+ PaymentParams(
719
+ sender=sender_address,
720
+ receiver=sender_address,
721
+ amount=AlgoAmount.from_micro_algo(0),
722
+ rekey_to=rekey_address,
723
+ signer=signer,
724
+ note=note,
725
+ lease=lease,
726
+ static_fee=static_fee,
727
+ extra_fee=extra_fee,
728
+ max_fee=max_fee,
729
+ validity_window=validity_window,
730
+ first_valid_round=first_valid_round,
731
+ last_valid_round=last_valid_round,
732
+ )
733
+ )
734
+ .send()
735
+ )
736
+
737
+ # If rekey_to is a signing account, set it as the signer for this account
738
+ if isinstance(rekey_to, AddressWithTransactionSigner | AddressWithSigners):
739
+ self.rekeyed(sender=account, account=rekey_to)
740
+
741
+ if not suppress_log:
742
+ config.logger.info(f"Rekeyed {sender_address} to {rekey_address} via transaction {result.tx_ids[-1]}")
743
+
744
+ return result
745
+
746
+ def ensure_funded( # noqa: PLR0913
747
+ self,
748
+ account_to_fund: str | AddressWithTransactionSigner | AddressWithSigners,
749
+ dispenser_account: str | AddressWithTransactionSigner | AddressWithSigners,
750
+ min_spending_balance: AlgoAmount,
751
+ min_funding_increment: AlgoAmount | None = None,
752
+ # Sender params
753
+ send_params: SendParams | None = None,
754
+ # Common txn params
755
+ signer: TransactionSigner | None = None,
756
+ rekey_to: str | None = None,
757
+ note: bytes | None = None,
758
+ lease: bytes | None = None,
759
+ static_fee: AlgoAmount | None = None,
760
+ extra_fee: AlgoAmount | None = None,
761
+ max_fee: AlgoAmount | None = None,
762
+ validity_window: int | None = None,
763
+ first_valid_round: int | None = None,
764
+ last_valid_round: int | None = None,
765
+ ) -> EnsureFundedResult | None:
766
+ """
767
+ Funds a given account using a dispenser account as a funding source.
768
+
769
+ Ensures the given account has a certain amount of Algo free to spend (accounting for
770
+ Algo locked in minimum balance requirement).
771
+
772
+ See `<https://dev.algorand.co/concepts/smart-contracts/costs-constraints#mbr>`_ for details.
773
+
774
+ :param account_to_fund: The account to fund
775
+ :param dispenser_account: The account to use as a dispenser funding source
776
+ :param min_spending_balance: The minimum balance of Algo that the account
777
+ should have available to spend
778
+ :param min_funding_increment: Optional minimum funding increment
779
+ :param send_params: Parameters for the send operation, defaults to None
780
+ :param signer: Optional transaction signer
781
+ :param rekey_to: Optional rekey address
782
+ :param note: Optional transaction note
783
+ :param lease: Optional transaction lease
784
+ :param static_fee: Optional static fee
785
+ :param extra_fee: Optional extra fee
786
+ :param max_fee: Optional maximum fee
787
+ :param validity_window: Optional validity window
788
+ :param first_valid_round: Optional first valid round
789
+ :param last_valid_round: Optional last valid round
790
+ :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed,
791
+ or None if no funds were needed
792
+
793
+ :example:
794
+ >>> # Basic example:
795
+ >>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount.from_algo(1))
796
+ >>> # With configuration:
797
+ >>> algorand.account.ensure_funded(
798
+ ... "ACCOUNTADDRESS",
799
+ ... "DISPENSERADDRESS",
800
+ ... AlgoAmount.from_algo(1),
801
+ ... min_funding_increment=AlgoAmount.from_algo(2),
802
+ ... fee=AlgoAmount.from_micro_algo(1000),
803
+ ... suppress_log=True
804
+ ... )
805
+ """
806
+ account_to_fund_addr = self._get_address(account_to_fund)
807
+ dispenser_account_addr = self._get_address(dispenser_account)
808
+ amount_funded = self._get_ensure_funded_amount(
809
+ account_to_fund_addr, min_spending_balance, min_funding_increment
810
+ )
811
+
812
+ if not amount_funded:
813
+ return None
814
+
815
+ result = (
816
+ self._get_composer()
817
+ .add_payment(
818
+ PaymentParams(
819
+ sender=dispenser_account_addr,
820
+ receiver=account_to_fund_addr,
821
+ amount=amount_funded,
822
+ signer=signer,
823
+ rekey_to=rekey_to,
824
+ note=note,
825
+ lease=lease,
826
+ static_fee=static_fee,
827
+ extra_fee=extra_fee,
828
+ max_fee=max_fee,
829
+ validity_window=validity_window,
830
+ first_valid_round=first_valid_round,
831
+ last_valid_round=last_valid_round,
832
+ )
833
+ )
834
+ .send(send_params)
835
+ )
836
+
837
+ base_result = SendSingleTransactionResult.from_composer_result(result)
838
+ return EnsureFundedResult(
839
+ **vars(base_result),
840
+ transaction_id=base_result.tx_id or result.tx_ids[0],
841
+ amount_funded=amount_funded,
842
+ )
843
+
844
+ def ensure_funded_from_environment( # noqa: PLR0913
845
+ self,
846
+ account_to_fund: str | AddressWithTransactionSigner | AddressWithSigners,
847
+ min_spending_balance: AlgoAmount,
848
+ *, # Force remaining params to be keyword-only
849
+ min_funding_increment: AlgoAmount | None = None,
850
+ # SendParams
851
+ send_params: SendParams | None = None,
852
+ # Common transaction params (omitting sender)
853
+ signer: TransactionSigner | None = None,
854
+ rekey_to: str | None = None,
855
+ note: bytes | None = None,
856
+ lease: bytes | None = None,
857
+ static_fee: AlgoAmount | None = None,
858
+ extra_fee: AlgoAmount | None = None,
859
+ max_fee: AlgoAmount | None = None,
860
+ validity_window: int | None = None,
861
+ first_valid_round: int | None = None,
862
+ last_valid_round: int | None = None,
863
+ ) -> EnsureFundedResult | None:
864
+ """
865
+ Ensure an account is funded from a dispenser account configured in environment.
866
+
867
+ Uses a dispenser account retrieved from the environment, per the `dispenser_from_environment` method,
868
+ as a funding source such that the given account has a certain amount of Algo free to spend
869
+ (accounting for Algo locked in minimum balance requirement).
870
+
871
+ See `<https://dev.algorand.co/concepts/smart-contracts/costs-constraints#mbr>`_ for details.
872
+
873
+ :param account_to_fund: The account to fund
874
+ :param min_spending_balance: The minimum balance of Algo that the account should have available to
875
+ spend
876
+ :param min_funding_increment: Optional minimum funding increment
877
+ :param send_params: Parameters for the send operation, defaults to None
878
+ :param signer: Optional transaction signer
879
+ :param rekey_to: Optional rekey address
880
+ :param note: Optional transaction note
881
+ :param lease: Optional transaction lease
882
+ :param static_fee: Optional static fee
883
+ :param extra_fee: Optional extra fee
884
+ :param max_fee: Optional maximum fee
885
+ :param validity_window: Optional validity window
886
+ :param first_valid_round: Optional first valid round
887
+ :param last_valid_round: Optional last valid round
888
+ :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or
889
+ None if no funds were needed
890
+
891
+ .. note::
892
+ The dispenser account is retrieved from the account mnemonic stored in
893
+ process.env.DISPENSER_MNEMONIC and optionally process.env.DISPENSER_SENDER
894
+ if it's a rekeyed account, or against default LocalNet if no environment variables present.
895
+
896
+ :example:
897
+ >>> # Basic example:
898
+ >>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount.from_algo(1))
899
+ >>> # With configuration:
900
+ >>> algorand.account.ensure_funded_from_environment(
901
+ ... "ACCOUNTADDRESS",
902
+ ... AlgoAmount.from_algo(1),
903
+ ... min_funding_increment=AlgoAmount.from_algo(2),
904
+ ... fee=AlgoAmount.from_micro_algo(1000),
905
+ ... suppress_log=True
906
+ ... )
907
+ """
908
+ account_to_fund_addr = self._get_address(account_to_fund)
909
+ dispenser_account = self.dispenser_from_environment()
910
+
911
+ amount_funded = self._get_ensure_funded_amount(
912
+ account_to_fund_addr, min_spending_balance, min_funding_increment
913
+ )
914
+
915
+ if not amount_funded:
916
+ return None
917
+
918
+ result = (
919
+ self._get_composer()
920
+ .add_payment(
921
+ PaymentParams(
922
+ sender=dispenser_account.addr,
923
+ receiver=account_to_fund_addr,
924
+ amount=amount_funded,
925
+ signer=signer,
926
+ rekey_to=rekey_to,
927
+ note=note,
928
+ lease=lease,
929
+ static_fee=static_fee,
930
+ extra_fee=extra_fee,
931
+ max_fee=max_fee,
932
+ validity_window=validity_window,
933
+ first_valid_round=first_valid_round,
934
+ last_valid_round=last_valid_round,
935
+ )
936
+ )
937
+ .send(send_params)
938
+ )
939
+
940
+ base_result = SendSingleTransactionResult.from_composer_result(result)
941
+ return EnsureFundedResult(
942
+ **vars(base_result),
943
+ transaction_id=base_result.tx_id or result.tx_ids[0],
944
+ amount_funded=amount_funded,
945
+ )
946
+
947
+ def ensure_funded_from_testnet_dispenser_api(
948
+ self,
949
+ account_to_fund: str | AddressWithTransactionSigner,
950
+ dispenser_client: TestNetDispenserApiClient,
951
+ min_spending_balance: AlgoAmount,
952
+ *,
953
+ min_funding_increment: AlgoAmount | None = None,
954
+ ) -> EnsureFundedFromTestnetDispenserApiResult | None:
955
+ """
956
+ Ensure an account is funded using the TestNet Dispenser API.
957
+
958
+ Uses the TestNet Dispenser API as a funding source such that the account has a certain amount
959
+ of Algo free to spend (accounting for Algo locked in minimum balance requirement).
960
+
961
+ See `<https://dev.algorand.co/concepts/smart-contracts/costs-constraints#mbr>`_ for details.
962
+
963
+ :param account_to_fund: The account to fund
964
+ :param dispenser_client: The TestNet dispenser funding client
965
+ :param min_spending_balance: The minimum balance of Algo that the account should have
966
+ available to spend
967
+ :param min_funding_increment: Optional minimum funding increment
968
+ :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or
969
+ None if no funds were needed
970
+ :raises ValueError: If attempting to fund on non-TestNet network
971
+
972
+ :example:
973
+ >>> # Basic example:
974
+ >>> account_manager.ensure_funded_from_testnet_dispenser_api(
975
+ ... "ACCOUNTADDRESS",
976
+ ... algorand.client.get_testnet_dispenser_from_environment(),
977
+ ... AlgoAmount.from_algo(1)
978
+ ... )
979
+ >>> # With configuration:
980
+ >>> account_manager.ensure_funded_from_testnet_dispenser_api(
981
+ ... "ACCOUNTADDRESS",
982
+ ... algorand.client.get_testnet_dispenser_from_environment(),
983
+ ... AlgoAmount.from_algo(1),
984
+ ... min_funding_increment=AlgoAmount.from_algo(2)
985
+ ... )
986
+ """
987
+ account_to_fund_addr = self._get_address(account_to_fund)
988
+
989
+ if not self._client_manager.is_testnet():
990
+ raise ValueError("Attempt to fund using TestNet dispenser API on non TestNet network.")
991
+
992
+ amount_funded = self._get_ensure_funded_amount(
993
+ account_to_fund_addr, min_spending_balance, min_funding_increment
994
+ )
995
+
996
+ if not amount_funded:
997
+ return None
998
+
999
+ result = dispenser_client.fund(address=account_to_fund_addr, amount=amount_funded.micro_algo)
1000
+
1001
+ return EnsureFundedFromTestnetDispenserApiResult(
1002
+ transaction_id=result.tx_id,
1003
+ amount_funded=AlgoAmount.from_micro_algo(result.amount),
1004
+ )
1005
+
1006
+ def _get_address(self, sender: str | AddressWithTransactionSigner | AddressWithSigners) -> str:
1007
+ # Check isinstance first for proper type narrowing
1008
+ if isinstance(sender, str):
1009
+ return sender
1010
+ # Both AddressWithSigners and AddressWithTransactionSigner now use 'addr'
1011
+ return sender.addr
1012
+
1013
+ def _get_composer(
1014
+ self, get_suggested_params: Callable[[], algod_models.SuggestedParams] | None = None
1015
+ ) -> TransactionComposer:
1016
+ get_suggested_params = get_suggested_params or self._client_manager.algod.suggested_params
1017
+
1018
+ return TransactionComposer(
1019
+ TransactionComposerParams(
1020
+ algod=self._client_manager.algod,
1021
+ get_signer=self.get_signer,
1022
+ get_suggested_params=get_suggested_params,
1023
+ )
1024
+ )
1025
+
1026
+ def _calculate_fund_amount(
1027
+ self,
1028
+ min_spending_balance: int,
1029
+ current_spending_balance: AlgoAmount,
1030
+ min_funding_increment: int,
1031
+ ) -> int | None:
1032
+ if min_spending_balance > current_spending_balance:
1033
+ min_fund_amount = (min_spending_balance - current_spending_balance).micro_algo
1034
+ return max(min_fund_amount, min_funding_increment)
1035
+ return None
1036
+
1037
+ def _get_ensure_funded_amount(
1038
+ self,
1039
+ sender: str,
1040
+ min_spending_balance: AlgoAmount,
1041
+ min_funding_increment: AlgoAmount | None = None,
1042
+ ) -> AlgoAmount | None:
1043
+ account_info = self.get_information(sender)
1044
+ current_spending_balance = account_info.amount - account_info.min_balance
1045
+
1046
+ min_increment = min_funding_increment.micro_algo if min_funding_increment else 0
1047
+ amount_funded = self._calculate_fund_amount(
1048
+ min_spending_balance.micro_algo, current_spending_balance, min_increment
1049
+ )
1050
+
1051
+ return AlgoAmount.from_micro_algo(amount_funded) if amount_funded is not None else None