Skip to content

Governance actions #425

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 27 additions & 26 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
alabaster==0.7.13 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
annotated-types==0.7.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
asn1crypto==1.5.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
attrs==24.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
babel==2.16.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
attrs==25.1.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
babel==2.17.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
black==24.8.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
blinker==1.8.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
blockfrost-python==0.6.0 ; python_full_version >= "3.8.1" and python_version < "4"
cachetools==5.5.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
cachetools==5.5.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
cardano-tools==2.1.0 ; python_full_version >= "3.8.1" and python_version < "4.0"
cbor2==5.6.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
certifi==2024.8.30 ; python_full_version >= "3.8.1" and python_version < "4"
cbor2==5.6.5 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
certifi==2025.1.31 ; python_full_version >= "3.8.1" and python_version < "4"
certvalidator==0.11.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
cffi==1.17.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
charset-normalizer==3.3.2 ; python_full_version >= "3.8.1" and python_version < "4"
click==8.1.7 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
charset-normalizer==3.4.1 ; python_full_version >= "3.8.1" and python_version < "4"
click==8.1.8 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
colorama==0.4.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and (sys_platform == "win32" or platform_system == "Windows")
coloredlogs==15.0.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
cose==0.9.dev8 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
coverage[toml]==7.6.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
cryptography==43.0.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
cryptography==43.0.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
decorator==5.1.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
docker==7.1.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
docutils==0.19 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
ecdsa==0.19.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
ecpy==1.2.5 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
exceptiongroup==1.2.2 ; python_full_version >= "3.8.1" and python_version < "3.11"
execnet==2.1.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
flake8==7.1.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
flake8==7.1.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
flask==2.3.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
frozendict==2.4.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
frozenlist==1.4.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
frozendict==2.4.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
frozenlist==1.5.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
humanfriendly==10.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
idna==3.10 ; python_full_version >= "3.8.1" and python_version < "4"
imagesize==1.4.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
importlib-metadata==8.5.0 ; python_full_version >= "3.8.1" and python_version < "3.10"
iniconfig==2.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
isort==5.13.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
itsdangerous==2.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
jinja2==3.1.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
jinja2==3.1.5 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
markupsafe==2.1.5 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
mccabe==0.7.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
mnemonic==0.21 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
mypy-extensions==1.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
mypy==1.11.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
ogmios==1.2.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
orjson==3.10.7 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
ogmios==1.3.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
orjson==3.10.15 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
oscrypto==1.3.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
packaging==24.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
packaging==24.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pathspec==0.12.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pexpect==4.9.0 ; python_full_version >= "3.8.1" and python_version < "4.0"
platformdirs==4.3.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
Expand All @@ -56,21 +56,21 @@ ptyprocess==0.7.0 ; python_full_version >= "3.8.1" and python_version < "4.0"
py==1.11.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pycodestyle==2.12.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pycparser==2.22 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pydantic-core==2.23.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pydantic==2.9.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pydantic-core==2.27.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pydantic==2.10.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pyflakes==3.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pygments==2.18.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pygments==2.19.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pynacl==1.5.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pyreadline3==3.5.4 ; sys_platform == "win32" and python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pytest-cov==5.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pytest-xdist==3.6.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pytest==8.3.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pytz==2024.2 ; python_full_version >= "3.8.1" and python_version < "3.9"
pywin32==306 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform == "win32"
pytest==8.3.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pytz==2025.1 ; python_full_version >= "3.8.1" and python_version < "3.9"
pywin32==308 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform == "win32"
requests==2.32.3 ; python_full_version >= "3.8.1" and python_version < "4"
retry==0.9.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
setuptools==75.1.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
six==1.16.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
setuptools==75.3.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
six==1.17.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
snowballstemmer==2.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
sphinx-copybutton==0.5.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
sphinx-rtd-theme==2.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
Expand All @@ -82,11 +82,12 @@ sphinxcontrib-jquery==4.1 ; python_full_version >= "3.8.1" and python_full_versi
sphinxcontrib-jsmath==1.0.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
sphinxcontrib-qthelp==1.0.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
sphinxcontrib-serializinghtml==1.1.5 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
tomli==2.0.1 ; python_full_version >= "3.8.1" and python_full_version <= "3.11.0a6"
typeguard==4.3.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
standard-imghdr==3.13.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
tomli==2.2.1 ; python_full_version >= "3.8.1" and python_full_version <= "3.11.0a6"
typeguard==4.4.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
typing-extensions==4.12.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
urllib3==2.2.3 ; python_full_version >= "3.8.1" and python_version < "4"
websocket-client==1.8.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
websockets==13.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
werkzeug==3.0.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
werkzeug==3.0.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
zipp==3.20.2 ; python_full_version >= "3.8.1" and python_version < "3.10"
7 changes: 7 additions & 0 deletions docs/source/api/pycardano.governance.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Governance
==============================

.. automodule:: pycardano.governance
:members:
:undoc-members:
:show-inheritance:
7 changes: 7 additions & 0 deletions docs/source/api/pycardano.poolparams.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Pool parameters
========================

.. automodule:: pycardano.pool_params
:members:
:undoc-members:
:show-inheritance:
2 changes: 2 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ making it a light-weight library that is easy and fast to set up in all kinds of
api/pycardano.crypto
api/pycardano.coinselection
api/pycardano.exception
api/pycardano.governance
api/pycardano.hash
api/pycardano.key
api/pycardano.metadata
api/pycardano.nativescript
api/pycardano.network
api/pycardano.plutus
api/pycardano.poolparams
api/pycardano.serialization
api/pycardano.transaction
api/pycardano.utils
Expand Down
3 changes: 1 addition & 2 deletions integration-test/configs/local-chang/conway-genesis.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,7 @@
"anchor": {
"url": "ipfs://QmQq5hWDNzvDR1ForEktAHrdCQmfSL2u5yctNpzDwoSBu4",
"dataHash": "23b43bebac48a4acc39e578715aa06635d6d900fa3ea7441dfffd6e43b914f7b"
},
"script": "edcd84c10e36ae810dc50847477083069db796219b39ccde790484e0"
}
},
"committee": {
"members": {
Expand Down
3 changes: 3 additions & 0 deletions integration-test/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ docker compose -f docker-compose-chang.yml up -d

export PAYMENT_KEY="$ROOT"/configs/local-chang/shelley/utxo-keys/utxo1.skey
export EXTENDED_PAYMENT_KEY="$ROOT"/keys/extended.skey
export POOL_COLD_KEY="$ROOT"/keys/pool/cold.skey
export POOL_PAYMENT_KEY="$ROOT"/keys/pool/payment.skey
export POOL_STAKE_KEY="$ROOT"/keys/pool/stake.skey
export POOL_ID=$(cat "$ROOT"/keys/pool/pool.id)

sleep 10
Expand Down
9 changes: 9 additions & 0 deletions integration-test/test/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class TestBase:

payment_key_path = os.environ.get("PAYMENT_KEY")
extended_key_path = os.environ.get("EXTENDED_PAYMENT_KEY")
pool_cold_key_path = os.environ.get("POOL_COLD_KEY")
pool_payment_key_path = os.environ.get("POOL_PAYMENT_KEY")
pool_stake_key_path = os.environ.get("POOL_STAKE_KEY")
if not payment_key_path or not extended_key_path:
raise Exception(
"Cannot find payment key. Please specify environment variable PAYMENT_KEY and extended_key_path"
Expand All @@ -44,6 +47,12 @@ class TestBase:
extended_payment_vkey = PaymentExtendedVerificationKey.from_signing_key(
extended_payment_skey
)
pool_cold_skey = PaymentSigningKey.load(pool_cold_key_path)
pool_cold_vkey = PaymentVerificationKey.from_signing_key(pool_cold_skey)
pool_payment_skey = PaymentSigningKey.load(pool_payment_key_path)
pool_payment_vkey = PaymentVerificationKey.from_signing_key(pool_payment_skey)
pool_stake_skey = StakeSigningKey.load(pool_stake_key_path)
pool_stake_vkey = StakeVerificationKey.from_signing_key(pool_stake_skey)

payment_key_pair = PaymentKeyPair.generate()
stake_key_pair = StakeKeyPair.generate()
Expand Down
10 changes: 5 additions & 5 deletions integration-test/test/test_certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ def test_stake_delegation(self):
stake_credential = StakeCredential(
self.stake_key_pair.verification_key.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)

drep = DRep(
DRepKind.VERIFICATION_KEY_HASH,
Expand Down Expand Up @@ -99,9 +97,11 @@ def test_stake_delegation(self):
keys=[stake_address.encode()]
)

stake_address_reward = rewards[stake_address.staking_part.payload.hex()][
"rewards"
]["ada"]["lovelace"]
stake_address_reward = 0
if stake_address.staking_part.payload.hex() in rewards:
stake_address_reward = rewards[stake_address.staking_part.payload.hex()][
"rewards"
]["ada"]["lovelace"]

builder.withdrawals = Withdrawals({bytes(stake_address): stake_address_reward})

Expand Down
185 changes: 185 additions & 0 deletions integration-test/test/test_governance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import os
import time

from retry import retry

from pycardano import *

from .base import TEST_RETRIES, TestBase


class TestGovernanceAction(TestBase):
@retry(tries=TEST_RETRIES, backoff=1.3, delay=2, jitter=(0, 10))
def test_governance_action_and_voting(self):
# Create new stake key pair
stake_key_pair = StakeKeyPair.generate()

# Create addresses for testing
address = Address(
self.payment_vkey.hash(),
stake_key_pair.verification_key.hash(),
self.NETWORK,
)

# Load pool cold key for signing
# pool_cold_skey = PaymentSigningKey.load(self.pool_cold_key_path)

# First, ensure we have enough funds
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, 110000000000))

signed_tx = builder.build_and_sign([self.payment_skey], giver_address)
print("############### Funding Transaction created ###############")
print(signed_tx)
print(signed_tx.to_cbor_hex())
print("############### Submitting funding transaction ###############")
self.chain_context.submit_tx(signed_tx)
time.sleep(5)

# Step 1: Register as a DRep first
drep_credential = DRepCredential(stake_key_pair.verification_key.hash())
anchor = Anchor(
url="https://test-drep.com",
data_hash=AnchorDataHash(bytes.fromhex("0" * 64)),
)

drep_registration = RegDRepCert(
drep_credential=drep_credential,
coin=500000000,
anchor=anchor,
)

stake_credential = StakeCredential(stake_key_pair.verification_key.hash())
pool_hash = PoolKeyHash(bytes.fromhex(os.environ.get("POOL_ID").strip()))

drep = DRep(
DRepKind.VERIFICATION_KEY_HASH,
stake_key_pair.verification_key.hash(),
)

all_in_one_cert = StakeRegistrationAndDelegationAndVoteDelegation(
stake_credential, pool_hash, drep, 1000000
)

# Create transaction for DRep registration
builder = TransactionBuilder(self.chain_context)
builder.add_input_address(address)
builder.add_output(TransactionOutput(address, 35000000))
builder.certificates = [drep_registration, all_in_one_cert]

signed_tx = builder.build_and_sign(
[stake_key_pair.signing_key, self.payment_skey],
address,
)
print("############### DRep Registration Transaction created ###############")
print(signed_tx)
print(signed_tx.to_cbor_hex())
print("############### Submitting DRep registration ###############")
self.chain_context.submit_tx(signed_tx)
time.sleep(5)

# Step 2: Create and submit parameter change action
param_update = ProtocolParamUpdate(
max_block_body_size=75536,
max_transaction_size=26384,
)

parameter_change_action = ParameterChangeAction(None, param_update, None)

# Create transaction for parameter change
builder = TransactionBuilder(self.chain_context)
builder.add_input_address(address)
builder.add_output(TransactionOutput(address, 35000000))
reward_account = Address(
staking_part=stake_key_pair.verification_key.hash(), network=self.NETWORK
)
builder.add_proposal(
100000000000,
bytes(reward_account),
parameter_change_action,
Anchor(
url="https://test-param-update.com",
data_hash=AnchorDataHash(bytes.fromhex("0" * 64)),
),
)

# Sign with both payment key and pool cold key for governance action
signed_tx = builder.build_and_sign(
[self.payment_skey, stake_key_pair.signing_key],
address,
)
print("############### Gov Action Transaction created ###############")
print(signed_tx)
print(signed_tx.to_cbor_hex())
print("############### Submitting gov action transaction ###############")
self.chain_context.submit_tx(signed_tx)
time.sleep(5)

# Get the governance action ID from the transaction
gov_action_id = GovActionId(
transaction_id=signed_tx.id,
gov_action_index=0, # First governance action in the transaction
)

# Step 3: Vote for the action as a DRep
drep_voter = Voter(
credential=stake_key_pair.verification_key.hash(),
voter_type=VoterType.DREP,
)

# Step 4: Vote for the action as a stake pool
pool_id = os.environ.get("POOL_ID").strip()

# Create transaction for voting
builder = TransactionBuilder(self.chain_context)
builder.add_input_address(address)
builder.add_output(TransactionOutput(address, 35000000))

# Add DRep vote using the helper method
builder.add_vote(
voter=drep_voter,
gov_action_id=gov_action_id,
vote=Vote.YES,
anchor=Anchor(
url="https://test-drep.com",
data_hash=AnchorDataHash(bytes.fromhex("0" * 64)),
),
)

# Add pool vote using the helper method
pool_voter = Voter(
credential=VerificationKeyHash(bytes.fromhex(pool_id)),
voter_type=VoterType.STAKING_POOL,
)
builder.add_vote(
voter=pool_voter,
gov_action_id=gov_action_id,
vote=Vote.YES,
anchor=Anchor(
url="https://test-pool.com",
data_hash=AnchorDataHash(bytes.fromhex("0" * 64)),
),
)

# Sign with all required keys
signed_tx = builder.build_and_sign(
[
stake_key_pair.signing_key,
self.payment_skey,
self.pool_cold_skey,
],
address,
)
print("############### Voting Transaction created ###############")
print(signed_tx)
print(signed_tx.to_cbor_hex())
print("############### Submitting voting transaction ###############")
self.chain_context.submit_tx(signed_tx)

print("############### Test completed successfully ###############")
Loading
Loading