Repository: Python-Cardano/pycardano

Issue:
Tx builder ends with the wrong collateral return amount
**Describe the bug**
Building a mint Tx that ends with a wrong collateral balance, with added native tokens.

The code I’m using to build the mint Tx is the following:

```py
def mint_badge(context, wallet_address: Address, validator, picked_utxo):
    builder = TransactionBuilder(context)
    builder.add_input_address(wallet_address)

    # Get the first wallet UTxOs to guarantee mint unicity
    builder.add_input(picked_utxo)

    # Mint the token
    policy_id = validator["hash"]
    mint = MultiAsset.from_primitive({policy_id: {b"": 1}})
    builder.mint = mint

    # Add the script to the witness set
    script = PlutusV3Script(validator["compiled_code"])
    builder.add_minting_script(script, Redeemer([]))

    # Build the tx
    tx_body = builder.build(change_address=wallet_address, auto_required_signers=False)
    tx_witness_set = builder.build_witness_set(True)
    return Transaction(tx_body, tx_witness_set)
```

Most of the time this works, but today it generated the following Tx for me (cbor)

```
84aa00d9010281825820e13ea17a7acef95585babfda6e621809ed7a2e81b5c95e463148ab16a1ce5ed6000181825839001d737b7461f723d962b5c47092d3ad6d15d6cd2956fe29ad42a6cf6670b46e985fa50328fcb3d80594b6c5c54974a08d2c766a47570bfa36821a0029188ca3581c2939dd4e122d5777bc1a8f6577e32a139dea8d24961230b5310f7cbea14001581c53c94b1a4a7a79eb71c45a17ddcb65207eea77fba087d43fccbd4035a14001581c9295b9e76efeb135a2d53b69f6bd907af9eef753f273596e0d319233a14001021a000318f2031a04bd941a081a04bd692209a1581c2939dd4e122d5777bc1a8f6577e32a139dea8d24961230b5310f7cbea140010b58203276ea13ff47fb5352d47c21329d17583751344bcac1e3752cfe3ee66be64e980dd9010281825820e13ea17a7acef95585babfda6e621809ed7a2e81b5c95e463148ab16a1ce5ed60010825839001d737b7461f723d962b5c47092d3ad6d15d6cd2956fe29ad42a6cf6670b46e985fa50328fcb3d80594b6c5c54974a08d2c766a47570bfa36821a002156bda2581c53c94b1a4a7a79eb71c45a17ddcb65207eea77fba087d43fccbd4035a14002581c9295b9e76efeb135a2d53b69f6bd907af9eef753f273596e0d319233a14002111a00370c3fa300d9010281825820699fcb42601d00a1735f7fd03c7e68adace25339a8444bcb384756b71fdd4a7e584066ea806021bfcccb630bc16508b55135b60fb35467b309de6b00f59fe5439dc20dcdc61fbc43359c648e11e1a1009788e31d01cb084b959e615442c52d464f0907d90102815901895901860101003229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cc0200092225980099b8748000c01cdd500144c8c966002601c0031325980099b880014800229462b30013370e002900144c8cc004004dd6180798061baa0052259800800c528456600266ebcc040c034dd51808000809c528c4cc008008c04400500b201c8a5040248048dd69806800c5900b1919800800992cc004cdc3a400460146ea8006297adef6c6089bab300e300b37540028048c8cc004004dd59807180798079807980798059baa0042259800800c5300103d87a8000899192cc004cdc8803000c56600266e3c018006266e95200033010300e0024bd7045300103d87a80004031133004004301200340306eb8c030004c03c00500d112cc004006297ae089980698059807000998010011807800a018375c601660106ea800a2c8030601000260066ea802229344d9590011300127d8799f5820e13ea17a7acef95585babfda6e621809ed7a2e81b5c95e463148ab16a1ce5ed600ff000105a1820100828082199e051a00d67925f5f6
```

As you can see in this screenshot from Eternl detailed Tx view, the collateral output contains more native tokens than the input being consumed

<img width="866" alt="Image" src="https://github.com/user-attachments/assets/b0829edc-b08a-4755-9c9c-9db92ff994dc" />

As a result, ogmios gave me the following error message.

```
ogmios.errors.ResponseError: Ogmios responded with error: {'jsonrpc': '2.0', 'method': 'submitTransaction', 'error': {'code': 3133, 'message': "One of the input provided as collateral carries something else than Ada tokens. Only Ada can be used as collateral. Since the Babbage era, you also have the option to set a 'collateral return' or 'collateral change' output in order to send the surplus non-Ada tokens to it. Regardless, the field 'data.unsuitableCollateralValue' indicates the actual collateral value found by the ledger", 'data': {'unsuitableCollateralValue': {'ada': {'lovelace': 2896254}, '53c94b1a4a7a79eb71c45a17ddcb65207eea77fba087d43fccbd4035': {'': 1}, '9295b9e76efeb135a2d53b69f6bd907af9eef753f273596e0d319233': {'': 1}}}}, 'id': None}
```

I’m using pycardano 0.13.2 on Macos.

Discussion:
@mpizenberg: It seems the builder selected the same utxo for the input and for the collateral in this case (which is possible). However, the collateral output appears to have double the amount of native tokens 

<img width="880" alt="Image" src="https://github.com/user-attachments/assets/dbf1438c-414e-4383-8df4-4b7ddc57cbed" />@cffls: Fixed in #444 .

Solution patch:
diff --git a/pycardano/serialization.py b/pycardano/serialization.py
index 63dced9..0c00dbe 100644
--- a/pycardano/serialization.py
+++ b/pycardano/serialization.py
@@ -461,14 +461,14 @@ class CBORSerializable:
         return self.to_cbor().hex()
 
     @classmethod
-    def from_cbor(cls, payload: Union[str, bytes]) -> CBORSerializable:
+    def from_cbor(cls: Type[CBORBase], payload: Union[str, bytes]) -> CBORBase:
         """Restore a CBORSerializable object from a CBOR.
 
         Args:
             payload (Union[str, bytes]): CBOR bytes or hex string to restore from.
 
         Returns:
-            CBORSerializable: Restored CBORSerializable object.
+            CBORBase: Restored CBORSerializable object of the specific subclass type.
 
         Examples:
 
diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py
index d3befc9..f767ba5 100644
--- a/pycardano/txbuilder.py
+++ b/pycardano/txbuilder.py
@@ -129,7 +129,9 @@ class TransactionBuilder:
 
     required_signers: Optional[List[VerificationKeyHash]] = field(default=None)
 
-    collaterals: List[UTxO] = field(default_factory=lambda: [])
+    collaterals: NonEmptyOrderedSet[UTxO] = field(
+        default_factory=lambda: NonEmptyOrderedSet[UTxO]()
+    )
 
     certificates: Optional[List[Certificate]] = field(default=None)
 
@@ -870,7 +872,7 @@ class TransactionBuilder:
 
     def _input_vkey_hashes(self) -> Set[VerificationKeyHash]:
         results = set()
-        for i in self.inputs + self.collaterals:
+        for i in self.inputs + list(self.collaterals):
             if isinstance(i.output.address.payment_part, VerificationKeyHash):
                 results.add(i.output.address.payment_part)
         return results
@@ -1526,6 +1528,7 @@ class TransactionBuilder:
                             "SCRIPT"
                         )
                         and candidate.output.amount.coin > 2000000
+                        and candidate not in self.collaterals
                     ):
                         self.collaterals.append(candidate)
                         cur_total += candidate.output.amount


Test patch:
diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py
index f31d038..b30ee79 100644
--- a/test/pycardano/test_txbuilder.py
+++ b/test/pycardano/test_txbuilder.py
@@ -2264,3 +2264,65 @@ def test_burning_all_assets_under_single_policy(chain_context):
 
         assert AssetName(b"AssetName3") not in multi_asset.get(policy_id_1, {})
         assert AssetName(b"AseetName4") not in multi_asset.get(policy_id_1, {})
+
+
+def test_collateral_no_duplicates(chain_context):
+    """
+    Test that a UTxO explicitly added as input is not reused for collateral.
+    """
+    # Setup: Define sender and a Plutus script for minting (requires collateral)
+    sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"
+    sender_address = Address.from_primitive(sender)
+    plutus_v2_script = PlutusV2Script(b"dummy mint script collateral reuse test")
+    policy_id = plutus_script_hash(plutus_v2_script)
+    redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000))
+
+    input_utxo = UTxO(
+        TransactionInput(TransactionId.from_primitive("a" * 64), 0),
+        TransactionOutput(sender_address, Value(coin=2_800_000)),
+    )
+    collateral_utxo = UTxO(
+        TransactionInput(TransactionId.from_primitive("b" * 64), 1),
+        TransactionOutput(sender_address, Value(coin=3_000_000)),
+    )
+
+    with patch.object(chain_context, "utxos") as mock_utxos:
+        mock_utxos.return_value = [input_utxo, collateral_utxo]
+
+        builder = TransactionBuilder(chain_context)
+
+        builder.add_input(input_utxo)
+        builder.add_input_address(sender_address)
+
+        mint_amount = 1
+        builder.mint = MultiAsset.from_primitive(
+            {policy_id.payload: {b"TestCollateralToken": mint_amount}}
+        )
+        builder.add_minting_script(plutus_v2_script, redeemer)
+
+        output_value = Value(coin=1_000_000)  # Send some ADA back
+        builder.add_output(TransactionOutput(sender_address, output_value))
+
+        tx_body = builder.build(change_address=sender_address)
+
+        assert input_utxo.input in tx_body.inputs
+
+        assert tx_body.collateral is not None
+        assert len(tx_body.collateral) > 0, "Collateral should have been selected"
+
+        assert (
+            collateral_utxo.input in tx_body.collateral
+        ), "The designated collateral UTxO was not selected"
+
+        assert (
+            input_utxo.input in tx_body.collateral
+        ), "The explicit input UTxO should be reused as collateral"
+
+        total_collateral_input = (
+            collateral_utxo.output.amount + input_utxo.output.amount
+        )
+
+        assert (
+            total_collateral_input
+            == Value(tx_body.total_collateral) + tx_body.collateral_return.amount
+        ), "The total collateral input amount should match the sum of the selected UTxOs"