From a7888861dad258d72715ba5f938ad16deb727bd2 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Fri, 10 Dec 2021 00:43:25 +0100 Subject: [PATCH] stake-pool-py: Create and deserialize stake pools (#2557) * stake-pool-py: Create and deserialize stake pools with CI * Add ability to add / remove validators * Add vote init instruction for cleaner tests * Fixup CI * Add deposit / withdraw sol * Add update instructions * Add increase / decrease stake * Add deposit / withdraw stake --- .github/workflows/fuzz-nightly.yml | 3 +- .../pull-request-binary-oracle-pair.yml | 3 +- .github/workflows/pull-request-examples.yml | 3 +- .../pull-request-feature-proposal.yml | 3 +- .github/workflows/pull-request-governance.yml | 3 +- .github/workflows/pull-request-libraries.yml | 3 +- .github/workflows/pull-request-memo.yml | 3 +- .../workflows/pull-request-name-service.yml | 3 +- .github/workflows/pull-request-record.yml | 3 +- .../workflows/pull-request-shared-memory.yml | 3 +- .github/workflows/pull-request-stake-pool.yml | 34 +- .../workflows/pull-request-token-lending.yml | 3 +- .github/workflows/pull-request-token-swap.yml | 3 +- .github/workflows/pull-request-token.yml | 3 +- .github/workflows/pull-request.yml | 3 +- ci/py-test-stake-pool.sh | 21 + stake-pool/README.md | 2 + stake-pool/py/.flake8 | 2 + stake-pool/py/.gitignore | 7 + stake-pool/py/README.md | 3 + stake-pool/py/requirements.txt | 35 + stake-pool/py/spl_token/__init__.py | 0 stake-pool/py/spl_token/actions.py | 57 ++ stake-pool/py/stake/__init__.py | 0 stake-pool/py/stake/actions.py | 87 ++ stake-pool/py/stake/constants.py | 12 + stake-pool/py/stake/instructions.py | 177 ++++ stake-pool/py/stake/state.py | 59 ++ stake-pool/py/stake_pool/__init__.py | 0 stake-pool/py/stake_pool/actions.py | 572 +++++++++++ stake-pool/py/stake_pool/constants.py | 74 ++ stake-pool/py/stake_pool/instructions.py | 912 ++++++++++++++++++ stake-pool/py/stake_pool/state.py | 321 ++++++ stake-pool/py/system/__init__.py | 0 stake-pool/py/system/actions.py | 9 + stake-pool/py/tests/conftest.py | 95 ++ stake-pool/py/tests/test_a_time_sensitive.py | 76 ++ stake-pool/py/tests/test_add_remove.py | 31 + stake-pool/py/tests/test_create.py | 68 ++ .../py/tests/test_deposit_withdraw_sol.py | 26 + .../py/tests/test_deposit_withdraw_stake.py | 43 + stake-pool/py/tests/test_stake.py | 32 + stake-pool/py/tests/test_system.py | 14 + stake-pool/py/tests/test_token.py | 16 + stake-pool/py/tests/test_vote.py | 16 + stake-pool/py/vote/__init__.py | 0 stake-pool/py/vote/actions.py | 45 + stake-pool/py/vote/constants.py | 8 + stake-pool/py/vote/instructions.py | 97 ++ 49 files changed, 2963 insertions(+), 30 deletions(-) create mode 100755 ci/py-test-stake-pool.sh create mode 100644 stake-pool/py/.flake8 create mode 100644 stake-pool/py/.gitignore create mode 100644 stake-pool/py/README.md create mode 100644 stake-pool/py/requirements.txt create mode 100644 stake-pool/py/spl_token/__init__.py create mode 100644 stake-pool/py/spl_token/actions.py create mode 100644 stake-pool/py/stake/__init__.py create mode 100644 stake-pool/py/stake/actions.py create mode 100644 stake-pool/py/stake/constants.py create mode 100644 stake-pool/py/stake/instructions.py create mode 100644 stake-pool/py/stake/state.py create mode 100644 stake-pool/py/stake_pool/__init__.py create mode 100644 stake-pool/py/stake_pool/actions.py create mode 100644 stake-pool/py/stake_pool/constants.py create mode 100644 stake-pool/py/stake_pool/instructions.py create mode 100644 stake-pool/py/stake_pool/state.py create mode 100644 stake-pool/py/system/__init__.py create mode 100644 stake-pool/py/system/actions.py create mode 100644 stake-pool/py/tests/conftest.py create mode 100644 stake-pool/py/tests/test_a_time_sensitive.py create mode 100644 stake-pool/py/tests/test_add_remove.py create mode 100644 stake-pool/py/tests/test_create.py create mode 100644 stake-pool/py/tests/test_deposit_withdraw_sol.py create mode 100644 stake-pool/py/tests/test_deposit_withdraw_stake.py 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/__init__.py create mode 100644 stake-pool/py/vote/actions.py create mode 100644 stake-pool/py/vote/constants.py create mode 100644 stake-pool/py/vote/instructions.py diff --git a/.github/workflows/fuzz-nightly.yml b/.github/workflows/fuzz-nightly.yml index 06173208b62..43d6a6c4883 100644 --- a/.github/workflows/fuzz-nightly.yml +++ b/.github/workflows/fuzz-nightly.yml @@ -44,8 +44,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} restore-keys: | solana- diff --git a/.github/workflows/pull-request-binary-oracle-pair.yml b/.github/workflows/pull-request-binary-oracle-pair.yml index 8033fc09abd..7ff971d52ae 100644 --- a/.github/workflows/pull-request-binary-oracle-pair.yml +++ b/.github/workflows/pull-request-binary-oracle-pair.yml @@ -45,8 +45,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-examples.yml b/.github/workflows/pull-request-examples.yml index 0fda3fe6909..9adbc062936 100644 --- a/.github/workflows/pull-request-examples.yml +++ b/.github/workflows/pull-request-examples.yml @@ -43,8 +43,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-feature-proposal.yml b/.github/workflows/pull-request-feature-proposal.yml index 2cdf88eb7d6..73afa62866d 100644 --- a/.github/workflows/pull-request-feature-proposal.yml +++ b/.github/workflows/pull-request-feature-proposal.yml @@ -45,8 +45,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-governance.yml b/.github/workflows/pull-request-governance.yml index bb5f9e1b219..57656b0e9d9 100644 --- a/.github/workflows/pull-request-governance.yml +++ b/.github/workflows/pull-request-governance.yml @@ -45,8 +45,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-libraries.yml b/.github/workflows/pull-request-libraries.yml index 4e60da42ef8..bae584bb012 100644 --- a/.github/workflows/pull-request-libraries.yml +++ b/.github/workflows/pull-request-libraries.yml @@ -43,8 +43,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-memo.yml b/.github/workflows/pull-request-memo.yml index b07a315ee5d..cff07b477b7 100644 --- a/.github/workflows/pull-request-memo.yml +++ b/.github/workflows/pull-request-memo.yml @@ -43,8 +43,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-name-service.yml b/.github/workflows/pull-request-name-service.yml index b505ce3ed5b..1a5f3411cf4 100644 --- a/.github/workflows/pull-request-name-service.yml +++ b/.github/workflows/pull-request-name-service.yml @@ -43,8 +43,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-record.yml b/.github/workflows/pull-request-record.yml index 4288304fdcd..f6e0dc153ad 100644 --- a/.github/workflows/pull-request-record.yml +++ b/.github/workflows/pull-request-record.yml @@ -43,8 +43,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-shared-memory.yml b/.github/workflows/pull-request-shared-memory.yml index d18f27d9ea8..57814b39b36 100644 --- a/.github/workflows/pull-request-shared-memory.yml +++ b/.github/workflows/pull-request-shared-memory.yml @@ -43,8 +43,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-stake-pool.yml b/.github/workflows/pull-request-stake-pool.yml index 8ae1978f2af..54f5f8d59b5 100644 --- a/.github/workflows/pull-request-stake-pool.yml +++ b/.github/workflows/pull-request-stake-pool.yml @@ -45,8 +45,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies @@ -57,3 +56,34 @@ jobs: - name: Build and test run: ./ci/cargo-test-bpf.sh stake-pool + + - name: Upload programs + uses: actions/upload-artifact@v2 + with: + name: stake-pool-programs + path: "target/deploy/*.so" + if-no-files-found: error + + py-test: + runs-on: ubuntu-latest + needs: cargo-test-bpf + steps: + - uses: actions/checkout@v2 + + - name: Setup Python version + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: pip-stake-pool-${{ hashFiles('stake-pool/py/requirements.txt') }} + + - name: Download programs + uses: actions/download-artifact@v2 + with: + name: stake-pool-programs + path: target/deploy + + - run: ./ci/py-test-stake-pool.sh diff --git a/.github/workflows/pull-request-token-lending.yml b/.github/workflows/pull-request-token-lending.yml index 416bbfe6d65..e764e1fb33b 100644 --- a/.github/workflows/pull-request-token-lending.yml +++ b/.github/workflows/pull-request-token-lending.yml @@ -45,8 +45,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-token-swap.yml b/.github/workflows/pull-request-token-swap.yml index b5c35372ed4..a24b5532c07 100644 --- a/.github/workflows/pull-request-token-swap.yml +++ b/.github/workflows/pull-request-token-swap.yml @@ -47,8 +47,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request-token.yml b/.github/workflows/pull-request-token.yml index 947d2772dd2..04044ce8aa4 100644 --- a/.github/workflows/pull-request-token.yml +++ b/.github/workflows/pull-request-token.yml @@ -45,8 +45,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f9ef786e5d6..e4d1d60e1bb 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -112,8 +112,7 @@ jobs: - uses: actions/cache@v2 with: - path: | - ~/.cache + path: ~/.cache/solana key: solana-${{ env.SOLANA_VERSION }} - name: Install dependencies diff --git a/ci/py-test-stake-pool.sh b/ci/py-test-stake-pool.sh new file mode 100755 index 00000000000..34074a7efbd --- /dev/null +++ b/ci/py-test-stake-pool.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -ex +cd "$(dirname "$0")/.." +source ./ci/solana-version.sh install + +cd stake-pool/py +python3 -m venv venv +source ./venv/bin/activate +pip3 install -r requirements.txt +check_dirs=( + "spl_token" + "stake" + "stake_pool" + "system" + "tests" + "vote" +) +flake8 "${check_dirs[@]}" +mypy "${check_dirs[@]}" +python3 -m pytest diff --git a/stake-pool/README.md b/stake-pool/README.md index af9a5003f27..179033a262e 100644 --- a/stake-pool/README.md +++ b/stake-pool/README.md @@ -5,3 +5,5 @@ Full documentation is available at https://spl.solana.com/stake-pool The command-line interface tool is available in the `./cli` directory. Javascript bindings are available in the `./js` directory. + +Python bindings are available in the `./py` directory. diff --git a/stake-pool/py/.flake8 b/stake-pool/py/.flake8 new file mode 100644 index 00000000000..aa079ec57f8 --- /dev/null +++ b/stake-pool/py/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length=120 diff --git a/stake-pool/py/.gitignore b/stake-pool/py/.gitignore new file mode 100644 index 00000000000..ad0e7548ff2 --- /dev/null +++ b/stake-pool/py/.gitignore @@ -0,0 +1,7 @@ +# python cache files +*__pycache__* +.pytest_cache +.mypy_cache + +# venv +venv/ diff --git a/stake-pool/py/README.md b/stake-pool/py/README.md new file mode 100644 index 00000000000..97e3605c699 --- /dev/null +++ b/stake-pool/py/README.md @@ -0,0 +1,3 @@ +# Stake-Pool Python Bindings + +WIP Python bindings to interact with the stake pool program. diff --git a/stake-pool/py/requirements.txt b/stake-pool/py/requirements.txt new file mode 100644 index 00000000000..32b09a63bdc --- /dev/null +++ b/stake-pool/py/requirements.txt @@ -0,0 +1,35 @@ +anyio==3.3.4 +attrs==21.2.0 +base58==2.1.0 +cachetools==4.2.4 +certifi==2021.10.8 +cffi==1.15.0 +charset-normalizer==2.0.7 +construct==2.10.67 +flake8==4.0.1 +h11==0.12.0 +httpcore==0.13.7 +httpx==0.20.0 +idna==3.3 +iniconfig==1.1.1 +mccabe==0.6.1 +mypy==0.910 +mypy-extensions==0.4.3 +packaging==21.2 +pluggy==1.0.0 +py==1.10.0 +pycodestyle==2.8.0 +pycparser==2.20 +pyflakes==2.4.0 +PyNaCl==1.4.0 +pyparsing==2.4.7 +pytest==6.2.5 +pytest-asyncio==0.16.0 +requests==2.26.0 +rfc3986==1.5.0 +six==1.16.0 +sniffio==1.2.0 +solana==0.18.1 +toml==0.10.2 +typing-extensions==3.10.0.2 +urllib3==1.26.7 diff --git a/stake-pool/py/spl_token/__init__.py b/stake-pool/py/spl_token/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/stake-pool/py/spl_token/actions.py b/stake-pool/py/spl_token/actions.py new file mode 100644 index 00000000000..6c00a9285cf --- /dev/null +++ b/stake-pool/py/spl_token/actions.py @@ -0,0 +1,57 @@ +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.transaction import Transaction +import solana.system_program as sys + +from spl.token.constants import TOKEN_PROGRAM_ID +from spl.token.async_client import AsyncToken +from spl.token._layouts import MINT_LAYOUT +import spl.token.instructions as spl_token + + +async def create_associated_token_account( + client: AsyncClient, + payer: Keypair, + owner: PublicKey, + mint: PublicKey +) -> PublicKey: + txn = Transaction() + create_txn = spl_token.create_associated_token_account( + payer=payer.public_key, owner=owner, mint=mint + ) + txn.add(create_txn) + await client.send_transaction(txn, payer, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + return create_txn.keys[1].pubkey + + +async def create_mint(client: AsyncClient, payer: Keypair, mint: Keypair, mint_authority: PublicKey): + mint_balance = await AsyncToken.get_min_balance_rent_for_exempt_for_mint(client) + print(f"Creating pool token mint {mint.public_key}") + txn = Transaction() + txn.add( + sys.create_account( + sys.CreateAccountParams( + from_pubkey=payer.public_key, + new_account_pubkey=mint.public_key, + lamports=mint_balance, + space=MINT_LAYOUT.sizeof(), + program_id=TOKEN_PROGRAM_ID, + ) + ) + ) + txn.add( + spl_token.initialize_mint( + spl_token.InitializeMintParams( + program_id=TOKEN_PROGRAM_ID, + mint=mint.public_key, + decimals=9, + mint_authority=mint_authority, + freeze_authority=None, + ) + ) + ) + await client.send_transaction( + txn, payer, mint, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) diff --git a/stake-pool/py/stake/__init__.py b/stake-pool/py/stake/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/stake-pool/py/stake/actions.py b/stake-pool/py/stake/actions.py new file mode 100644 index 00000000000..2963a43da62 --- /dev/null +++ b/stake-pool/py/stake/actions.py @@ -0,0 +1,87 @@ +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_STAKE_HISTORY_PUBKEY +from solana.transaction import Transaction +import solana.system_program as sys + +from stake.constants import STAKE_LEN, STAKE_PROGRAM_ID, SYSVAR_STAKE_CONFIG_ID +from stake.state import Authorized, Lockup, StakeAuthorize +import stake.instructions as st + + +async def create_stake(client: AsyncClient, payer: Keypair, stake: Keypair, authority: PublicKey, lamports: int): + print(f"Creating stake {stake.public_key}") + resp = await client.get_minimum_balance_for_rent_exemption(STAKE_LEN) + txn = Transaction() + txn.add( + sys.create_account( + sys.CreateAccountParams( + from_pubkey=payer.public_key, + new_account_pubkey=stake.public_key, + lamports=resp['result'] + lamports, + space=STAKE_LEN, + program_id=STAKE_PROGRAM_ID, + ) + ) + ) + txn.add( + st.initialize( + st.InitializeParams( + stake=stake.public_key, + authorized=Authorized( + staker=authority, + withdrawer=authority, + ), + lockup=Lockup( + unix_timestamp=0, + epoch=0, + custodian=sys.SYS_PROGRAM_ID, + ) + ) + ) + ) + await client.send_transaction( + txn, payer, stake, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def delegate_stake(client: AsyncClient, payer: Keypair, staker: Keypair, stake: PublicKey, vote: PublicKey): + txn = Transaction() + txn.add( + st.delegate_stake( + st.DelegateStakeParams( + stake=stake, + vote=vote, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + stake_config_id=SYSVAR_STAKE_CONFIG_ID, + staker=staker.public_key, + ) + ) + ) + signers = [payer, staker] if payer != staker else [payer] + await client.send_transaction( + txn, *signers, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def authorize( + client: AsyncClient, payer: Keypair, authority: Keypair, stake: PublicKey, + new_authority: PublicKey, stake_authorize: StakeAuthorize +): + txn = Transaction() + txn.add( + st.authorize( + st.AuthorizeParams( + stake=stake, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + authority=authority.public_key, + new_authority=new_authority, + stake_authorize=stake_authorize, + ) + ) + ) + signers = [payer, authority] if payer != authority else [payer] + await client.send_transaction( + txn, *signers, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) diff --git a/stake-pool/py/stake/constants.py b/stake-pool/py/stake/constants.py new file mode 100644 index 00000000000..39e6c4a6919 --- /dev/null +++ b/stake-pool/py/stake/constants.py @@ -0,0 +1,12 @@ +"""Stake Program Constants.""" + +from solana.publickey import PublicKey + +STAKE_PROGRAM_ID: PublicKey = PublicKey("Stake11111111111111111111111111111111111111") +"""Public key that identifies the Stake program.""" + +SYSVAR_STAKE_CONFIG_ID: PublicKey = PublicKey("StakeConfig11111111111111111111111111111111") +"""Public key that identifies the Stake config sysvar.""" + +STAKE_LEN: int = 200 +"""Size of stake account.""" diff --git a/stake-pool/py/stake/instructions.py b/stake-pool/py/stake/instructions.py new file mode 100644 index 00000000000..29378e520a9 --- /dev/null +++ b/stake-pool/py/stake/instructions.py @@ -0,0 +1,177 @@ +"""Stake Program Instructions.""" + +from enum import IntEnum +from typing import NamedTuple + +from construct import Switch # type: ignore +from construct import Int32ul, Pass # type: ignore +from construct import Struct + +from solana._layouts.shared import PUBLIC_KEY_LAYOUT +from solana.publickey import PublicKey +from solana.sysvar import SYSVAR_RENT_PUBKEY +from solana.transaction import AccountMeta, TransactionInstruction + +from stake.constants import STAKE_PROGRAM_ID +from stake.state import AUTHORIZED_LAYOUT, LOCKUP_LAYOUT, Authorized, Lockup, StakeAuthorize + + +class InitializeParams(NamedTuple): + """Initialize stake transaction params.""" + + stake: PublicKey + """`[w]` Uninitialized stake account.""" + authorized: Authorized + """Information about the staker and withdrawer keys.""" + lockup: Lockup + """Stake lockup, if any.""" + + +class DelegateStakeParams(NamedTuple): + """Initialize stake transaction params.""" + + stake: PublicKey + """`[w]` Uninitialized stake account.""" + vote: PublicKey + """`[]` Vote account to which this stake will be delegated.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + stake_history_sysvar: PublicKey + """`[]` Stake history sysvar that carries stake warmup/cooldown history.""" + stake_config_id: PublicKey + """`[]` Address of config account that carries stake config.""" + staker: PublicKey + """`[s]` Stake authority.""" + + +class AuthorizeParams(NamedTuple): + """Authorize stake transaction params.""" + + stake: PublicKey + """`[w]` Initialized stake account to modify.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + authority: PublicKey + """`[s]` Current stake authority.""" + + # Params + new_authority: PublicKey + """New authority's public key.""" + stake_authorize: StakeAuthorize + """Type of authority to modify, staker or withdrawer.""" + + +class InstructionType(IntEnum): + """Stake Instruction Types.""" + + INITIALIZE = 0 + AUTHORIZE = 1 + DELEGATE_STAKE = 2 + SPLIT = 3 + WITHDRAW = 4 + DEACTIVATE = 5 + SET_LOCKUP = 6 + MERGE = 7 + AUTHORIZE_WITH_SEED = 8 + INITIALIZE_CHECKED = 9 + AUTHORIZED_CHECKED = 10 + AUTHORIZED_CHECKED_WITH_SEED = 11 + SET_LOCKUP_CHECKED = 12 + + +INITIALIZE_LAYOUT = Struct( + "authorized" / AUTHORIZED_LAYOUT, + "lockup" / LOCKUP_LAYOUT, +) + + +AUTHORIZE_LAYOUT = Struct( + "new_authority" / PUBLIC_KEY_LAYOUT, + "stake_authorize" / Int32ul, +) + + +INSTRUCTIONS_LAYOUT = Struct( + "instruction_type" / Int32ul, + "args" + / Switch( + lambda this: this.instruction_type, + { + InstructionType.INITIALIZE: INITIALIZE_LAYOUT, + InstructionType.AUTHORIZE: AUTHORIZE_LAYOUT, + InstructionType.DELEGATE_STAKE: Pass, + InstructionType.SPLIT: Pass, + InstructionType.WITHDRAW: Pass, + InstructionType.DEACTIVATE: Pass, + InstructionType.SET_LOCKUP: Pass, + InstructionType.MERGE: Pass, + InstructionType.AUTHORIZE_WITH_SEED: Pass, + InstructionType.INITIALIZE_CHECKED: Pass, + InstructionType.AUTHORIZED_CHECKED: Pass, + InstructionType.AUTHORIZED_CHECKED_WITH_SEED: Pass, + InstructionType.SET_LOCKUP_CHECKED: Pass, + }, + ), +) + + +def initialize(params: InitializeParams) -> TransactionInstruction: + """Creates a transaction instruction to initialize a new stake.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False), + ], + program_id=STAKE_PROGRAM_ID, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.INITIALIZE, + args=dict( + authorized=params.authorized.as_bytes_dict(), + lockup=params.lockup.as_bytes_dict(), + ), + ) + ) + ) + + +def delegate_stake(params: DelegateStakeParams) -> TransactionInstruction: + """Creates an instruction to delegate a stake account.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.vote, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_config_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), + ], + program_id=STAKE_PROGRAM_ID, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.DELEGATE_STAKE, + args=None, + ) + ) + ) + + +def authorize(params: AuthorizeParams) -> TransactionInstruction: + """Creates an instruction to change the authority on a stake account.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.authority, is_signer=True, is_writable=False), + ], + program_id=STAKE_PROGRAM_ID, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.AUTHORIZE, + args={ + 'new_authority': bytes(params.new_authority), + 'stake_authorize': params.stake_authorize, + }, + ) + ) + ) diff --git a/stake-pool/py/stake/state.py b/stake-pool/py/stake/state.py new file mode 100644 index 00000000000..38932e8d183 --- /dev/null +++ b/stake-pool/py/stake/state.py @@ -0,0 +1,59 @@ +"""Stake State.""" + +from enum import IntEnum +from typing import NamedTuple, Dict +from construct import Container, Struct, Int64ul # type: ignore + +from solana.publickey import PublicKey +from solana._layouts.shared import PUBLIC_KEY_LAYOUT + + +class Lockup(NamedTuple): + """Lockup for a stake account.""" + unix_timestamp: int + epoch: int + custodian: PublicKey + + @classmethod + def decode_container(cls, container: Container): + return Lockup( + unix_timestamp=container['unix_timestamp'], + epoch=container['epoch'], + custodian=PublicKey(container['custodian']), + ) + + def as_bytes_dict(self) -> Dict: + self_dict = self._asdict() + self_dict['custodian'] = bytes(self_dict['custodian']) + return self_dict + + +class Authorized(NamedTuple): + """Define who is authorized to change a stake.""" + staker: PublicKey + withdrawer: PublicKey + + def as_bytes_dict(self) -> Dict: + return { + 'staker': bytes(self.staker), + 'withdrawer': bytes(self.withdrawer), + } + + +class StakeAuthorize(IntEnum): + """Stake Authorization Types.""" + STAKER = 0 + WITHDRAWER = 1 + + +LOCKUP_LAYOUT = Struct( + "unix_timestamp" / Int64ul, + "epoch" / Int64ul, + "custodian" / PUBLIC_KEY_LAYOUT, +) + + +AUTHORIZED_LAYOUT = Struct( + "staker" / PUBLIC_KEY_LAYOUT, + "withdrawer" / PUBLIC_KEY_LAYOUT, +) diff --git a/stake-pool/py/stake_pool/__init__.py b/stake-pool/py/stake_pool/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/stake-pool/py/stake_pool/actions.py b/stake-pool/py/stake_pool/actions.py new file mode 100644 index 00000000000..54e4e3a6b66 --- /dev/null +++ b/stake-pool/py/stake_pool/actions.py @@ -0,0 +1,572 @@ +from typing import Tuple + +from solana.keypair import Keypair +from solana.publickey import PublicKey +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, SYSVAR_STAKE_HISTORY_PUBKEY +from solana.transaction import Transaction +import solana.system_program as sys + +from spl.token.constants import TOKEN_PROGRAM_ID + +from stake.constants import STAKE_PROGRAM_ID, STAKE_LEN, SYSVAR_STAKE_CONFIG_ID +import stake.instructions as st +from stake.state import StakeAuthorize +from stake_pool.constants import \ + MAX_VALIDATORS_TO_UPDATE, \ + STAKE_POOL_PROGRAM_ID, \ + find_stake_program_address, \ + find_transient_stake_program_address, \ + find_withdraw_authority_program_address +from stake_pool.state import STAKE_POOL_LAYOUT, ValidatorList, Fee, StakePool +import stake_pool.instructions as sp + +from stake.actions import create_stake +from spl_token.actions import create_mint, create_associated_token_account + + +async def create(client: AsyncClient, manager: Keypair, + stake_pool: Keypair, validator_list: Keypair, + pool_mint: PublicKey, reserve_stake: PublicKey, + manager_fee_account: PublicKey, fee: Fee, referral_fee: int): + resp = await client.get_minimum_balance_for_rent_exemption(STAKE_POOL_LAYOUT.sizeof()) + pool_balance = resp['result'] + txn = Transaction() + txn.add( + sys.create_account( + sys.CreateAccountParams( + from_pubkey=manager.public_key, + new_account_pubkey=stake_pool.public_key, + lamports=pool_balance, + space=STAKE_POOL_LAYOUT.sizeof(), + program_id=STAKE_POOL_PROGRAM_ID, + ) + ) + ) + max_validators = 3950 # current supported max by the program, go big! + validator_list_size = ValidatorList.calculate_validator_list_size(max_validators) + resp = await client.get_minimum_balance_for_rent_exemption(validator_list_size) + validator_list_balance = resp['result'] + txn.add( + sys.create_account( + sys.CreateAccountParams( + from_pubkey=manager.public_key, + new_account_pubkey=validator_list.public_key, + lamports=validator_list_balance, + space=validator_list_size, + program_id=STAKE_POOL_PROGRAM_ID, + ) + ) + ) + await client.send_transaction( + txn, manager, stake_pool, validator_list, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + txn = Transaction() + txn.add( + sp.initialize( + sp.InitializeParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool.public_key, + manager=manager.public_key, + staker=manager.public_key, + validator_list=validator_list.public_key, + reserve_stake=reserve_stake, + pool_mint=pool_mint, + manager_fee_account=manager_fee_account, + token_program_id=TOKEN_PROGRAM_ID, + epoch_fee=fee, + withdrawal_fee=fee, + deposit_fee=fee, + referral_fee=referral_fee, + max_validators=max_validators, + ) + ) + ) + await client.send_transaction( + txn, manager, validator_list, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def create_all(client: AsyncClient, manager: Keypair, fee: Fee, referral_fee: int) -> Tuple[PublicKey, PublicKey]: + 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 create_stake(client, manager, reserve_stake, pool_withdraw_authority, 1) + + pool_mint = Keypair() + await create_mint(client, manager, pool_mint, pool_withdraw_authority) + + manager_fee_account = await create_associated_token_account( + client, + manager, + manager.public_key, + pool_mint.public_key, + ) + + fee = Fee(numerator=1, denominator=1000) + referral_fee = 20 + await create( + client, manager, stake_pool, validator_list, pool_mint.public_key, + reserve_stake.public_key, manager_fee_account, fee, referral_fee) + return (stake_pool.public_key, validator_list.public_key) + + +async def add_validator_to_pool( + client: AsyncClient, funder: Keypair, + stake_pool_address: PublicKey, validator: PublicKey +): + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + txn = Transaction() + txn.add( + sp.add_validator_to_pool_with_vote( + STAKE_POOL_PROGRAM_ID, + stake_pool_address, + stake_pool.staker, + stake_pool.validator_list, + funder.public_key, + validator, + ) + ) + await client.send_transaction( + txn, funder, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def remove_validator_from_pool( + client: AsyncClient, staker: Keypair, + stake_pool_address: PublicKey, validator: PublicKey +): + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator) + destination_stake = Keypair() + txn = Transaction() + txn.add( + sys.create_account( + sys.CreateAccountParams( + from_pubkey=staker.public_key, + new_account_pubkey=destination_stake.public_key, + lamports=0, # will get filled by split + space=STAKE_LEN, + program_id=STAKE_PROGRAM_ID, + ) + ) + ) + txn.add( + sp.remove_validator_from_pool_with_vote( + STAKE_POOL_PROGRAM_ID, + stake_pool_address, + stake_pool.staker, + stake_pool.validator_list, + staker.public_key, + validator, + validator_info.transient_seed_suffix_start, + destination_stake.public_key + ) + ) + await client.send_transaction( + txn, staker, destination_stake, + opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def deposit_sol( + client: AsyncClient, funder: Keypair, stake_pool_address: PublicKey, + destination_token_account: PublicKey, amount: int, +): + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + + (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) + + txn = Transaction() + txn.add( + sp.deposit_sol( + sp.DepositSolParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + withdraw_authority=withdraw_authority, + reserve_stake=stake_pool.reserve_stake, + funding_account=funder.public_key, + destination_pool_account=destination_token_account, + manager_fee_account=stake_pool.manager_fee_account, + referral_pool_account=destination_token_account, + pool_mint=stake_pool.pool_mint, + system_program_id=sys.SYS_PROGRAM_ID, + token_program_id=stake_pool.token_program_id, + amount=amount, + deposit_authority=None, + ) + ) + ) + await client.send_transaction( + txn, funder, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def withdraw_sol( + client: AsyncClient, owner: Keypair, source_token_account: PublicKey, + stake_pool_address: PublicKey, destination_system_account: PublicKey, amount: int, +): + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + + (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) + + txn = Transaction() + txn.add( + sp.withdraw_sol( + sp.WithdrawSolParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + withdraw_authority=withdraw_authority, + source_transfer_authority=owner.public_key, + source_pool_account=source_token_account, + reserve_stake=stake_pool.reserve_stake, + destination_system_account=destination_system_account, + manager_fee_account=stake_pool.manager_fee_account, + pool_mint=stake_pool.pool_mint, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + stake_program_id=STAKE_PROGRAM_ID, + token_program_id=stake_pool.token_program_id, + amount=amount, + sol_withdraw_authority=None, + ) + ) + ) + await client.send_transaction( + txn, owner, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def deposit_stake( + client: AsyncClient, + deposit_stake_authority: Keypair, + stake_pool_address: PublicKey, + validator_vote: PublicKey, + deposit_stake: PublicKey, + destination_pool_account: PublicKey, +): + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + + (withdraw_authority, _) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) + (validator_stake, _) = find_stake_program_address( + STAKE_POOL_PROGRAM_ID, + validator_vote, + stake_pool_address, + ) + + txn = Transaction() + txn.add( + st.authorize( + st.AuthorizeParams( + stake=deposit_stake, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + authority=deposit_stake_authority.public_key, + new_authority=stake_pool.stake_deposit_authority, + stake_authorize=StakeAuthorize.STAKER, + ) + ) + ) + txn.add( + st.authorize( + st.AuthorizeParams( + stake=deposit_stake, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + authority=deposit_stake_authority.public_key, + new_authority=stake_pool.stake_deposit_authority, + stake_authorize=StakeAuthorize.WITHDRAWER, + ) + ) + ) + txn.add( + sp.deposit_stake( + sp.DepositStakeParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + validator_list=stake_pool.validator_list, + deposit_authority=stake_pool.stake_deposit_authority, + withdraw_authority=withdraw_authority, + deposit_stake=deposit_stake, + validator_stake=validator_stake, + reserve_stake=stake_pool.reserve_stake, + destination_pool_account=destination_pool_account, + manager_fee_account=stake_pool.manager_fee_account, + referral_pool_account=destination_pool_account, + pool_mint=stake_pool.pool_mint, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + token_program_id=stake_pool.token_program_id, + stake_program_id=STAKE_PROGRAM_ID, + ) + ) + ) + await client.send_transaction( + txn, deposit_stake_authority, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def withdraw_stake( + client: AsyncClient, + payer: Keypair, + source_transfer_authority: Keypair, + destination_stake: Keypair, + stake_pool_address: PublicKey, + validator_vote: PublicKey, + destination_stake_authority: PublicKey, + source_pool_account: PublicKey, + amount: int, +): + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + + (withdraw_authority, _) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) + (validator_stake, _) = find_stake_program_address( + STAKE_POOL_PROGRAM_ID, + validator_vote, + stake_pool_address, + ) + + resp = await client.get_minimum_balance_for_rent_exemption(STAKE_LEN) + stake_rent_exemption = resp['result'] + + txn = Transaction() + txn.add( + sys.create_account( + sys.CreateAccountParams( + from_pubkey=payer.public_key, + new_account_pubkey=destination_stake.public_key, + lamports=stake_rent_exemption, + space=STAKE_LEN, + program_id=STAKE_PROGRAM_ID, + ) + ) + ) + txn.add( + sp.withdraw_stake( + sp.WithdrawStakeParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + validator_list=stake_pool.validator_list, + withdraw_authority=withdraw_authority, + validator_stake=validator_stake, + destination_stake=destination_stake.public_key, + destination_stake_authority=destination_stake_authority, + source_transfer_authority=source_transfer_authority.public_key, + source_pool_account=source_pool_account, + manager_fee_account=stake_pool.manager_fee_account, + pool_mint=stake_pool.pool_mint, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + token_program_id=stake_pool.token_program_id, + stake_program_id=STAKE_PROGRAM_ID, + amount=amount, + ) + ) + ) + signers = [payer, source_transfer_authority, destination_stake] \ + if payer != source_transfer_authority else [payer, destination_stake] + await client.send_transaction( + txn, *signers, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def update_stake_pool(client: AsyncClient, payer: Keypair, stake_pool_address: PublicKey): + """Create and send all instructions to completely update a stake pool after epoch change.""" + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) + update_list_instructions = [] + validator_chunks = [ + validator_list.validators[i:i+MAX_VALIDATORS_TO_UPDATE] + for i in range(0, len(validator_list.validators), MAX_VALIDATORS_TO_UPDATE) + ] + start_index = 0 + for validator_chunk in validator_chunks: + validator_and_transient_stake_pairs = [] + for validator in validator_chunk: + (validator_stake_address, _) = find_stake_program_address( + STAKE_POOL_PROGRAM_ID, + validator.vote_account_address, + stake_pool_address, + ) + validator_and_transient_stake_pairs.append(validator_stake_address) + (transient_stake_address, _) = find_transient_stake_program_address( + STAKE_POOL_PROGRAM_ID, + validator.vote_account_address, + stake_pool_address, + validator.transient_seed_suffix_start, + ) + validator_and_transient_stake_pairs.append(transient_stake_address) + update_list_instructions.append( + sp.update_validator_list_balance( + sp.UpdateValidatorListBalanceParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + withdraw_authority=withdraw_authority, + validator_list=stake_pool.validator_list, + reserve_stake=stake_pool.reserve_stake, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + stake_program_id=STAKE_PROGRAM_ID, + validator_and_transient_stake_pairs=validator_and_transient_stake_pairs, + start_index=start_index, + no_merge=False, + ) + ) + ) + start_index += MAX_VALIDATORS_TO_UPDATE + if update_list_instructions: + last_instruction = update_list_instructions.pop() + for update_list_instruction in update_list_instructions: + txn = Transaction() + txn.add(update_list_instruction) + await client.send_transaction( + txn, payer, opts=TxOpts(skip_confirmation=True, preflight_commitment=Confirmed)) + txn = Transaction() + txn.add(last_instruction) + await client.send_transaction( + txn, payer, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + txn = Transaction() + txn.add( + sp.update_stake_pool_balance( + sp.UpdateStakePoolBalanceParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + withdraw_authority=withdraw_authority, + validator_list=stake_pool.validator_list, + reserve_stake=stake_pool.reserve_stake, + manager_fee_account=stake_pool.manager_fee_account, + pool_mint=stake_pool.pool_mint, + token_program_id=stake_pool.token_program_id, + ) + ) + ) + txn.add( + sp.cleanup_removed_validator_entries( + sp.CleanupRemovedValidatorEntriesParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + validator_list=stake_pool.validator_list, + ) + ) + ) + await client.send_transaction( + txn, payer, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def increase_validator_stake( + client: AsyncClient, payer: Keypair, staker: Keypair, stake_pool_address: PublicKey, + validator_vote: PublicKey, lamports: int +): + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + + resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) + + validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator_vote) + transient_stake_seed = validator_info.transient_seed_suffix_start + 1 # bump up by one to avoid reuse + (transient_stake, _) = find_transient_stake_program_address( + STAKE_POOL_PROGRAM_ID, + validator_info.vote_account_address, + stake_pool_address, + transient_stake_seed, + ) + + txn = Transaction() + txn.add( + sp.increase_validator_stake( + sp.IncreaseValidatorStakeParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + staker=staker.public_key, + withdraw_authority=withdraw_authority, + validator_list=stake_pool.validator_list, + reserve_stake=stake_pool.reserve_stake, + transient_stake=transient_stake, + validator_vote=validator_vote, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + rent_sysvar=SYSVAR_RENT_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + stake_config_sysvar=SYSVAR_STAKE_CONFIG_ID, + system_program_id=sys.SYS_PROGRAM_ID, + stake_program_id=STAKE_PROGRAM_ID, + lamports=lamports, + transient_stake_seed=transient_stake_seed, + ) + ) + ) + + signers = [payer, staker] if payer != staker else [payer] + await client.send_transaction( + txn, *signers, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + + +async def decrease_validator_stake( + client: AsyncClient, payer: Keypair, staker: Keypair, stake_pool_address: PublicKey, + validator_vote: PublicKey, lamports: int +): + resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + + resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) + + validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator_vote) + (validator_stake, _) = find_stake_program_address( + STAKE_POOL_PROGRAM_ID, + validator_info.vote_account_address, + stake_pool_address, + ) + transient_stake_seed = validator_info.transient_seed_suffix_start + 1 # bump up by one to avoid reuse + (transient_stake, _) = find_transient_stake_program_address( + STAKE_POOL_PROGRAM_ID, + validator_info.vote_account_address, + stake_pool_address, + transient_stake_seed, + ) + + txn = Transaction() + txn.add( + sp.decrease_validator_stake( + sp.DecreaseValidatorStakeParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + staker=staker.public_key, + withdraw_authority=withdraw_authority, + validator_list=stake_pool.validator_list, + validator_stake=validator_stake, + transient_stake=transient_stake, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + rent_sysvar=SYSVAR_RENT_PUBKEY, + system_program_id=sys.SYS_PROGRAM_ID, + stake_program_id=STAKE_PROGRAM_ID, + lamports=lamports, + transient_stake_seed=transient_stake_seed, + ) + ) + ) + + signers = [payer, staker] if payer != staker else [payer] + await client.send_transaction( + txn, *signers, opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) diff --git a/stake-pool/py/stake_pool/constants.py b/stake-pool/py/stake_pool/constants.py new file mode 100644 index 00000000000..6e9f6a35ae5 --- /dev/null +++ b/stake-pool/py/stake_pool/constants.py @@ -0,0 +1,74 @@ +"""SPL Stake Pool Constants.""" + +from typing import Tuple + +from solana.publickey import PublicKey + +STAKE_POOL_PROGRAM_ID: PublicKey = PublicKey("SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy") +"""Public key that identifies the SPL Stake Pool program.""" + +MAX_VALIDATORS_TO_UPDATE: int = 5 +"""Maximum number of validators to update during UpdateValidatorListBalance.""" + + +def find_deposit_authority_program_address( + program_id: PublicKey, + stake_pool_address: PublicKey, +) -> Tuple[PublicKey, int]: + """Generates the deposit authority program address for the stake pool""" + return PublicKey.find_program_address( + [bytes(stake_pool_address), AUTHORITY_DEPOSIT], + program_id, + ) + + +def find_withdraw_authority_program_address( + program_id: PublicKey, + stake_pool_address: PublicKey, +) -> Tuple[PublicKey, int]: + """Generates the withdraw authority program address for the stake pool""" + return PublicKey.find_program_address( + [bytes(stake_pool_address), AUTHORITY_WITHDRAW], + program_id, + ) + + +def find_stake_program_address( + program_id: PublicKey, + vote_account_address: PublicKey, + stake_pool_address: PublicKey, +) -> Tuple[PublicKey, int]: + """Generates the stake program address for a validator's vote account""" + return PublicKey.find_program_address( + [ + bytes(vote_account_address), + bytes(stake_pool_address), + ], + program_id, + ) + + +def find_transient_stake_program_address( + program_id: PublicKey, + vote_account_address: PublicKey, + stake_pool_address: PublicKey, + seed: int, +) -> Tuple[PublicKey, int]: + """Generates the stake program address for a validator's vote account""" + return PublicKey.find_program_address( + [ + TRANSIENT_STAKE_SEED_PREFIX, + bytes(vote_account_address), + bytes(stake_pool_address), + seed.to_bytes(8, 'little'), + ], + program_id, + ) + + +AUTHORITY_DEPOSIT = b"deposit" +"""Seed used to derive the default stake pool deposit authority.""" +AUTHORITY_WITHDRAW = b"withdraw" +"""Seed used to derive the stake pool withdraw authority.""" +TRANSIENT_STAKE_SEED_PREFIX = b"transient" +"""Seed used to derive transient stake accounts.""" diff --git a/stake-pool/py/stake_pool/instructions.py b/stake-pool/py/stake_pool/instructions.py new file mode 100644 index 00000000000..e0f17abf6ee --- /dev/null +++ b/stake-pool/py/stake_pool/instructions.py @@ -0,0 +1,912 @@ +"""SPL Stake Pool Instructions.""" + +from enum import IntEnum +from typing import List, NamedTuple, Optional +from construct import Struct, Switch, Int8ul, Int32ul, Int64ul, Pass # type: ignore + +from solana.publickey import PublicKey +from solana.transaction import AccountMeta, TransactionInstruction +from solana.system_program import SYS_PROGRAM_ID +from solana.sysvar import SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, SYSVAR_STAKE_HISTORY_PUBKEY +from spl.token.constants import TOKEN_PROGRAM_ID + +from stake.constants import STAKE_PROGRAM_ID, SYSVAR_STAKE_CONFIG_ID +from stake_pool.constants import find_stake_program_address, find_transient_stake_program_address +from stake_pool.constants import find_withdraw_authority_program_address +from stake_pool.constants import STAKE_POOL_PROGRAM_ID +from stake_pool.state import Fee, FEE_LAYOUT + + +class PreferredValidatorType(IntEnum): + """Specifies the validator type for SetPreferredValidator instruction.""" + + DEPOSIT = 0 + """Specifies the preferred deposit validator.""" + WITHDRAW = 1 + """Specifies the preferred withdraw validator.""" + + +class FundingType(IntEnum): + """Defines which authority to update in the `SetFundingAuthority` instruction.""" + + STAKE_DEPOSIT = 0 + """Sets the stake deposit authority.""" + SOL_DEPOSIT = 1 + """Sets the SOL deposit authority.""" + SOL_WITHDRAW = 2 + """Sets the SOL withdraw authority.""" + + +class InitializeParams(NamedTuple): + """Initialize token mint transaction params.""" + + # Accounts + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """[w] Stake Pool account to initialize.""" + manager: PublicKey + """[s] Manager for new stake pool.""" + staker: PublicKey + """[] Staker for the new stake pool.""" + validator_list: PublicKey + """[w] Uninitialized validator list account for the new stake pool.""" + reserve_stake: PublicKey + """[] Reserve stake account.""" + pool_mint: PublicKey + """[] Pool token mint account.""" + manager_fee_account: PublicKey + """[] Manager's fee account""" + token_program_id: PublicKey + """[] SPL Token program id.""" + + # Params + epoch_fee: Fee + """Fee assessed as percentage of rewards.""" + withdrawal_fee: Fee + """Fee charged per withdrawal.""" + deposit_fee: Fee + """Fee charged per deposit.""" + referral_fee: int + """Percentage [0-100] of deposit fee that goes to referrer.""" + max_validators: int + """Maximum number of possible validators in the pool.""" + + # Optional + deposit_authority: Optional[PublicKey] = None + """[] Optional deposit authority that must sign all deposits.""" + + +class AddValidatorToPoolParams(NamedTuple): + """(Staker only) Adds stake account delegated to validator to the pool's list of managed validators.""" + + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[w]` Stake pool.""" + staker: PublicKey + """`[s]` Staker.""" + funding_account: PublicKey + """`[ws]` Funding account (must be a system account).""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + validator_stake: PublicKey + """`[w]` Stake account to add to the pool.""" + validator_vote: PublicKey + """`[]` Validator this stake account will be delegated to.""" + rent_sysvar: PublicKey + """`[]` Rent sysvar.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + stake_history_sysvar: PublicKey + """'[]' Stake history sysvar.""" + stake_config_sysvar: PublicKey + """'[]' Stake config sysvar.""" + system_program_id: PublicKey + """`[]` System program.""" + stake_program_id: PublicKey + """`[]` Stake program.""" + + +class RemoveValidatorFromPoolParams(NamedTuple): + """(Staker only) Removes validator from the pool.""" + + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[w]` Stake pool.""" + staker: PublicKey + """`[s]` Staker.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + new_stake_authority: PublicKey + """`[]` New stake / withdraw authority on the split stake account.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + validator_stake: PublicKey + """`[w]` Stake account to remove from the pool.""" + transient_stake: PublicKey + """`[]` Transient stake account, to check that there's no activation ongoing.""" + destination_stake: PublicKey + """`[w]` Destination stake account, to receive the minimum SOL from the validator stake account.""" + clock_sysvar: PublicKey + """'[]' Stake config sysvar.""" + stake_program_id: PublicKey + """`[]` Stake program.""" + + +class DecreaseValidatorStakeParams(NamedTuple): + """(Staker only) Decrease active stake on a validator, eventually moving it to the reserve""" + + # Accounts + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[]` Stake pool.""" + staker: PublicKey + """`[s]` Staker.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + validator_stake: PublicKey + """`[w]` Canonical stake to split from.""" + transient_stake: PublicKey + """`[w]` Transient stake account to receive split.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + rent_sysvar: PublicKey + """`[]` Rent sysvar.""" + system_program_id: PublicKey + """`[]` System program.""" + stake_program_id: PublicKey + """`[]` Stake program.""" + + # Params + lamports: int + """Amount of lamports to split into the transient stake account.""" + transient_stake_seed: int + """Seed to used to create the transient stake account.""" + + +class IncreaseValidatorStakeParams(NamedTuple): + """(Staker only) Increase stake on a validator from the reserve account.""" + + # Accounts + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[]` Stake pool.""" + staker: PublicKey + """`[s]` Staker.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + reserve_stake: PublicKey + """`[w]` Stake pool's reserve.""" + transient_stake: PublicKey + """`[w]` Transient stake account to receive split.""" + validator_vote: PublicKey + """`[]` Validator vote account to delegate to.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + rent_sysvar: PublicKey + """`[]` Rent sysvar.""" + stake_history_sysvar: PublicKey + """'[]' Stake history sysvar.""" + stake_config_sysvar: PublicKey + """'[]' Stake config sysvar.""" + system_program_id: PublicKey + """`[]` System program.""" + stake_program_id: PublicKey + """`[]` Stake program.""" + + # Params + lamports: int + """Amount of lamports to split into the transient stake account.""" + transient_stake_seed: int + """Seed to used to create the transient stake account.""" + + +class SetPreferredValidatorParams(NamedTuple): + pass + + +class UpdateValidatorListBalanceParams(NamedTuple): + """Updates balances of validator and transient stake accounts in the pool.""" + + # Accounts + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[]` Stake pool.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + reserve_stake: PublicKey + """`[w]` Stake pool's reserve.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + stake_history_sysvar: PublicKey + """'[]' Stake history sysvar.""" + stake_program_id: PublicKey + """`[]` Stake program.""" + validator_and_transient_stake_pairs: List[PublicKey] + """[] N pairs of validator and transient stake accounts""" + + # Params + start_index: int + """Index to start updating on the validator list.""" + no_merge: bool + """If true, don't try merging transient stake accounts.""" + + +class UpdateStakePoolBalanceParams(NamedTuple): + """Updates total pool balance based on balances in the reserve and validator list.""" + + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[w]` Stake pool.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + reserve_stake: PublicKey + """`[w]` Stake pool's reserve.""" + manager_fee_account: PublicKey + """`[w]` Account to receive pool fee tokens.""" + pool_mint: PublicKey + """`[w]` Pool mint account.""" + token_program_id: PublicKey + """`[]` Pool token program.""" + + +class CleanupRemovedValidatorEntriesParams(NamedTuple): + """Cleans up validator stake account entries marked as `ReadyForRemoval`""" + + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[w]` Stake pool.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + + +class DepositStakeParams(NamedTuple): + """Deposits a stake account into the pool in exchange for pool tokens""" + + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[w]` Stake pool""" + validator_list: PublicKey + """`[w]` Validator stake list storage account""" + deposit_authority: PublicKey + """`[s]/[]` Stake pool deposit authority""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority""" + deposit_stake: PublicKey + """`[w]` Stake account to join the pool (stake's withdraw authority set to the stake pool deposit authority)""" + validator_stake: PublicKey + """`[w]` Validator stake account for the stake account to be merged with""" + reserve_stake: PublicKey + """`[w]` Reserve stake account, to withdraw rent exempt reserve""" + destination_pool_account: PublicKey + """`[w]` User account to receive pool tokens""" + manager_fee_account: PublicKey + """`[w]` Account to receive pool fee tokens""" + referral_pool_account: PublicKey + """`[w]` Account to receive a portion of pool fee tokens as referral fees""" + pool_mint: PublicKey + """`[w]` Pool token mint account""" + clock_sysvar: PublicKey + """`[]` Sysvar clock account""" + stake_history_sysvar: PublicKey + """`[]` Sysvar stake history account""" + token_program_id: PublicKey + """`[]` Pool token program id""" + stake_program_id: PublicKey + """`[]` Stake program id""" + + +class WithdrawStakeParams(NamedTuple): + """Withdraws a stake account from the pool in exchange for pool tokens""" + + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[w]` Stake pool""" + validator_list: PublicKey + """`[w]` Validator stake list storage account""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority""" + validator_stake: PublicKey + """`[w]` Validator or reserve stake account to split""" + destination_stake: PublicKey + """`[w]` Unitialized stake account to receive withdrawal""" + destination_stake_authority: PublicKey + """`[]` User account to set as a new withdraw authority""" + source_transfer_authority: PublicKey + """`[s]` User transfer authority, for pool token account""" + source_pool_account: PublicKey + """`[w]` User account with pool tokens to burn from""" + manager_fee_account: PublicKey + """`[w]` Account to receive pool fee tokens""" + pool_mint: PublicKey + """`[w]` Pool token mint account""" + clock_sysvar: PublicKey + """`[]` Sysvar clock account""" + token_program_id: PublicKey + """`[]` Pool token program id""" + stake_program_id: PublicKey + """`[]` Stake program id""" + + # Params + amount: int + """Amount of pool tokens to burn in exchange for stake""" + + +class SetManagerParams(NamedTuple): + pass + + +class SetFeeParams(NamedTuple): + pass + + +class SetStakerParams(NamedTuple): + pass + + +class DepositSolParams(NamedTuple): + """Deposit SOL directly into the pool's reserve account. The output is a "pool" token + representing ownership into the pool. Inputs are converted to the current ratio.""" + + # Accounts + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[w]` Stake pool.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + reserve_stake: PublicKey + """`[w]` Stake pool's reserve.""" + funding_account: PublicKey + """`[ws]` Funding account (must be a system account).""" + destination_pool_account: PublicKey + """`[w]` User account to receive pool tokens.""" + manager_fee_account: PublicKey + """`[w]` Manager's pool token account to receive deposit fee.""" + referral_pool_account: PublicKey + """`[w]` Referrer pool token account to receive referral fee.""" + pool_mint: PublicKey + """`[w]` Pool token mint.""" + system_program_id: PublicKey + """`[]` System program.""" + token_program_id: PublicKey + """`[]` Token program.""" + + # Params + amount: int + """Amount of SOL to deposit""" + + # Optional + deposit_authority: Optional[PublicKey] = None + """`[s]` (Optional) Stake pool sol deposit authority.""" + + +class SetFundingAuthorityParams(NamedTuple): + pass + + +class WithdrawSolParams(NamedTuple): + """Withdraw SOL directly from the pool's reserve account.""" + + # Accounts + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[w]` Stake pool.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + source_transfer_authority: PublicKey + """`[s]` Transfer authority for user pool token account.""" + source_pool_account: PublicKey + """`[w]` User's pool token account to burn pool tokens.""" + reserve_stake: PublicKey + """`[w]` Stake pool's reserve.""" + destination_system_account: PublicKey + """`[w]` Destination system account to receive lamports from the reserve.""" + manager_fee_account: PublicKey + """`[w]` Manager's pool token account to receive fee.""" + pool_mint: PublicKey + """`[w]` Pool token mint.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + stake_history_sysvar: PublicKey + """'[]' Stake history sysvar.""" + stake_program_id: PublicKey + """`[]` Stake program.""" + token_program_id: PublicKey + """`[]` Token program.""" + + # Params + amount: int + """Amount of pool tokens to burn""" + + # Optional + sol_withdraw_authority: Optional[PublicKey] = None + """`[s]` (Optional) Stake pool sol withdraw authority.""" + + +class InstructionType(IntEnum): + """Stake Pool Instruction Types.""" + + INITIALIZE = 0 + ADD_VALIDATOR_TO_POOL = 1 + REMOVE_VALIDATOR_FROM_POOL = 2 + DECREASE_VALIDATOR_STAKE = 3 + INCREASE_VALIDATOR_STAKE = 4 + SET_PREFERRED_VALIDATOR = 5 + UPDATE_VALIDATOR_LIST_BALANCE = 6 + UPDATE_STAKE_POOL_BALANCE = 7 + CLEANUP_REMOVED_VALIDATOR_ENTRIES = 8 + DEPOSIT_STAKE = 9 + WITHDRAW_STAKE = 10 + SET_MANAGER = 11 + SET_FEE = 12 + SET_STAKER = 13 + DEPOSIT_SOL = 14 + SET_FUNDING_AUTHORITY = 15 + WITHDRAW_SOL = 16 + + +INITIALIZE_LAYOUT = Struct( + "epoch_fee" / FEE_LAYOUT, + "withdrawal_fee" / FEE_LAYOUT, + "deposit_fee" / FEE_LAYOUT, + "referral_fee" / Int8ul, + "max_validators" / Int32ul, +) + +MOVE_STAKE_LAYOUT = Struct( + "lamports" / Int64ul, + "transient_stake_seed" / Int64ul, +) + +UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT = Struct( + "start_index" / Int32ul, + "no_merge" / Int8ul, +) + +AMOUNT_LAYOUT = Struct( + "amount" / Int64ul +) + +INSTRUCTIONS_LAYOUT = Struct( + "instruction_type" / Int8ul, + "args" + / Switch( + lambda this: this.instruction_type, + { + InstructionType.INITIALIZE: INITIALIZE_LAYOUT, + InstructionType.ADD_VALIDATOR_TO_POOL: Pass, + InstructionType.REMOVE_VALIDATOR_FROM_POOL: Pass, + InstructionType.DECREASE_VALIDATOR_STAKE: MOVE_STAKE_LAYOUT, + InstructionType.INCREASE_VALIDATOR_STAKE: MOVE_STAKE_LAYOUT, + InstructionType.SET_PREFERRED_VALIDATOR: Pass, # TODO + InstructionType.UPDATE_VALIDATOR_LIST_BALANCE: UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT, + InstructionType.UPDATE_STAKE_POOL_BALANCE: Pass, + InstructionType.CLEANUP_REMOVED_VALIDATOR_ENTRIES: Pass, + InstructionType.DEPOSIT_STAKE: Pass, + InstructionType.WITHDRAW_STAKE: AMOUNT_LAYOUT, + InstructionType.SET_MANAGER: Pass, + InstructionType.SET_FEE: Pass, # TODO + InstructionType.SET_STAKER: Pass, + InstructionType.DEPOSIT_SOL: AMOUNT_LAYOUT, + InstructionType.SET_FUNDING_AUTHORITY: Pass, # TODO + InstructionType.WITHDRAW_SOL: AMOUNT_LAYOUT, + }, + ), +) + + +def initialize(params: InitializeParams) -> TransactionInstruction: + """Creates a transaction instruction to initialize a new stake pool.""" + + data = INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.INITIALIZE, + args=dict( + epoch_fee=params.epoch_fee._asdict(), + withdrawal_fee=params.withdrawal_fee._asdict(), + deposit_fee=params.deposit_fee._asdict(), + referral_fee=params.referral_fee, + max_validators=params.max_validators + ), + ) + ) + keys = [ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.manager, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.staker, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=False), + AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), + ] + if params.deposit_authority: + keys.append( + AccountMeta(pubkey=params.deposit_authority, is_signer=True, is_writable=False), + ) + return TransactionInstruction( + keys=keys, + program_id=params.program_id, + data=data, + ) + + +def add_validator_to_pool(params: AddValidatorToPoolParams) -> TransactionInstruction: + """Creates instruction to add a validator to the pool.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.funding_account, is_signer=True, is_writable=True), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_vote, is_signer=False, is_writable=False), + 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.stake_history_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_config_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.ADD_VALIDATOR_TO_POOL, + args=None + ) + ) + ) + + +def add_validator_to_pool_with_vote( + program_id: PublicKey, + stake_pool: PublicKey, + staker: PublicKey, + validator_list: PublicKey, + funder: PublicKey, + validator: PublicKey +) -> TransactionInstruction: + """Creates instruction to add a validator based on their vote account address.""" + (withdraw_authority, seed) = find_withdraw_authority_program_address(program_id, stake_pool) + (validator_stake, seed) = find_stake_program_address(program_id, validator, stake_pool) + return add_validator_to_pool( + AddValidatorToPoolParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool, + staker=staker, + funding_account=funder, + withdraw_authority=withdraw_authority, + validator_list=validator_list, + validator_stake=validator_stake, + validator_vote=validator, + rent_sysvar=SYSVAR_RENT_PUBKEY, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + stake_config_sysvar=SYSVAR_STAKE_CONFIG_ID, + system_program_id=SYS_PROGRAM_ID, + stake_program_id=STAKE_PROGRAM_ID, + ) + ) + + +def remove_validator_from_pool(params: RemoveValidatorFromPoolParams) -> TransactionInstruction: + """Creates instruction to remove a validator from the pool.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.new_stake_authority, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.destination_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.REMOVE_VALIDATOR_FROM_POOL, + args=None + ) + ) + ) + + +def remove_validator_from_pool_with_vote( + program_id: PublicKey, + stake_pool: PublicKey, + staker: PublicKey, + validator_list: PublicKey, + new_stake_authority: PublicKey, + validator: PublicKey, + transient_stake_seed: int, + destination_stake: PublicKey, +) -> TransactionInstruction: + """Creates instruction to remove a validator based on their vote account address.""" + (withdraw_authority, seed) = find_withdraw_authority_program_address(program_id, stake_pool) + (validator_stake, seed) = find_stake_program_address(program_id, validator, stake_pool) + (transient_stake, seed) = find_transient_stake_program_address( + program_id, validator, stake_pool, transient_stake_seed) + return remove_validator_from_pool( + RemoveValidatorFromPoolParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool, + staker=staker, + withdraw_authority=withdraw_authority, + new_stake_authority=new_stake_authority, + validator_list=validator_list, + validator_stake=validator_stake, + transient_stake=transient_stake, + destination_stake=destination_stake, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + stake_program_id=STAKE_PROGRAM_ID, + ) + ) + + +def deposit_stake(params: DepositStakeParams) -> TransactionInstruction: + """Creates a transaction instruction to deposit SOL into a stake pool.""" + keys = [ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.deposit_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.deposit_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.destination_pool_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.referral_pool_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ] + return TransactionInstruction( + keys=keys, + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.DEPOSIT_STAKE, + args=None, + ) + ) + ) + + +def withdraw_stake(params: WithdrawStakeParams) -> TransactionInstruction: + """Creates a transaction instruction to withdraw SOL from a stake pool.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.destination_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.destination_stake_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.source_transfer_authority, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.source_pool_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.WITHDRAW_STAKE, + args={'amount': params.amount} + ) + ) + ) + + +def deposit_sol(params: DepositSolParams) -> TransactionInstruction: + """Creates a transaction instruction to deposit SOL into a stake pool.""" + keys = [ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.funding_account, is_signer=True, is_writable=True), + AccountMeta(pubkey=params.destination_pool_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.referral_pool_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), + ] + if params.deposit_authority: + keys.append(AccountMeta(pubkey=params.deposit_authority, is_signer=True, is_writable=False)) + return TransactionInstruction( + keys=keys, + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.DEPOSIT_SOL, + args={'amount': params.amount} + ) + ) + ) + + +def withdraw_sol(params: WithdrawSolParams) -> TransactionInstruction: + """Creates a transaction instruction to withdraw SOL from a stake pool.""" + keys = [ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.source_transfer_authority, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.source_pool_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.destination_system_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), + ] + + if params.sol_withdraw_authority: + AccountMeta(pubkey=params.sol_withdraw_authority, is_signer=True, is_writable=False) + + return TransactionInstruction( + keys=keys, + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.WITHDRAW_SOL, + args={'amount': params.amount} + ) + ) + ) + + +def update_validator_list_balance(params: UpdateValidatorListBalanceParams) -> TransactionInstruction: + """Creates instruction to update a set of validators in the stake pool.""" + keys = [ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ] + keys.extend([ + AccountMeta(pubkey=pubkey, is_signer=False, is_writable=True) + for pubkey in params.validator_and_transient_stake_pairs + ]) + return TransactionInstruction( + keys=keys, + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.UPDATE_VALIDATOR_LIST_BALANCE, + args={'start_index': params.start_index, 'no_merge': params.no_merge} + ) + ) + ) + + +def update_stake_pool_balance(params: UpdateStakePoolBalanceParams) -> TransactionInstruction: + """Creates instruction to update the overall stake pool balance.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.UPDATE_STAKE_POOL_BALANCE, + args=None, + ) + ) + ) + + +def cleanup_removed_validator_entries(params: CleanupRemovedValidatorEntriesParams) -> TransactionInstruction: + """Creates instruction to cleanup removed validator entries.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.CLEANUP_REMOVED_VALIDATOR_ENTRIES, + args=None, + ) + ) + ) + + +def increase_validator_stake(params: IncreaseValidatorStakeParams) -> TransactionInstruction: + """Creates instruction to increase the stake on a validator.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_vote, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.rent_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_config_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.INCREASE_VALIDATOR_STAKE, + args={ + 'lamports': params.lamports, + 'transient_stake_seed': params.transient_stake_seed + } + ) + ) + ) + + +def decrease_validator_stake(params: DecreaseValidatorStakeParams) -> TransactionInstruction: + """Creates instruction to decrease the stake on a validator.""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.rent_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.DECREASE_VALIDATOR_STAKE, + args={ + 'lamports': params.lamports, + 'transient_stake_seed': params.transient_stake_seed + } + ) + ) + ) diff --git a/stake-pool/py/stake_pool/state.py b/stake-pool/py/stake_pool/state.py new file mode 100644 index 00000000000..93acc103c15 --- /dev/null +++ b/stake-pool/py/stake_pool/state.py @@ -0,0 +1,321 @@ +"""SPL Stake Pool State.""" + +from enum import IntEnum +from typing import List, NamedTuple, Optional +from construct import Container, Struct, Switch, Int8ul, Int32ul, Int64ul, Pass # type: ignore + +from solana.publickey import PublicKey +from solana.utils.helpers import decode_byte_string +from solana._layouts.shared import PUBLIC_KEY_LAYOUT +from stake.state import Lockup, LOCKUP_LAYOUT + + +def decode_optional_publickey(container: Container) -> Optional[PublicKey]: + if container: + return PublicKey(container) + else: + return None + + +class Fee(NamedTuple): + """Fee assessed by the stake pool, expressed as numerator / denominator.""" + numerator: int + denominator: int + + @classmethod + def decode_container(cls, container: Container): + return Fee( + numerator=container['numerator'], + denominator=container['denominator'], + ) + + @classmethod + def decode_optional_container(cls, container: Container): + if container: + return cls.decode_container(container) + else: + return None + + +class StakePool(NamedTuple): + """Stake pool and all its data.""" + manager: PublicKey + staker: PublicKey + stake_deposit_authority: PublicKey + stake_withdraw_bump_seed: int + validator_list: PublicKey + reserve_stake: PublicKey + pool_mint: PublicKey + manager_fee_account: PublicKey + token_program_id: PublicKey + total_lamports: int + pool_token_supply: int + last_update_epoch: int + lockup: Lockup + epoch_fee: Fee + next_epoch_fee: Optional[Fee] + preferred_deposit_validator: Optional[PublicKey] + preferred_withdraw_validator: Optional[PublicKey] + stake_deposit_fee: Fee + stake_withdrawal_fee: Fee + next_stake_withdrawal_fee: Optional[Fee] + stake_referral_fee: int + sol_deposit_authority: Optional[PublicKey] + sol_deposit_fee: Fee + sol_referral_fee: int + sol_withdraw_authority: Optional[PublicKey] + sol_withdrawal_fee: Fee + next_sol_withdrawal_fee: Optional[Fee] + last_epoch_pool_token_supply: int + last_epoch_total_lamports: int + + @classmethod + def decode(cls, data: str, encoding: str): + data_bytes = decode_byte_string(data, encoding) + parsed = DECODE_STAKE_POOL_LAYOUT.parse(data_bytes) + return StakePool( + manager=PublicKey(parsed['manager']), + staker=PublicKey(parsed['staker']), + stake_deposit_authority=PublicKey(parsed['stake_deposit_authority']), + stake_withdraw_bump_seed=parsed['stake_withdraw_bump_seed'], + validator_list=PublicKey(parsed['validator_list']), + reserve_stake=PublicKey(parsed['reserve_stake']), + pool_mint=PublicKey(parsed['pool_mint']), + manager_fee_account=PublicKey(parsed['manager_fee_account']), + token_program_id=PublicKey(parsed['token_program_id']), + total_lamports=parsed['total_lamports'], + pool_token_supply=parsed['pool_token_supply'], + last_update_epoch=parsed['last_update_epoch'], + lockup=Lockup.decode_container(parsed['lockup']), + epoch_fee=Fee.decode_container(parsed['epoch_fee']), + next_epoch_fee=Fee.decode_optional_container(parsed['next_epoch_fee']), + preferred_deposit_validator=decode_optional_publickey(parsed['preferred_deposit_validator']), + preferred_withdraw_validator=decode_optional_publickey(parsed['preferred_withdraw_validator']), + stake_deposit_fee=Fee.decode_container(parsed['stake_deposit_fee']), + stake_withdrawal_fee=Fee.decode_container(parsed['stake_withdrawal_fee']), + next_stake_withdrawal_fee=Fee.decode_optional_container(parsed['next_stake_withdrawal_fee']), + stake_referral_fee=parsed['stake_referral_fee'], + sol_deposit_authority=decode_optional_publickey(parsed['sol_deposit_authority']), + sol_deposit_fee=Fee.decode_container(parsed['sol_deposit_fee']), + sol_referral_fee=parsed['sol_referral_fee'], + sol_withdraw_authority=decode_optional_publickey(parsed['sol_withdraw_authority']), + sol_withdrawal_fee=Fee.decode_container(parsed['sol_withdrawal_fee']), + next_sol_withdrawal_fee=Fee.decode_optional_container(parsed['next_sol_withdrawal_fee']), + last_epoch_pool_token_supply=parsed['last_epoch_pool_token_supply'], + last_epoch_total_lamports=parsed['last_epoch_total_lamports'], + ) + + +class StakeStatus(IntEnum): + """Specifies the status of a stake on a validator in a stake pool.""" + + ACTIVE = 0 + """Stake is active and normal.""" + DEACTIVATING_TRANSIENT = 1 + """Stake has been removed, but a deactivating transient stake still exists.""" + READY_FOR_REMOVAL = 2 + """No more validator stake accounts exist, entry ready for removal.""" + + +class ValidatorStakeInfo(NamedTuple): + active_stake_lamports: int + """Amount of active stake delegated to this validator.""" + + transient_stake_lamports: int + """Amount of transient stake delegated to this validator.""" + + last_update_epoch: int + """Last epoch the active and transient stake lamports fields were updated.""" + + transient_seed_suffix_start: int + """Start of the validator transient account seed suffixes.""" + + transient_seed_suffix_end: int + """End of the validator transient account seed suffixes.""" + + status: StakeStatus + """Status of the validator stake account.""" + + vote_account_address: PublicKey + """Validator vote account address.""" + + @classmethod + def decode_container(cls, container: Container): + return ValidatorStakeInfo( + active_stake_lamports=container['active_stake_lamports'], + transient_stake_lamports=container['transient_stake_lamports'], + last_update_epoch=container['last_update_epoch'], + transient_seed_suffix_start=container['transient_seed_suffix_start'], + transient_seed_suffix_end=container['transient_seed_suffix_end'], + status=container['status'], + vote_account_address=PublicKey(container['vote_account_address']), + ) + + +class ValidatorList(NamedTuple): + """List of validators and amount staked, associated to a stake pool.""" + + max_validators: int + """Maximum number of validators possible in the list.""" + + validators: List[ValidatorStakeInfo] + """Info for each validator in the stake pool.""" + + @staticmethod + def calculate_validator_list_size(max_validators: int) -> int: + layout = VALIDATOR_LIST_LAYOUT + VALIDATOR_INFO_LAYOUT[max_validators] + return layout.sizeof() + + @classmethod + def decode(cls, data: str, encoding: str): + data_bytes = decode_byte_string(data, encoding) + parsed = DECODE_VALIDATOR_LIST_LAYOUT.parse(data_bytes) + print(parsed) + return ValidatorList( + max_validators=parsed['max_validators'], + validators=[ValidatorStakeInfo.decode_container(container) for container in parsed['validators']], + ) + + +FEE_LAYOUT = Struct( + "denominator" / Int64ul, + "numerator" / Int64ul, +) + +STAKE_POOL_LAYOUT = Struct( + "account_type" / Int8ul, + "manager" / PUBLIC_KEY_LAYOUT, + "staker" / PUBLIC_KEY_LAYOUT, + "stake_deposit_authority" / PUBLIC_KEY_LAYOUT, + "stake_withdraw_bump_seed" / Int8ul, + "validator_list" / PUBLIC_KEY_LAYOUT, + "reserve_stake" / PUBLIC_KEY_LAYOUT, + "pool_mint" / PUBLIC_KEY_LAYOUT, + "manager_fee_account" / PUBLIC_KEY_LAYOUT, + "token_program_id" / PUBLIC_KEY_LAYOUT, + "total_lamports" / Int64ul, + "pool_token_supply" / Int64ul, + "last_update_epoch" / Int64ul, + "lockup" / LOCKUP_LAYOUT, + "epoch_fee" / FEE_LAYOUT, + "next_epoch_fee_option" / Int8ul, + "next_epoch_fee" / FEE_LAYOUT, + "preferred_deposit_validator_option" / Int8ul, + "preferred_deposit_validator" / PUBLIC_KEY_LAYOUT, + "preferred_withdraw_validator_option" / Int8ul, + "preferred_withdraw_validator" / PUBLIC_KEY_LAYOUT, + "stake_deposit_fee" / FEE_LAYOUT, + "stake_withdrawal_fee" / FEE_LAYOUT, + "next_stake_withdrawal_fee_option" / Int8ul, + "next_stake_withdrawal_fee" / FEE_LAYOUT, + "stake_referral_fee" / Int8ul, + "sol_deposit_authority_option" / Int8ul, + "sol_deposit_authority" / PUBLIC_KEY_LAYOUT, + "sol_deposit_fee" / FEE_LAYOUT, + "sol_referral_fee" / Int8ul, + "sol_withdraw_authority_option" / Int8ul, + "sol_withdraw_authority" / PUBLIC_KEY_LAYOUT, + "sol_withdrawal_fee" / FEE_LAYOUT, + "next_sol_withdrawal_fee_option" / Int8ul, + "next_sol_withdrawal_fee" / FEE_LAYOUT, + "last_epoch_pool_token_supply" / Int64ul, + "last_epoch_total_lamports" / Int64ul, +) + +DECODE_STAKE_POOL_LAYOUT = Struct( + "account_type" / Int8ul, + "manager" / PUBLIC_KEY_LAYOUT, + "staker" / PUBLIC_KEY_LAYOUT, + "stake_deposit_authority" / PUBLIC_KEY_LAYOUT, + "stake_withdraw_bump_seed" / Int8ul, + "validator_list" / PUBLIC_KEY_LAYOUT, + "reserve_stake" / PUBLIC_KEY_LAYOUT, + "pool_mint" / PUBLIC_KEY_LAYOUT, + "manager_fee_account" / PUBLIC_KEY_LAYOUT, + "token_program_id" / PUBLIC_KEY_LAYOUT, + "total_lamports" / Int64ul, + "pool_token_supply" / Int64ul, + "last_update_epoch" / Int64ul, + "lockup" / LOCKUP_LAYOUT, + "epoch_fee" / FEE_LAYOUT, + "next_epoch_fee_option" / Int8ul, + "next_epoch_fee" / Switch( + lambda this: this.next_epoch_fee_option, + { + 0: Pass, + 1: FEE_LAYOUT, + }), + "preferred_deposit_validator_option" / Int8ul, + "preferred_deposit_validator" / Switch( + lambda this: this.preferred_deposit_validator_option, + { + 0: Pass, + 1: PUBLIC_KEY_LAYOUT, + }), + "preferred_withdraw_validator_option" / Int8ul, + "preferred_withdraw_validator" / Switch( + lambda this: this.preferred_withdraw_validator_option, + { + 0: Pass, + 1: PUBLIC_KEY_LAYOUT, + }), + "stake_deposit_fee" / FEE_LAYOUT, + "stake_withdrawal_fee" / FEE_LAYOUT, + "next_stake_withdrawal_fee_option" / Int8ul, + "next_stake_withdrawal_fee" / Switch( + lambda this: this.next_stake_withdrawal_fee_option, + { + 0: Pass, + 1: FEE_LAYOUT, + }), + "stake_referral_fee" / Int8ul, + "sol_deposit_authority_option" / Int8ul, + "sol_deposit_authority" / Switch( + lambda this: this.sol_deposit_authority_option, + { + 0: Pass, + 1: PUBLIC_KEY_LAYOUT, + }), + "sol_deposit_fee" / FEE_LAYOUT, + "sol_referral_fee" / Int8ul, + "sol_withdraw_authority_option" / Int8ul, + "sol_withdraw_authority" / Switch( + lambda this: this.sol_withdraw_authority_option, + { + 0: Pass, + 1: PUBLIC_KEY_LAYOUT, + }), + "sol_withdrawal_fee" / FEE_LAYOUT, + "next_sol_withdrawal_fee_option" / Int8ul, + "next_sol_withdrawal_fee" / Switch( + lambda this: this.next_sol_withdrawal_fee_option, + { + 0: Pass, + 1: FEE_LAYOUT, + }), + "last_epoch_pool_token_supply" / Int64ul, + "last_epoch_total_lamports" / Int64ul, +) + +VALIDATOR_INFO_LAYOUT = Struct( + "active_stake_lamports" / Int64ul, + "transient_stake_lamports" / Int64ul, + "last_update_epoch" / Int64ul, + "transient_seed_suffix_start" / Int64ul, + "transient_seed_suffix_end" / Int64ul, + "status" / Int8ul, + "vote_account_address" / PUBLIC_KEY_LAYOUT, +) + +VALIDATOR_LIST_LAYOUT = Struct( + "account_type" / Int8ul, + "max_validators" / Int32ul, + "validators_len" / Int32ul, +) + +DECODE_VALIDATOR_LIST_LAYOUT = Struct( + "account_type" / Int8ul, + "max_validators" / Int32ul, + "validators_len" / Int32ul, + "validators" / VALIDATOR_INFO_LAYOUT[lambda this: this.validators_len], +) 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/system/actions.py b/stake-pool/py/system/actions.py new file mode 100644 index 00000000000..0b16a04587f --- /dev/null +++ b/stake-pool/py/system/actions.py @@ -0,0 +1,9 @@ +from solana.publickey import PublicKey +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed + + +async def airdrop(client: AsyncClient, receiver: PublicKey, lamports: int): + print(f"Airdropping {lamports} lamports to {receiver}...") + resp = await client.request_airdrop(receiver, lamports, Confirmed) + await client.confirm_transaction(resp['result'], Confirmed) diff --git a/stake-pool/py/tests/conftest.py b/stake-pool/py/tests/conftest.py new file mode 100644 index 00000000000..32fa07975d4 --- /dev/null +++ b/stake-pool/py/tests/conftest.py @@ -0,0 +1,95 @@ +import asyncio +import pytest +import os +import shutil +import tempfile +import time +from typing import Iterator, List, Tuple +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 +from stake_pool.actions import create_all, add_validator_to_pool +from stake_pool.state import Fee + + +@pytest.fixture(scope="session") +def solana_test_validator(): + old_cwd = os.getcwd() + newpath = tempfile.mkdtemp() + os.chdir(newpath) + validator = Popen([ + "solana-test-validator", + "--reset", "--quiet", + "--bpf-program", "SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy", + f"{old_cwd}/../../target/deploy/spl_stake_pool.so", + "--slots-per-epoch", "64", + ],) + yield + validator.kill() + os.chdir(old_cwd) + shutil.rmtree(newpath) + + +@pytest.fixture +def validators(event_loop, async_client, payer) -> List[PublicKey]: + num_validators = 3 + validators = [] + futures = [] + for i in range(num_validators): + 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 + + +@pytest.fixture +def stake_pool_addresses(event_loop, async_client, payer, validators) -> Tuple[PublicKey, PublicKey]: + fee = Fee(numerator=1, denominator=1000) + referral_fee = 20 + stake_pool_addresses = event_loop.run_until_complete( + create_all(async_client, payer, fee, referral_fee) + ) + futures = [ + add_validator_to_pool(async_client, payer, stake_pool_addresses[0], validator) + for validator in validators + ] + event_loop.run_until_complete(asyncio.gather(*futures)) + return stake_pool_addresses + + +@pytest.fixture +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + loop.close() + + +@pytest.fixture +def async_client(event_loop, solana_test_validator) -> Iterator[AsyncClient]: + async_client = AsyncClient(commitment=Confirmed) + total_attempts = 10 + current_attempt = 0 + while not event_loop.run_until_complete(async_client.is_connected()): + if current_attempt == total_attempts: + raise Exception("Could not connect to test validator") + else: + current_attempt += 1 + 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_a_time_sensitive.py b/stake-pool/py/tests/test_a_time_sensitive.py new file mode 100644 index 00000000000..e32679832d8 --- /dev/null +++ b/stake-pool/py/tests/test_a_time_sensitive.py @@ -0,0 +1,76 @@ +"""Time sensitive test, so run it first out of the bunch.""" +import asyncio +import pytest +from solana.rpc.commitment import Confirmed +from spl.token.instructions import get_associated_token_address + +from stake.constants import STAKE_LEN +from stake_pool.state import StakePool, ValidatorList +from stake_pool.actions import deposit_sol, decrease_validator_stake, increase_validator_stake, update_stake_pool + + +@pytest.mark.asyncio +async def test_increase_decrease_this_is_very_slow(async_client, validators, payer, stake_pool_addresses): + (stake_pool_address, validator_list_address) = stake_pool_addresses + resp = await async_client.get_minimum_balance_for_rent_exemption(STAKE_LEN) + stake_rent_exemption = resp['result'] + increase_amount = 100_000_000 + decrease_amount = increase_amount // 2 + deposit_amount = (increase_amount + stake_rent_exemption) * len(validators) + + resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + token_account = get_associated_token_address(payer.public_key, stake_pool.pool_mint) + await deposit_sol(async_client, payer, stake_pool_address, token_account, deposit_amount) + + # increase to all + futures = [ + increase_validator_stake(async_client, payer, payer, stake_pool_address, validator, increase_amount) + for validator in validators + ] + await asyncio.gather(*futures) + + resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + for validator in validator_list.validators: + assert validator.transient_stake_lamports == increase_amount + stake_rent_exemption + assert validator.active_stake_lamports == 0 + + print("Waiting for epoch to roll over, roughly 24 seconds") + await asyncio.sleep(24.0) + await update_stake_pool(async_client, payer, stake_pool_address) + + resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + for validator in validator_list.validators: + assert validator.last_update_epoch != 0 + assert validator.transient_stake_lamports == 0 + assert validator.active_stake_lamports == increase_amount # rent exemption brought back to reserve + + # decrease from all + futures = [ + decrease_validator_stake(async_client, payer, payer, stake_pool_address, validator, decrease_amount) + for validator in validators + ] + await asyncio.gather(*futures) + + resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + for validator in validator_list.validators: + assert validator.transient_stake_lamports == decrease_amount + assert validator.active_stake_lamports == increase_amount - decrease_amount + + print("Waiting for epoch to roll over, roughly 24 seconds") + await asyncio.sleep(24.0) + await update_stake_pool(async_client, payer, stake_pool_address) + + resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + for validator in validator_list.validators: + assert validator.transient_stake_lamports == 0 + assert validator.active_stake_lamports == increase_amount - decrease_amount diff --git a/stake-pool/py/tests/test_add_remove.py b/stake-pool/py/tests/test_add_remove.py new file mode 100644 index 00000000000..5937a2299e7 --- /dev/null +++ b/stake-pool/py/tests/test_add_remove.py @@ -0,0 +1,31 @@ +import asyncio +import pytest +from solana.rpc.commitment import Confirmed + +from stake_pool.state import ValidatorList, StakeStatus +from stake_pool.actions import remove_validator_from_pool + + +@pytest.mark.asyncio +async def test_add_remove_validators(async_client, validators, payer, stake_pool_addresses): + (stake_pool_address, validator_list_address) = stake_pool_addresses + resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + assert len(validator_list.validators) == len(validators) + futures = [] + for validator_info in validator_list.validators: + assert validator_info.vote_account_address in validators + assert validator_info.active_stake_lamports == 0 + assert validator_info.transient_stake_lamports == 0 + assert validator_info.status == StakeStatus.ACTIVE + futures.append( + remove_validator_from_pool(async_client, payer, stake_pool_address, validator_info.vote_account_address) + ) + await asyncio.gather(*futures) + + resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + for validator_info in validator_list.validators: + assert validator_info.status == StakeStatus.READY_FOR_REMOVAL diff --git a/stake-pool/py/tests/test_create.py b/stake-pool/py/tests/test_create.py new file mode 100644 index 00000000000..ee37be58378 --- /dev/null +++ b/stake-pool/py/tests/test_create.py @@ -0,0 +1,68 @@ +import pytest +from solana.keypair import Keypair +from solana.rpc.commitment import Confirmed +from spl.token.constants import TOKEN_PROGRAM_ID + +from stake_pool.constants import find_withdraw_authority_program_address, STAKE_POOL_PROGRAM_ID +from stake_pool.state import StakePool, Fee + +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_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 create_stake(async_client, payer, reserve_stake, pool_withdraw_authority, 1) + + pool_mint = Keypair() + await create_mint(async_client, payer, pool_mint, pool_withdraw_authority) + + manager_fee_account = await create_associated_token_account( + async_client, + payer, + payer.public_key, + pool_mint.public_key, + ) + + fee = Fee(numerator=1, denominator=1000) + referral_fee = 20 + 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 == 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 + assert pool_data.pool_mint == pool_mint.public_key + assert pool_data.manager_fee_account == manager_fee_account + assert pool_data.token_program_id == TOKEN_PROGRAM_ID + assert pool_data.total_lamports == 0 + assert pool_data.pool_token_supply == 0 + assert pool_data.epoch_fee == fee + assert pool_data.next_epoch_fee is None + assert pool_data.preferred_deposit_validator is None + assert pool_data.preferred_withdraw_validator is None + assert pool_data.stake_deposit_fee == fee + assert pool_data.stake_withdrawal_fee == fee + assert pool_data.next_stake_withdrawal_fee is None + assert pool_data.stake_referral_fee == referral_fee + assert pool_data.sol_deposit_authority is None + assert pool_data.sol_deposit_fee == fee + assert pool_data.sol_referral_fee == referral_fee + assert pool_data.sol_withdraw_authority is None + assert pool_data.sol_withdrawal_fee == fee + assert pool_data.next_sol_withdrawal_fee is None + assert pool_data.last_epoch_pool_token_supply == 0 + assert pool_data.last_epoch_total_lamports == 0 diff --git a/stake-pool/py/tests/test_deposit_withdraw_sol.py b/stake-pool/py/tests/test_deposit_withdraw_sol.py new file mode 100644 index 00000000000..c61a353f592 --- /dev/null +++ b/stake-pool/py/tests/test_deposit_withdraw_sol.py @@ -0,0 +1,26 @@ +import pytest +from solana.rpc.commitment import Confirmed +from solana.keypair import Keypair +from spl.token.instructions import get_associated_token_address + +from stake_pool.state import Fee, StakePool +from stake_pool.actions import create_all, deposit_sol, withdraw_sol + + +@pytest.mark.asyncio +async def test_deposit_withdraw_sol(async_client, payer): + fee = Fee(numerator=1, denominator=1000) + referral_fee = 20 + (stake_pool_address, validator_list_address) = await create_all(async_client, payer, fee, referral_fee) + resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + token_account = get_associated_token_address(payer.public_key, stake_pool.pool_mint) + deposit_amount = 100_000_000 + await deposit_sol(async_client, payer, stake_pool_address, token_account, deposit_amount) + pool_token_balance = await async_client.get_token_account_balance(token_account, Confirmed) + assert pool_token_balance['result']['value']['amount'] == str(deposit_amount) + recipient = Keypair() + await withdraw_sol(async_client, payer, token_account, stake_pool_address, recipient.public_key, deposit_amount) + pool_token_balance = await async_client.get_token_account_balance(token_account, Confirmed) + assert pool_token_balance['result']['value']['amount'] == str('0') diff --git a/stake-pool/py/tests/test_deposit_withdraw_stake.py b/stake-pool/py/tests/test_deposit_withdraw_stake.py new file mode 100644 index 00000000000..3341d0d9bfa --- /dev/null +++ b/stake-pool/py/tests/test_deposit_withdraw_stake.py @@ -0,0 +1,43 @@ +import pytest +from solana.rpc.commitment import Confirmed +from solana.keypair import Keypair +from spl.token.instructions import get_associated_token_address + +from stake.actions import create_stake, delegate_stake +from stake.constants import STAKE_LEN +from stake_pool.actions import deposit_stake, withdraw_stake +from stake_pool.state import StakePool + + +@pytest.mark.asyncio +async def test_deposit_withdraw_stake(async_client, validators, payer, stake_pool_addresses): + (stake_pool_address, validator_list_address) = stake_pool_addresses + resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) + data = resp['result']['value']['data'] + stake_pool = StakePool.decode(data[0], data[1]) + token_account = get_associated_token_address(payer.public_key, stake_pool.pool_mint) + + resp = await async_client.get_minimum_balance_for_rent_exemption(STAKE_LEN) + stake_rent_exemption = resp['result'] + + stake_amount = 1_000_000 + for validator in validators: + stake = Keypair() + await create_stake(async_client, payer, stake, payer.public_key, stake_amount) + await delegate_stake(async_client, payer, payer, stake.public_key, validator) + await deposit_stake(async_client, payer, stake_pool_address, validator, stake.public_key, token_account) + + pool_token_balance = await async_client.get_token_account_balance(token_account, Confirmed) + pool_token_balance = pool_token_balance['result']['value']['amount'] + assert pool_token_balance == str((stake_amount + stake_rent_exemption) * len(validators)) + + for validator in validators: + destination_stake = Keypair() + await withdraw_stake( + async_client, payer, payer, destination_stake, stake_pool_address, validator, + payer.public_key, token_account, stake_amount + ) + + pool_token_balance = await async_client.get_token_account_balance(token_account, Confirmed) + pool_token_balance = pool_token_balance['result']['value']['amount'] + assert pool_token_balance == str(stake_rent_exemption * len(validators)) diff --git a/stake-pool/py/tests/test_stake.py b/stake-pool/py/tests/test_stake.py new file mode 100644 index 00000000000..152a487ce74 --- /dev/null +++ b/stake-pool/py/tests/test_stake.py @@ -0,0 +1,32 @@ +import asyncio +import pytest +from solana.keypair import Keypair + +from stake.state import StakeAuthorize +from stake.actions import authorize, create_stake, delegate_stake + + +@pytest.mark.asyncio +async def test_create_stake(async_client, payer): + stake = Keypair() + await create_stake(async_client, payer, stake, payer.public_key, 100_000) + + +@pytest.mark.asyncio +async def test_delegate_stake(async_client, validators, payer): + validator = validators[0] + stake = Keypair() + await create_stake(async_client, payer, stake, payer.public_key, 1) + await delegate_stake(async_client, payer, payer, stake.public_key, validator) + + +@pytest.mark.asyncio +async def test_authorize_stake(async_client, payer): + stake = Keypair() + new_authority = Keypair() + await create_stake(async_client, payer, stake, payer.public_key, 1_000) + await asyncio.gather( + authorize(async_client, payer, payer, stake.public_key, new_authority.public_key, StakeAuthorize.STAKER), + authorize(async_client, payer, payer, stake.public_key, new_authority.public_key, StakeAuthorize.WITHDRAWER) + ) + await authorize(async_client, payer, new_authority, stake.public_key, payer.public_key, StakeAuthorize.WITHDRAWER) 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..b0861d465fd --- /dev/null +++ b/stake-pool/py/tests/test_vote.py @@ -0,0 +1,16 @@ +import pytest +from solana.keypair import Keypair +from solana.publickey import PublicKey +from solana.rpc.commitment import Confirmed + +from vote.actions import create_vote +from vote.constants import VOTE_PROGRAM_ID + + +@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) + resp = await async_client.get_account_info(vote.public_key, commitment=Confirmed) + assert PublicKey(resp['result']['value']['owner']) == VOTE_PROGRAM_ID diff --git a/stake-pool/py/vote/__init__.py b/stake-pool/py/vote/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100644 index 00000000000..21f006e4a52 --- /dev/null +++ b/stake-pool/py/vote/constants.py @@ -0,0 +1,8 @@ +from solana.publickey import PublicKey + + +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..38106535faf --- /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 or SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.clock_sysvar or SYSVAR_CLOCK_PUBKEY, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.node, is_signer=True, is_writable=False), + ], + program_id=VOTE_PROGRAM_ID, + data=data, + )