Skip to content

Commit f3c67f8

Browse files
committed
refactor: move transaction validation to occur during building and signing
1 parent 501c360 commit f3c67f8

File tree

4 files changed

+28
-28
lines changed

4 files changed

+28
-28
lines changed

src/algokit_transact/ops/validate.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
MAX_LOCAL_STATE_KEYS,
1919
MAX_OVERALL_REFERENCES,
2020
PROGRAM_PAGE_SIZE,
21+
SIGNATURE_BYTE_LENGTH,
2122
)
23+
from algokit_transact import SignedTransaction
2224
from algokit_transact.exceptions import TransactionValidationError
2325
from algokit_transact.models.app_call import AppCallTransactionFields
2426
from algokit_transact.models.asset_config import AssetConfigTransactionFields
@@ -55,6 +57,19 @@ def _issue(
5557
return ValidationIssue(code=code, message=message, field=field, context=context)
5658

5759

60+
def validate_signed_transaction(stx: SignedTransaction) -> None:
61+
validate_transaction(stx.txn)
62+
63+
signatures = {stx.sig, stx.msig, stx.lsig} - {None}
64+
if not signatures:
65+
raise ValueError("At least one signature type must be set")
66+
if len(signatures) > 1:
67+
raise ValueError("Only one signature type can be set")
68+
69+
if stx.sig is not None and len(stx.sig) != SIGNATURE_BYTE_LENGTH:
70+
raise ValueError("Signature must be 64 bytes")
71+
72+
5873
def validate_transaction(transaction: Transaction) -> None:
5974
if not transaction.sender:
6075
raise TransactionValidationError("Transaction sender is required")

src/algokit_utils/transactions/builders/common.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Protocol, runtime_checkable
44

55
from algokit_algod_client import models as algod_models
6+
from algokit_transact import validate_transaction
67
from algokit_transact.models.app_call import AppCallTransactionFields
78
from algokit_transact.models.asset_config import AssetConfigTransactionFields
89
from algokit_transact.models.asset_freeze import AssetFreezeTransactionFields
@@ -119,7 +120,7 @@ def build_transaction(
119120
application_call: AppCallTransactionFields | None = None,
120121
key_registration: KeyRegistrationTransactionFields | None = None,
121122
) -> Transaction:
122-
return Transaction(
123+
txn = Transaction(
123124
transaction_type=txn_type,
124125
sender=header.sender,
125126
first_valid=header.first_valid,
@@ -136,6 +137,8 @@ def build_transaction(
136137
application_call=application_call,
137138
key_registration=key_registration,
138139
)
140+
validate_transaction(txn)
141+
return txn
139142

140143

141144
def apply_transaction_fees(

src/algokit_utils/transactions/transaction_composer.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import base64
22
import json
33
import re
4-
from collections.abc import Callable, Iterable, Sequence
4+
from collections.abc import Callable, Sequence
55
from dataclasses import dataclass, replace
66
from typing import Any, TypeAlias, TypedDict, cast
77

@@ -10,14 +10,14 @@
1010
from algokit_algod_client import models as algod_models
1111
from algokit_algod_client.exceptions import UnexpectedStatusError
1212
from algokit_algod_client.models import SimulateTransactionResult
13-
from algokit_common.constants import MAX_TRANSACTION_GROUP_SIZE, SIGNATURE_BYTE_LENGTH
13+
from algokit_common.constants import MAX_TRANSACTION_GROUP_SIZE
1414
from algokit_transact import decode_signed_transaction, encode_signed_transactions, make_empty_transaction_signer
1515
from algokit_transact.models.signed_transaction import SignedTransaction
1616
from algokit_transact.models.transaction import Transaction, TransactionType
1717
from algokit_transact.ops.fees import calculate_fee
1818
from algokit_transact.ops.group import group_transactions
1919
from algokit_transact.ops.ids import get_transaction_id
20-
from algokit_transact.ops.validate import validate_transaction
20+
from algokit_transact.ops.validate import validate_signed_transaction, validate_transaction
2121
from algokit_transact.signer import AddressWithTransactionSigner, TransactionSigner
2222
from algokit_utils.applications.abi import ABIReturn
2323
from algokit_utils.applications.app_manager import AppManager
@@ -293,6 +293,7 @@ def register_error_transformer(self, transformer: ErrorTransformer) -> "Transact
293293
return self
294294

295295
def add_transaction(self, txn: Transaction, signer: TransactionSigner | None = None) -> "TransactionComposer":
296+
validate_transaction(txn)
296297
self._ensure_not_built()
297298
self._queued.append(_QueuedTransaction(txn=self._sanitize_transaction(txn), signer=signer))
298299
return self
@@ -501,7 +502,6 @@ def send(self, params: SendParams | None = None) -> SendTransactionComposerResul
501502

502503
# Send transactions and handle network errors
503504
try:
504-
_validate_signed_transactions(signed_transactions)
505505
blobs = encode_signed_transactions(signed_transactions)
506506
self._algod.send_raw_transaction(blobs)
507507

@@ -1370,7 +1370,9 @@ def _sign_transactions(self, txns_with_signers: Sequence[TransactionWithSigner])
13701370
for key, (_, indexes) in signer_groups.items():
13711371
blobs = signed_blobs[key]
13721372
for blob_index, txn_index in enumerate(indexes):
1373-
ordered[txn_index] = decode_signed_transaction(blobs[blob_index])
1373+
signed_txn = decode_signed_transaction(blobs[blob_index])
1374+
validate_signed_transaction(signed_txn)
1375+
ordered[txn_index] = signed_txn
13741376

13751377
if any(item is None for item in ordered):
13761378
raise ValueError("One or more transactions were not signed")
@@ -1557,24 +1559,6 @@ def _extract_algod_error_message(payload: object) -> str | None: # noqa: PLR091
15571559
return text
15581560

15591561

1560-
def _validate_signed_transactions(stxns: Iterable[SignedTransaction]) -> None:
1561-
for stxn in stxns:
1562-
_validate_signed_transaction(stxn)
1563-
1564-
1565-
def _validate_signed_transaction(stx: SignedTransaction) -> None:
1566-
validate_transaction(stx.txn)
1567-
1568-
signatures = {stx.sig, stx.msig, stx.lsig} - {None}
1569-
if not signatures:
1570-
raise ValueError("At least one signature type must be set")
1571-
if len(signatures) > 1:
1572-
raise ValueError("Only one signature type can be set")
1573-
1574-
if stx.sig is not None and len(stx.sig) != SIGNATURE_BYTE_LENGTH:
1575-
raise ValueError("Signature must be 64 bytes")
1576-
1577-
15781562
def _wait_for_confirmation(
15791563
algod: AlgodClient,
15801564
tx_id: str,

tests/transactions/test_transaction_composer.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
PaymentParams,
2222
SendTransactionComposerResults,
2323
TransactionComposer,
24-
TransactionComposerError,
2524
TransactionComposerParams,
2625
)
2726

@@ -428,11 +427,10 @@ def transformer(error: Exception) -> Exception:
428427
def test_validation_occurs_on_send(algorand: AlgorandClient, funded_account: AddressWithSigners) -> None:
429428
params = AssetDestroyParams(asset_id=0, sender=funded_account.addr)
430429
with pytest.raises(
431-
TransactionComposerError,
430+
TransactionValidationError,
432431
match="Asset config validation failed: Total is required",
433-
) as ex:
432+
):
434433
algorand.send.asset_destroy(params)
435-
assert isinstance(ex.value.__cause__, TransactionValidationError)
436434

437435

438436
def test_simulate_does_not_throw_when_disabled(algorand: AlgorandClient, funded_account: AddressWithSigners) -> None:

0 commit comments

Comments
 (0)