From c41b5f415224d76c8dc268d35234910d2d4910bc Mon Sep 17 00:00:00 2001 From: SCM Date: Sat, 5 Oct 2024 10:06:12 +0100 Subject: [PATCH 1/7] initial changes --- pycardano/txbuilder.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index b2a7e854..98248964 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -158,6 +158,10 @@ class TransactionBuilder: _withdrawal_script_to_redeemers: List[Tuple[ScriptType, Optional[Redeemer]]] = ( 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,42 @@ 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. + + 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.CERT: + raise InvalidArgumentException( + f"Expect the redeemer tag's type to be {RedeemerTag.CERT}, " + f"but got {redeemer.tag} instead." + ) + redeemer.tag = RedeemerTag.CERT + 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` @@ -471,6 +511,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()) @@ -497,6 +540,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 +923,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 if self.mint: sorted_mint_policies = sorted(self.mint.keys(), key=lambda x: x.to_cbor()) From b3ad8e78ad43fe79d44045770e18dcb8bd2dcd22 Mon Sep 17 00:00:00 2001 From: SCM Date: Mon, 14 Oct 2024 11:34:32 +0100 Subject: [PATCH 2/7] CERT to CERTIFICATE --- pycardano/plutus.py | 2 +- pycardano/txbuilder.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 98248964..38587222 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -405,12 +405,12 @@ def add_certificate_script( TransactionBuilder: Current transaction builder. """ if redeemer: - if redeemer.tag is not None and redeemer.tag != RedeemerTag.CERT: + if redeemer.tag is not None and redeemer.tag != RedeemerTag.CERTIFICATE: raise InvalidArgumentException( - f"Expect the redeemer tag's type to be {RedeemerTag.CERT}, " + f"Expect the redeemer tag's type to be {RedeemerTag.CERTIFICATE}, " f"but got {redeemer.tag} instead." ) - redeemer.tag = RedeemerTag.CERT + redeemer.tag = RedeemerTag.CERTIFICATE self._consolidate_redeemer(redeemer) if isinstance(script, UTxO): From 3161ef91e045df461758357063e6df76fc37c0c8 Mon Sep 17 00:00:00 2001 From: SCM Date: Mon, 14 Oct 2024 15:36:02 +0100 Subject: [PATCH 3/7] automatically set redeemer index to last added certificate --- pycardano/txbuilder.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 38587222..2e90298d 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -396,6 +396,7 @@ def add_certificate_script( 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. @@ -410,6 +411,10 @@ def add_certificate_script( 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) @@ -924,7 +929,7 @@ 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 + # 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()) From bed9167d2b4307e39ad7dae56fd4ea3e54cfd2e4 Mon Sep 17 00:00:00 2001 From: SCM Date: Mon, 14 Oct 2024 16:15:57 +0100 Subject: [PATCH 4/7] formatting --- pycardano/txbuilder.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 2e90298d..f1f2ff50 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -158,7 +158,7 @@ class TransactionBuilder: _withdrawal_script_to_redeemers: List[Tuple[ScriptType, Optional[Redeemer]]] = ( field(init=False, default_factory=lambda: []) ) - + _certificate_script_to_redeemers: List[Tuple[ScriptType, Optional[Redeemer]]] = ( field(init=False, default_factory=lambda: []) ) @@ -396,7 +396,8 @@ def add_certificate_script( 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. + 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. @@ -411,9 +412,10 @@ def add_certificate_script( 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" + 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) @@ -516,7 +518,7 @@ 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 From 3753fb615a052f6f5852438a35a69b3a54f0a6f3 Mon Sep 17 00:00:00 2001 From: SCM Date: Sun, 20 Oct 2024 13:16:56 +0100 Subject: [PATCH 5/7] add test --- test/pycardano/test_txbuilder.py | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 2d0f9344..105ec305 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -1363,6 +1363,40 @@ 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_stake_pool_registration(chain_context, pool_params): tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])]) sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" From a944c2a1b3b1b0f44bd9571bc7007e84dc288523 Mon Sep 17 00:00:00 2001 From: SCM Date: Sun, 20 Oct 2024 14:27:28 +0100 Subject: [PATCH 6/7] more tests to bring coverage patch to 100% --- test/pycardano/test_txbuilder.py | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 105ec305..1fc50f7f 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -1397,6 +1397,54 @@ def test_tx_builder_certificates_script(chain_context): 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" From c95fb6149e60252db9ea2eafbe030d17ac800a07 Mon Sep 17 00:00:00 2001 From: SCM Date: Tue, 22 Oct 2024 13:16:49 +0100 Subject: [PATCH 7/7] Basic test --- .../pass_certifying_and_rewarding.plutus | 1 + integration-test/test/test_certificate.py | 2 +- .../test/test_certificate_script.py | 97 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 integration-test/plutus_scripts/pass_certifying_and_rewarding.plutus create mode 100644 integration-test/test/test_certificate_script.py 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) +#