Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 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
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,30 @@ 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)

```
export CONTRACT=...

PYTHONPATH=. python3 ./wizard/vote_via_liquid_staking.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce> --vote yes --auth=$AUTH_REGISTRATION --contract=$CONTRACT
```

## Simple report on governance (voting)

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

## Guardians
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

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
16 changes: 10 additions & 6 deletions wizard/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ class Configuration:
deep_history_url: str
explorer_url: str
legacy_delegation_contract: str
governance_contract: str
system_governance_contract: str
cosigner_url: str
liquid_staking_contracts: list[str]


CONFIGURATIONS = {
Expand All @@ -43,8 +44,9 @@ class Configuration:
deep_history_url=ENV_MAINNET_DEEP_HISTORY_URL or DEFAULT_MAINNET_DEEP_HISTORY_URL,
explorer_url="https://explorer.multiversx.com",
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt",
governance_contract="erd1qqqqqqqqqqqqqpgqfn2mu8l0dte34eqh6qtgmpjpxpkhunccrl4sy2sp07",
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
cosigner_url="https://tools.multiversx.com",
liquid_staking_contracts=["erd1qqqqqqqqqqqqqpgq2khda0rx207gvlqg92dq5rh0z03a8dqf78ssu0qlcc", "erd1qqqqqqqqqqqqqpgqdnpmeseu3j5t7grds9dfj8ttt70pev66ah0sydkq9x"]
),
"devnet": Configuration(
chain_id="D",
Expand All @@ -53,8 +55,9 @@ class Configuration:
deep_history_url=ENV_DEVNET_DEEP_HISTORY_URL or DEFAULT_DEVNET_DEEP_HISTORY_URL,
explorer_url="https://devnet-explorer.multiversx.com",
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep",
governance_contract="erd1qqqqqqqqqqqqqpgqahutnw3r4s95gxz4keecvlyyl3wlsu2mdthq06swcp",
cosigner_url="https://devnet-tools.multiversx.com"
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
cosigner_url="https://devnet-tools.multiversx.com",
liquid_staking_contracts=["erd1qqqqqqqqqqqqqpgqlavy2909f0pa9yf66es5cwh53m0wue28u7hs79g2m2"],
),
"testnet": Configuration(
chain_id="T",
Expand All @@ -63,7 +66,8 @@ class Configuration:
deep_history_url=ENV_TESTNET_DEEP_HISTORY_URL or DEFAULT_TESTNET_DEEP_HISTORY_URL,
explorer_url="https://testnet-explorer.multiversx.com",
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep",
governance_contract="erd1qqqqqqqqqqqqqpgqahutnw3r4s95gxz4keecvlyyl3wlsu2mdthq06swcp",
cosigner_url="https://testnet-tcs-api.multiversx.com"
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
cosigner_url="https://testnet-tcs-api.multiversx.com",
liquid_staking_contracts=[],
),
}
1 change: 1 addition & 0 deletions wizard/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
TRANSACTION_AWAITING_PATIENCE_IN_MILLISECONDS = 8000
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_CLAIM_REWARDS = 50
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_REWARDS = 10_000
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_VOTE = 10
MAX_NUM_CUSTOM_TOKENS_TO_FETCH = 10_000
METACHAIN_ID = 4294967295
ONE_QUINTILLION = 1000000000000000000
Expand Down
138 changes: 119 additions & 19 deletions wizard/entrypoint.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import base64
import time
from datetime import datetime, timedelta, timezone
from multiprocessing.dummy import Pool
from typing import Any, Callable, Optional

Expand All @@ -8,7 +10,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 @@ -21,7 +24,8 @@
COSIGNER_SIGN_TRANSACTIONS_RETRY_DELAY_IN_SECONDS,
DEFAULT_CHUNK_SIZE_OF_SEND_TRANSACTIONS, MAX_NUM_CUSTOM_TOKENS_TO_FETCH,
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_CLAIM_REWARDS,
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_REWARDS, METACHAIN_ID,
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_REWARDS,
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_VOTE, METACHAIN_ID,
NETWORK_PROVIDER_NUM_RETRIES, NETWORK_PROVIDER_TIMEOUT_SECONDS,
NETWORK_PROVIDERS_RETRY_DELAY_IN_SECONDS,
NUM_PARALLEL_GET_GUARDIAN_DATA_REQUESTS, NUM_PARALLEL_GET_NONCE_REQUESTS,
Expand All @@ -30,6 +34,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 +49,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 @@ -119,12 +124,16 @@ def get_claimable_rewards_legacy(self, delegator: Address) -> int:
return int(amount)

def recall_nonces(self, accounts_wrappers: list[AccountWrapper]):
print("Recalling nonces...")

def recall_nonce(wrapper: AccountWrapper):
wrapper.account.nonce = self.network_entrypoint.recall_account_nonce(wrapper.account.address)

Pool(NUM_PARALLEL_GET_NONCE_REQUESTS).map(recall_nonce, accounts_wrappers)

def recall_guardians(self, accounts: list[AccountWrapper]):
print("Recalling guardians...")

def recall_guardian(wrapper: AccountWrapper):
guardian_data = self.get_guardian_data(wrapper.account.address)
wrapper.guardian = Address.new_from_bech32(guardian_data.active_guardian) if guardian_data.is_guarded else None
Expand Down Expand Up @@ -263,19 +272,67 @@ def transfer_funds(self, sender: AccountWrapper, receiver: Address, transfer: To
guardian=sender.guardian
)

def vote_on_governance(self, sender: AccountWrapper, proposal: int, choice: int, power: int, proof: bytes, gas_price: int) -> Transaction:
governance_contract = Address.new_from_bech32(self.configuration.governance_contract)
def get_direct_voting_power(self, voter: Address):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe we should move some of the functions below to sdk-py? CC: @popenta

Copy link
Contributor

Choose a reason for hiding this comment

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

will think about it, but I am not sure it's necessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

actually, it might be a good idea :)

controller = self.network_entrypoint.create_governance_controller()
return controller.get_voting_power(voter)

def vote_directly(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(),
proposal_nonce=proposal,
vote=vote,
gas_price=gas_price,
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=75_000_000,
gas_price=gas_price,
guardian=sender.guardian
)

return transaction

def vote_via_liquid_staking(self, sender: AccountWrapper, contract: str, proposal: int, vote: VoteType, power: int, proof: bytes, gas_price: int) -> Transaction:
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=governance_contract,
gas_limit=50_000_000,
function="vote",
contract=Address.new_from_bech32(contract),
# Gas estimator might not work, thus we hard-code a value here.
gas_limit=100_000_000,
function="delegate_vote",
arguments=[
U64Value(proposal),
U64Value(choice),
StringValue(vote.value),
BigUIntValue(power),
BytesValue(proof)
],
Expand All @@ -285,16 +342,59 @@ def vote_on_governance(self, sender: AccountWrapper, proposal: int, choice: int,

return transaction

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(),
proposal_nonce=proposal,
vote=vote,
gas_price=gas_price,
guardian=sender.guardian,
)
def get_direct_vote(self, voter: Address, proposal: int) -> Optional[OnChainVote]:
return self._get_past_vote(voter.to_bech32(), self.configuration.system_governance_contract, "vote", "vote", proposal)

def get_vote_via_legacy_delegation(self, voter: Address, proposal: int) -> Optional[OnChainVote]:
return self._get_past_vote(voter.to_bech32(), self.configuration.legacy_delegation_contract, "delegateVote", "delegateVote", proposal)

def get_vote_via_liquid_staking(self, voter: Address, contract: str, proposal: int) -> Optional[OnChainVote]:
return self._get_past_vote(voter.to_bech32(), contract, "delegate_vote", "delegateVote", proposal)

def _get_past_vote(self, voter: str, contract: str, function: str, event_identifier: str, proposal: int) -> Optional[OnChainVote]:
url = f"accounts/{voter}/transactions"
size = MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_VOTE
reasonably_recent_timestamp = int((datetime.now(timezone.utc) - timedelta(days=30)).timestamp())
Copy link
Collaborator Author

@andreibancioiu andreibancioiu Oct 28, 2025

Choose a reason for hiding this comment

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

Workaround (limitations): one month, at most 10 votes 🤞


transactions = self._api_do_get(url, {
"status": "success",
"receiver": contract,
"function": function,
"withLogs": "true",
"withScResults": "true",
"size": size,
"after": reasonably_recent_timestamp
})

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

for transaction in transactions:
timestamp = transaction.get("timestamp", 0)

all_events: list[Any] = []
all_events.extend(transaction.get("logs", {}).get("events", []))

for result in transaction.get("results"):
all_events.extend(result.get("logs", {}).get("events", []))

for event in all_events:
if event.get("identifier") != event_identifier:
continue

topics = event.get("topics", [])

event_proposal_base64 = topics[0]
event_proposal_bytes = base64.b64decode(event_proposal_base64)
event_proposal = U64Value()
event_proposal.decode_top_level(event_proposal_bytes)
event_vote_type_base64 = topics[1]
event_vote_type = VoteType(base64.b64decode(event_vote_type_base64).decode())

if event_proposal.value == proposal:
return OnChainVote(voter, proposal, contract, timestamp, event_vote_type)

return None

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

import json
from pathlib import Path
from typing import Any

from multiversx_sdk import Address
from multiversx_sdk import Address, VoteType


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

return cls(address, power, proof)

@classmethod
def load_many_from_proofs_file(cls, proofs_file: Path):
json_content = proofs_file.read_text()
data = json.loads(json_content)
records = [GovernanceRecord.new_from_dictionary(item) for item in data]

records_by_adresses: dict[str, GovernanceRecord] = {
item.address.to_bech32(): item for item in records
}

return records_by_adresses


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


def convert_string_to_vote_type(input: str) -> VoteType:
input = input.lower()

return {
"yes": VoteType.YES,
"no": VoteType.NO,
"abstain": VoteType.ABSTAIN,
"veto": VoteType.VETO
}[input]
Loading