diff --git a/integration-test/plutus_scripts/pass_certifying_and_rewarding.plutus b/integration-test/plutus_scripts/pass_certifying_and_rewarding.plutus new file mode 100644 index 00000000..806ac42b --- /dev/null +++ b/integration-test/plutus_scripts/pass_certifying_and_rewarding.plutus @@ -0,0 +1 @@ +5901e9010000323232323232323232323232322232323232323232323232323374a90001bb1498c8c8ccccd400c01001401840084004030030488888c8c8c94ccd5cd19180d88009980c180aa8012400c2930992999ab9a32301c100133019301650034801052615333573464603820029404c00452613263357389201136e6f7420612076616c696420707572706f7365004988c00852624984004c0454004405840584c98cd5ce2481104e616d654572726f723a207e626f6f6c004984c98cd5ce2481144e616d654572726f723a2076616c696461746f72004984c98cd5ce2481124e616d654572726f723a20707572706f7365004984c98cd5ce2481104e616d654572726f723a20646174756d004984c98cd5ce2481124e616d654572726f723a20636f6e74657874004984c98cd5ce2481144e616d654572726f723a20526577617264696e67004984c98cd5ce2481154e616d654572726f723a2043657274696679696e67004980080088c010c0200048c0140040108c018c94ccd55cf800899319ab9c49010a496e6465784572726f72004984d5d1000800919000a80091aab9d3754002e1c8d55cf1baa00123253335573e002264c66ae712410a496e6465784572726f72004984d5d0800800919ba548018cd5d028009bb14988cdd2a400866ae814004dd8a4c1 \ No newline at end of file diff --git a/integration-test/test/test_certificate.py b/integration-test/test/test_certificate.py index cb6cdcd1..a16d4985 100644 --- a/integration-test/test/test_certificate.py +++ b/integration-test/test/test_certificate.py @@ -25,7 +25,7 @@ def test_stake_delegation(self): builder = TransactionBuilder(self.chain_context) builder.add_input_address(giver_address) - builder.add_output(TransactionOutput(address, 440000000000)) + builder.add_output(TransactionOutput(address, 44000000000)) signed_tx = builder.build_and_sign([self.payment_skey], giver_address) diff --git a/integration-test/test/test_certificate_script.py b/integration-test/test/test_certificate_script.py new file mode 100644 index 00000000..b4ea1c73 --- /dev/null +++ b/integration-test/test/test_certificate_script.py @@ -0,0 +1,97 @@ +import os +import time + +import cbor2 +from retry import retry + +from pycardano import * + +from .base import TEST_RETRIES, TestBase + + +class TestDelegation(TestBase): + @retry(tries=TEST_RETRIES, backoff=1.5, delay=6, jitter=(0, 4)) + def test_stake_delegation(self): + with open("./plutus_scripts/pass_certifying_and_rewarding.plutus", "r") as f: + script_hex = f.read() + stake_script = PlutusV2Script(bytes.fromhex(script_hex)) + cert_script_hash = plutus_script_hash(stake_script) + address = Address( + self.payment_key_pair.verification_key.hash(), + cert_script_hash, + self.NETWORK, + ) + + utxos = self.chain_context.utxos(address) + + if not utxos: + giver_address = Address(self.payment_vkey.hash(), network=self.NETWORK) + + builder = TransactionBuilder(self.chain_context) + + builder.add_input_address(giver_address) + builder.add_output(TransactionOutput(address, 44000000000)) + + signed_tx = builder.build_and_sign([self.payment_skey], giver_address) + + print("############### Transaction created ###############") + print(signed_tx) + print(signed_tx.to_cbor_hex()) + print("############### Submitting transaction ###############") + self.chain_context.submit_tx(signed_tx) + + time.sleep(3) + + stake_credential = StakeCredential(cert_script_hash) + stake_registration = StakeRegistration(stake_credential) + pool_hash = PoolKeyHash(bytes.fromhex(os.environ.get("POOL_ID").strip())) + stake_delegation = StakeDelegation(stake_credential, pool_keyhash=pool_hash) + + builder = TransactionBuilder(self.chain_context) + + builder.add_input_address(address) + builder.add_output(TransactionOutput(address, 35000000)) + builder.certificates = [stake_registration, stake_delegation] + redeemer = Redeemer(0) + builder.add_certificate_script(stake_script, redeemer=redeemer) + + signed_tx = builder.build_and_sign( + [self.payment_key_pair.signing_key], + address, + ) + + print("############### Transaction created ###############") + print(signed_tx) + print(signed_tx.to_cbor_hex()) + print("############### Submitting transaction ###############") + self.chain_context.submit_tx(signed_tx) + + +# time.sleep(8) +# +# builder = TransactionBuilder(self.chain_context) +# +# builder.add_input_address(address) +# +# stake_address = Address( +# staking_part=cert_script_hash, +# network=self.NETWORK, +# ) +# +# builder.withdrawals = Withdrawals({bytes(stake_address): 0}) +# +# builder.add_output(TransactionOutput(address, 1000000)) +# redeemer = Redeemer(0) +# builder.add_withdrawal_script(stake_script, redeemer=redeemer) +# +# signed_tx = builder.build_and_sign( +# [self.payment_key_pair.signing_key], +# address, +# ) +# +# print("############### Transaction created ###############") +# print(signed_tx) +# print(signed_tx.to_cbor_hex()) +# print("############### Submitting transaction ###############") +# self.chain_context.submit_tx(signed_tx) +# diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 87172c63..1f32f713 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -945,7 +945,7 @@ class RedeemerTag(CBORSerializable, Enum): SPEND = 0 MINT = 1 - CERT = 2 + CERTIFICATE = 2 WITHDRAWAL = 3 def to_primitive(self) -> int: diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index b2a7e854..f1f2ff50 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -159,6 +159,10 @@ class TransactionBuilder: field(init=False, default_factory=lambda: []) ) + _certificate_script_to_redeemers: List[Tuple[ScriptType, Optional[Redeemer]]] = ( + field(init=False, default_factory=lambda: []) + ) + _inputs_to_scripts: Dict[UTxO, ScriptType] = field( init=False, default_factory=lambda: {} ) @@ -384,6 +388,49 @@ def add_withdrawal_script( self._withdrawal_script_to_redeemers.append((script, redeemer)) return self + def add_certificate_script( + self, + script: Union[ + UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script + ], + redeemer: Optional[Redeemer] = None, + ) -> TransactionBuilder: + """Add a certificate script along with its redeemer to this transaction. + WARNING: The order of operations matters. + The index of the redeemer will be set to the index of the last certificate added. + + Args: + script (Union[UTxO, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script. + redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO. + + Returns: + TransactionBuilder: Current transaction builder. + """ + if redeemer: + if redeemer.tag is not None and redeemer.tag != RedeemerTag.CERTIFICATE: + raise InvalidArgumentException( + f"Expect the redeemer tag's type to be {RedeemerTag.CERTIFICATE}, " + f"but got {redeemer.tag} instead." + ) + assert self.certificates is not None and len(self.certificates) >= 1, ( + "self.certificates is None. redeemer.index needs to be set to the index of the corresponding" + "certificate (defaulting to the last certificate) however no certificates could be found" + ) + redeemer.index = len(self.certificates) - 1 + redeemer.tag = RedeemerTag.CERTIFICATE + self._consolidate_redeemer(redeemer) + + if isinstance(script, UTxO): + assert script.output.script is not None + self._certificate_script_to_redeemers.append( + (script.output.script, redeemer) + ) + self.reference_inputs.add(script) + self._reference_scripts.append(script.output.script) + else: + self._certificate_script_to_redeemers.append((script, redeemer)) + return self + def add_input_address(self, address: Union[Address, str]) -> TransactionBuilder: """Add an address to transaction's input address. Unlike :meth:`add_input`, which deterministically adds a UTxO to the transaction's inputs, `add_input_address` @@ -472,6 +519,9 @@ def all_scripts(self) -> List[ScriptType]: for s, _ in self._withdrawal_script_to_redeemers: scripts[script_hash(s)] = s + for s, _ in self._certificate_script_to_redeemers: + scripts[script_hash(s)] = s + return list(scripts.values()) @property @@ -497,6 +547,7 @@ def _redeemer_list(self) -> List[Redeemer]: [r for r in self._inputs_to_redeemers.values() if r is not None] + [r for _, r in self._minting_script_to_redeemers if r is not None] + [r for _, r in self._withdrawal_script_to_redeemers if r is not None] + + [r for _, r in self._certificate_script_to_redeemers if r is not None] ) def redeemers(self) -> Redeemers: @@ -879,6 +930,8 @@ def _dfs(script: NativeScript): def _set_redeemer_index(self): # Set redeemers' index according to section 4.1 in # https://hydra.iohk.io/build/13099856/download/1/alonzo-changes.pdf + # + # There is no way to determine certificate index here if self.mint: sorted_mint_policies = sorted(self.mint.keys(), key=lambda x: x.to_cbor()) diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 2d0f9344..1fc50f7f 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -1363,6 +1363,88 @@ def test_tx_builder_certificates(chain_context): assert expected == tx_body.to_primitive() +def test_tx_builder_certificates_script(chain_context): + tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])]) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + sender_address = Address.from_primitive(sender) + + plutus_script = PlutusV2Script(b"dummy test script") + script_hash = plutus_script_hash(plutus_script) + + stake_credential = StakeCredential(script_hash) + + pool_hash = PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE) + + stake_registration = StakeRegistration(stake_credential) + + stake_delegation = StakeDelegation(stake_credential, pool_hash) + + # Add sender address as input + tx_builder.add_input_address(sender).add_output( + TransactionOutput.from_primitive([sender, 500000]) + ) + + tx_builder.certificates = [stake_registration, stake_delegation] + redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000)) + tx_builder.add_certificate_script(plutus_script, redeemer=redeemer) + tx_builder.ttl = 123456 + + tx_builder.build(change_address=sender_address) + tx_builder.use_redeemer_map = False + witness = tx_builder.build_witness_set() + assert [redeemer] == witness.redeemer + assert witness.redeemer[0].index == 1 + assert [plutus_script] == witness.plutus_v2_script + + +def test_tx_builder_cert_redeemer_wrong_tag(chain_context): + tx_builder = TransactionBuilder(chain_context) + plutus_script = PlutusV2Script(b"dummy test script") + redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000)) + redeemer.tag = RedeemerTag.MINT + with pytest.raises(InvalidArgumentException) as e: + tx_builder.add_certificate_script(plutus_script, redeemer=redeemer) + + +def test_add_cert_script_from_utxo(chain_context): + tx_builder = TransactionBuilder(chain_context) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + sender_address = Address.from_primitive(sender) + plutus_script = PlutusV2Script(b"dummy test script") + script_hash = plutus_script_hash(plutus_script) + script_address = Address(script_hash) + existing_script_utxo = UTxO( + TransactionInput.from_primitive( + [ + "41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", + 1, + ] + ), + TransactionOutput(script_address, 1234567, script=plutus_script), + ) + + stake_credential = StakeCredential(script_hash) + pool_hash = PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE) + stake_registration = StakeRegistration(stake_credential) + stake_delegation = StakeDelegation(stake_credential, pool_hash) + tx_builder.certificates = [stake_registration, stake_delegation] + tx_builder.add_input_address(sender).add_output( + TransactionOutput.from_primitive([sender, 500000]) + ) + + redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000)) + tx_builder.add_certificate_script(existing_script_utxo, redeemer=redeemer) + tx_builder.ttl = 123456 + + tx_body = tx_builder.build(change_address=sender_address) + tx_builder.use_redeemer_map = False + witness = tx_builder.build_witness_set() + assert witness.plutus_data is None + assert [redeemer] == witness.redeemer + assert witness.plutus_v2_script is None + assert [existing_script_utxo.input] == tx_body.reference_inputs + + def test_tx_builder_stake_pool_registration(chain_context, pool_params): tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])]) sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"