Skip to content

Commit d250316

Browse files
authored
Merge pull request #444 from cffls/fix_duplicate_collateral
Fix duplicate collateral
2 parents 991803e + bd731b1 commit d250316

File tree

3 files changed

+69
-4
lines changed

3 files changed

+69
-4
lines changed

pycardano/serialization.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -461,14 +461,14 @@ def to_cbor_hex(self) -> str:
461461
return self.to_cbor().hex()
462462

463463
@classmethod
464-
def from_cbor(cls, payload: Union[str, bytes]) -> CBORSerializable:
464+
def from_cbor(cls: Type[CBORBase], payload: Union[str, bytes]) -> CBORBase:
465465
"""Restore a CBORSerializable object from a CBOR.
466466
467467
Args:
468468
payload (Union[str, bytes]): CBOR bytes or hex string to restore from.
469469
470470
Returns:
471-
CBORSerializable: Restored CBORSerializable object.
471+
CBORBase: Restored CBORSerializable object of the specific subclass type.
472472
473473
Examples:
474474

pycardano/txbuilder.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ class TransactionBuilder:
129129

130130
required_signers: Optional[List[VerificationKeyHash]] = field(default=None)
131131

132-
collaterals: List[UTxO] = field(default_factory=lambda: [])
132+
collaterals: NonEmptyOrderedSet[UTxO] = field(
133+
default_factory=lambda: NonEmptyOrderedSet[UTxO]()
134+
)
133135

134136
certificates: Optional[List[Certificate]] = field(default=None)
135137

@@ -870,7 +872,7 @@ def _required_signer_vkey_hashes(self) -> Set[VerificationKeyHash]:
870872

871873
def _input_vkey_hashes(self) -> Set[VerificationKeyHash]:
872874
results = set()
873-
for i in self.inputs + self.collaterals:
875+
for i in self.inputs + list(self.collaterals):
874876
if isinstance(i.output.address.payment_part, VerificationKeyHash):
875877
results.add(i.output.address.payment_part)
876878
return results
@@ -1526,6 +1528,7 @@ def _add_collateral_input(cur_total, candidate_inputs):
15261528
"SCRIPT"
15271529
)
15281530
and candidate.output.amount.coin > 2000000
1531+
and candidate not in self.collaterals
15291532
):
15301533
self.collaterals.append(candidate)
15311534
cur_total += candidate.output.amount

test/pycardano/test_txbuilder.py

+62
Original file line numberDiff line numberDiff line change
@@ -2264,3 +2264,65 @@ def test_burning_all_assets_under_single_policy(chain_context):
22642264

22652265
assert AssetName(b"AssetName3") not in multi_asset.get(policy_id_1, {})
22662266
assert AssetName(b"AseetName4") not in multi_asset.get(policy_id_1, {})
2267+
2268+
2269+
def test_collateral_no_duplicates(chain_context):
2270+
"""
2271+
Test that a UTxO explicitly added as input is not reused for collateral.
2272+
"""
2273+
# Setup: Define sender and a Plutus script for minting (requires collateral)
2274+
sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"
2275+
sender_address = Address.from_primitive(sender)
2276+
plutus_v2_script = PlutusV2Script(b"dummy mint script collateral reuse test")
2277+
policy_id = plutus_script_hash(plutus_v2_script)
2278+
redeemer = Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000))
2279+
2280+
input_utxo = UTxO(
2281+
TransactionInput(TransactionId.from_primitive("a" * 64), 0),
2282+
TransactionOutput(sender_address, Value(coin=2_800_000)),
2283+
)
2284+
collateral_utxo = UTxO(
2285+
TransactionInput(TransactionId.from_primitive("b" * 64), 1),
2286+
TransactionOutput(sender_address, Value(coin=3_000_000)),
2287+
)
2288+
2289+
with patch.object(chain_context, "utxos") as mock_utxos:
2290+
mock_utxos.return_value = [input_utxo, collateral_utxo]
2291+
2292+
builder = TransactionBuilder(chain_context)
2293+
2294+
builder.add_input(input_utxo)
2295+
builder.add_input_address(sender_address)
2296+
2297+
mint_amount = 1
2298+
builder.mint = MultiAsset.from_primitive(
2299+
{policy_id.payload: {b"TestCollateralToken": mint_amount}}
2300+
)
2301+
builder.add_minting_script(plutus_v2_script, redeemer)
2302+
2303+
output_value = Value(coin=1_000_000) # Send some ADA back
2304+
builder.add_output(TransactionOutput(sender_address, output_value))
2305+
2306+
tx_body = builder.build(change_address=sender_address)
2307+
2308+
assert input_utxo.input in tx_body.inputs
2309+
2310+
assert tx_body.collateral is not None
2311+
assert len(tx_body.collateral) > 0, "Collateral should have been selected"
2312+
2313+
assert (
2314+
collateral_utxo.input in tx_body.collateral
2315+
), "The designated collateral UTxO was not selected"
2316+
2317+
assert (
2318+
input_utxo.input in tx_body.collateral
2319+
), "The explicit input UTxO should be reused as collateral"
2320+
2321+
total_collateral_input = (
2322+
collateral_utxo.output.amount + input_utxo.output.amount
2323+
)
2324+
2325+
assert (
2326+
total_collateral_input
2327+
== Value(tx_body.total_collateral) + tx_body.collateral_return.amount
2328+
), "The total collateral input amount should match the sum of the selected UTxOs"

0 commit comments

Comments
 (0)