Skip to content

Commit 03c74b2

Browse files
committed
Add 'add_minting_script' to txbuilder
1 parent b0d79c1 commit 03c74b2

File tree

5 files changed

+281
-13
lines changed

5 files changed

+281
-13
lines changed

integration-test/test/test_all.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,23 @@ def assert_output(self, target_address, target_output):
5757

5858
assert found, f"Cannot find target UTxO in address: {target_address}"
5959

60+
@retry(tries=4, delay=6, backoff=2, jitter=(1, 3))
61+
def fund(self, source_address, source_key, target_address, amount=5000000):
62+
builder = TransactionBuilder(self.chain_context)
63+
64+
builder.add_input_address(source_address)
65+
output = TransactionOutput(target_address, amount)
66+
builder.add_output(output)
67+
68+
signed_tx = builder.build_and_sign([source_key], source_address)
69+
70+
print("############### Transaction created ###############")
71+
print(signed_tx)
72+
print(signed_tx.to_cbor())
73+
print("############### Submitting transaction ###############")
74+
self.chain_context.submit_tx(signed_tx.to_cbor())
75+
self.assert_output(target_address, target_output=output)
76+
6077
@retry(tries=4, delay=6, backoff=2, jitter=(1, 3))
6178
def test_mint(self):
6279
address = Address(self.payment_vkey.hash(), network=self.NETWORK)
@@ -372,3 +389,94 @@ def test_stake_delegation(self):
372389
print(signed_tx.to_cbor())
373390
print("############### Submitting transaction ###############")
374391
self.chain_context.submit_tx(signed_tx.to_cbor())
392+
393+
@retry(tries=4, delay=6, backoff=2, jitter=(1, 3))
394+
def test_mint_nft_with_script(self):
395+
address = Address(self.payment_vkey.hash(), network=self.NETWORK)
396+
397+
with open("./plutus_scripts/fortytwo.plutus", "r") as f:
398+
script_hex = f.read()
399+
forty_two_script = cbor2.loads(bytes.fromhex(script_hex))
400+
401+
policy_id = plutus_script_hash(forty_two_script)
402+
403+
my_nft = MultiAsset.from_primitive(
404+
{
405+
policy_id.payload: {
406+
b"MY_SCRIPT_NFT_1": 1, # Name of our NFT1 # Quantity of this NFT
407+
b"MY_SCRIPT_NFT_2": 1, # Name of our NFT2 # Quantity of this NFT
408+
}
409+
}
410+
)
411+
412+
metadata = {
413+
721: {
414+
policy_id.payload.hex(): {
415+
"MY_SCRIPT_NFT_1": {
416+
"description": "This is my first NFT thanks to PyCardano",
417+
"name": "PyCardano NFT example token 1",
418+
"id": 1,
419+
"image": "ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw",
420+
},
421+
"MY_SCRIPT_NFT_2": {
422+
"description": "This is my second NFT thanks to PyCardano",
423+
"name": "PyCardano NFT example token 2",
424+
"id": 2,
425+
"image": "ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw",
426+
},
427+
}
428+
}
429+
}
430+
431+
# Place metadata in AuxiliaryData, the format acceptable by a transaction.
432+
auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(metadata)))
433+
434+
# Create a transaction builder
435+
builder = TransactionBuilder(self.chain_context)
436+
437+
# Add our own address as the input address
438+
builder.add_input_address(address)
439+
440+
# Add minting script with an empty datum and a minting redeemer
441+
builder.add_minting_script(
442+
forty_two_script, redeemer=Redeemer(RedeemerTag.MINT, 42)
443+
)
444+
445+
# Set nft we want to mint
446+
builder.mint = my_nft
447+
448+
# Set transaction metadata
449+
builder.auxiliary_data = auxiliary_data
450+
451+
# Calculate the minimum amount of lovelace that need to hold the NFT we are going to mint
452+
min_val = min_lovelace(Value(0, my_nft), self.chain_context)
453+
454+
# Send the NFT to our own address
455+
nft_output = TransactionOutput(address, Value(min_val, my_nft))
456+
builder.add_output(nft_output)
457+
458+
# Create a collateral
459+
self.fund(address, self.payment_skey, address)
460+
461+
non_nft_utxo = None
462+
for utxo in self.chain_context.utxos(str(address)):
463+
# multi_asset should be empty for collateral utxo
464+
if not utxo.output.amount.multi_asset:
465+
non_nft_utxo = utxo
466+
break
467+
468+
builder.collaterals.append(non_nft_utxo)
469+
470+
# Build and sign transaction
471+
signed_tx = builder.build_and_sign([self.payment_skey], address)
472+
# signed_tx.transaction_witness_set.plutus_data
473+
474+
print("############### Transaction created ###############")
475+
print(signed_tx)
476+
print(signed_tx.to_cbor())
477+
478+
# Submit signed transaction to the network
479+
print("############### Submitting transaction ###############")
480+
self.chain_context.submit_tx(signed_tx.to_cbor())
481+
482+
self.assert_output(address, nft_output)

pycardano/txbuilder.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from copy import copy, deepcopy
44
from dataclasses import dataclass, field, fields
5-
from typing import Dict, List, Optional, Set, Union
5+
from typing import Dict, List, Optional, Set, Tuple, Union
66

77
from pycardano.address import Address, AddressType
88
from pycardano.backend.base import ChainContext
@@ -115,6 +115,10 @@ class TransactionBuilder:
115115
init=False, default_factory=lambda: {}
116116
)
117117

118+
_minting_script_to_redeemers: List[Tuple[bytes, Redeemer]] = field(
119+
init=False, default_factory=lambda: []
120+
)
121+
118122
_inputs_to_scripts: Dict[UTxO, bytes] = field(
119123
init=False, default_factory=lambda: {}
120124
)
@@ -160,7 +164,11 @@ def _consolidate_redeemer(self, redeemer):
160164
redeemer.ex_units = ExecutionUnits(0, 0)
161165

162166
def add_script_input(
163-
self, utxo: UTxO, script: bytes, datum: Datum, redeemer: Redeemer
167+
self,
168+
utxo: UTxO,
169+
script: bytes,
170+
datum: Optional[Datum] = None,
171+
redeemer: Optional[Redeemer] = None,
164172
) -> TransactionBuilder:
165173
"""Add a script UTxO to transaction's inputs.
166174
@@ -178,18 +186,44 @@ def add_script_input(
178186
f"Expect the output address of utxo to be script type, "
179187
f"but got {utxo.output.address.address_type} instead."
180188
)
181-
if utxo.output.datum_hash != datum.hash():
189+
if utxo.output.datum_hash and utxo.output.datum_hash != datum_hash(datum):
182190
raise InvalidArgumentException(
183191
f"Datum hash in transaction output is {utxo.output.datum_hash}, "
184-
f"but actual datum hash from input datum is {datum.hash()}."
192+
f"but actual datum hash from input datum is {datum_hash(datum)}."
185193
)
186-
self.datums[datum.hash()] = datum
187-
self._consolidate_redeemer(redeemer)
188-
self._inputs_to_redeemers[utxo] = redeemer
194+
if datum:
195+
self.datums[datum_hash(datum)] = datum
196+
if redeemer:
197+
self._consolidate_redeemer(redeemer)
198+
self._inputs_to_redeemers[utxo] = redeemer
189199
self._inputs_to_scripts[utxo] = script
190200
self.inputs.append(utxo)
191201
return self
192202

203+
def add_minting_script(
204+
self,
205+
script: bytes,
206+
redeemer: Optional[Redeemer] = None,
207+
) -> TransactionBuilder:
208+
"""Add a minting script along with its datum and redeemer to this transaction.
209+
210+
Args:
211+
script (Optional[bytes]): A plutus script.
212+
redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO.
213+
214+
Returns:
215+
TransactionBuilder: Current transaction builder.
216+
"""
217+
if redeemer:
218+
if redeemer.tag != RedeemerTag.MINT:
219+
raise InvalidArgumentException(
220+
f"Expect the redeemer tag's type to be {RedeemerTag.MINT}, "
221+
f"but got {redeemer.tag} instead."
222+
)
223+
self._consolidate_redeemer(redeemer)
224+
self._minting_script_to_redeemers.append((script, redeemer))
225+
return self
226+
193227
def add_input_address(self, address: Union[Address, str]) -> TransactionBuilder:
194228
"""Add an address to transaction's input address.
195229
Unlike :meth:`add_input`, which deterministically adds a UTxO to the transaction's inputs, `add_input_address`
@@ -225,7 +259,7 @@ def add_output(
225259
tx_out.datum_hash = datum_hash(datum)
226260
self.outputs.append(tx_out)
227261
if add_datum_to_witness:
228-
self.datums[datum.hash()] = datum
262+
self.datums[datum_hash(datum)] = datum
229263
return self
230264

231265
@property
@@ -258,15 +292,20 @@ def fee(self, fee: int):
258292

259293
@property
260294
def scripts(self) -> List[bytes]:
261-
return list(set(self._inputs_to_scripts.values()))
295+
return list(
296+
set(self._inputs_to_scripts.values())
297+
| {s for s, _ in self._minting_script_to_redeemers}
298+
)
262299

263300
@property
264301
def datums(self) -> Dict[DatumHash, Datum]:
265302
return self._datums
266303

267304
@property
268305
def redeemers(self) -> List[Redeemer]:
269-
return list(self._inputs_to_redeemers.values())
306+
return list(self._inputs_to_redeemers.values()) + [
307+
r for _, r in self._minting_script_to_redeemers
308+
]
270309

271310
@property
272311
def script_data_hash(self) -> Optional[ScriptDataHash]:
@@ -585,6 +624,10 @@ def _set_redeemer_index(self):
585624
redeemer.index = sorted_mint_policies.index(
586625
plutus_script_hash(self._inputs_to_scripts[utxo])
587626
)
627+
628+
for script, redeemer in self._minting_script_to_redeemers:
629+
redeemer.index = sorted_mint_policies.index(plutus_script_hash(script))
630+
588631
self.redeemers.sort(key=lambda r: r.index)
589632

590633
def _build_tx_body(self) -> TransactionBody:

pycardano/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ def script_data_hash(
135135
cost_models = COST_MODELS
136136

137137
redeemer_bytes = cbor2.dumps(redeemers, default=default_encoder)
138-
datum_bytes = cbor2.dumps(datums, default=default_encoder)
138+
if datums:
139+
datum_bytes = cbor2.dumps(datums, default=default_encoder)
140+
else:
141+
datum_bytes = b""
139142
cost_models_bytes = cbor2.dumps(cost_models, default=default_encoder)
140143

141144
return ScriptDataHash(

test/pycardano/test_txbuilder.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pycardano.coinselection import RandomImproveMultiAsset
1010
from pycardano.exception import (
1111
InsufficientUTxOBalanceException,
12+
InvalidArgumentException,
1213
InvalidTransactionException,
1314
UTxOSelectionException,
1415
)
@@ -482,6 +483,112 @@ def test_add_script_input(chain_context):
482483
)
483484

484485

486+
def test_wrong_redeemer_execution_units(chain_context):
487+
tx_builder = TransactionBuilder(chain_context)
488+
tx_in1 = TransactionInput.from_primitive(
489+
["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0]
490+
)
491+
tx_in2 = TransactionInput.from_primitive(
492+
["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 1]
493+
)
494+
plutus_script = b"dummy test script"
495+
script_hash = plutus_script_hash(plutus_script)
496+
script_address = Address(script_hash)
497+
datum = PlutusData()
498+
utxo1 = UTxO(
499+
tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash())
500+
)
501+
mint = MultiAsset.from_primitive({script_hash.payload: {b"TestToken": 1}})
502+
utxo2 = UTxO(
503+
tx_in2,
504+
TransactionOutput(
505+
script_address, Value(10000000, mint), datum_hash=datum.hash()
506+
),
507+
)
508+
redeemer1 = Redeemer(RedeemerTag.SPEND, PlutusData())
509+
redeemer2 = Redeemer(RedeemerTag.MINT, PlutusData())
510+
redeemer3 = Redeemer(
511+
RedeemerTag.MINT, PlutusData(), ExecutionUnits(1000000, 1000000)
512+
)
513+
tx_builder.mint = mint
514+
tx_builder.add_script_input(utxo1, plutus_script, datum, redeemer1)
515+
tx_builder.add_script_input(utxo1, plutus_script, datum, redeemer2)
516+
with pytest.raises(InvalidArgumentException):
517+
tx_builder.add_script_input(utxo2, plutus_script, datum, redeemer3)
518+
519+
520+
def test_all_redeemer_should_provide_execution_units(chain_context):
521+
tx_builder = TransactionBuilder(chain_context)
522+
tx_in1 = TransactionInput.from_primitive(
523+
["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0]
524+
)
525+
tx_in2 = TransactionInput.from_primitive(
526+
["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 1]
527+
)
528+
plutus_script = b"dummy test script"
529+
script_hash = plutus_script_hash(plutus_script)
530+
script_address = Address(script_hash)
531+
datum = PlutusData()
532+
utxo1 = UTxO(
533+
tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash())
534+
)
535+
mint = MultiAsset.from_primitive({script_hash.payload: {b"TestToken": 1}})
536+
redeemer1 = Redeemer(
537+
RedeemerTag.SPEND, PlutusData(), ExecutionUnits(1000000, 1000000)
538+
)
539+
redeemer2 = Redeemer(RedeemerTag.MINT, PlutusData())
540+
tx_builder.mint = mint
541+
tx_builder.add_script_input(utxo1, plutus_script, datum, redeemer1)
542+
with pytest.raises(InvalidArgumentException):
543+
tx_builder.add_script_input(utxo1, plutus_script, datum, redeemer2)
544+
545+
546+
def test_add_minting_script(chain_context):
547+
tx_builder = TransactionBuilder(chain_context)
548+
tx_in1 = TransactionInput.from_primitive(
549+
["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0]
550+
)
551+
plutus_script = b"dummy test script"
552+
script_hash = plutus_script_hash(plutus_script)
553+
script_address = Address(script_hash)
554+
utxo1 = UTxO(tx_in1, TransactionOutput(script_address, 10000000))
555+
mint = MultiAsset.from_primitive({script_hash.payload: {b"TestToken": 1}})
556+
redeemer1 = Redeemer(
557+
RedeemerTag.MINT, PlutusData(), ExecutionUnits(1000000, 1000000)
558+
)
559+
tx_builder.mint = mint
560+
tx_builder.add_input(utxo1)
561+
tx_builder.add_minting_script(plutus_script, redeemer1)
562+
receiver = Address.from_primitive(
563+
"addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"
564+
)
565+
tx_builder.add_output(TransactionOutput(receiver, Value(5000000, mint)))
566+
tx_body = tx_builder.build(change_address=receiver)
567+
witness = tx_builder.build_witness_set()
568+
assert [plutus_script] == witness.plutus_script
569+
assert (
570+
"a5008182582018cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad2143159"
571+
"0f7e6643438ef00018282581d60f6532850e1bccee9c72a9113ad98bcc5dbb3"
572+
"0d2ac960262444f6e5f4821a004c4b40a1581c876f19078b059c928258d848c"
573+
"8cd871864d281eb6776ed7f80b68536a14954657374546f6b656e0182581d60"
574+
"f6532850e1bccee9c72a9113ad98bcc5dbb30d2ac960262444f6e5f41a0048d"
575+
"8f3021a0003724d09a1581c876f19078b059c928258d848c8cd871864d281eb"
576+
"6776ed7f80b68536a14954657374546f6b656e010b58205fcf68adc7eb6e507"
577+
"d15fb07d1c4e39d908bc9dfe642368afcddd881c5d46517" == tx_body.to_cbor()
578+
)
579+
580+
581+
def test_add_minting_script_wrong_redeemer_type(chain_context):
582+
tx_builder = TransactionBuilder(chain_context)
583+
plutus_script = b"dummy test script"
584+
redeemer1 = Redeemer(
585+
RedeemerTag.SPEND, PlutusData(), ExecutionUnits(1000000, 1000000)
586+
)
587+
588+
with pytest.raises(InvalidArgumentException):
589+
tx_builder.add_minting_script(plutus_script, redeemer1)
590+
591+
485592
def test_excluded_input(chain_context):
486593
tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])])
487594
sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"

test/pycardano/test_util.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,14 @@ def test_script_data_hash():
145145

146146
def test_script_data_hash_datum_only():
147147
unit = PlutusData()
148-
redeemers = []
149148
assert ScriptDataHash.from_primitive(
150149
"2f50ea2546f8ce020ca45bfcf2abeb02ff18af2283466f888ae489184b3d2d39"
151-
) == script_data_hash(redeemers=redeemers, datums=[unit])
150+
) == script_data_hash(redeemers=[], datums=[unit])
151+
152+
153+
def test_script_data_hash_redeemer_only():
154+
unit = PlutusData()
155+
redeemers = []
156+
assert ScriptDataHash.from_primitive(
157+
"a88fe2947b8d45d1f8b798e52174202579ecf847b8f17038c7398103df2d27b0"
158+
) == script_data_hash(redeemers=redeemers, datums=[])

0 commit comments

Comments
 (0)