diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 0ebd7ff9..8ea8c94c 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -869,6 +869,9 @@ def build( change_address: Optional[Address] = None, merge_change: Optional[bool] = False, collateral_change_address: Optional[Address] = None, + auto_validity_start_offset: Optional[int] = None, + auto_ttl_offset: Optional[int] = None, + auto_required_signers: Optional[bool] = None, ) -> TransactionBody: """Build a transaction body from all constraints set through the builder. @@ -878,11 +881,41 @@ def build( merge_change (Optional[bool]): If the change address match one of the transaction output, the change amount will be directly added to that transaction output, instead of being added as a separate output. collateral_change_address (Optional[Address]): Address to which collateral changes will be returned. + auto_validity_start_offset (Optional[int]): Automatically set the validity start interval of the transaction + to the current slot number + the given offset (default -1000). + A manually set validity start will always take precedence. + auto_ttl_offset (Optional[int]): Automatically set the validity end interval (ttl) of the transaction + to the current slot number + the given offset (default 10_000). + A manually set ttl will always take precedence. + auto_required_signers (Optional[bool]): Automatically add all pubkeyhashes of transaction inputs + to required signatories (default only for Smart Contract transactions). + Manually set required signers will always take precedence. Returns: TransactionBody: A transaction body. """ self._ensure_no_input_exclusion_conflict() + + # only automatically set the validity interval and required signers if scripts are involved + is_smart = bool(self.scripts) + + # Automatically set the validity range to a tight value around transaction creation + if ( + is_smart or auto_validity_start_offset is not None + ) and self.validity_start is None: + last_slot = self.context.last_block_slot + # If None is provided, the default value is -1000 + if auto_validity_start_offset is None: + auto_validity_start_offset = -1000 + self.validity_start = max(0, last_slot + auto_validity_start_offset) + + if (is_smart or auto_ttl_offset is not None) and self.ttl is None: + last_slot = self.context.last_block_slot + # If None is provided, the default value is 10_000 + if auto_ttl_offset is None: + auto_ttl_offset = 10_000 + self.ttl = max(0, last_slot + auto_ttl_offset) + selected_utxos = [] selected_amount = Value() for i in self.inputs: @@ -1014,6 +1047,14 @@ def build( self.inputs[:] = selected_utxos[:] + # Automatically set the required signers for smart transactions + if ( + is_smart and auto_required_signers is not False + ) and self.required_signers is None: + # Collect all signatories from explicitly defined + # transaction inputs and collateral inputs, and input addresses + self.required_signers = list(self._input_vkey_hashes()) + self._set_redeemer_index() self._set_collateral_return(collateral_change_address or change_address) @@ -1165,6 +1206,9 @@ def build_and_sign( change_address: Optional[Address] = None, merge_change: Optional[bool] = False, collateral_change_address: Optional[Address] = None, + auto_validity_start_offset: Optional[int] = None, + auto_ttl_offset: Optional[int] = None, + auto_required_signers: Optional[bool] = None, ) -> Transaction: """Build a transaction body from all constraints set through the builder and sign the transaction with provided signing keys. @@ -1177,6 +1221,15 @@ def build_and_sign( merge_change (Optional[bool]): If the change address match one of the transaction output, the change amount will be directly added to that transaction output, instead of being added as a separate output. collateral_change_address (Optional[Address]): Address to which collateral changes will be returned. + auto_validity_start_offset (Optional[int]): Automatically set the validity start interval of the transaction + to the current slot number + the given offset (default -1000). + A manually set validity start will always take precedence. + auto_ttl_offset (Optional[int]): Automatically set the validity end interval (ttl) of the transaction + to the current slot number + the given offset (default 10_000). + A manually set ttl will always take precedence. + auto_required_signers (Optional[bool]): Automatically add all pubkeyhashes of transaction inputs + to required signatories (default only for Smart Contract transactions). + Manually set required signers will always take precedence. Returns: Transaction: A signed transaction. @@ -1186,6 +1239,9 @@ def build_and_sign( change_address=change_address, merge_change=merge_change, collateral_change_address=collateral_change_address, + auto_validity_start_offset=auto_validity_start_offset, + auto_ttl_offset=auto_ttl_offset, + auto_required_signers=auto_required_signers, ) witness_set = self.build_witness_set() witness_set.vkey_witnesses = [] diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 0d2957a5..7c96bbb7 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -178,7 +178,9 @@ def test_tx_builder_raises_utxo_selection(chain_context): ) with pytest.raises(UTxOSelectionException) as e: - tx_body = tx_builder.build(change_address=sender_address) + tx_body = tx_builder.build( + change_address=sender_address, + ) # The unfulfilled amount includes requested (991000000) and estimated fees (161277) assert "Unfulfilled amount:\n {\n 'coin': 991161277" in e.value.args[0] @@ -301,7 +303,7 @@ def test_tx_builder_mint_multi_asset(chain_context): tx_builder = TransactionBuilder(chain_context) sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" - sender_address = Address.from_primitive(sender) + sender_address: Address = Address.from_primitive(sender) # Add sender address as input mint = {policy_id.payload: {b"Token1": 1}} @@ -328,14 +330,16 @@ def test_tx_builder_mint_multi_asset(chain_context): [ sender_address.to_primitive(), [ - 5811267, + 5809683, {b"1111111111111111111111111111": {b"Token1": 1, b"Token2": 2}}, ], ], ], - 2: 188733, + 2: 190317, 3: 123456789, + 8: 1000, 9: mint, + 14: [sender_address.payment_part.to_primitive()], } assert expected == tx_body.to_primitive() @@ -827,7 +831,10 @@ def test_build_and_sign(chain_context): tx_builder2.add_input_address(sender).add_output( TransactionOutput.from_primitive([sender, 500000]) ) - tx = tx_builder2.build_and_sign([SK], change_address=sender_address) + tx = tx_builder2.build_and_sign( + [SK], + change_address=sender_address, + ) assert tx.transaction_witness_set.vkey_witnesses == [ VerificationKeyWitness(SK.to_verification_key(), SK.sign(tx_body.hash())) @@ -1002,7 +1009,10 @@ def test_tx_builder_no_output(chain_context): tx_builder.add_input(utxo1) - tx_body = tx_builder.build(change_address=sender_address, merge_change=True) + tx_body = tx_builder.build( + change_address=sender_address, + merge_change=True, + ) expected = { 0: [[b"11111111111111111111111111111111", 3]], @@ -1029,7 +1039,10 @@ def test_tx_builder_merge_change_to_output(chain_context): tx_builder.add_input(utxo1) tx_builder.add_output(TransactionOutput.from_primitive([sender, 10000])) - tx_body = tx_builder.build(change_address=sender_address, merge_change=True) + tx_body = tx_builder.build( + change_address=sender_address, + merge_change=True, + ) expected = { 0: [[b"11111111111111111111111111111111", 3]], @@ -1060,7 +1073,10 @@ def test_tx_builder_merge_change_to_output_2(chain_context): tx_builder.add_output(TransactionOutput.from_primitive([receiver, 10000])) tx_builder.add_output(TransactionOutput.from_primitive([sender, 0])) - tx_body = tx_builder.build(change_address=sender_address, merge_change=True) + tx_body = tx_builder.build( + change_address=sender_address, + merge_change=True, + ) expected = { 0: [[b"11111111111111111111111111111111", 3]], @@ -1089,7 +1105,10 @@ def test_tx_builder_merge_change_to_zero_amount_output(chain_context): tx_builder.add_input(utxo1) tx_builder.add_output(TransactionOutput.from_primitive([sender, 0])) - tx_body = tx_builder.build(change_address=sender_address, merge_change=True) + tx_body = tx_builder.build( + change_address=sender_address, + merge_change=True, + ) expected = { 0: [[b"11111111111111111111111111111111", 3]], @@ -1116,7 +1135,10 @@ def test_tx_builder_merge_change_smaller_than_min_utxo(chain_context): tx_builder.add_input(utxo1) tx_builder.add_output(TransactionOutput.from_primitive([sender, 9800000])) - tx_body = tx_builder.build(change_address=sender_address, merge_change=True) + tx_body = tx_builder.build( + change_address=sender_address, + merge_change=True, + ) expected = { 0: [[b"11111111111111111111111111111111", 3]], diff --git a/test/pycardano/util.py b/test/pycardano/util.py index 24de8053..a691415c 100644 --- a/test/pycardano/util.py +++ b/test/pycardano/util.py @@ -90,7 +90,7 @@ def epoch(self) -> int: return 300 @property - def slot(self) -> int: + def last_block_slot(self) -> int: """Current slot number""" return 2000