Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d6d07b3
Prepare scripts for governance call (work in progress).
andreibancioiu Oct 21, 2025
996c21b
Micro-refactor.
andreibancioiu Oct 22, 2025
5714f85
Sketch functions for voting via legacy delegation.
andreibancioiu Oct 22, 2025
ac6ceb3
Sketch script for voting via legacy delegation.
andreibancioiu Oct 22, 2025
66c4483
Improve voting (on-chain). A bit more robust.
andreibancioiu Oct 22, 2025
f5b3901
Fix voting for direct staking.
andreibancioiu Oct 22, 2025
4973bb5
Improve vote via legacy delegation.
andreibancioiu Oct 22, 2025
9678f67
Display previous votes etc.
andreibancioiu Oct 22, 2025
86378bc
Refactor, adjust getting previous votes.
andreibancioiu Oct 22, 2025
65c75b1
More robust flow.
andreibancioiu Oct 22, 2025
44c7c45
Get on-chain delegated votes, apply some checks when voting via legac…
andreibancioiu Oct 22, 2025
6f5feac
Simple report on governance (voting).
andreibancioiu Oct 22, 2025
c34aaab
Adjust readme.
andreibancioiu Oct 22, 2025
2ec932e
Rename files, cleanup etc.
andreibancioiu Oct 22, 2025
c3f68ba
Fix gas limit.
andreibancioiu Oct 22, 2025
9942b53
Add proofs for governance, for liquid staking (delegating votes).
andreibancioiu Oct 23, 2025
0d7bb39
Vote via liquid staking (work in progress).
andreibancioiu Oct 23, 2025
6b528f2
Adjust report etc. Refactoring.
andreibancioiu Oct 23, 2025
b907494
Fix function name.
andreibancioiu Oct 23, 2025
8e8d364
Update proofs for governance.
andreibancioiu Oct 24, 2025
cb556aa
Fix logic around getting past votes.
andreibancioiu Oct 27, 2025
420e6f3
Fix decoding.
andreibancioiu Oct 27, 2025
f3c391d
Fix getting timestamp etc.
andreibancioiu Oct 27, 2025
a4b2a58
Fix after self-review (refactoring).
andreibancioiu Oct 28, 2025
165442d
Update governance proofs (Hatom).
andreibancioiu Oct 29, 2025
f169fd2
Fix after review.
andreibancioiu Oct 31, 2025
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
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,28 @@ PYTHONPATH=. python3 ./wizard/prepare_custom_tokens.py --token=WEGLD-a28c59 --ne
PYTHONPATH=. python3 ./wizard/do_transfers.py --network=devnet --wallets=$WALLETS_CONFIG --infile=custom_transfers.json --receiver=${RECEIVER} --auth=$AUTH_REGISTRATION
```

## Vote on governance - outdated
## Governance: direct vote

```
export PROOFS="./proofs.json"
PYTHONPATH=. python3 ./wizard/vote_on_governance.py --network=devnet --wallets=$WALLETS_CONFIG --proofs=${PROOFS} --auth=$AUTH_REGISTRATION
PYTHONPATH=. python3 ./wizard/vote_directly.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce> --vote yes --auth=$AUTH_REGISTRATION
```

## Vote on on-chain governance
## Governance: delegated vote (via legacy delegation)

```
PYTHONPATH=. python3 ./wizard/vote_on_onchain_governance.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce> --vote <yes/no/abstain/veto> --auth=$AUTH_REGISTRATION
PYTHONPATH=. python3 ./wizard/vote_via_legacy_delegation.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce> --vote yes --auth=$AUTH_REGISTRATION
```

## Governance: delegated vote (via liquid staking contracts)

```
...
```

## Simple report on governance (voting)

```
PYTHONPATH=. python3 ./wizard/voting_report.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce>
```

## Guardians
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
requests>=2.32.0,<3.0.0
ledgercomm[hid]
rich==13.3.4
multiversx-sdk[ledger]==2.1.0
multiversx-sdk[ledger]==2.3.2
pyotp==2.9.0
4 changes: 4 additions & 0 deletions wizard/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Configuration:
explorer_url: str
legacy_delegation_contract: str
governance_contract: str
system_governance_contract: str
cosigner_url: str


Expand All @@ -44,6 +45,7 @@ class Configuration:
explorer_url="https://explorer.multiversx.com",
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt",
governance_contract="erd1qqqqqqqqqqqqqpgqfn2mu8l0dte34eqh6qtgmpjpxpkhunccrl4sy2sp07",
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
cosigner_url="https://tools.multiversx.com",
),
"devnet": Configuration(
Expand All @@ -54,6 +56,7 @@ class Configuration:
explorer_url="https://devnet-explorer.multiversx.com",
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep",
governance_contract="erd1qqqqqqqqqqqqqpgqahutnw3r4s95gxz4keecvlyyl3wlsu2mdthq06swcp",
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
cosigner_url="https://devnet-tools.multiversx.com"
),
"testnet": Configuration(
Expand All @@ -64,6 +67,7 @@ class Configuration:
explorer_url="https://testnet-explorer.multiversx.com",
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep",
governance_contract="erd1qqqqqqqqqqqqqpgqahutnw3r4s95gxz4keecvlyyl3wlsu2mdthq06swcp",
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
cosigner_url="https://testnet-tcs-api.multiversx.com"
),
}
1 change: 1 addition & 0 deletions wizard/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_CLAIM_REWARDS = 50
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_REWARDS = 10_000
MAX_NUM_CUSTOM_TOKENS_TO_FETCH = 10_000
MAX_NUM_EVENTS_TO_FETCH = 10_000
METACHAIN_ID = 4294967295
ONE_QUINTILLION = 1000000000000000000
CONTRACT_RESULTS_CODE_OK_ENCODED = "QDZmNmI="
Expand Down
124 changes: 122 additions & 2 deletions wizard/entrypoint.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
from datetime import datetime, timedelta, timezone
from multiprocessing.dummy import Pool
from typing import Any, Callable, Optional

Expand All @@ -8,7 +9,8 @@
NetworkProviderConfig, NetworkProviderError,
ProxyNetworkProvider, Token, TokenTransfer,
Transaction, TransactionOnNetwork, VoteType)
from multiversx_sdk.abi import BigUIntValue, BytesValue, U64Value
from multiversx_sdk.abi import (AddressValue, BigUIntValue, BytesValue,
StringValue, U64Value)
from rich import print

from wizard import ux
Expand All @@ -20,6 +22,7 @@
CONTRACT_RESULTS_CODE_OK_ENCODED, COSIGNER_SERVICE_ID,
COSIGNER_SIGN_TRANSACTIONS_RETRY_DELAY_IN_SECONDS,
DEFAULT_CHUNK_SIZE_OF_SEND_TRANSACTIONS, MAX_NUM_CUSTOM_TOKENS_TO_FETCH,
MAX_NUM_EVENTS_TO_FETCH,
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_CLAIM_REWARDS,
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_REWARDS, METACHAIN_ID,
NETWORK_PROVIDER_NUM_RETRIES, NETWORK_PROVIDER_TIMEOUT_SECONDS,
Expand All @@ -30,6 +33,7 @@
TRANSACTION_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS)
from wizard.currencies import is_native_currency
from wizard.errors import KnownError, TransientError
from wizard.governance import OnChainVote
from wizard.guardians import (AuthApp, AuthRegistrationEntry, CosignerClient,
GuardianData)
from wizard.rewards import ClaimableRewards, ReceivedRewards, RewardsType
Expand All @@ -44,7 +48,7 @@ def __init__(
configuration: Configuration,
use_gas_estimator: Optional[bool] = None,
gas_limit_multiplier: Optional[float] = None
) -> None:
) -> None:
self.configuration = configuration

self.network_entrypoint = NetworkEntrypoint(
Expand Down Expand Up @@ -285,8 +289,13 @@ def vote_on_governance(self, sender: AccountWrapper, proposal: int, choice: int,

return transaction

def get_voting_power_on_onchain_governance(self, voter: Address):
controller = self.network_entrypoint.create_governance_controller()
return controller.get_voting_power(voter)

def vote_on_onchain_governance(self, sender: AccountWrapper, proposal: int, vote: VoteType, gas_price: int) -> Transaction:
controller = self.network_entrypoint.create_governance_controller()

return controller.create_transaction_for_voting(
sender=sender.account,
nonce=sender.account.get_nonce_then_increment(),
Expand All @@ -296,6 +305,117 @@ def vote_on_onchain_governance(self, sender: AccountWrapper, proposal: int, vote
guardian=sender.guardian,
)

def get_voting_power_via_legacy_delegation(self, voter: Address) -> int:
legacy_delegation_contract = Address.new_from_bech32(self.configuration.legacy_delegation_contract)

controller = self.network_entrypoint.create_smart_contract_controller()
[power_encoded] = controller.query(
contract=legacy_delegation_contract,
function="getVotingPower",
arguments=[AddressValue.new_from_address(voter)],
)

power = BigUIntValue()
power.decode_top_level(power_encoded)
return power.value

def vote_via_legacy_delegation(self, sender: AccountWrapper, proposal: int, vote: VoteType, gas_price: int):
legacy_delegation_contract = Address.new_from_bech32(self.configuration.legacy_delegation_contract)

controller = self.network_entrypoint.create_smart_contract_controller()
transaction = controller.create_transaction_for_execute(
sender=sender.account,
nonce=sender.account.get_nonce_then_increment(),
contract=legacy_delegation_contract,
function="delegateVote",
arguments=[U64Value(proposal), StringValue(vote.value)],
# Gas estimator might not work, thus we hard-code a value here.
gas_limit=80_000_000,
gas_price=gas_price,
guardian=sender.guardian
)

return transaction

def get_onchain_direct_votes(self, voter: Address, proposal: int) -> list[OnChainVote]:
size = MAX_NUM_EVENTS_TO_FETCH
reasonably_recent_timestamp = int((datetime.now(timezone.utc) - timedelta(days=10)).timestamp())
contract = self.configuration.system_governance_contract

events: list[dict[str, Any]] = self.api_network_provider.do_get_generic(
f"events", {
"from": 0,
"size": size,
"identifier": "vote",
"address": voter.to_bech32(),
"after": reasonably_recent_timestamp
})

if len(events) == size:
print(f"\tRetrieved {size} events. [red]There could be more![/red]")

votes: list[OnChainVote] = []

for event in events:
topics = event.get("topics", [])

event_proposal_hex = topics[0]
event_proposal = int(event_proposal_hex, 16)
event_vote_type_hex = topics[1]
event_vote_type = VoteType(bytes.fromhex(event_vote_type_hex).decode())
event_log_address = event.get("logAddress", "")
event_timestamp = event.get("timestamp", 0)

if event_proposal != proposal:
continue
if event_log_address != contract:
continue

vote = OnChainVote(voter.to_bech32(), proposal, contract, event_timestamp, event_vote_type)
votes.append(vote)

return votes

def get_onchain_delegated_votes(self, proposal: int, contract: str) -> dict[str, list[OnChainVote]]:
size = MAX_NUM_EVENTS_TO_FETCH
reasonably_recent_timestamp = int((datetime.now(timezone.utc) - timedelta(days=10)).timestamp())

events: list[dict[str, Any]] = self.api_network_provider.do_get_generic(
f"events", {
"from": 0,
"size": size,
"identifier": "delegateVote",
"address": contract,
"after": reasonably_recent_timestamp
})

if len(events) == size:
print(f"\tRetrieved {size} events. [red]There could be more![/red]")

votes: list[OnChainVote] = []
votes_by_voter: dict[str, list[OnChainVote]] = {}

for event in events:
topics = event.get("topics", [])

event_proposal_hex = topics[0]
event_proposal = int(event_proposal_hex, 16)
event_vote_type_hex = topics[1]
event_vote_type = VoteType(bytes.fromhex(event_vote_type_hex).decode())
event_voter = Address.new_from_hex(topics[2])
event_timestamp = event.get("timestamp", 0)

if event_proposal != proposal:
continue

vote = OnChainVote(event_voter.to_bech32(), proposal, contract, event_timestamp, event_vote_type)
votes.append(vote)

for vote in votes:
votes_by_voter.setdefault(vote.voter, []).append(vote)

return votes_by_voter

def get_guardian_data(self, address: Address):
response = self.proxy_network_provider.do_get_generic(f"address/{address.to_bech32()}/guardian-data")
response_payload = response.get("guardianData", {})
Expand Down
11 changes: 10 additions & 1 deletion wizard/governance.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

from typing import Any

from multiversx_sdk import Address
from multiversx_sdk import Address, VoteType


class GovernanceRecord:
Expand All @@ -17,3 +17,12 @@ def new_from_dictionary(cls, data: dict[str, Any]):
proof = bytes.fromhex(data["proof"])

return cls(address, power, proof)


class OnChainVote:
def __init__(self, voter: str, proposal: int, contract: str, timestamp: int, vote_type: VoteType) -> None:
self.voter = voter
self.proposal = proposal
self.contract = contract
self.timestamp = timestamp
self.vote_type = vote_type
112 changes: 112 additions & 0 deletions wizard/vote_directly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import sys
import traceback
from argparse import ArgumentParser
from pathlib import Path
from typing import List

from multiversx_sdk import VoteType
from multiversx_sdk.gas_estimator.errors import GasLimitEstimationError
from multiversx_sdk.smart_contracts.errors import SmartContractQueryError
from rich import print

from wizard import errors, ux
from wizard.accounts import load_accounts
from wizard.configuration import CONFIGURATIONS
from wizard.constants import DEFAULT_GAS_PRICE
from wizard.entrypoint import MyEntrypoint
from wizard.guardians import AuthApp
from wizard.transactions import TransactionWrapper
from wizard.utils import format_time


def get_vote_type_from_args(choice: str) -> VoteType:
return {
"yes": VoteType.YES,
"no": VoteType.NO,
"abstain": VoteType.ABSTAIN,
"veto": VoteType.VETO
}[choice]


def main(cli_args: list[str] = sys.argv[1:]):
try:
_do_main(cli_args)
except errors.KnownError as err:
ux.show_critical_error(traceback.format_exc())
ux.show_critical_error(err.get_pretty())
return 1


def _do_main(cli_args: List[str]):
parser = ArgumentParser()
parser.add_argument("--network", choices=CONFIGURATIONS.keys(), required=True, help="network name")
parser.add_argument("--wallets", required=True, help="path to wallets configuration file")
parser.add_argument("--auth", required=True, help="auth registration file")
parser.add_argument("--gas-price", type=int, default=DEFAULT_GAS_PRICE, help="min gas price")
parser.add_argument("--proposal", type=int, required=True, help="proposal nonce / id")
parser.add_argument("--vote", choices=["yes", "no", "abstain", "veto"], required=True, help="vote choice")

args = parser.parse_args(cli_args)

network = args.network
configuration = CONFIGURATIONS[network]
entrypoint = MyEntrypoint(
configuration=configuration,
use_gas_estimator=True,
gas_limit_multiplier=1.1,
)

accounts_wrappers = load_accounts(Path(args.wallets))
auth_app = AuthApp.new_from_registration_file(Path(args.auth)) if args.auth else AuthApp([])

entrypoint.recall_nonces(accounts_wrappers)
entrypoint.recall_guardians(accounts_wrappers)

transactions_wrappers: List[TransactionWrapper] = []

proposal = args.proposal
vote = get_vote_type_from_args(args.vote)
gas_price = args.gas_price

ux.confirm_continuation(
f"Submit bulk votes on proposal [green]{proposal}[/green] with choice [green]{vote.value.upper()}[/green]?"
)

for account_wrapper in accounts_wrappers:
address = account_wrapper.account.address

print(f"[yellow]{account_wrapper.wallet_name}[/yellow]", address.to_bech32())

try:
voting_power = entrypoint.get_voting_power_on_onchain_governance(address)
if not voting_power:
print(f"\t[red]has no voting power[/red]")
continue

print(f"\t[blue]has voting power[/blue]", voting_power)

previous_votes = entrypoint.get_onchain_direct_votes(address, proposal)

for previous_vote in previous_votes:
print(f"\tprevious vote at {format_time(previous_vote.timestamp)}:", previous_vote.vote_type)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If voted previously, should be skipped. The transaction is going to fail on a second vote on the same proposal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


tx = entrypoint.vote_on_onchain_governance(
sender=account_wrapper,
proposal=proposal,
vote=vote,
gas_price=gas_price,
)

transactions_wrappers.append(TransactionWrapper(tx, account_wrapper.wallet_name))
except SmartContractQueryError as error:
print(f"\t[red]{error}[/red]")
except GasLimitEstimationError as error:
print(f"\t[red]{error.error}[/red]")

ux.confirm_continuation(f"Ready to send [green]{len(transactions_wrappers)}[/green] transaction(s)?")
entrypoint.send_multiple(auth_app, transactions_wrappers)
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Loading