From bcaeb32c87301a5c067d1ca69d1fc255f42d2937 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Wed, 8 Dec 2021 22:43:13 +0100 Subject: [PATCH] Add vote init instruction for cleaner tests --- .../py/{actions => spl_token}/__init__.py | 0 .../token.py => spl_token/actions.py} | 0 .../py/{actions/stake.py => stake/actions.py} | 0 .../stake_pool.py => stake_pool/actions.py} | 10 +- stake-pool/py/system/__init__.py | 0 .../{actions/system.py => system/actions.py} | 0 stake-pool/py/tests/conftest.py | 38 ++++---- stake-pool/py/tests/test_add_remove.py | 16 +-- stake-pool/py/tests/test_create.py | 64 +++--------- stake-pool/py/tests/test_stake.py | 10 ++ stake-pool/py/tests/test_system.py | 14 +++ stake-pool/py/tests/test_token.py | 16 +++ stake-pool/py/tests/test_vote.py | 11 +++ stake-pool/py/vote/actions.py | 45 +++++++++ stake-pool/py/vote/constants.py | 3 + stake-pool/py/vote/instructions.py | 97 +++++++++++++++++++ 16 files changed, 240 insertions(+), 84 deletions(-) rename stake-pool/py/{actions => spl_token}/__init__.py (100%) rename stake-pool/py/{actions/token.py => spl_token/actions.py} (100%) rename stake-pool/py/{actions/stake.py => stake/actions.py} (100%) rename stake-pool/py/{actions/stake_pool.py => stake_pool/actions.py} (94%) create mode 100644 stake-pool/py/system/__init__.py rename stake-pool/py/{actions/system.py => system/actions.py} (100%) create mode 100644 stake-pool/py/tests/test_stake.py create mode 100644 stake-pool/py/tests/test_system.py create mode 100644 stake-pool/py/tests/test_token.py create mode 100644 stake-pool/py/tests/test_vote.py create mode 100644 stake-pool/py/vote/actions.py create mode 100644 stake-pool/py/vote/instructions.py diff --git a/stake-pool/py/actions/__init__.py b/stake-pool/py/spl_token/__init__.py similarity index 100% rename from stake-pool/py/actions/__init__.py rename to stake-pool/py/spl_token/__init__.py diff --git a/stake-pool/py/actions/token.py b/stake-pool/py/spl_token/actions.py similarity index 100% rename from stake-pool/py/actions/token.py rename to stake-pool/py/spl_token/actions.py diff --git a/stake-pool/py/actions/stake.py b/stake-pool/py/stake/actions.py similarity index 100% rename from stake-pool/py/actions/stake.py rename to stake-pool/py/stake/actions.py diff --git a/stake-pool/py/actions/stake_pool.py b/stake-pool/py/stake_pool/actions.py similarity index 94% rename from stake-pool/py/actions/stake_pool.py rename to stake-pool/py/stake_pool/actions.py index 42af6f301c5..17508408fc7 100644 --- a/stake-pool/py/actions/stake_pool.py +++ b/stake-pool/py/stake_pool/actions.py @@ -15,8 +15,8 @@ from stake_pool.state import STAKE_POOL_LAYOUT, ValidatorList, Fee, StakePool import stake_pool.instructions as sp -import actions.stake -import actions.token +from stake.actions import create_stake +from spl_token.actions import create_mint, create_associated_token_account async def create(client: AsyncClient, manager: Keypair, @@ -87,12 +87,12 @@ async def create_all(client: AsyncClient, manager: Keypair, fee: Fee, referral_f STAKE_POOL_PROGRAM_ID, stake_pool.public_key) reserve_stake = Keypair() - await actions.stake.create_stake(client, manager, reserve_stake, pool_withdraw_authority) + await create_stake(client, manager, reserve_stake, pool_withdraw_authority) pool_mint = Keypair() - await actions.token.create_mint(client, manager, pool_mint, pool_withdraw_authority) + await create_mint(client, manager, pool_mint, pool_withdraw_authority) - manager_fee_account = await actions.token.create_associated_token_account( + manager_fee_account = await create_associated_token_account( client, manager, manager.public_key, diff --git a/stake-pool/py/system/__init__.py b/stake-pool/py/system/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/stake-pool/py/actions/system.py b/stake-pool/py/system/actions.py similarity index 100% rename from stake-pool/py/actions/system.py rename to stake-pool/py/system/actions.py diff --git a/stake-pool/py/tests/conftest.py b/stake-pool/py/tests/conftest.py index 105240ae52c..709875293ee 100644 --- a/stake-pool/py/tests/conftest.py +++ b/stake-pool/py/tests/conftest.py @@ -4,13 +4,17 @@ import shutil import tempfile import time -from typing import Iterator -from subprocess import run, Popen +from typing import Iterator, List +from subprocess import Popen +from solana.keypair import Keypair from solana.publickey import PublicKey from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed +from vote.actions import create_vote +from system.actions import airdrop + @pytest.fixture(scope="session") def solana_test_validator(): @@ -27,24 +31,16 @@ def solana_test_validator(): @pytest.fixture -def validators(async_client): +def validators(event_loop, async_client, payer) -> List[PublicKey]: num_validators = 3 validators = [] + futures = [] for i in range(num_validators): - tf = tempfile.NamedTemporaryFile() - identity = f"{tf.name}-identity-{i}.json" - run(["solana-keygen", "new", "-s", "-o", identity]) - vote = f"{tf.name}-vote-{i}.json" - run(["solana-keygen", "new", "-s", "-o", vote]) - withdrawer = f"{tf.name}-withdrawer-{i}.json" - run(["solana-keygen", "new", "-s", "-o", withdrawer]) - run(["solana", "create-vote-account", - vote, identity, withdrawer, - "--commission", "1", - "--commitment", "confirmed", - "-ul"]) - output = run(["solana-keygen", "pubkey", vote], capture_output=True) - validators.append(PublicKey(output.stdout.decode('utf-8').strip())) + vote = Keypair() + node = Keypair() + futures.append(create_vote(async_client, payer, vote, node, payer.public_key, payer.public_key, 10)) + validators.append(vote.public_key) + event_loop.run_until_complete(asyncio.gather(*futures)) return validators @@ -68,3 +64,11 @@ def async_client(event_loop, solana_test_validator) -> Iterator[AsyncClient]: time.sleep(1) yield async_client event_loop.run_until_complete(async_client.close()) + + +@pytest.fixture +def payer(event_loop, async_client) -> Keypair: + payer = Keypair() + airdrop_lamports = 10_000_000_000 + event_loop.run_until_complete(airdrop(async_client, payer.public_key, airdrop_lamports)) + return payer diff --git a/stake-pool/py/tests/test_add_remove.py b/stake-pool/py/tests/test_add_remove.py index d1247c3d904..18a361ad152 100644 --- a/stake-pool/py/tests/test_add_remove.py +++ b/stake-pool/py/tests/test_add_remove.py @@ -1,27 +1,21 @@ import pytest -from solana.keypair import Keypair from solana.publickey import PublicKey from solana.rpc.commitment import Confirmed -import actions.system -import actions.stake_pool from vote.constants import VOTE_PROGRAM_ID from stake_pool.state import Fee, ValidatorList, StakeStatus +from stake_pool.actions import create_all, add_validator_to_pool, remove_validator_from_pool @pytest.mark.asyncio -async def test_add_validator(async_client, validators): - manager = Keypair() - airdrop_lamports = 1_000_000_000_000 - await actions.system.airdrop(async_client, manager.public_key, airdrop_lamports) - +async def test_add_validator(async_client, validators, payer): fee = Fee(numerator=1, denominator=1000) referral_fee = 20 - (stake_pool, validator_list_address) = await actions.stake_pool.create_all(async_client, manager, fee, referral_fee) + (stake_pool, validator_list_address) = await create_all(async_client, payer, fee, referral_fee) for validator in validators: resp = await async_client.get_account_info(validator, commitment=Confirmed) assert PublicKey(resp['result']['value']['owner']) == VOTE_PROGRAM_ID - await actions.stake_pool.add_validator_to_pool(async_client, manager, stake_pool, validator) + await add_validator_to_pool(async_client, payer, stake_pool, validator) resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) data = resp['result']['value']['data'] @@ -33,7 +27,7 @@ async def test_add_validator(async_client, validators): assert validator_info.transient_stake_lamports == 0 assert validator_info.last_update_epoch == 0 assert validator_info.status == StakeStatus.ACTIVE - await actions.stake_pool.remove_validator_from_pool(async_client, manager, stake_pool, validator) + await remove_validator_from_pool(async_client, payer, stake_pool, validator) resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) data = resp['result']['value']['data'] diff --git a/stake-pool/py/tests/test_create.py b/stake-pool/py/tests/test_create.py index a88b8239c6f..6ab8e201a9e 100644 --- a/stake-pool/py/tests/test_create.py +++ b/stake-pool/py/tests/test_create.py @@ -6,80 +6,42 @@ from stake_pool.constants import find_withdraw_authority_program_address, STAKE_POOL_PROGRAM_ID from stake_pool.state import StakePool, Fee -import actions.system -import actions.stake -import actions.stake_pool -import actions.token +from stake.actions import create_stake +from stake_pool.actions import create +from spl_token.actions import create_mint, create_associated_token_account @pytest.mark.asyncio -async def test_airdrop(async_client): - manager = Keypair() - airdrop_lamports = 1_000_000 - await actions.system.airdrop(async_client, manager.public_key, airdrop_lamports) - resp = await async_client.get_balance(manager.public_key, commitment=Confirmed) - assert resp['result']['value'] == airdrop_lamports - - -@pytest.mark.asyncio -async def test_create_stake(async_client): - owner = Keypair() - reserve_stake = Keypair() - airdrop_lamports = 1_000_000_000 - await actions.system.airdrop(async_client, owner.public_key, airdrop_lamports) - await actions.stake.create_stake(async_client, owner, reserve_stake, owner.public_key) - - -@pytest.mark.asyncio -async def test_create_mint(async_client): - owner = Keypair() - airdrop_lamports = 1_000_000_000 - await actions.system.airdrop(async_client, owner.public_key, airdrop_lamports) - pool_mint = Keypair() - await actions.token.create_mint(async_client, owner, pool_mint, owner.public_key) - await actions.token.create_associated_token_account( - async_client, - owner, - owner.public_key, - pool_mint.public_key, - ) - - -@pytest.mark.asyncio -async def test_create_stake_pool(async_client): - manager = Keypair() - airdrop_lamports = 1_000_000_000_000 - await actions.system.airdrop(async_client, manager.public_key, airdrop_lamports) - +async def test_create_stake_pool(async_client, payer): stake_pool = Keypair() validator_list = Keypair() (pool_withdraw_authority, seed) = find_withdraw_authority_program_address( STAKE_POOL_PROGRAM_ID, stake_pool.public_key) reserve_stake = Keypair() - await actions.stake.create_stake(async_client, manager, reserve_stake, pool_withdraw_authority) + await create_stake(async_client, payer, reserve_stake, pool_withdraw_authority) pool_mint = Keypair() - await actions.token.create_mint(async_client, manager, pool_mint, pool_withdraw_authority) + await create_mint(async_client, payer, pool_mint, pool_withdraw_authority) - manager_fee_account = await actions.token.create_associated_token_account( + manager_fee_account = await create_associated_token_account( async_client, - manager, - manager.public_key, + payer, + payer.public_key, pool_mint.public_key, ) fee = Fee(numerator=1, denominator=1000) referral_fee = 20 - await actions.stake_pool.create( - async_client, manager, stake_pool, validator_list, pool_mint.public_key, + await create( + async_client, payer, stake_pool, validator_list, pool_mint.public_key, reserve_stake.public_key, manager_fee_account, fee, referral_fee) resp = await async_client.get_account_info(stake_pool.public_key, commitment=Confirmed) assert resp['result']['value']['owner'] == str(STAKE_POOL_PROGRAM_ID) data = resp['result']['value']['data'] pool_data = StakePool.decode(data[0], data[1]) - assert pool_data.manager == manager.public_key - assert pool_data.staker == manager.public_key + assert pool_data.manager == payer.public_key + assert pool_data.staker == payer.public_key assert pool_data.stake_withdraw_bump_seed == seed assert pool_data.validator_list == validator_list.public_key assert pool_data.reserve_stake == reserve_stake.public_key diff --git a/stake-pool/py/tests/test_stake.py b/stake-pool/py/tests/test_stake.py new file mode 100644 index 00000000000..443cc38ea23 --- /dev/null +++ b/stake-pool/py/tests/test_stake.py @@ -0,0 +1,10 @@ +import pytest +from solana.keypair import Keypair + +from stake.actions import create_stake + + +@pytest.mark.asyncio +async def test_create_stake(async_client, payer): + reserve_stake = Keypair() + await create_stake(async_client, payer, reserve_stake, payer.public_key) diff --git a/stake-pool/py/tests/test_system.py b/stake-pool/py/tests/test_system.py new file mode 100644 index 00000000000..31d2af3437a --- /dev/null +++ b/stake-pool/py/tests/test_system.py @@ -0,0 +1,14 @@ +import pytest +from solana.keypair import Keypair +from solana.rpc.commitment import Confirmed + +import system.actions + + +@pytest.mark.asyncio +async def test_airdrop(async_client): + manager = Keypair() + airdrop_lamports = 1_000_000 + await system.actions.airdrop(async_client, manager.public_key, airdrop_lamports) + resp = await async_client.get_balance(manager.public_key, commitment=Confirmed) + assert resp['result']['value'] == airdrop_lamports diff --git a/stake-pool/py/tests/test_token.py b/stake-pool/py/tests/test_token.py new file mode 100644 index 00000000000..1d92c179db7 --- /dev/null +++ b/stake-pool/py/tests/test_token.py @@ -0,0 +1,16 @@ +import pytest +from solana.keypair import Keypair + +from spl_token.actions import create_mint, create_associated_token_account + + +@pytest.mark.asyncio +async def test_create_mint(async_client, payer): + pool_mint = Keypair() + await create_mint(async_client, payer, pool_mint, payer.public_key) + await create_associated_token_account( + async_client, + payer, + payer.public_key, + pool_mint.public_key, + ) diff --git a/stake-pool/py/tests/test_vote.py b/stake-pool/py/tests/test_vote.py new file mode 100644 index 00000000000..f59050553e8 --- /dev/null +++ b/stake-pool/py/tests/test_vote.py @@ -0,0 +1,11 @@ +import pytest +from solana.keypair import Keypair + +from vote.actions import create_vote + + +@pytest.mark.asyncio +async def test_create_vote(async_client, payer): + vote = Keypair() + node = Keypair() + await create_vote(async_client, payer, vote, node, payer.public_key, payer.public_key, 10) diff --git a/stake-pool/py/vote/actions.py b/stake-pool/py/vote/actions.py new file mode 100644 index 00000000000..9f3fc49f197 --- /dev/null +++ b/stake-pool/py/vote/actions.py @@ -0,0 +1,45 @@ +from solana.publickey import PublicKey +from solana.keypair import Keypair +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts +from solana.sysvar import SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY +from solana.transaction import Transaction +import solana.system_program as sys + +from vote.constants import VOTE_PROGRAM_ID, VOTE_STATE_LEN +from vote.instructions import initialize, InitializeParams + + +async def create_vote( + client: AsyncClient, payer: Keypair, vote: Keypair, node: Keypair, + voter: PublicKey, withdrawer: PublicKey, commission: int): + print(f"Creating vote account {vote.public_key}") + resp = await client.get_minimum_balance_for_rent_exemption(VOTE_STATE_LEN) + txn = Transaction() + txn.add( + sys.create_account( + sys.CreateAccountParams( + from_pubkey=payer.public_key, + new_account_pubkey=vote.public_key, + lamports=resp['result'], + space=VOTE_STATE_LEN, + program_id=VOTE_PROGRAM_ID, + ) + ) + ) + txn.add( + initialize( + InitializeParams( + vote=vote.public_key, + rent_sysvar=SYSVAR_RENT_PUBKEY, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + node=node.public_key, + authorized_voter=voter, + authorized_withdrawer=withdrawer, + commission=commission, + ) + ) + ) + await client.send_transaction( + txn, payer, vote, node, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) diff --git a/stake-pool/py/vote/constants.py b/stake-pool/py/vote/constants.py index 4748714cc82..21f006e4a52 100644 --- a/stake-pool/py/vote/constants.py +++ b/stake-pool/py/vote/constants.py @@ -3,3 +3,6 @@ VOTE_PROGRAM_ID = PublicKey("Vote111111111111111111111111111111111111111") """Program id for the native vote program.""" + +VOTE_STATE_LEN: int = 3731 +"""Size of vote account.""" diff --git a/stake-pool/py/vote/instructions.py b/stake-pool/py/vote/instructions.py new file mode 100644 index 00000000000..1ae3a78f6dc --- /dev/null +++ b/stake-pool/py/vote/instructions.py @@ -0,0 +1,97 @@ +"""Vote Program Instructions.""" + +from enum import IntEnum +from typing import NamedTuple + +from construct import Struct, Switch, Int8ul, Int32ul, Pass # type: ignore + +from solana.publickey import PublicKey +from solana.sysvar import SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY +from solana.transaction import AccountMeta, TransactionInstruction +from solana._layouts.shared import PUBLIC_KEY_LAYOUT + +from vote.constants import VOTE_PROGRAM_ID + + +class InitializeParams(NamedTuple): + """Initialize vote account params.""" + + vote: PublicKey + """`[w]` Uninitialized vote account""" + rent_sysvar: PublicKey + """`[]` Rent sysvar.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + node: PublicKey + """`[s]` New validator identity.""" + + authorized_voter: PublicKey + """The authorized voter for this vote account.""" + authorized_withdrawer: PublicKey + """The authorized withdrawer for this vote account.""" + commission: int + """Commission, represented as a percentage""" + + +class InstructionType(IntEnum): + """Vote Instruction Types.""" + + INITIALIZE = 0 + AUTHORIZE = 1 + VOTE = 2 + WITHDRAW = 3 + UPDATE_VALIDATOR_IDENTITY = 4 + UPDATE_COMMISSION = 5 + VOTE_SWITCH = 6 + AUTHORIZE_CHECKED = 7 + + +INITIALIZE_LAYOUT = Struct( + "node" / PUBLIC_KEY_LAYOUT, + "authorized_voter" / PUBLIC_KEY_LAYOUT, + "authorized_withdrawer" / PUBLIC_KEY_LAYOUT, + "commission" / Int8ul, +) + +INSTRUCTIONS_LAYOUT = Struct( + "instruction_type" / Int32ul, + "args" + / Switch( + lambda this: this.instruction_type, + { + InstructionType.INITIALIZE: INITIALIZE_LAYOUT, + InstructionType.AUTHORIZE: Pass, # TODO + InstructionType.VOTE: Pass, # TODO + InstructionType.WITHDRAW: Pass, # TODO + InstructionType.UPDATE_VALIDATOR_IDENTITY: Pass, # TODO + InstructionType.UPDATE_COMMISSION: Pass, # TODO + InstructionType.VOTE_SWITCH: Pass, # TODO + InstructionType.AUTHORIZE_CHECKED: Pass, # TODO + }, + ), +) + + +def initialize(params: InitializeParams) -> TransactionInstruction: + """Creates a transaction instruction to initialize a new stake.""" + data = INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.INITIALIZE, + args=dict( + node=bytes(params.node), + authorized_voter=bytes(params.authorized_voter), + authorized_withdrawer=bytes(params.authorized_withdrawer), + commission=params.commission, + ), + ) + ) + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.vote, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.rent_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.node, is_signer=True, is_writable=False), + ], + program_id=VOTE_PROGRAM_ID, + data=data, + )