From 580b7709002700e9143a959cf2db66e0be46fa17 Mon Sep 17 00:00:00 2001 From: Neil Campbell Date: Fri, 19 Sep 2025 00:29:34 +0800 Subject: [PATCH] fix: allow simulating with empty signatures like TS supports --- .../transactions/transaction_composer.py | 33 +++++++---- .../transactions/test_transaction_composer.py | 55 +++++++++++++++++++ 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 59a7c6a4..0f75f033 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1812,7 +1812,7 @@ def build(self) -> TransactionComposerBuildResult: txn_with_signers: list[TransactionWithSignerAndContext] = [] for txn in self._txns: - txn_with_signers.extend(self._build_txn(txn, suggested_params)) + txn_with_signers.extend(self._build_txn(txn, suggested_params, include_signer=True)) for ts in txn_with_signers: self._atc.add_transaction(ts) @@ -1852,9 +1852,9 @@ def build_transactions(self) -> BuiltTransactions: txn_with_signers: list[TransactionWithSigner] = [] if isinstance(txn, MethodCallParams): - txn_with_signers.extend(self._build_method_call(txn, suggested_params)) + txn_with_signers.extend(self._build_method_call(txn, suggested_params, include_signer=False)) else: - txn_with_signers.extend(self._build_txn(txn, suggested_params)) + txn_with_signers.extend(self._build_txn(txn, suggested_params, include_signer=False)) for ts in txn_with_signers: transactions.append(ts.txn) @@ -2129,7 +2129,11 @@ def _common_txn_build_step( # noqa: C901 ) def _build_method_call( # noqa: C901, PLR0912, PLR0915 - self, params: MethodCallParams, suggested_params: algosdk.transaction.SuggestedParams + self, + params: MethodCallParams, + suggested_params: algosdk.transaction.SuggestedParams, + *, + include_signer: bool, ) -> list[TransactionWithSignerAndContext]: method_args: list[ABIValue | TransactionWithSigner] = [] txns_for_group: list[TransactionWithSignerAndContext] = [] @@ -2159,7 +2163,9 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 method_args.append( TransactionWithSignerAndContext( txn=arg, - signer=signer if signer is not None else self._get_signer(params.sender), + signer=signer + if signer is not None + else (NULL_SIGNER if not include_signer else self._get_signer(params.sender)), context=TransactionContext(abi_method=None), ) ) @@ -2171,7 +2177,9 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 | AppUpdateMethodCallParams() | AppDeleteMethodCallParams() ): - temp_txn_with_signers = self._build_method_call(arg, suggested_params) + temp_txn_with_signers = self._build_method_call( + arg, suggested_params, include_signer=include_signer + ) # Add all transactions except the last one in reverse order txns_for_group.extend(temp_txn_with_signers[:-1]) # Add the last transaction to method_args @@ -2208,7 +2216,7 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 method_args.append( TransactionWithSignerAndContext( txn=txn.txn, - signer=signer or self._get_signer(params.sender), + signer=signer or (NULL_SIGNER if not include_signer else self._get_signer(params.sender)), context=TransactionContext(abi_method=params.method), ) ) @@ -2255,7 +2263,8 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 "sp": suggested_params, "signer": params.signer if params.signer is not None - else self._get_signer(params.sender) or algosdk.atomic_transaction_composer.EmptySigner(), + else (NULL_SIGNER if not include_signer else self._get_signer(params.sender)) + or algosdk.atomic_transaction_composer.EmptySigner(), "method_args": list(reversed(method_args)), "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, "boxes": [AppManager.get_box_reference(ref) for ref in params.box_references] @@ -2496,6 +2505,8 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 self, txn: TransactionWithSigner | TxnParams | AtomicTransactionComposer, suggested_params: algosdk.transaction.SuggestedParams, + *, + include_signer: bool, ) -> list[TransactionWithSignerAndContext]: match txn: case TransactionWithSigner(): @@ -2505,7 +2516,7 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 case AtomicTransactionComposer(): return self._build_atc(txn) case algosdk.transaction.Transaction(): - signer = self._get_signer(txn.sender) + signer = NULL_SIGNER if not include_signer else self._get_signer(txn.sender) return [TransactionWithSignerAndContext(txn=txn, signer=signer, context=TransactionContext.empty())] case ( AppCreateMethodCallParams() @@ -2513,10 +2524,10 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 | AppUpdateMethodCallParams() | AppDeleteMethodCallParams() ): - return self._build_method_call(txn, suggested_params) + return self._build_method_call(txn, suggested_params, include_signer=include_signer) signer = txn.signer.signer if isinstance(txn.signer, TransactionSignerAccountProtocol) else txn.signer # type: ignore[assignment] - signer = signer or self._get_signer(txn.sender) + signer = signer or (NULL_SIGNER if not include_signer else self._get_signer(txn.sender)) match txn: case PaymentParams(): diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 692dc157..290cdd57 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -247,6 +247,61 @@ def test_simulate(algorand: AlgorandClient, funded_account: SigningAccount) -> N assert simulate_response +def test_simulate_without_signer(algorand: AlgorandClient, funded_secondary_account: SigningAccount) -> None: + """Test that simulate works without a signer being available when skip_signatures=True.""" + + # No signer is loaded for funded_secondary_account + composer = algorand.new_group() + composer.add_payment( + PaymentParams( + sender=funded_secondary_account.address, + receiver=funded_secondary_account.address, + amount=AlgoAmount.from_algo(1), + ) + ) + + simulate_response = composer.simulate(skip_signatures=True) + assert simulate_response + assert len(simulate_response.transactions) == 1 + + +def test_build_transactions_without_signer(algorand: AlgorandClient, funded_secondary_account: SigningAccount) -> None: + """Test that build_transactions work without a signer being available""" + + # No signer is loaded for funded_secondary_account + composer = algorand.new_group() + composer.add_payment( + PaymentParams( + sender=funded_secondary_account.address, + receiver=funded_secondary_account.address, + amount=AlgoAmount.from_algo(1), + ) + ) + + built = composer.build_transactions() + assert len(built.transactions) == 1 + assert len(built.signers) == 0 + + +def test_fails_to_build_without_signers(algorand: AlgorandClient, funded_secondary_account: SigningAccount) -> None: + """Test that build does not work without a signer being available""" + + # No signer is loaded for funded_secondary_account + composer = algorand.new_group() + composer.add_payment( + PaymentParams( + sender=funded_secondary_account.address, + receiver=funded_secondary_account.address, + amount=AlgoAmount.from_algo(1), + ) + ) + + with pytest.raises(Exception) as e: # noqa: PT011 + composer.build() + + assert str(e.value) == f"No signer found for address {funded_secondary_account.address}" + + def test_send(algorand: AlgorandClient, funded_account: SigningAccount) -> None: composer = TransactionComposer( algod=algorand.client.algod,