diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 52a0f989..b2923593 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -250,24 +250,42 @@ def add_script_input( self._consolidate_redeemer(redeemer) self._inputs_to_redeemers[utxo] = redeemer + input_script_hash = utxo.output.address.payment_part + + # collect potential scripts to fulfill the input + candidate_scripts: List[ + Tuple[Union[NativeScript, PlutusV1Script, PlutusV2Script], Optional[UTxO]] + ] = [] if utxo.output.script: - self._inputs_to_scripts[utxo] = utxo.output.script - self.reference_inputs.add(utxo) - self._reference_scripts.append(utxo.output.script) + candidate_scripts.append((utxo.output.script, utxo)) elif not script: for i in self.context.utxos(utxo.output.address): if i.output.script: - self._inputs_to_scripts[utxo] = i.output.script - self.reference_inputs.add(i) - self._reference_scripts.append(i.output.script) - break + candidate_scripts.append((i.output.script, i)) elif isinstance(script, UTxO): - assert script.output.script is not None - self._inputs_to_scripts[utxo] = script.output.script - self.reference_inputs.add(script) - self._reference_scripts.append(script.output.script) + if script.output.script is None: + raise InvalidArgumentException( + f"Expect the output of the reference UTxO {utxo}" + " to have a script, but got None instead." + ) + candidate_scripts.append((script.output.script, script)) else: - self._inputs_to_scripts[utxo] = script + candidate_scripts.append((script, None)) + + found_valid_script = False + for candidate_script, candidate_utxo in candidate_scripts: + if script_hash(candidate_script) != input_script_hash: + continue + found_valid_script = True + self._inputs_to_scripts[utxo] = candidate_script + if candidate_utxo is not None: + self.reference_inputs.add(candidate_utxo) + self._reference_scripts.append(candidate_script) + if not found_valid_script: + raise InvalidArgumentException( + f"Cannot find a valid script to fulfill the input UTxO: {utxo.input}." + "Supplied scripts do not match the payment part of the input address." + ) self.inputs.append(utxo) return self diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 7ba70819..3363cb19 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -576,6 +576,32 @@ def test_add_script_input_no_script(chain_context): assert witness.plutus_v1_script is None +def test_add_script_input_payment_script(chain_context): + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + plutus_script = PlutusV1Script(b"dummy test script") + vk1 = VerificationKey.from_cbor( + "58206443a101bdb948366fc87369336224595d36d8b0eee5602cba8b81a024e58473" + ) + script_address = Address(vk1.hash()) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, + TransactionOutput(script_address, 10000000, datum_hash=datum.hash()), + ) + redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + datum=datum, + redeemer=redeemer, + script=plutus_script, + ) + + def test_add_script_input_find_script(chain_context): original_utxos = chain_context.utxos( "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" @@ -658,6 +684,158 @@ def test_add_script_input_with_script_from_specified_utxo(chain_context): assert [existing_script_utxo.input] == tx_body.reference_inputs +def test_add_script_input_incorrect_script(chain_context): + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + tx_in2 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 1] + ) + plutus_script = PlutusV1Script(b"dummy test script") + script_hash = plutus_script_hash(plutus_script) + incorrect_plutus_script = PlutusV2Script(b"dummy test script2") + script_address = Address(script_hash) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash()) + ) + mint = MultiAsset.from_primitive({script_hash.payload: {b"TestToken": 1}}) + UTxO( + tx_in2, + TransactionOutput( + script_address, Value(10000000, mint), datum_hash=datum.hash() + ), + ) + redeemer1 = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + script=incorrect_plutus_script, + datum=datum, + redeemer=redeemer1, + ) + + +def test_add_script_input_no_script_no_attached_script(chain_context): + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + plutus_script = PlutusV1Script(b"dummy test script") + script_hash = plutus_script_hash(plutus_script) + script_address = Address(script_hash) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, + TransactionOutput(script_address, 10000000, datum_hash=datum.hash()), + ) + redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + datum=datum, + redeemer=redeemer, + ) + + +def test_add_script_input_find_incorrect_script(chain_context): + original_utxos = chain_context.utxos( + "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + ) + with patch.object(chain_context, "utxos") as mock_utxos: + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + plutus_script = PlutusV1Script(b"dummy test script") + incorrect_plutus_script = PlutusV2Script(b"dummy test script2") + script_hash = plutus_script_hash(plutus_script) + script_address = Address(script_hash) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash()) + ) + + existing_script_utxo = UTxO( + TransactionInput.from_primitive( + [ + "41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", + 1, + ] + ), + TransactionOutput(script_address, 1234567, script=incorrect_plutus_script), + ) + + mock_utxos.return_value = original_utxos + [existing_script_utxo] + + redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + datum=datum, + redeemer=redeemer, + ) + + +def test_add_script_input_with_script_from_specified_utxo_with_incorrect_script( + chain_context, +): + tx_builder = TransactionBuilder(chain_context) + tx_in1 = TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ) + plutus_script = PlutusV2Script(b"dummy test script") + incorrect_plutus_script = PlutusV1Script(b"dummy test script2") + script_hash = plutus_script_hash(plutus_script) + script_address = Address(script_hash) + datum = PlutusData() + utxo1 = UTxO( + tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash()) + ) + + existing_script_utxo = UTxO( + TransactionInput.from_primitive( + [ + "41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", + 1, + ] + ), + TransactionOutput(script_address, 1234567, script=incorrect_plutus_script), + ) + + redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + script=existing_script_utxo, + datum=datum, + redeemer=redeemer, + ) + + existing_script_utxo = UTxO( + TransactionInput.from_primitive( + [ + "41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", + 1, + ] + ), + TransactionOutput(script_address, 1234567, script=None), + ) + pytest.raises( + InvalidArgumentException, + tx_builder.add_script_input, + utxo1, + script=existing_script_utxo, + datum=datum, + redeemer=redeemer, + ) + + def test_add_script_input_multiple_redeemers(chain_context): tx_builder = TransactionBuilder(chain_context) tx_in1 = TransactionInput.from_primitive(