├── .bumpversion.cfg ├── .codecov.yml ├── .env ├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── bin └── localnet.sh ├── docs ├── core │ ├── api.md │ ├── utils.md │ └── vote_program.md ├── css │ └── mkdocstrings.css ├── img │ ├── favicon.ico │ └── solana-py-logo.jpeg ├── index.md ├── rpc │ ├── api.md │ ├── async_api.md │ ├── commitment.md │ ├── providers.md │ ├── types.md │ └── websocket.md └── spl │ ├── intro.md │ ├── memo │ ├── constants.md │ ├── instructions.md │ └── intro.md │ └── token │ ├── async_client.md │ ├── client.md │ ├── constants.md │ ├── core.md │ └── instructions.md ├── mkdocs.yml ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── src ├── solana │ ├── __init__.py │ ├── _layouts │ │ ├── __init__.py │ │ └── vote_instructions.py │ ├── constants.py │ ├── exceptions.py │ ├── py.typed │ ├── rpc │ │ ├── __init__.py │ │ ├── api.py │ │ ├── async_api.py │ │ ├── commitment.py │ │ ├── core.py │ │ ├── providers │ │ │ ├── __init__.py │ │ │ ├── async_base.py │ │ │ ├── async_http.py │ │ │ ├── base.py │ │ │ ├── core.py │ │ │ └── http.py │ │ ├── types.py │ │ └── websocket_api.py │ ├── utils │ │ ├── __init__.py │ │ ├── cluster.py │ │ ├── security_txt.py │ │ └── validate.py │ └── vote_program.py └── spl │ ├── __init__.py │ ├── memo │ ├── __init__.py │ ├── constants.py │ └── instructions.py │ ├── py.typed │ └── token │ ├── __init__.py │ ├── _layouts.py │ ├── async_client.py │ ├── client.py │ ├── constants.py │ ├── core.py │ └── instructions.py └── tests ├── __init__.py ├── conftest.py ├── docker-compose.yml ├── integration ├── __init__.py ├── test_async_http_client.py ├── test_async_token_client.py ├── test_http_client.py ├── test_memo.py ├── test_recent_performance_samples.py ├── test_token_client.py └── test_websockets.py ├── unit ├── test_async_client.py ├── test_client.py ├── test_cluster_api_url.py ├── test_memo_program.py ├── test_security_txt.py ├── test_spl_token_instructions.py └── test_vote_program.py └── utils.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.36.7 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:pyproject.toml] 7 | search = version = "{current_version}" 8 | replace = version = "{new_version}" 9 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | project: 12 | default: 13 | target: auto 14 | threshold: 1.5% 15 | patch: no 16 | changes: no 17 | 18 | comment: 19 | layout: "header, diff" 20 | behavior: default 21 | require_changes: no 22 | 23 | ignore: 24 | - tests 25 | - docs/conf.py 26 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # env-vars required for things to work from command line and in IDE 2 | PYTHONPATH=./src:$PYTHONPATH 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Docs 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [master] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | build: 14 | # The type of runner that the job will run on 15 | runs-on: ubuntu-latest 16 | 17 | # Steps represent a sequence of tasks that will be executed as part of the job 18 | steps: 19 | - name: checkout repo 20 | uses: actions/checkout@v4 21 | - name: Set up Python 3.9 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: 3.9 25 | 26 | #---------------------------------------------- 27 | # ----- install & configure poetry ----- 28 | #---------------------------------------------- 29 | - name: Install and configure Poetry 30 | uses: snok/install-poetry@v1 31 | with: 32 | version: 1.8.4 33 | virtualenvs-create: true 34 | virtualenvs-in-project: true 35 | installer-parallel: true 36 | #---------------------------------------------- 37 | # load cached venv if cache exists 38 | #---------------------------------------------- 39 | - name: Load cached venv 40 | id: cached-poetry-dependencies 41 | uses: actions/cache@v4 42 | with: 43 | path: .venv 44 | key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} 45 | #---------------------------------------------- 46 | # install dependencies if cache does not exist 47 | #---------------------------------------------- 48 | - name: Install dependencies 49 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 50 | run: poetry install --no-interaction --no-root 51 | #---------------------------------------------- 52 | # install your root project 53 | #---------------------------------------------- 54 | - name: Install library 55 | run: poetry install --no-interaction 56 | 57 | - name: Deploy docs 58 | run: poetry run mkdocs gh-deploy --force 59 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | lint: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: checkout repo 20 | uses: actions/checkout@v4 21 | - name: Set up Python 3.11 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: 3.11 25 | 26 | #---------------------------------------------- 27 | # ----- install & configure poetry ----- 28 | #---------------------------------------------- 29 | - name: Install and configure Poetry 30 | uses: snok/install-poetry@v1 31 | with: 32 | version: 1.8.4 33 | virtualenvs-create: true 34 | virtualenvs-in-project: true 35 | installer-parallel: true 36 | - name: Install dependencies 37 | run: poetry install --no-interaction --no-root 38 | #---------------------------------------------- 39 | # install your root project 40 | #---------------------------------------------- 41 | - name: Install library 42 | run: poetry install --no-interaction 43 | 44 | - name: Run linters 45 | run: | 46 | make lint 47 | 48 | tests: 49 | # The type of runner that the job will run on 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | # first list entry: lower bound, second list entry: upper bound 55 | # of the supported python versions, versions in between are assumed 56 | # to be supported as well without further testing 57 | python-version: [3.9, 3.13] 58 | os: [ubuntu-latest, windows-latest, macos-latest] 59 | 60 | defaults: 61 | run: 62 | shell: bash 63 | 64 | steps: 65 | - name: checkout repo 66 | uses: actions/checkout@v4 67 | - name: Set up Python 3.9 68 | id: setup-python 69 | uses: actions/setup-python@v5 70 | with: 71 | python-version: ${{ matrix.python-version }} 72 | 73 | #---------------------------------------------- 74 | # ----- install & configure poetry ----- 75 | #---------------------------------------------- 76 | - name: Install and configure Poetry 77 | uses: snok/install-poetry@v1 78 | with: 79 | version: 1.8.4 80 | virtualenvs-create: true 81 | virtualenvs-in-project: true 82 | installer-parallel: true 83 | #---------------------------------------------- 84 | # load cached venv if cache exists 85 | #---------------------------------------------- 86 | - name: Load cached venv 87 | id: cached-poetry-dependencies 88 | uses: actions/cache@v4 89 | with: 90 | path: .venv 91 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 92 | #---------------------------------------------- 93 | # install dependencies if cache does not exist 94 | #---------------------------------------------- 95 | - name: Install dependencies 96 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 97 | run: poetry install --no-interaction --no-root 98 | #---------------------------------------------- 99 | # install your root project 100 | #---------------------------------------------- 101 | - name: Install library 102 | run: poetry install --no-interaction 103 | 104 | - name: Run all tests 105 | if: matrix.os == 'ubuntu-latest' 106 | run: | 107 | make tests-parallel 108 | 109 | - name: Run unit tests 110 | if: matrix.os != 'ubuntu-latest' 111 | run: | 112 | make unit-tests 113 | 114 | - name: Upload html coverage 115 | uses: actions/upload-artifact@v4 116 | with: 117 | name: coverage-html-${{ runner.os }}-${{ matrix.python-version }} 118 | path: htmlcov 119 | include-hidden-files: true 120 | if-no-files-found: error 121 | 122 | - name: Upload coverage to Codecov 123 | uses: codecov/codecov-action@v5 124 | # Do not upload/fail for PRs in forks 125 | if: github.repository == 'michaelhly/solana-py' 126 | with: 127 | token: ${{ secrets.CODECOV_TOKEN }} 128 | flags: ${{ runner.os }},${{ runner.arch }},${{ matrix.python-version }} 129 | # Specify whether or not CI build should fail if Codecov runs into an error during upload 130 | fail_ci_if_error: true 131 | 132 | test: 133 | # https://github.community/t/is-it-possible-to-require-all-github-actions-tasks-to-pass-without-enumerating-them/117957/4?u=graingert 134 | runs-on: ubuntu-latest 135 | needs: tests 136 | steps: 137 | - name: note that all tests succeeded 138 | run: echo "🎉" 139 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: 3.9 16 | #---------------------------------------------- 17 | # ----- install & configure poetry ----- 18 | #---------------------------------------------- 19 | - name: Install and configure Poetry 20 | uses: snok/install-poetry@v1 21 | with: 22 | version: 1.8.4 23 | virtualenvs-create: true 24 | virtualenvs-in-project: true 25 | installer-parallel: true 26 | - run: poetry build 27 | - run: poetry publish --username=__token__ --password=${{ secrets.PYPI_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # notebook checkpoints 2 | .ipynb_checkpoints 3 | 4 | # python cache files 5 | *__pycache__* 6 | .pytest_cache 7 | 8 | # log files 9 | log/* 10 | *.log 11 | 12 | # vim temp files 13 | .~ 14 | *.swp 15 | *.swo 16 | 17 | # other temps files 18 | .DS_Store 19 | .tmp/* 20 | 21 | # sphinx stuff 22 | _build 23 | make.bat 24 | 25 | # dist stuff 26 | build 27 | dist 28 | *.egg 29 | *.egg-info 30 | 31 | # solana validator logs 32 | test-ledger/ 33 | 34 | # IDE Files 35 | .idea/ 36 | 37 | # pytest coverage 38 | .coverage 39 | coverage.xml 40 | htmlcov/ 41 | 42 | # manual tests 43 | testing.py 44 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.envFile": "${workspaceFolder}/.env", 3 | "python.testing.unittestEnabled": false, 4 | "python.testing.pytestEnabled": true, 5 | "python.formatting.provider": "black", 6 | "python.formatting.blackArgs": [ 7 | "--check", 8 | "--line-length", 9 | "120" 10 | ], 11 | "editor.formatOnSave": true, 12 | "python.languageServer": "Pylance", 13 | "ruff.args": [ 14 | "--config=pyproject.toml" 15 | ] 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.34.0] - 2024-04-17 4 | 5 | ### Added 6 | 7 | - Add block subscribe/unsubscribe websocket methods [(#418)](https://github.com/michaelhly/solana-py/pull/418) 8 | - Add getInflationReward RPC method [(#413)](https://github.com/michaelhly/solana-py/pull/413) 9 | 10 | 11 | ## [0.33.0] - 2024-03-29 12 | 13 | ### Changed 14 | 15 | - Relax httpx dependency [(#408)](https://github.com/michaelhly/solana-py/pull/408) 16 | - Remove experimental and flawed BlockhashCache [(#408)](https://github.com/michaelhly/solana-py/pull/408). This was a footgun because it broke a lot. 17 | - Upgrade to Solders 0.21.0 (fixes aarch64 compatibility issues) [(#402)](https://github.com/michaelhly/solana-py/pull/402) 18 | 19 | ## [0.32.0] - 2024-02-12 20 | 21 | ### Changed 22 | 23 | - Upgrade to Solders 0.20 [(#394)](https://github.com/michaelhly/solana-py/pull/394) 24 | 25 | ## [0.31.0] - 2024-01-02 26 | 27 | ### Added 28 | 29 | - Add sync_native instruction [(#376)](https://github.com/michaelhly/solana-py/pull/376) 30 | 31 | ### Changed 32 | 33 | - Update to latest Solders version [(#383)](https://github.com/michaelhly/solana-py/pull/383) 34 | - Increase confirmation timeout to 90 seconds [(#382)](https://github.com/michaelhly/solana-py/pull/382) 35 | 36 | ## [0.30.2] - 2023-06-02 37 | 38 | - Use latest Solders [(#363)](https://github.com/michaelhly/solana-py/pull/363) 39 | 40 | ## [0.30.1] - 2023-05-14 41 | 42 | ### Changed 43 | 44 | - Use latest Solders [(#359)](https://github.com/michaelhly/solana-py/pull/359) 45 | - Relax websockets to allow 11.x [(#355)](https://github.com/michaelhly/solana-py/pull/355) 46 | 47 | ## [0.30.0] - 2023-05-05 48 | 49 | ### Changed 50 | 51 | Use latest Solders [(#352)](https://github.com/michaelhly/solana-py/pull/352) 52 | 53 | ## [0.29.2] - 2023-04-24 54 | 55 | ### Changed 56 | 57 | Relaxed websockets dependency [(#347)](https://github.com/michaelhly/solana-py/pull/347) 58 | 59 | ## [0.29.1] - 2022-02-02 60 | 61 | ### Fixed 62 | 63 | Fix accidentally ignoring tx_opts in `send_transaction` methods [(#343)](https://github.com/michaelhly/solana-py/pull/343) 64 | 65 | ## [0.29.0] - 2023-01-13 66 | 67 | ### Added 68 | 69 | - Add VersionedTransaction support to `send_transaction` and `simulate_transaction` methods [(#334)](https://github.com/michaelhly/solana-py/pull/334) 70 | - Support VersionedMessage in `get_fee_for_message` methods [(#337)](https://github.com/michaelhly/solana-py/pull/337) 71 | 72 | ### Changed 73 | 74 | - Remove redundant classes and modules ([#329](https://github.com/michaelhly/solana-py/pull/329), [#335](https://github.com/michaelhly/solana-py/pull/335) and [#338](https://github.com/michaelhly/solana-py/pull/338)): 75 | - Remove `PublicKey`, in favour of `solders.pubkey.Pubkey`. 76 | - Remove `AccountMeta` in favour of `solders.instruction.AccountMeta`. 77 | - Remove `TransactionInstruction` in favour of `solders.instruction.Instruction`. 78 | - Remove `Keypair` in favour of `solders.keypair.Keypair`. Your code will need to change as follows: 79 | - `my_keypair.public_key` -> `my_keypair.pubkey()` 80 | - `my_keypair.secret_key` -> `bytes(my_keypair)` 81 | - `my_keypair.seed` -> `my_keypair.secret()` 82 | - `my_keypair.sign(msg)` -> `my_keypair.sign_message(msg)` 83 | - `Keypair.from_secret_key(key)` -> `Keypair.from_bytes(key)` 84 | - Remove `Message` in favour of `solders.message.Message`. 85 | - Remove `system_program` in favour of `solders.system_program`. Note: where previously a params object like `AssignParams` called a field `program_id`, it now calls it `owner`. 86 | - Remove `sysvar` in favour of `solders.sysvar`. The constants in `solders.sysvar` have short names, so instead of `solana.sysvar.SYSVAR_RENT_PUBKEY` you'll use `solders.sysvar.RENT`. 87 | - Remove `solana.blockhash.Blockhash` in favour of `solders.hash.Hash`. Note: `Blockhash(my_str)` -> `Hash.from_str(my_str)`. 88 | - Remove `solana.transaction.TransactionSignature` newtype. This was unused - solana-py is already using `solders.signature.Signature`. 89 | - Remove constant `solana.transaction.SIG_LENGTH` in favour of `solders.signature.Signature.LENGTH`. 90 | - Remove unused `solana.transaction.SigPubkeyPair`. 91 | - Use latest solders [(#334)](https://github.com/michaelhly/solana-py/pull/334) 92 | - Use new `solders.rpc.requests.SendRawTransaction` in `send_raw_transaction` methods [(#332)](https://github.com/michaelhly/solana-py/pull/332) 93 | 94 | ## [0.28.1] - 2022-12-29 95 | 96 | ### Fixed 97 | 98 | Fix conversion of MemcmpOpts in `get_program_accounts` methods [(#328)](https://github.com/michaelhly/solana-py/pull/328) 99 | 100 | ## [0.28.0] - 2022-10-31 101 | 102 | ### Changed 103 | 104 | - Use latest `solders`. Note that the parsed fields of jsonParsed responses are now dicts rather than strings. [(#318)](https://github.com/michaelhly/solana-py/pull/318) 105 | - Remove `requests` dependency [(#315)](https://github.com/michaelhly/solana-py/pull/315) 106 | 107 | ### Fixed 108 | 109 | - Fix flakiness in token client transactions [(#314)](https://github.com/michaelhly/solana-py/pull/314) 110 | 111 | ## [0.27.2] - 2022-10-15 112 | 113 | ### Changed 114 | 115 | - Use latest `solders` [(#312)](https://github.com/michaelhly/solana-py/pull/312) 116 | 117 | ## [0.27.1] - 2022-10-14 118 | 119 | ### Fixed 120 | 121 | - Fix incorrect `encoding` arg in `_simulate_transaction_body` [(#311)](https://github.com/michaelhly/solana-py/pull/311) 122 | 123 | ## [0.27.0] - 2022-10-14 124 | 125 | ### Changed 126 | 127 | - Replace SubscriptionError.code with SubscriptionError.type [(#309)](https://github.com/michaelhly/solana-py/pull/309) 128 | 129 | ### Fixed 130 | 131 | - Fix parsing of RPC error messages [(#309)](https://github.com/michaelhly/solana-py/pull/309) 132 | - Correctly filter by program_id in _get_token_accounts_convert [(#308)](https://github.com/michaelhly/solana-py/pull/308) 133 | 134 | 135 | ## [0.26.0] - 2022-10-13 136 | 137 | ### Added 138 | 139 | - Added batch request methods `(Async)HTTPProvider.make_batch_request(_unparsed)` [(#304)](https://github.com/michaelhly/solana-py/pull/304) 140 | - Added `make_request_unparsed` to `(Async)HTTPProvider` [(#304)](https://github.com/michaelhly/solana-py/pull/304) 141 | 142 | ### Changed 143 | 144 | - Use solders for parsing RPC requests [(#302)](https://github.com/michaelhly/solana-py/pull/302): 145 | - **Breaking change**: Every RPC method now returns a strongly typed object instead of a dictionary. 146 | For example, `client.get_balance` returns `GetBalanceResp`. 147 | - **Breaking change**: RPC methods now raise `RPCException` if the RPC returns an error result. 148 | Previously only the transaction sending methods did this. 149 | - **Breaking change**: RPC methods that can return `jsonParsed` data now have their own dedicated Python 150 | method you should use. For example, instead of `client.get_account_info(..., encoding="jsonParsed")` 151 | you should do `client.get_account_info_json_parsed(...)`. This is done for the sake of static typing. 152 | - **Breaking change**: The `get_accounts` method on the SPL Token client has been split into four separate methods: 153 | `get_accounts_by_delegate`, `get_accounts_by_owner`, `get_accounts_by_delegate_json_parsed`, and `get_accounts_by_owner_json_parsed`. 154 | - **Breaking change**: `solana.rpc.responses` has been removed and supplanted by `solders.rpc.responses`. 155 | - Remove unused deps: `apischema`, `based58`, `jsonrpcclient`, `jsonrpcserver`. 156 | 157 | - Use Solders for building RPC requests: 158 | - **Breaking change**: Removed deprecated RPC methods. 159 | - **Breaking change**: Functions that accepted Union[PublicKey, str] now only accept PublicKey. 160 | - **Breaking change**: RPC functions that accepted a `str` signature param now expect a `solders.signature.Signature`. 161 | 162 | ### Fixed 163 | 164 | - `send_raw_transaction` now defaults to the client's commitment level if `preflight_commitment` is not otherwise specified. 165 | 166 | ## [0.25.0] - 2022-06-21 167 | 168 | ### Fixed 169 | 170 | - Use latest Solders version to make objects pickleable again [(#252)](https://github.com/michaelhly/solana-py/pull/252). 171 | 172 | 173 | ### Changed 174 | 175 | - Updated httpx to fix critical vulnerability [(#248)](https://github.com/michaelhly/solana-py/pull/248). 176 | - Updated pytest, websockets, pytest-docker, pytest-asyncio to latest. [(#254)](https://github.com/michaelhly/solana-py/pull/254). 177 | - Updated apischema to latest. [(#254)](https://github.com/michaelhly/solana-py/pull/254). 178 | 179 | 180 | ### Added 181 | 182 | - Added `get_latest_blockhash` RPC Call. [(#254)](https://github.com/michaelhly/solana-py/pull/254). 183 | - Added `get_fee_for_message` RPC Call. [(#254)](https://github.com/michaelhly/solana-py/pull/254). 184 | - Added confirmation strategy which checks if the transaction has exceeded last valid blockheight. [(#254)](https://github.com/michaelhly/solana-py/pull/254). 185 | - Added `asyncio_mode = auto` in pytest.ini. [(#248)](https://github.com/michaelhly/solana-py/pull/254). 186 | - Added an optional `verify_signature` bool when `transaction.serialize()` is called [(#249)](https://github.com/michaelhly/solana-py/pull/249). 187 | - Added Memo program [(#249)](https://github.com/michaelhly/solana-py/pull/249). 188 | 189 | 190 | ## [0.24.0] - 2022-06-04 191 | 192 | ### Changed 193 | 194 | - Use [solders](https://github.com/kevinheavey/solders) under the hood for keypairs and pubkeys [(#237)](https://github.com/michaelhly/solana-py/pull/237). 195 | - Remove deprecated `Account` entirely [(#238)](https://github.com/michaelhly/solana-py/pull/238). 196 | - Use [solders](https://github.com/kevinheavey/solders) under the hood for `Message` [(#239)](https://github.com/michaelhly/solana-py/pull/239). 197 | - Remove unused and very old instruction.py file [(#240)](https://github.com/michaelhly/solana-py/pull/240). 198 | - Default to client's commitment in confirm_transaction, send_transaction and the `Token` client [(#242)](https://github.com/michaelhly/solana-py/pull/242). 199 | - Use [solders](https://github.com/kevinheavey/solders) under the hood for `Transaction` [(#241)](https://github.com/michaelhly/solana-py/pull/241). BREAKING CHANGES: 200 | - `Transaction.__init__` no longer accepts a `signatures` argument. If you want to construct a transaction with certain signatures, you can still use `Transaction.populate`. 201 | - `Transaction.add_signer` has been removed (it was removed from web3.js in September 2020). 202 | - The `signatures` attribute of `Transaction` has been changed to a read-only property. 203 | - Where previously a "signature" was represented as `bytes`, it is now expected to be a `solders.signature.Signature`. 204 | This affects the following properties and functions: `Transaction.signature`, `Transacton.signatures`, `Transaction.add_signature`, `Transaction.populate` 205 | - The `keypairs` in `Transaction.sign_partial` are now only allowed to be `Keypair` objects. Previously `Union[PublicKey, Keypair]` was allowed. 206 | - The `.signatures` property of an unsigned transaction is now a list of `solders.signature.Signature.default()` instead 207 | of an empty list. 208 | - Use [solders](https://github.com/kevinheavey/solders) under the hood for system instructions [(#243)](https://github.com/michaelhly/solana-py/pull/243) 209 | 210 | ### Added 211 | 212 | - Expose `client.commmitment` as a property like in web3.js [(#242)](https://github.com/michaelhly/solana-py/pull/242). 213 | 214 | ## [0.23.3] - 2022-04-29 215 | 216 | ### Fixed 217 | 218 | - Make transaction message compilation consistent with [@solana/web3.js](https://github.com/solana-labs/solana-web3.js/) ([228](https://github.com/michaelhly/solana-py/pull/228)) 219 | 220 | ## [0.23.2] - 2022-04-17 221 | 222 | ### Changed 223 | 224 | - Relax typing-extensions contraint ([#220](https://github.com/michaelhly/solana-py/pull/220)) 225 | 226 | ## [0.23.1] - 2022-03-31 227 | 228 | ### Fixed 229 | 230 | - Fix str seed input for sp.create_account_with_seed ([#206](https://github.com/michaelhly/solana-py/pull/206)) 231 | 232 | ### Changed 233 | 234 | - Update `jsonrpcserver` dependency ([#205](https://github.com/michaelhly/solana-py/pull/205)) 235 | 236 | ## [0.23.0] - 2022-03-06 237 | 238 | ### Added 239 | 240 | - Implement `__hash__` for PublicKey ([#202](https://github.com/michaelhly/solana-py/pull/202)) 241 | 242 | ## [0.22.0] - 2022-03-03 243 | 244 | ### Added 245 | 246 | - Add default RPC client commitment to token client ([#187](https://github.com/michaelhly/solana-py/pull/187)) 247 | - Add cluster_api_url function ([#193](https://github.com/michaelhly/solana-py/pull/193)) 248 | - Add getBlockHeight RPC method ([#200](https://github.com/michaelhly/solana-py/pulls?q=is%3Apr+is%3Aclosed)) 249 | 250 | ### Changed 251 | 252 | - Replace base58 library with based58 [#192](https://github.com/michaelhly/solana-py/pull/192) 253 | 254 | ## [0.21.0] - 2022-01-13 255 | 256 | ### Fixed 257 | 258 | - Make `program_ids` list deterministic in `compile_message` ([#164](https://github.com/michaelhly/solana-py/pull/164)) 259 | 260 | ### Changed 261 | 262 | - Throw more specific Exception in API client on failure to retrieve RPC result ([#166](https://github.com/michaelhly/solana-py/pull/166/files)) 263 | 264 | ### Added 265 | 266 | - Add max_retries option to sendTransaction and commitment option to get_transaction ([#165](https://github.com/michaelhly/solana-py/pull/165)) 267 | - Add a partial support for vote program ([#167](https://github.com/michaelhly/solana-py/pull/167)) 268 | 269 | ## [0.20.0] - 2021-12-30 270 | 271 | ### Changed 272 | 273 | - Make keypair hashable and move setters out of property functions ([#158](https://github.com/michaelhly/solana-py/pull/158)) 274 | 275 | ### Added 276 | 277 | - Optional Commitment parameter to `get_signatures_for_address` ([#157](https://github.com/michaelhly/solana-py/pull/157)) 278 | - More SYSVAR constants ([#159](https://github.com/michaelhly/solana-py/pull/159)) 279 | 280 | ## [0.19.1] - 2021-12-21 281 | 282 | ### Added 283 | 284 | - Custom solana-py RPC error handling ([#152](https://github.com/michaelhly/solana-py/pull/152)) 285 | 286 | ## [0.19.0] - 2021-12-02 287 | 288 | ### Added 289 | 290 | - Websockets support ([#144](https://github.com/michaelhly/solana-py/pull/144)) 291 | - New client functions ([#139](https://github.com/michaelhly/solana-py/pull/139)) 292 | - A timeout param for `Client` and `AsyncClient` ([#146](https://github.com/michaelhly/solana-py/pull/146)) 293 | 294 | ## [0.18.3] - 2021-11-20 295 | 296 | ### Fixed 297 | 298 | - Always return the tx signature when sending transaction (the async method was returning signature status if we were confirming the tx) 299 | 300 | ### Changed 301 | 302 | - Raise OnCurveException instead of generic Exception in `create_program_address` 303 | ([#128](https://github.com/michaelhly/solana-py/pull/128)) 304 | 305 | ### Added 306 | 307 | - Add `until` parameter to `get_signatures_for_address` ([#133](https://github.com/michaelhly/solana-py/pull/133)) 308 | - This changelog. 309 | 310 | ## [0.15.0] - 2021-9-26 311 | 312 | ### Changed 313 | 314 | - To reduce RPC calls from fetching recent blockhashes - allow user-supplied blockhash to `.send_transaction` and dependent fns, and introduce an opt-in blockhash cache ([#102](https://github.com/michaelhly/solana-py/pull/102)) 315 | - ReadTheDocs theme and doc changes ([#103](https://github.com/michaelhly/solana-py/pull/103)) 316 | - Deprecate `Account` and replace with `Keypair` ([#105](https://github.com/michaelhly/solana-py/pull/105)) 317 | 318 | ### Added 319 | 320 | - Implement methods for `solana.system_program` similar to [solana-web3](https://github.com/solana-labs/solana-web3.js/blob/44f32d9857e765dd26647ffd33b0ea0927f73b7a/src/system-program.ts#L743-L771): `create_account_with_seed`, `decode_create_account_with_seed` ([#101](https://github.com/michaelhly/solana-py/pull/101)) 321 | - Support for [getMultipleAccounts RPC method](https://docs.solana.com/developing/clients/jsonrpc-api#getmultipleaccounts) ([#103](https://github.com/michaelhly/solana-py/pull/103)) 322 | - Support for `solana.rpc.api` methods `get_token_largest_accounts`, `get_token_supply` ([#104](https://github.com/michaelhly/solana-py/pull/104)) 323 | 324 | ## [0.12.1] - 2021-8-28 325 | 326 | ### Fixed 327 | 328 | - Issue with importing `Token` from spl.token.client` ([#91](https://github.com/michaelhly/solana-py/pull/91)) 329 | - Packaging fixes ([#85](https://github.com/michaelhly/solana-py/pull/85)) 330 | 331 | ### Added 332 | 333 | - Missing `spl.token.async_client` methods - `create_multisig`, `get_mint_info`, `get_account_info`, `approve`, `revoke`, `burn`, `close_account`, `freeze_account`, `thaw_account`, `transfer_checked`, `approve_checked`, `mint_to_checked`, `burn_checked`. Missing `spl.token.client` methods - `create_multisig`, `get_mint_info`, `get_account_info`, `approve`, `revoke`, set_authority`, close_account`, `freeze_account`, `thaw_account`, `transfer_checked`, `approve_checked`, `mint_to_checked`, `burn_checked` ([#89](https://github.com/michaelhly/solana-py/pull/89)) 334 | 335 | ## [0.11.1] - 2021-8-4 336 | 337 | ### Fixed 338 | 339 | - Valid instruction can contain no keys ([#70](https://github.com/michaelhly/solana-py/pull/70)) 340 | 341 | ### Changed 342 | 343 | - Commitment levels - deprecated `max`, `root`, `singleGossip`, `recent` and added `processed`, `confirmed`, `finalized` ([#82](https://github.com/michaelhly/solana-py/pull/82)) 344 | 345 | ### Added 346 | 347 | - Allocate instruction for system program - `solana.system_program.decode_allocate`, `solana.system_program.decode_allocate_with_seed`, `solana.system_program.allocate` ([#79](https://github.com/michaelhly/solana-py/pull/79)) 348 | - Async support - `AsyncClient` and `AsyncToken` classes, refactors sync code, httpx dependency ([#83](https://github.com/michaelhly/solana-py/pull/83)) 349 | 350 | ## [0.10.0] - 2021-7-6 351 | 352 | ### Fixed 353 | 354 | - Valid instruction can contain no keys ([#70](https://github.com/michaelhly/solana-py/pull/70)) 355 | 356 | ### Changed 357 | 358 | - Pipenv update 359 | - Use new devnet api endpoint, deprecate `solana.rpc.api.getConfirmedSignaturesForAddress2` and use `solana.rpc.api.getSignaturesForAddress` instead ([#77](https://github.com/michaelhly/solana-py/pull/77)) 360 | 361 | ### Added 362 | 363 | - `spl.client.token.set_authority` ([#73](https://github.com/michaelhly/solana-py/pull/73/files)) 364 | - `spl.client.token.create_wrapped_native_account` ([#74](https://github.com/michaelhly/solana-py/pull/74)) 365 | 366 | ## [0.9.1] - 2021-5-31 367 | 368 | ### Fixed 369 | 370 | - Integration tests 371 | 372 | ### Added 373 | 374 | - `solana.publickey.create_with_seed` ([#69](https://github.com/michaelhly/solana-py/pull/69)) 375 | 376 | ## [0.9.0] - 2021-5-26 377 | 378 | ### Fixed 379 | 380 | - Mismatch in annotation ([#63](https://github.com/michaelhly/solana-py/pull/63)) 381 | - unused imports 382 | 383 | ### Changed 384 | 385 | - Use python-pure25519 curve check util instead of crypto_core_ed25519_is_valid_point 386 | 387 | ### Added ([#66](https://github.com/michaelhly/solana-py/pull/66)) 388 | 389 | - python-pure25519 curve check util 390 | - `spl.token.client.create_associated_token_account` 391 | - `spl.token.instructions.get_associated_token_address` 392 | - `spl.token.instructions.create_associated_token_account` 393 | - ATA constant `ASSOCIATED_TOKEN_PROGRAM_ID` 394 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2020 Michael Huang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -rf dist build __pycache__ *.egg-info 3 | 4 | format: 5 | poetry run ruff format src tests 6 | 7 | lint: 8 | poetry run ruff format --check --diff src tests 9 | poetry run ruff check src tests 10 | poetry run mypy src 11 | 12 | publish: clean 13 | poetry build 14 | poetry publish 15 | 16 | test-publish: clean 17 | poetry build 18 | poetry publish -r testpypi 19 | 20 | tests: 21 | poetry run pytest 22 | 23 | tests-parallel: 24 | poetry run pytest -n auto 25 | 26 | unit-tests: 27 | poetry run pytest -m "not integration" --doctest-modules 28 | 29 | int-tests: 30 | poetry run pytest -m integration 31 | 32 | update-localnet: 33 | ./bin/localnet.sh update 34 | 35 | start-localnet: 36 | ./bin/localnet.sh up 37 | 38 | stop-localnet: 39 | ./bin/localnet.sh down 40 | 41 | serve: 42 | mkdocs serve 43 | 44 | .PHONY: $(MAKECMDGOALS) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | --- 6 | 7 | [![Actions 8 | Status](https://github.com/michaelhly/solanapy/workflows/CI/badge.svg)](https://github.com/michaelhly/solanapy/actions?query=workflow%3ACI) 9 | [![PyPI version](https://badge.fury.io/py/solana.svg)](https://badge.fury.io/py/solana) 10 | [![Python versions](https://img.shields.io/pypi/pyversions/solana.svg)]( https://pypi.python.org/pypi/solana) 11 | [![Codecov](https://codecov.io/gh/michaelhly/solana-py/branch/master/graph/badge.svg)](https://codecov.io/gh/michaelhly/solana-py/branch/master) 12 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/michaelhly/solana-py/blob/master/LICENSE) 13 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/solana)](https://pypistats.org/packages/solana) 14 | 15 | # Solana.py 16 | 17 | **🐍 The Solana Python SDK 🐍** 18 | 19 | Solana.py is the base Python library for interacting with Solana. 20 | You can use it to build transactions and interact 21 | with the 22 | [Solana JSON RPC API](https://docs.solana.com/apps/jsonrpc-api), 23 | much like you would do with 24 | [solana-web3.js](https://github.com/solana-labs/solana-web3.js/) 25 | 26 | It also covers the 27 | [SPL Token Program](https://spl.solana.com/token). 28 | 29 | [Latest Documentation](https://michaelhly.github.io/solana-py/). 30 | 31 | Note: This library uses many core types from the [Solders](https://github.com/kevinheavey/solders) package which used to be provided by `solana-py` itself. If you are upgrading from an old version and you're looking for something that was deleted, it's probably in `solders` now. 32 | 33 | **⚓︎ See also: [AnchorPy](https://github.com/kevinheavey/anchorpy),** 34 | **a Python client for** 35 | **[Anchor](https://project-serum.github.io/anchor/getting-started/introduction.html)-based** 36 | **programs on Solana. ⚓︎** 37 | 38 | ## ⚡ Quickstart 39 | 40 | ### Installation 41 | 1. Install [Python bindings](https://kevinheavey.github.io/solders/) for the [solana-sdk](https://docs.rs/solana-sdk/latest/solana_sdk/). 42 | ```sh 43 | pip install solders 44 | ``` 45 | 46 | 2. Install this package to interact with the [Solana JSON RPC API](https://solana.com/docs/rpc). 47 | ```sh 48 | pip install solana 49 | ``` 50 | 51 | ### General Usage 52 | 53 | Note: check out the 54 | [Solana Cookbook](https://solanacookbook.com/) 55 | for more detailed examples! 56 | 57 | ```py 58 | import solana 59 | ``` 60 | 61 | ### API Client 62 | 63 | ```py 64 | from solana.rpc.api import Client 65 | 66 | http_client = Client("https://api.devnet.solana.com") 67 | ``` 68 | 69 | ### Async API Client 70 | 71 | ```py 72 | import asyncio 73 | from solana.rpc.async_api import AsyncClient 74 | 75 | async def main(): 76 | async with AsyncClient("https://api.devnet.solana.com") as client: 77 | res = await client.is_connected() 78 | print(res) # True 79 | 80 | # Alternatively, close the client explicitly instead of using a context manager: 81 | client = AsyncClient("https://api.devnet.solana.com") 82 | res = await client.is_connected() 83 | print(res) # True 84 | await client.close() 85 | 86 | asyncio.run(main()) 87 | ``` 88 | 89 | ### Websockets Client 90 | 91 | ```py 92 | import asyncio 93 | from asyncstdlib import enumerate 94 | from solana.rpc.websocket_api import connect 95 | 96 | async def main(): 97 | async with connect("wss://api.devnet.solana.com") as websocket: 98 | await websocket.logs_subscribe() 99 | first_resp = await websocket.recv() 100 | subscription_id = first_resp[0].result 101 | next_resp = await websocket.recv() 102 | print(next_resp) 103 | await websocket.logs_unsubscribe(subscription_id) 104 | 105 | # Alternatively, use the client as an infinite asynchronous iterator: 106 | async with connect("wss://api.devnet.solana.com") as websocket: 107 | await websocket.logs_subscribe() 108 | first_resp = await websocket.recv() 109 | subscription_id = first_resp[0].result 110 | async for idx, msg in enumerate(websocket): 111 | if idx == 3: 112 | break 113 | print(msg) 114 | await websocket.logs_unsubscribe(subscription_id) 115 | 116 | asyncio.run(main()) 117 | ``` 118 | 119 | ## 🔨 Development 120 | 121 | ### Setup 122 | 123 | 1. Install [poetry](https://python-poetry.org/docs/#installation) 124 | 2. Install dev dependencies: 125 | 126 | ```sh 127 | poetry install 128 | 129 | ``` 130 | 131 | 3. Activate the poetry shell. 132 | 133 | ```sh 134 | poetry shell 135 | ``` 136 | 137 | ### Lint 138 | 139 | ```sh 140 | make lint 141 | ``` 142 | 143 | ### Tests 144 | 145 | ```sh 146 | # All tests 147 | make tests 148 | # Unit tests only 149 | make unit-tests 150 | # Integration tests only 151 | make int-tests 152 | ``` 153 | -------------------------------------------------------------------------------- /bin/localnet.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Original Source: https://github.com/solana-labs/solana/blob/d7b9aca87b0327266cde4f0116113a4203642130/web3.js/bin/localnet.sh 3 | 4 | set -e 5 | 6 | channel="edge" 7 | 8 | usage() { 9 | exitcode=0 10 | if [[ -n "$1" ]]; then 11 | exitcode=1 12 | echo "Error: $*" 13 | fi 14 | cat < - Optional Docker image tag to use 29 | 30 | up-specific options: 31 | - Optional Docker image tag to use 32 | -n - Optional Docker network to join 33 | 34 | Default channel: $channel 35 | 36 | down-specific options: 37 | none 38 | 39 | EOF 40 | exit $exitcode 41 | } 42 | 43 | [[ -n $1 ]] || usage 44 | cmd="$1" 45 | shift 46 | 47 | docker --version || usage "It appears that docker is not installed" 48 | case $cmd in 49 | update) 50 | if [[ -n $1 ]]; then 51 | channel="$1" 52 | fi 53 | ( 54 | set -x 55 | docker pull solanalabs/solana:"$channel" 56 | ) 57 | ;; 58 | up) 59 | while [[ -n $1 ]]; do 60 | if [[ $1 = -n ]]; then 61 | [[ -n $2 ]] || usage "Invalid $1 argument" 62 | network="$2" 63 | shift 2 64 | else 65 | channel=$1 66 | shift 1 67 | fi 68 | done 69 | 70 | ( 71 | set -x 72 | RUST_LOG=${RUST_LOG:-solana=info} 73 | ARGS=( 74 | --detach 75 | --name solana-localnet 76 | --rm 77 | --publish 8899:8899 78 | --publish 8900:8900 79 | --publish 9900:9900 80 | --tty 81 | --env "RUST_LOG=$RUST_LOG" 82 | ) 83 | if [[ -n $network ]]; then 84 | ARGS+=(--network "$network") 85 | fi 86 | 87 | docker run "${ARGS[@]}" solanalabs/solana:"$channel" 88 | 89 | for _ in 1 2 3 4 5; do 90 | if curl \ 91 | -X POST \ 92 | -H "Content-Type: application/json" \ 93 | -d '{"jsonrpc":"2.0","id":1, "method":"getTransactionCount"}' \ 94 | http://localhost:8899; then 95 | break; 96 | fi 97 | sleep 1 98 | done 99 | ) 100 | ;; 101 | down) 102 | ( 103 | set -x 104 | if [[ -n "$(docker ps --filter "name=^solana-localnet$" -q)" ]]; then 105 | docker stop --time 0 solana-localnet 106 | fi 107 | ) 108 | ;; 109 | logs) 110 | follow=false 111 | if [[ -n $1 ]]; then 112 | if [[ $1 = "-f" ]]; then 113 | follow=true 114 | else 115 | usage "Unknown argument: $1" 116 | fi 117 | fi 118 | 119 | while $follow; do 120 | if [[ -n $(docker ps -q -f name=solana-localnet) ]]; then 121 | ( 122 | set -x 123 | docker logs solana-localnet -f 124 | ) || true 125 | fi 126 | sleep 1 127 | done 128 | 129 | ( 130 | set -x 131 | docker logs solana-localnet 132 | ) 133 | ;; 134 | *) 135 | usage "Unknown command: $cmd" 136 | esac 137 | 138 | exit 0 139 | -------------------------------------------------------------------------------- /docs/core/api.md: -------------------------------------------------------------------------------- 1 | # Moved 2 | ️ 3 | This library uses many core types from the [Solders](https://kevinheavey.github.io/solders/) package which used to be provided by solana-py itself. 4 | 5 | If you are upgrading from an old version and you're looking for something that was deleted, it's probably in solders now. -------------------------------------------------------------------------------- /docs/core/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | :::solana.utils 4 | -------------------------------------------------------------------------------- /docs/core/vote_program.md: -------------------------------------------------------------------------------- 1 | # Vote Program 2 | 3 | :::solana.vote_program 4 | -------------------------------------------------------------------------------- /docs/css/mkdocstrings.css: -------------------------------------------------------------------------------- 1 | /* Indentation. */ 2 | div.doc-contents:not(.first) { 3 | padding-left: 25px; 4 | border-left: 4px solid rgba(230, 230, 230); 5 | margin-bottom: 80px; 6 | } 7 | -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhly/solana-py/4c329fa086d762274ded5c665dd267ded056c744/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/img/solana-py-logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhly/solana-py/4c329fa086d762274ded5c665dd267ded056c744/docs/img/solana-py-logo.jpeg -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --8<-- "README.md" 2 | -------------------------------------------------------------------------------- /docs/rpc/api.md: -------------------------------------------------------------------------------- 1 | # API Client 2 | 3 | :::solana.rpc.api 4 | -------------------------------------------------------------------------------- /docs/rpc/async_api.md: -------------------------------------------------------------------------------- 1 | # Async API Client 2 | 3 | :::solana.rpc.async_api 4 | -------------------------------------------------------------------------------- /docs/rpc/commitment.md: -------------------------------------------------------------------------------- 1 | # Commitment 2 | 3 | :::solana.rpc.commitment 4 | -------------------------------------------------------------------------------- /docs/rpc/providers.md: -------------------------------------------------------------------------------- 1 | # RPC Providers 2 | 3 | :::solana.rpc.providers 4 | -------------------------------------------------------------------------------- /docs/rpc/types.md: -------------------------------------------------------------------------------- 1 | # RPC Types 2 | 3 | :::solana.rpc.types 4 | -------------------------------------------------------------------------------- /docs/rpc/websocket.md: -------------------------------------------------------------------------------- 1 | # Websocket Client 2 | 3 | :::solana.rpc.websocket_api 4 | -------------------------------------------------------------------------------- /docs/spl/intro.md: -------------------------------------------------------------------------------- 1 | # Solana Program Library (SPL) 2 | 3 | The Solana Program Library (SPL) is a collection of on-chain programs targeting the 4 | [Sealevel parallel runtime](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192). 5 | These programs are tested against Solana's implementation of Sealevel, solana-runtime, and deployed to its mainnet. 6 | 7 | !!! note 8 | 9 | The Python SPL library is separate from the main Python Solana library, 10 | so you import it with `import spl` instead of `import solana`. 11 | 12 | You don't install it separately though: it gets installed 13 | when you run `pip install solana`. 14 | -------------------------------------------------------------------------------- /docs/spl/memo/constants.md: -------------------------------------------------------------------------------- 1 | # Constants 2 | 3 | :::spl.memo.constants 4 | -------------------------------------------------------------------------------- /docs/spl/memo/instructions.md: -------------------------------------------------------------------------------- 1 | # Memo Program 2 | 3 | :::spl.memo.instructions 4 | -------------------------------------------------------------------------------- /docs/spl/memo/intro.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | The Memo program is a simple program that validates a string of UTF-8 encoded 4 | characters and verifies that any accounts provided are signers of the 5 | transaction. The program also logs the memo, as well as any verified signer 6 | addresses, to the transaction log, so that anyone can easily observe memos and 7 | know they were approved by zero or more addresses by inspecting the transaction 8 | log from a trusted provider. 9 | 10 | ## Background 11 | 12 | Solana's programming model and the definitions of the Solana terms used in this 13 | document are available at: 14 | 15 | - [https://docs.solana.com/apps](https://docs.solana.com/apps) 16 | - [https://docs.solana.com/terminology](https://docs.solana.com/terminology) 17 | 18 | ## Source 19 | 20 | The Memo Program's source is available on 21 | [github](https://github.com/solana-labs/solana-program-library) 22 | 23 | ## Interface 24 | 25 | The on-chain Memo Program is written in Rust and available on crates.io as 26 | [spl-memo](https://crates.io/crates/spl-memo) and 27 | [docs.rs](https://docs.rs/spl-memo). 28 | 29 | The crate provides a `build_memo()` method to easily create a properly 30 | constructed Instruction. 31 | 32 | ## Operational Notes 33 | 34 | If zero accounts are provided to the signed-memo instruction, the program 35 | succeeds when the memo is valid UTF-8, and logs the memo to the transaction log. 36 | 37 | If one or more accounts are provided to the signed-memo instruction, all must be 38 | valid signers of the transaction for the instruction to succeed. 39 | 40 | ### Logs 41 | 42 | This section details expected log output for memo instructions. 43 | 44 | Logging begins with entry into the program: 45 | `Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr invoke [1]` 46 | 47 | The program will include a separate log for each verified signer: 48 | `Program log: Signed by ` 49 | 50 | Then the program logs the memo length and UTF-8 text: 51 | `Program log: Memo (len 4): "🐆"` 52 | 53 | If UTF-8 parsing fails, the program will log the failure point: 54 | `Program log: Invalid UTF-8, from byte 4` 55 | 56 | Logging ends with the status of the instruction, one of: 57 | `Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr success` 58 | `Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr failed: missing required signature for instruction` 59 | `Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr failed: invalid instruction data` 60 | 61 | For more information about exposing program logs on a node, head to the 62 | [developer 63 | docs](https://docs.solana.com/developing/on-chain-programs/debugging#logging) 64 | 65 | ### Compute Limits 66 | 67 | Like all programs, the Memo Program is subject to the cluster's [compute 68 | budget](https://docs.solana.com/developing/programming-model/runtime#compute-budget). 69 | In Memo, compute is used for parsing UTF-8, verifying signers, and logging, 70 | limiting the memo length and number of signers that can be processed 71 | successfully in a single instruction. The longer or more complex the UTF-8 memo, 72 | the fewer signers can be supported, and vice versa. 73 | 74 | As of v1.5.1, an unsigned instruction can support single-byte UTF-8 of up to 566 75 | bytes. An instruction with a simple memo of 32 bytes can support up to 12 76 | signers. -------------------------------------------------------------------------------- /docs/spl/token/async_client.md: -------------------------------------------------------------------------------- 1 | # Async Client 2 | 3 | :::spl.token.async_client 4 | -------------------------------------------------------------------------------- /docs/spl/token/client.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | :::spl.token.client 4 | -------------------------------------------------------------------------------- /docs/spl/token/constants.md: -------------------------------------------------------------------------------- 1 | # Constants 2 | 3 | :::spl.token.constants 4 | -------------------------------------------------------------------------------- /docs/spl/token/core.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | :::spl.token.core 4 | -------------------------------------------------------------------------------- /docs/spl/token/instructions.md: -------------------------------------------------------------------------------- 1 | # Instructions 2 | 3 | :::spl.token.instructions 4 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Solana.py 2 | theme: 3 | name: material 4 | icon: 5 | logo: material/snake 6 | favicon: img/favicon.ico 7 | features: 8 | - navigation.tabs 9 | - navigation.top 10 | - navigation.instant 11 | palette: 12 | - scheme: default 13 | primary: deep purple 14 | toggle: 15 | icon: material/toggle-switch-off-outline 16 | name: Switch to dark mode 17 | - scheme: slate 18 | toggle: 19 | icon: material/toggle-switch 20 | name: Switch to light mode 21 | markdown_extensions: 22 | - pymdownx.highlight 23 | - pymdownx.superfences 24 | - admonition 25 | - pymdownx.snippets 26 | - meta 27 | - pymdownx.tabbed: 28 | alternate_style: true 29 | repo_url: https://github.com/michaelhly/solana-py 30 | repo_name: michaelhly/solana-py 31 | site_url: https://michaelhly.github.io/solana-py/ 32 | site_author: Kevin Heavey & Michael Huang 33 | plugins: 34 | - mkdocstrings: 35 | handlers: 36 | python: 37 | selection: 38 | filters: 39 | - "!^_" # exlude all members starting with _ 40 | - "^__init__$" # but always include __init__ modules and methods 41 | rendering: 42 | show_root_heading: true 43 | show_bases: false 44 | - search 45 | nav: 46 | - index.md 47 | - RPC API: 48 | - rpc/api.md 49 | - rpc/async_api.md 50 | - rpc/websocket.md 51 | - rpc/commitment.md 52 | - rpc/types.md 53 | - rpc/providers.md 54 | - Core API: 55 | - core/api.md 56 | - Solana Program Library (SPL): 57 | - spl/intro.md 58 | - Token: 59 | - spl/token/client.md 60 | - spl/token/async_client.md 61 | - spl/token/constants.md 62 | - spl/token/instructions.md 63 | - spl/token/core.md 64 | - Memo Program: 65 | - spl/memo/intro.md 66 | - spl/memo/constants.md 67 | - spl/memo/instructions.md 68 | extra_css: 69 | - css/mkdocstrings.css 70 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "solana" 3 | version = "0.36.7" 4 | description = "Solana Python API" 5 | authors = [ 6 | "Michael Huang ", 7 | "Kevin Heavey ", 8 | ] 9 | license = "MIT" 10 | readme = "README.md" 11 | homepage = "https://github.com/michaelhly/solanapy" 12 | documentation = "https://michaelhly.github.io/solana-py/" 13 | keywords = ["solana", "blockchain", "web3"] 14 | classifiers = [ 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Natural Language :: English", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | ] 24 | packages = [ 25 | { include = "solana", from = "src" }, 26 | { include = "spl", from = "src" }, 27 | ] 28 | 29 | [tool.poetry.dependencies] 30 | python = "^3.9" 31 | construct-typing = "^0.6.2" 32 | httpx = ">=0.23.0" 33 | typing-extensions = ">=4.2.0" 34 | websockets = ">=9.0,<=15.0.1" 35 | solders = ">=0.23,<0.27" 36 | 37 | [tool.poetry.dev-dependencies] 38 | pytest = "^8.3.4" 39 | mypy = "^1.15" 40 | pyyaml = "6.0.2" 41 | pytest-docker = "^3.2.0" 42 | bump2version = "^1.0.1" 43 | pytest-asyncio = "^0.18.3" 44 | pytest-cov = "^6.0.0" 45 | pytest-html = "^4.1.1" 46 | pytest-xdist = "^3.6.1" 47 | asyncstdlib = "^3.10.2" 48 | mkdocstrings = "^0.18.0" 49 | mkdocs-material = "^8.2.1" 50 | ruff = "^0.7.3" 51 | 52 | 53 | [build-system] 54 | requires = ["poetry-core>=1.0.0"] 55 | build-backend = "poetry.core.masonry.api" 56 | 57 | [tool.pytest.ini_options] 58 | addopts = [ 59 | "--doctest-modules", 60 | "-p no:anyio", 61 | "--cov", 62 | "--color=yes", 63 | "--cov-report=xml", 64 | "--cov-report=html", 65 | "--container-scope=module", # restart container for each module 66 | "--dist=loadscope", # distribute tests by module 67 | "-vv", 68 | "-s", 69 | ] 70 | asyncio_mode = "auto" 71 | markers = ["integration: mark a test as a integration test."] 72 | testpaths = ["src", "tests"] 73 | 74 | [tool.ruff] 75 | line-length = 120 76 | # Assume Python 3.9 (lower bound of supported python versions) 77 | target-version = "py39" 78 | 79 | [tool.ruff.lint] 80 | # Enable Pyflakes `E` and `F` codes by default. 81 | select = [ 82 | "A", 83 | "B", 84 | "D", 85 | "E", 86 | "F", 87 | "I", 88 | "ARG", 89 | "BLE", 90 | "C4", 91 | "SIM", 92 | "PLC", 93 | "PLE", 94 | "PLR", 95 | "PLW", 96 | "RUF", 97 | ] 98 | ignore = ["D203", "I001"] 99 | 100 | # Exclude a variety of commonly ignored directories. 101 | exclude = [ 102 | ".bzr", 103 | ".direnv", 104 | ".eggs", 105 | ".git", 106 | ".hg", 107 | ".mypy_cache", 108 | ".nox", 109 | ".pants.d", 110 | ".ruff_cache", 111 | ".svn", 112 | ".tox", 113 | ".venv", 114 | "__pypackages__", 115 | "_build", 116 | "buck-out", 117 | "build", 118 | "dist", 119 | "node_modules", 120 | "venv", 121 | ] 122 | 123 | # Allow unused variables when underscore-prefixed. 124 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 125 | 126 | [tool.ruff.lint.pydocstyle] 127 | convention = "google" 128 | 129 | [tool.ruff.lint.per-file-ignores] 130 | "src/**/*.py" = ["PLR0913", "RUF012"] 131 | "src/solana/blockhash.py" = ["A003"] 132 | "src/solana/rpc/api.py" = ["PLR0912"] 133 | "tests/**/*.py" = ["D100", "D103", "ARG001", "PLR2004"] 134 | 135 | [tool.pyright] 136 | reportMissingModuleSource = false 137 | 138 | [tool.mypy] 139 | check_untyped_defs = true 140 | -------------------------------------------------------------------------------- /src/solana/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | import sys 3 | 4 | if sys.version_info < (3, 7): 5 | raise EnvironmentError("Python 3.7 or above is required.") 6 | -------------------------------------------------------------------------------- /src/solana/_layouts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhly/solana-py/4c329fa086d762274ded5c665dd267ded056c744/src/solana/_layouts/__init__.py -------------------------------------------------------------------------------- /src/solana/_layouts/vote_instructions.py: -------------------------------------------------------------------------------- 1 | """Byte layouts for vote program instructions.""" 2 | 3 | from enum import IntEnum 4 | 5 | from construct import ( 6 | Int32ul, 7 | Int64ul, 8 | Switch, # type: ignore 9 | ) 10 | from construct import Struct as cStruct 11 | 12 | 13 | class InstructionType(IntEnum): 14 | """Instruction types for vote program.""" 15 | 16 | WITHDRAW_FROM_VOTE_ACCOUNT = 3 17 | 18 | 19 | _WITHDRAW_FROM_VOTE_ACCOUNT_LAYOUT = cStruct("lamports" / Int64ul) 20 | 21 | VOTE_INSTRUCTIONS_LAYOUT = cStruct( 22 | "instruction_type" / Int32ul, 23 | "args" 24 | / Switch( 25 | lambda this: this.instruction_type, 26 | { 27 | InstructionType.WITHDRAW_FROM_VOTE_ACCOUNT: _WITHDRAW_FROM_VOTE_ACCOUNT_LAYOUT, 28 | }, 29 | ), 30 | ) 31 | -------------------------------------------------------------------------------- /src/solana/constants.py: -------------------------------------------------------------------------------- 1 | """Solana constants.""" 2 | 3 | from solders.pubkey import Pubkey 4 | 5 | LAMPORTS_PER_SOL: int = 1_000_000_000 6 | """Number of lamports per SOL, where 1 SOL equals 1 billion lamports.""" 7 | 8 | SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111") 9 | """Program ID for the System Program.""" 10 | 11 | CONFIG_PROGRAM_ID: Pubkey = Pubkey.from_string("Config1111111111111111111111111111111111111") 12 | """Program ID for the Config Program.""" 13 | 14 | STAKE_PROGRAM_ID: Pubkey = Pubkey.from_string("Stake11111111111111111111111111111111111111") 15 | """Program ID for the Stake Program.""" 16 | 17 | VOTE_PROGRAM_ID: Pubkey = Pubkey.from_string("Vote111111111111111111111111111111111111111") 18 | """Program ID for the Vote Program.""" 19 | 20 | ADDRESS_LOOKUP_TABLE_PROGRAM_ID: Pubkey = Pubkey.from_string("AddressLookupTab1e1111111111111111111111111") 21 | """Program ID for the Address Lookup Table Program.""" 22 | 23 | BPF_LOADER_PROGRAM_ID: Pubkey = Pubkey.from_string("BPFLoaderUpgradeab1e11111111111111111111111") 24 | """Program ID for the BPF Loader Program.""" 25 | 26 | ED25519_PROGRAM_ID: Pubkey = Pubkey.from_string("Ed25519SigVerify111111111111111111111111111") 27 | """Program ID for the Ed25519 Program.""" 28 | 29 | SECP256K1_PROGRAM_ID: Pubkey = Pubkey.from_string("KeccakSecp256k11111111111111111111111111111") 30 | """Program ID for the Secp256k1 Program.""" 31 | -------------------------------------------------------------------------------- /src/solana/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions native to solana-py.""" 2 | 3 | import sys 4 | from typing import Any, Callable, Coroutine, Type, TypeVar 5 | 6 | 7 | class SolanaExceptionBase(Exception): 8 | """Base class for Solana-py exceptions.""" 9 | 10 | def __init__(self, exc: Exception, func: Callable[[Any], Any], *args: Any, **kwargs: Any) -> None: 11 | """Init.""" 12 | super().__init__() 13 | self.error_msg = self._build_error_message(exc, func, *args, **kwargs) 14 | 15 | @staticmethod 16 | def _build_error_message( 17 | exc: Exception, 18 | func: Callable[[Any], Any], 19 | *args: Any, # noqa: ARG004 20 | **kwargs: Any, # noqa: ARG004 21 | ) -> str: 22 | return f"{type(exc)} raised in {func} invokation" 23 | 24 | 25 | class SolanaRpcException(SolanaExceptionBase): 26 | """Class for Solana-py RPC exceptions.""" 27 | 28 | @staticmethod 29 | def _build_error_message( 30 | exc: Exception, 31 | func: Callable[[Any], Any], # noqa: ARG004 32 | *args: Any, 33 | **kwargs: Any, # noqa: ARG004 34 | ) -> str: 35 | rpc_method = args[1].__class__.__name__ 36 | return f'{type(exc)} raised in "{rpc_method}" endpoint request' 37 | 38 | 39 | # Because we need to support python version older then 3.10 we don't always have access to ParamSpec, 40 | # so in order to remove code duplication we have to share an untyped function 41 | def _untyped_handle_exceptions(internal_exception_cls, *exception_types_caught): 42 | def func_decorator(func): 43 | def argument_decorator(*args, **kwargs): 44 | try: 45 | return func(*args, **kwargs) 46 | except exception_types_caught as exc: 47 | raise internal_exception_cls(exc, func, *args, **kwargs) from exc 48 | 49 | return argument_decorator 50 | 51 | return func_decorator 52 | 53 | 54 | def _untyped_handle_async_exceptions( 55 | internal_exception_cls: Type[SolanaRpcException], *exception_types_caught: Type[Exception] 56 | ): 57 | def func_decorator(func): 58 | async def argument_decorator(*args, **kwargs): 59 | try: 60 | return await func(*args, **kwargs) 61 | except exception_types_caught as exc: 62 | raise internal_exception_cls(exc, func, *args, **kwargs) from exc 63 | 64 | return argument_decorator 65 | 66 | return func_decorator 67 | 68 | 69 | T = TypeVar("T") 70 | if sys.version_info >= (3, 10): 71 | from typing import ParamSpec 72 | 73 | P = ParamSpec("P") 74 | 75 | def handle_exceptions( 76 | internal_exception_cls: Type[SolanaRpcException], *exception_types_caught: Type[Exception] 77 | ) -> Callable[[Callable[P, T]], Callable[P, T]]: 78 | """Decorator for handling non-async exception.""" 79 | return _untyped_handle_exceptions(internal_exception_cls, *exception_types_caught) # type: ignore 80 | 81 | def handle_async_exceptions( 82 | internal_exception_cls: Type[SolanaRpcException], *exception_types_caught: Type[Exception] 83 | ) -> Callable[[Callable[P, Coroutine[Any, Any, T]]], Callable[P, Coroutine[Any, Any, T]]]: 84 | """Decorator for handling async exception.""" 85 | return _untyped_handle_async_exceptions(internal_exception_cls, *exception_types_caught) # type: ignore 86 | 87 | else: 88 | 89 | def handle_exceptions(internal_exception_cls: Type[SolanaRpcException], *exception_types_caught: Type[Exception]): 90 | """Decorator for handling non-async exception.""" 91 | return _untyped_handle_exceptions(internal_exception_cls, *exception_types_caught) 92 | 93 | def handle_async_exceptions( 94 | internal_exception_cls: Type[SolanaRpcException], *exception_types_caught: Type[Exception] 95 | ): 96 | """Decorator for handling async exception.""" 97 | return _untyped_handle_async_exceptions(internal_exception_cls, *exception_types_caught) 98 | -------------------------------------------------------------------------------- /src/solana/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhly/solana-py/4c329fa086d762274ded5c665dd267ded056c744/src/solana/py.typed -------------------------------------------------------------------------------- /src/solana/rpc/__init__.py: -------------------------------------------------------------------------------- 1 | """Clients for the Soloana JSON RPC API.""" 2 | -------------------------------------------------------------------------------- /src/solana/rpc/commitment.py: -------------------------------------------------------------------------------- 1 | """Commitment options. 2 | 3 | Solana nodes choose which bank state to query based on a commitment requirement set by the client. 4 | 5 | In descending order of commitment (most finalized to least finalized), clients may specify: 6 | """ 7 | 8 | from typing import NewType 9 | 10 | Commitment = NewType("Commitment", str) 11 | """Type for commitment.""" 12 | 13 | Finalized = Commitment("finalized") 14 | """The node will query the most recent block confirmed by supermajority of the cluster as having reached maximum 15 | lockout, meaning the cluster has recognized this block as finalized.""" 16 | 17 | Confirmed = Commitment("confirmed") 18 | """The node will query the most recent block that has been voted on by supermajority of the cluster. 19 | 20 | - It incorporates votes from gossip and replay. 21 | - It does not count votes on descendants of a block, only direct votes on that block. 22 | - This confirmation level also upholds "optimistic confirmation" guarantees in release 1.3 and onwards. 23 | """ 24 | 25 | Processed = Commitment("processed") 26 | """The node will query its most recent block. Note that the block may not be complete.""" 27 | 28 | 29 | Max = Commitment("max") 30 | """Deprecated""" 31 | 32 | Root = Commitment("root") 33 | """Deprecated""" 34 | 35 | Single = Commitment("singleGossip") 36 | """Deprecated""" 37 | 38 | Recent = Commitment("recent") 39 | """Deprecated""" 40 | -------------------------------------------------------------------------------- /src/solana/rpc/providers/__init__.py: -------------------------------------------------------------------------------- 1 | """RPC Providers.""" 2 | -------------------------------------------------------------------------------- /src/solana/rpc/providers/async_base.py: -------------------------------------------------------------------------------- 1 | """Async base RPC Provider.""" 2 | 3 | from typing import Type 4 | 5 | from solders.rpc.requests import Body 6 | 7 | from .core import T 8 | 9 | 10 | class AsyncBaseProvider: 11 | """Base class for async RPC providers to implement.""" 12 | 13 | async def make_request(self, body: Body, parser: Type[T]) -> T: 14 | """Make a request ot the rpc endpoint.""" 15 | raise NotImplementedError("Providers must implement this method") 16 | -------------------------------------------------------------------------------- /src/solana/rpc/providers/async_http.py: -------------------------------------------------------------------------------- 1 | """Async HTTP RPC Provider.""" 2 | 3 | from typing import Dict, Optional, Tuple, Type, overload 4 | 5 | import httpx 6 | from solders.rpc.requests import Body 7 | from solders.rpc.responses import RPCResult 8 | 9 | from ...exceptions import SolanaRpcException, handle_async_exceptions 10 | from .async_base import AsyncBaseProvider 11 | from .core import ( 12 | DEFAULT_TIMEOUT, 13 | T, 14 | _after_request_unparsed, 15 | _BodiesTup, 16 | _BodiesTup1, 17 | _BodiesTup2, 18 | _BodiesTup3, 19 | _BodiesTup4, 20 | _BodiesTup5, 21 | _HTTPProviderCore, 22 | _parse_raw, 23 | _parse_raw_batch, 24 | _RespTup, 25 | _RespTup1, 26 | _RespTup2, 27 | _RespTup3, 28 | _RespTup4, 29 | _RespTup5, 30 | _Tup, 31 | _Tup1, 32 | _Tup2, 33 | _Tup3, 34 | _Tup4, 35 | _Tup5, 36 | _Tuples, 37 | ) 38 | 39 | 40 | class AsyncHTTPProvider(AsyncBaseProvider, _HTTPProviderCore): 41 | """Async HTTP provider to interact with the http rpc endpoint.""" 42 | 43 | def __init__( 44 | self, 45 | endpoint: Optional[str] = None, 46 | extra_headers: Optional[Dict[str, str]] = None, 47 | timeout: float = DEFAULT_TIMEOUT, 48 | proxy: Optional[str] = None, 49 | ): 50 | """Init AsyncHTTPProvider.""" 51 | super().__init__(endpoint, extra_headers) 52 | self.session = httpx.AsyncClient(timeout=timeout, proxy=proxy) 53 | 54 | def __str__(self) -> str: 55 | """String definition for HTTPProvider.""" 56 | return f"Async HTTP RPC connection {self.endpoint_uri}" 57 | 58 | @handle_async_exceptions(SolanaRpcException, httpx.HTTPError) 59 | async def make_request(self, body: Body, parser: Type[T]) -> T: 60 | """Make an async HTTP request to an http rpc endpoint.""" 61 | raw = await self.make_request_unparsed(body) 62 | return _parse_raw(raw, parser=parser) 63 | 64 | async def make_request_unparsed(self, body: Body) -> str: 65 | """Make an async HTTP request to an http rpc endpoint.""" 66 | request_kwargs = self._before_request(body=body) 67 | raw_response = await self.session.post(**request_kwargs) 68 | return _after_request_unparsed(raw_response) 69 | 70 | async def make_batch_request_unparsed(self, reqs: Tuple[Body, ...]) -> str: 71 | """Make an async HTTP request to an http rpc endpoint.""" 72 | request_kwargs = self._before_batch_request(reqs) 73 | raw_response = await self.session.post(**request_kwargs) 74 | return _after_request_unparsed(raw_response) 75 | 76 | @overload 77 | async def make_batch_request(self, reqs: _BodiesTup, parsers: _Tup) -> _RespTup: ... 78 | 79 | @overload 80 | async def make_batch_request(self, reqs: _BodiesTup1, parsers: _Tup1) -> _RespTup1: ... 81 | 82 | @overload 83 | async def make_batch_request(self, reqs: _BodiesTup2, parsers: _Tup2) -> _RespTup2: ... 84 | 85 | @overload 86 | async def make_batch_request(self, reqs: _BodiesTup3, parsers: _Tup3) -> _RespTup3: ... 87 | 88 | @overload 89 | async def make_batch_request(self, reqs: _BodiesTup4, parsers: _Tup4) -> _RespTup4: ... 90 | 91 | @overload 92 | async def make_batch_request(self, reqs: _BodiesTup5, parsers: _Tup5) -> _RespTup5: ... 93 | 94 | async def make_batch_request(self, reqs: Tuple[Body, ...], parsers: _Tuples) -> Tuple[RPCResult, ...]: 95 | """Make an async HTTP batch request to an http rpc endpoint. 96 | 97 | Args: 98 | reqs: A tuple of request objects from ``solders.rpc.requests``. 99 | parsers: A tuple of response classes from ``solders.rpc.responses``. 100 | Note: ``parsers`` should line up with ``reqs``. 101 | 102 | Example: 103 | >>> from solana.rpc.providers.async_http import AsyncHTTPProvider 104 | >>> from solders.rpc.requests import GetBlockHeight, GetFirstAvailableBlock 105 | >>> from solders.rpc.responses import GetBlockHeightResp, GetFirstAvailableBlockResp 106 | >>> provider = AsyncHTTPProvider("https://api.devnet.solana.com") 107 | >>> reqs = (GetBlockHeight(), GetFirstAvailableBlock()) 108 | >>> parsers = (GetBlockHeightResp, GetFirstAvailableBlockResp) 109 | >>> await provider.make_batch_request(reqs, parsers) # doctest: +SKIP 110 | (GetBlockHeightResp( 111 | 158613909, 112 | ), GetFirstAvailableBlockResp( 113 | 86753592, 114 | )) 115 | """ 116 | raw = await self.make_batch_request_unparsed(reqs) 117 | return _parse_raw_batch(raw, parsers) 118 | 119 | async def __aenter__(self) -> "AsyncHTTPProvider": 120 | """Use as a context manager.""" 121 | await self.session.__aenter__() 122 | return self 123 | 124 | async def __aexit__(self, _exc_type, _exc, _tb): 125 | """Exits the context manager.""" 126 | await self.close() 127 | 128 | async def close(self) -> None: 129 | """Close session.""" 130 | await self.session.aclose() 131 | -------------------------------------------------------------------------------- /src/solana/rpc/providers/base.py: -------------------------------------------------------------------------------- 1 | """Base RPC Provider.""" 2 | 3 | from solders.rpc.requests import Body 4 | from typing_extensions import Type 5 | 6 | from .core import T 7 | 8 | 9 | class BaseProvider: 10 | """Base class for RPC providers to implement.""" 11 | 12 | def make_request(self, body: Body, parser: Type[T]) -> T: 13 | """Make a request to the rpc endpoint.""" 14 | raise NotImplementedError("Providers must implement this method") 15 | -------------------------------------------------------------------------------- /src/solana/rpc/providers/core.py: -------------------------------------------------------------------------------- 1 | """Helper code for HTTP provider classes.""" 2 | 3 | import itertools 4 | import logging 5 | import os 6 | from typing import Any, Dict, Optional, Tuple, Type, TypeVar, Union, overload 7 | 8 | import httpx 9 | from solders.rpc.requests import Body 10 | from solders.rpc.requests import batch_to_json as batch_req_json 11 | from solders.rpc.responses import Resp, RPCError, RPCResult 12 | from solders.rpc.responses import batch_from_json as batch_resp_json 13 | 14 | from ..core import RPCException 15 | from ..types import URI 16 | 17 | DEFAULT_TIMEOUT = 10 18 | 19 | 20 | T = TypeVar("T", bound=RPCResult) 21 | # hacky solution for parsing batches of up to six 22 | _T1 = TypeVar("_T1", bound=RPCResult) 23 | _T2 = TypeVar("_T2", bound=RPCResult) 24 | _T3 = TypeVar("_T3", bound=RPCResult) 25 | _T4 = TypeVar("_T4", bound=RPCResult) 26 | _T5 = TypeVar("_T5", bound=RPCResult) 27 | 28 | _Tup = Tuple[Type[T]] 29 | _Tup1 = Tuple[Type[T], Type[_T1]] 30 | _Tup2 = Tuple[Type[T], Type[_T1], Type[_T2]] 31 | _Tup3 = Tuple[Type[T], Type[_T1], Type[_T2], Type[_T3]] 32 | _Tup4 = Tuple[Type[T], Type[_T1], Type[_T2], Type[_T3], Type[_T4]] 33 | _Tup5 = Tuple[Type[T], Type[_T1], Type[_T2], Type[_T3], Type[_T4], Type[_T5]] 34 | _Tuples = Union[_Tup, _Tup1, _Tup2, _Tup3, _Tup4, _Tup5] 35 | 36 | _RespTup = Tuple[Resp[T]] 37 | _RespTup1 = Tuple[Resp[T], Resp[_T1]] 38 | _RespTup2 = Tuple[Resp[T], Resp[_T1], Resp[_T2]] 39 | _RespTup3 = Tuple[Resp[T], Resp[_T1], Resp[_T2], Resp[_T3]] 40 | _RespTup4 = Tuple[Resp[T], Resp[_T1], Resp[_T2], Resp[_T3], Resp[_T4]] 41 | _RespTup5 = Tuple[Resp[T], Resp[_T1], Resp[_T2], Resp[_T3], Resp[_T4], Resp[_T5]] 42 | 43 | _BodiesTup = Tuple[Body] 44 | _BodiesTup1 = Tuple[Body, Body] 45 | _BodiesTup2 = Tuple[Body, Body, Body] 46 | _BodiesTup3 = Tuple[Body, Body, Body, Body] 47 | _BodiesTup4 = Tuple[Body, Body, Body, Body, Body] 48 | _BodiesTup5 = Tuple[Body, Body, Body, Body, Body, Body] 49 | 50 | 51 | def get_default_endpoint() -> URI: 52 | """Get the default http rpc endpoint.""" 53 | return URI(os.environ.get("SOLANARPC_HTTP_URI", "http://localhost:8899")) 54 | 55 | 56 | class _HTTPProviderCore: # pylint: disable=too-few-public-methods 57 | logger = logging.getLogger("solanaweb3.rpc.httprpc.HTTPClient") 58 | 59 | def __init__( 60 | self, 61 | endpoint: Optional[str] = None, 62 | extra_headers: Optional[Dict[str, str]] = None, 63 | timeout: float = DEFAULT_TIMEOUT, 64 | ): 65 | """Init.""" 66 | self._request_counter = itertools.count() 67 | self.endpoint_uri = get_default_endpoint() if not endpoint else URI(endpoint) 68 | self.health_uri = URI(f"{self.endpoint_uri}/health") 69 | self.timeout = timeout 70 | self.extra_headers = extra_headers 71 | 72 | def _build_common_request_kwargs(self) -> Dict[str, Any]: 73 | headers = {"Content-Type": "application/json"} 74 | if self.extra_headers: 75 | headers.update(self.extra_headers) 76 | return {"url": self.endpoint_uri, "headers": headers} 77 | 78 | def _build_request_kwargs(self, body: Body) -> Dict[str, Any]: 79 | common_kwargs = self._build_common_request_kwargs() 80 | data = body.to_json() 81 | return {**common_kwargs, "content": data} 82 | 83 | def _build_batch_request_kwargs(self, reqs: Tuple[Body, ...]) -> Dict[str, Any]: 84 | common_kwargs = self._build_common_request_kwargs() 85 | data = batch_req_json(reqs) 86 | return {**common_kwargs, "content": data} 87 | 88 | def _before_request(self, body: Body) -> Dict[str, Any]: 89 | return self._build_request_kwargs(body=body) 90 | 91 | def _before_batch_request(self, reqs: Tuple[Body, ...]) -> Dict[str, Any]: 92 | return self._build_batch_request_kwargs(reqs) 93 | 94 | 95 | def _parse_raw(raw: str, parser: Type[T]) -> T: 96 | parsed = parser.from_json(raw) # type: ignore 97 | if isinstance(parsed, RPCError.__args__): # type: ignore # TODO: drop py37 and use typing.get_args 98 | raise RPCException(parsed) 99 | return parsed # type: ignore 100 | 101 | 102 | @overload 103 | def _parse_raw_batch(raw: str, parsers: _Tup) -> _RespTup: ... 104 | 105 | 106 | @overload 107 | def _parse_raw_batch(raw: str, parsers: _Tup1) -> _RespTup1: ... 108 | 109 | 110 | @overload 111 | def _parse_raw_batch(raw: str, parsers: _Tup2) -> _RespTup2: ... 112 | 113 | 114 | @overload 115 | def _parse_raw_batch(raw: str, parsers: _Tup3) -> _RespTup3: ... 116 | 117 | 118 | @overload 119 | def _parse_raw_batch(raw: str, parsers: _Tup4) -> _RespTup4: ... 120 | 121 | 122 | @overload 123 | def _parse_raw_batch(raw: str, parsers: _Tup5) -> _RespTup5: ... 124 | 125 | 126 | def _parse_raw_batch(raw: str, parsers: _Tuples) -> Tuple[RPCResult, ...]: 127 | return tuple(batch_resp_json(raw, parsers)) 128 | 129 | 130 | def _after_request_unparsed(raw_response: httpx.Response) -> str: 131 | raw_response.raise_for_status() 132 | return raw_response.text 133 | 134 | 135 | @overload 136 | def _after_batch_request(raw_response: httpx.Response, parsers: _Tup) -> _RespTup: ... 137 | 138 | 139 | @overload 140 | def _after_batch_request(raw_response: httpx.Response, parsers: _Tup1) -> _RespTup1: ... 141 | 142 | 143 | @overload 144 | def _after_batch_request(raw_response: httpx.Response, parsers: _Tup2) -> _RespTup2: ... 145 | 146 | 147 | @overload 148 | def _after_batch_request(raw_response: httpx.Response, parsers: _Tup3) -> _RespTup3: ... 149 | 150 | 151 | @overload 152 | def _after_batch_request(raw_response: httpx.Response, parsers: _Tup4) -> _RespTup4: ... 153 | 154 | 155 | @overload 156 | def _after_batch_request(raw_response: httpx.Response, parsers: _Tup5) -> _RespTup5: ... 157 | 158 | 159 | def _after_batch_request(raw_response: httpx.Response, parsers: _Tuples) -> Tuple[RPCResult, ...]: 160 | text = _after_request_unparsed(raw_response) 161 | return _parse_raw_batch(text, parsers) # type: ignore 162 | -------------------------------------------------------------------------------- /src/solana/rpc/providers/http.py: -------------------------------------------------------------------------------- 1 | """HTTP RPC Provider.""" 2 | 3 | from typing import Optional, Tuple, Dict, Type, overload 4 | 5 | import httpx 6 | from solders.rpc.requests import Body 7 | from solders.rpc.responses import RPCResult 8 | 9 | from ...exceptions import SolanaRpcException, handle_exceptions 10 | from .base import BaseProvider 11 | from .core import ( 12 | DEFAULT_TIMEOUT, 13 | T, 14 | _after_request_unparsed, 15 | _BodiesTup, 16 | _BodiesTup1, 17 | _BodiesTup2, 18 | _BodiesTup3, 19 | _BodiesTup4, 20 | _BodiesTup5, 21 | _HTTPProviderCore, 22 | _parse_raw, 23 | _parse_raw_batch, 24 | _RespTup, 25 | _RespTup1, 26 | _RespTup2, 27 | _RespTup3, 28 | _RespTup4, 29 | _RespTup5, 30 | _Tup, 31 | _Tup1, 32 | _Tup2, 33 | _Tup3, 34 | _Tup4, 35 | _Tup5, 36 | _Tuples, 37 | ) 38 | 39 | 40 | class HTTPProvider(BaseProvider, _HTTPProviderCore): 41 | """HTTP provider to interact with the http rpc endpoint.""" 42 | 43 | def __init__( 44 | self, 45 | endpoint: Optional[str] = None, 46 | extra_headers: Optional[Dict[str, str]] = None, 47 | timeout: float = DEFAULT_TIMEOUT, 48 | proxy: Optional[str] = None, 49 | ): 50 | """Init HTTPProvider.""" 51 | super().__init__(endpoint, extra_headers) 52 | self.session = httpx.Client(timeout=timeout, proxy=proxy) 53 | 54 | def __str__(self) -> str: 55 | """String definition for HTTPProvider.""" 56 | return f"HTTP RPC connection {self.endpoint_uri}" 57 | 58 | @handle_exceptions(SolanaRpcException, httpx.HTTPError) 59 | def make_request(self, body: Body, parser: Type[T]) -> T: 60 | """Make an HTTP request to an http rpc endpoint.""" 61 | raw = self.make_request_unparsed(body) 62 | return _parse_raw(raw, parser=parser) 63 | 64 | def make_request_unparsed(self, body: Body) -> str: 65 | """Make an async HTTP request to an http rpc endpoint.""" 66 | request_kwargs = self._before_request(body=body) 67 | raw_response = self.session.post(**request_kwargs) 68 | return _after_request_unparsed(raw_response) 69 | 70 | def make_batch_request_unparsed(self, reqs: Tuple[Body, ...]) -> str: 71 | """Make an async HTTP request to an http rpc endpoint.""" 72 | request_kwargs = self._before_batch_request(reqs) 73 | raw_response = self.session.post(**request_kwargs) 74 | return _after_request_unparsed(raw_response) 75 | 76 | @overload 77 | def make_batch_request(self, reqs: _BodiesTup, parsers: _Tup) -> _RespTup: ... 78 | 79 | @overload 80 | def make_batch_request(self, reqs: _BodiesTup1, parsers: _Tup1) -> _RespTup1: ... 81 | 82 | @overload 83 | def make_batch_request(self, reqs: _BodiesTup2, parsers: _Tup2) -> _RespTup2: ... 84 | 85 | @overload 86 | def make_batch_request(self, reqs: _BodiesTup3, parsers: _Tup3) -> _RespTup3: ... 87 | 88 | @overload 89 | def make_batch_request(self, reqs: _BodiesTup4, parsers: _Tup4) -> _RespTup4: ... 90 | 91 | @overload 92 | def make_batch_request(self, reqs: _BodiesTup5, parsers: _Tup5) -> _RespTup5: ... 93 | 94 | def make_batch_request(self, reqs: Tuple[Body, ...], parsers: _Tuples) -> Tuple[RPCResult, ...]: 95 | """Make a HTTP batch request to an http rpc endpoint. 96 | 97 | Args: 98 | reqs: A tuple of request objects from ``solders.rpc.requests``. 99 | parsers: A tuple of response classes from ``solders.rpc.responses``. 100 | Note: ``parsers`` should line up with ``reqs``. 101 | 102 | Example: 103 | >>> from solana.rpc.providers.http import HTTPProvider 104 | >>> from solders.rpc.requests import GetBlockHeight, GetFirstAvailableBlock 105 | >>> from solders.rpc.responses import GetBlockHeightResp, GetFirstAvailableBlockResp 106 | >>> provider = HTTPProvider("https://api.devnet.solana.com") 107 | >>> reqs = (GetBlockHeight(), GetFirstAvailableBlock()) 108 | >>> parsers = (GetBlockHeightResp, GetFirstAvailableBlockResp) 109 | >>> provider.make_batch_request(reqs, parsers) # doctest: +SKIP 110 | (GetBlockHeightResp( 111 | 158613909, 112 | ), GetFirstAvailableBlockResp( 113 | 86753592, 114 | )) 115 | """ 116 | raw = self.make_batch_request_unparsed(reqs) 117 | return _parse_raw_batch(raw, parsers) 118 | -------------------------------------------------------------------------------- /src/solana/rpc/types.py: -------------------------------------------------------------------------------- 1 | """RPC types.""" 2 | 3 | from typing import NamedTuple, NewType, Optional 4 | 5 | from solders.pubkey import Pubkey 6 | from typing_extensions import TypedDict 7 | 8 | from .commitment import Commitment, Finalized 9 | 10 | URI = NewType("URI", str) 11 | """Type for endpoint URI.""" 12 | 13 | RPCMethod = NewType("RPCMethod", str) 14 | """Type for RPC method.""" 15 | 16 | 17 | class RPCError(TypedDict): 18 | """RPC error.""" 19 | 20 | code: int 21 | """HTTP status code.""" 22 | message: str 23 | """Error message.""" 24 | 25 | 26 | class DataSliceOpts(NamedTuple): 27 | """Option to limit the returned account data, only available for "base58" or "base64" encoding.""" 28 | 29 | offset: int 30 | """Limit the returned account data using the provided offset: .""" 31 | length: int 32 | """Limit the returned account data using the provided length: .""" 33 | 34 | 35 | class MemcmpOpts(NamedTuple): 36 | """Option to compare a provided series of bytes with program account data at a particular offset.""" 37 | 38 | offset: int 39 | """Offset into program account data to start comparison: .""" 40 | bytes: str 41 | """Data to match, as base-58 encoded string: .""" 42 | 43 | 44 | class TokenAccountOpts(NamedTuple): 45 | """Options when querying token accounts. 46 | 47 | Provide one of mint or program_id. 48 | """ 49 | 50 | mint: Optional[Pubkey] = None 51 | """Public key of the specific token Mint to limit accounts to.""" 52 | program_id: Optional[Pubkey] = None 53 | """Public key of the Token program ID that owns the accounts.""" 54 | encoding: str = "base64" 55 | """Encoding for Account data, either "base58" (slow) or "base64".""" 56 | data_slice: Optional[DataSliceOpts] = None 57 | """Option to limit the returned account data, only available for "base58" or "base64" encoding.""" 58 | 59 | 60 | class TxOpts(NamedTuple): 61 | """Options to specify when broadcasting a transaction.""" 62 | 63 | skip_confirmation: bool = True 64 | """If false, `send_transaction` will try to confirm that the transaction was successfully broadcasted. 65 | 66 | When confirming a transaction, `send_transaction` will block for a maximum of 30 seconds. Wrap the call 67 | inside a thread to make it asynchronous. 68 | """ 69 | skip_preflight: bool = False 70 | """If true, skip the preflight transaction checks.""" 71 | preflight_commitment: Commitment = Finalized 72 | """Commitment level to use for preflight.""" 73 | max_retries: Optional[int] = None 74 | """Maximum number of times for the RPC node to retry sending the transaction to the leader. 75 | If this parameter not provided, the RPC node will retry the transaction until it is finalized 76 | or until the blockhash expires. 77 | """ 78 | last_valid_block_height: Optional[int] = None 79 | """Pass the latest valid block height here, to be consumed by confirm_transaction. 80 | Valid only if skip_confirmation is False. 81 | """ 82 | -------------------------------------------------------------------------------- /src/solana/rpc/websocket_api.py: -------------------------------------------------------------------------------- 1 | """This module contains code for interacting with the RPC Websocket endpoint.""" 2 | 3 | import itertools 4 | from typing import Any, Dict, List, Optional, Sequence, Union, cast 5 | 6 | from solders.account_decoder import UiDataSliceConfig 7 | from solders.pubkey import Pubkey 8 | from solders.rpc.config import ( 9 | RpcAccountInfoConfig, 10 | RpcBlockSubscribeConfig, 11 | RpcBlockSubscribeFilter, 12 | RpcBlockSubscribeFilterMentions, 13 | RpcProgramAccountsConfig, 14 | RpcSignatureSubscribeConfig, 15 | RpcTransactionLogsConfig, 16 | RpcTransactionLogsFilter, 17 | RpcTransactionLogsFilterMentions, 18 | ) 19 | from solders.rpc.filter import Memcmp 20 | from solders.rpc.requests import ( 21 | AccountSubscribe, 22 | AccountUnsubscribe, 23 | BlockSubscribe, 24 | BlockUnsubscribe, 25 | Body, 26 | LogsSubscribe, 27 | LogsUnsubscribe, 28 | ProgramSubscribe, 29 | ProgramUnsubscribe, 30 | RootSubscribe, 31 | RootUnsubscribe, 32 | SignatureSubscribe, 33 | SignatureUnsubscribe, 34 | SlotSubscribe, 35 | SlotsUpdatesSubscribe, 36 | SlotsUpdatesUnsubscribe, 37 | SlotUnsubscribe, 38 | VoteSubscribe, 39 | VoteUnsubscribe, 40 | batch_to_json, 41 | ) 42 | from solders.rpc.responses import Notification 43 | from solders.rpc.responses import SubscriptionError as SoldersSubscriptionError 44 | from solders.rpc.responses import SubscriptionResult, parse_websocket_message 45 | from solders.signature import Signature 46 | from solders.transaction_status import TransactionDetails 47 | from websockets.legacy.client import WebSocketClientProtocol 48 | from websockets.legacy.client import connect as ws_connect 49 | 50 | from solana.rpc import types 51 | from solana.rpc.commitment import Commitment 52 | from solana.rpc.core import _ACCOUNT_ENCODING_TO_SOLDERS, _COMMITMENT_TO_SOLDERS, _TX_ENCODING_TO_SOLDERS 53 | 54 | 55 | class SubscriptionError(Exception): 56 | """Raise when subscribing to an RPC feed fails.""" 57 | 58 | def __init__(self, err: SoldersSubscriptionError, subscription: Body) -> None: 59 | """Init. 60 | 61 | Args: 62 | err: The RPC error object. 63 | subscription: The subscription message that caused the error. 64 | 65 | """ 66 | self.type = err.error.__class__ 67 | self.msg: str = err.error.message # type: ignore # TODO: narrow this union type 68 | self.subscription = subscription 69 | super().__init__(f"{self.type.__name__}: {self.msg}\n Caused by subscription: {subscription}") 70 | 71 | 72 | class SolanaWsClientProtocol(WebSocketClientProtocol): 73 | """Subclass of `websockets.WebSocketClientProtocol` tailored for Solana RPC websockets.""" 74 | 75 | def __init__(self, *args, **kwargs): 76 | """Init. Args and kwargs are passed to `websockets.WebSocketClientProtocol`.""" 77 | super().__init__(*args, **kwargs) 78 | self.subscriptions: Dict[int, Body] = {} 79 | self.sent_subscriptions: Dict[int, Body] = {} 80 | self.failed_subscriptions = {} 81 | self.request_counter = itertools.count() 82 | 83 | def increment_counter_and_get_id(self) -> int: 84 | """Increment self.request_counter and return the latest id.""" 85 | return next(self.request_counter) + 1 86 | 87 | async def send_data(self, message: Union[Body, List[Body]]) -> None: 88 | """Send a subscribe/unsubscribe request or list of requests. 89 | 90 | Basically `.send` from `websockets` with extra parsing. 91 | 92 | Args: 93 | message: The request(s) to send. 94 | """ 95 | if isinstance(message, list): 96 | to_send = batch_to_json(message) 97 | for req in message: 98 | self.sent_subscriptions[req.id] = req 99 | else: 100 | to_send = message.to_json() 101 | self.sent_subscriptions[message.id] = message 102 | await super().send(to_send) # type: ignore 103 | 104 | async def recv( # type: ignore 105 | self, 106 | ) -> List[Union[Notification, SubscriptionResult]]: 107 | """Receive the next message. 108 | 109 | Basically `.recv` from `websockets` with extra parsing. 110 | """ 111 | data = await super().recv() 112 | return self._process_rpc_response(cast(str, data)) 113 | 114 | async def account_subscribe( 115 | self, 116 | pubkey: Pubkey, 117 | commitment: Optional[Commitment] = None, 118 | encoding: Optional[str] = None, 119 | ) -> None: 120 | """Subscribe to an account to receive notifications when the lamports or data change. 121 | 122 | Args: 123 | pubkey: Account pubkey. 124 | commitment: Commitment level. 125 | encoding: Encoding to use. 126 | """ 127 | req_id = self.increment_counter_and_get_id() 128 | commitment_to_use = None if commitment is None else _COMMITMENT_TO_SOLDERS[commitment] 129 | encoding_to_use = None if encoding is None else _ACCOUNT_ENCODING_TO_SOLDERS[encoding] 130 | config = ( 131 | None 132 | if commitment_to_use is None and encoding_to_use is None 133 | else RpcAccountInfoConfig(encoding=encoding_to_use, commitment=commitment_to_use) 134 | ) 135 | req = AccountSubscribe(pubkey, config, req_id) 136 | await self.send_data(req) 137 | 138 | async def account_unsubscribe( 139 | self, 140 | subscription: int, 141 | ) -> None: 142 | """Unsubscribe from account notifications. 143 | 144 | Args: 145 | subscription: ID of subscription to cancel. 146 | """ 147 | req_id = self.increment_counter_and_get_id() 148 | req = AccountUnsubscribe(subscription, req_id) 149 | await self.send_data(req) 150 | del self.subscriptions[subscription] 151 | 152 | async def logs_subscribe( 153 | self, 154 | filter_: Union[RpcTransactionLogsFilter, RpcTransactionLogsFilterMentions] = RpcTransactionLogsFilter.All, 155 | commitment: Optional[Commitment] = None, 156 | ) -> None: 157 | """Subscribe to transaction logging. 158 | 159 | Args: 160 | filter_: filter criteria for the logs. 161 | commitment: The commitment level to use. 162 | """ 163 | req_id = self.increment_counter_and_get_id() 164 | commitment_to_use = None if commitment is None else _COMMITMENT_TO_SOLDERS[commitment] 165 | config = RpcTransactionLogsConfig(commitment_to_use) 166 | req = LogsSubscribe(filter_, config, req_id) 167 | await self.send_data(req) 168 | 169 | async def logs_unsubscribe( 170 | self, 171 | subscription: int, 172 | ) -> None: 173 | """Unsubscribe from transaction logging. 174 | 175 | Args: 176 | subscription: ID of subscription to cancel. 177 | """ 178 | req_id = self.increment_counter_and_get_id() 179 | req = LogsUnsubscribe(subscription, req_id) 180 | await self.send_data(req) 181 | del self.subscriptions[subscription] 182 | 183 | async def block_subscribe( 184 | self, 185 | filter_: Union[RpcBlockSubscribeFilter, RpcBlockSubscribeFilterMentions] = RpcBlockSubscribeFilter.All, 186 | commitment: Optional[Commitment] = None, 187 | encoding: Optional[str] = None, 188 | transaction_details: Union[TransactionDetails, None] = None, 189 | show_rewards: Optional[bool] = None, 190 | max_supported_transaction_version: Optional[int] = None, 191 | ) -> None: 192 | """Subscribe to blocks. 193 | 194 | Args: 195 | filter_: filter criteria for the blocks. 196 | commitment: The commitment level to use. 197 | encoding: Encoding to use. 198 | transaction_details: level of transaction detail to return. 199 | show_rewards: whether to populate the rewards array. 200 | max_supported_transaction_version: the max transaction version to return in responses. 201 | """ 202 | req_id = self.increment_counter_and_get_id() 203 | commitment_to_use = None if commitment is None else _COMMITMENT_TO_SOLDERS[commitment] 204 | encoding_to_use = None if encoding is None else _TX_ENCODING_TO_SOLDERS[encoding] 205 | config = RpcBlockSubscribeConfig( 206 | commitment=commitment_to_use, 207 | encoding=encoding_to_use, 208 | transaction_details=transaction_details, 209 | show_rewards=show_rewards, 210 | max_supported_transaction_version=max_supported_transaction_version, 211 | ) 212 | req = BlockSubscribe(filter_, config, req_id) 213 | await self.send_data(req) 214 | 215 | async def block_unsubscribe( 216 | self, 217 | subscription: int, 218 | ) -> None: 219 | """Unsubscribe from blocks. 220 | 221 | Args: 222 | subscription: ID of subscription to cancel. 223 | """ 224 | req_id = self.increment_counter_and_get_id() 225 | req = BlockUnsubscribe(subscription, req_id) 226 | await self.send_data(req) 227 | del self.subscriptions[subscription] 228 | 229 | async def program_subscribe( # pylint: disable=too-many-arguments 230 | self, 231 | program_id: Pubkey, 232 | commitment: Optional[Commitment] = None, 233 | encoding: Optional[str] = None, 234 | data_slice: Optional[types.DataSliceOpts] = None, 235 | filters: Optional[Sequence[Union[int, types.MemcmpOpts]]] = None, 236 | ) -> None: 237 | """Receive notifications when the lamports or data for a given account owned by the program changes. 238 | 239 | Args: 240 | program_id: The program ID. 241 | commitment: Commitment level to use. 242 | encoding: Encoding to use. 243 | data_slice: (optional) Limit the returned account data using the provided `offset`: and 244 | ` length`: fields; only available for "base58" or "base64" encoding. 245 | filters: (optional) Options to compare a provided series of bytes with program account data at a particular offset. 246 | Note: an int entry is converted to a `dataSize` filter. 247 | """ # noqa: E501 # pylint: disable=line-too-long 248 | req_id = self.increment_counter_and_get_id() 249 | if commitment is None and encoding is None and data_slice is None and filters is None: 250 | config = None 251 | else: 252 | encoding_to_use = None if encoding is None else _ACCOUNT_ENCODING_TO_SOLDERS[encoding] 253 | commitment_to_use = None if commitment is None else _COMMITMENT_TO_SOLDERS[commitment] 254 | data_slice_to_use = ( 255 | None if data_slice is None else UiDataSliceConfig(offset=data_slice.offset, length=data_slice.length) 256 | ) 257 | account_config = RpcAccountInfoConfig( 258 | encoding=encoding_to_use, 259 | commitment=commitment_to_use, 260 | data_slice=data_slice_to_use, 261 | ) 262 | filters_to_use: Optional[List[Union[int, Memcmp]]] = ( 263 | None if filters is None else [x if isinstance(x, int) else Memcmp(*x) for x in filters] 264 | ) 265 | config = RpcProgramAccountsConfig(account_config, filters_to_use) 266 | req = ProgramSubscribe(program_id, config, req_id) 267 | await self.send_data(req) 268 | 269 | async def program_unsubscribe( 270 | self, 271 | subscription: int, 272 | ) -> None: 273 | """Unsubscribe from program account notifications. 274 | 275 | Args: 276 | subscription: ID of subscription to cancel. 277 | """ 278 | req_id = self.increment_counter_and_get_id() 279 | req = ProgramUnsubscribe(subscription, req_id) 280 | await self.send_data(req) 281 | del self.subscriptions[subscription] 282 | 283 | async def signature_subscribe( 284 | self, 285 | signature: Signature, 286 | commitment: Optional[Commitment] = None, 287 | ) -> None: 288 | """Subscribe to a transaction signature to receive notification when the transaction is confirmed. 289 | 290 | Args: 291 | signature: The transaction signature to subscribe to. 292 | commitment: Commitment level. 293 | """ 294 | req_id = self.increment_counter_and_get_id() 295 | commitment_to_use = None if commitment is None else _COMMITMENT_TO_SOLDERS[commitment] 296 | config = None if commitment_to_use is None else RpcSignatureSubscribeConfig(commitment=commitment_to_use) 297 | req = SignatureSubscribe(signature, config, req_id) 298 | await self.send_data(req) 299 | 300 | async def signature_unsubscribe( 301 | self, 302 | subscription: int, 303 | ) -> None: 304 | """Unsubscribe from signature notifications. 305 | 306 | Args: 307 | subscription: ID of subscription to cancel. 308 | """ 309 | req_id = self.increment_counter_and_get_id() 310 | req = SignatureUnsubscribe(subscription, req_id) 311 | await self.send_data(req) 312 | del self.subscriptions[subscription] 313 | 314 | async def slot_subscribe(self) -> None: 315 | """Subscribe to receive notification anytime a slot is processed by the validator.""" 316 | req_id = self.increment_counter_and_get_id() 317 | req = SlotSubscribe(req_id) 318 | await self.send_data(req) 319 | 320 | async def slot_unsubscribe( 321 | self, 322 | subscription: int, 323 | ) -> None: 324 | """Unsubscribe from slot notifications. 325 | 326 | Args: 327 | subscription: ID of subscription to cancel. 328 | """ 329 | req_id = self.increment_counter_and_get_id() 330 | req = SlotUnsubscribe(subscription, req_id) 331 | await self.send_data(req) 332 | del self.subscriptions[subscription] 333 | 334 | async def slots_updates_subscribe(self) -> None: 335 | """Subscribe to receive a notification from the validator on a variety of updates on every slot.""" 336 | req_id = self.increment_counter_and_get_id() 337 | req = SlotsUpdatesSubscribe(req_id) 338 | await self.send_data(req) 339 | 340 | async def slots_updates_unsubscribe( 341 | self, 342 | subscription: int, 343 | ) -> None: 344 | """Unsubscribe from slot update notifications. 345 | 346 | Args: 347 | subscription: ID of subscription to cancel. 348 | """ 349 | req_id = self.increment_counter_and_get_id() 350 | req = SlotsUpdatesUnsubscribe(subscription, req_id) 351 | await self.send_data(req) 352 | del self.subscriptions[subscription] 353 | 354 | async def root_subscribe(self) -> None: 355 | """Subscribe to receive notification anytime a new root is set by the validator.""" 356 | req_id = self.increment_counter_and_get_id() 357 | req = RootSubscribe(req_id) 358 | await self.send_data(req) 359 | 360 | async def root_unsubscribe( 361 | self, 362 | subscription: int, 363 | ) -> None: 364 | """Unsubscribe from root notifications. 365 | 366 | Args: 367 | subscription: ID of subscription to cancel. 368 | """ 369 | req_id = self.increment_counter_and_get_id() 370 | req = RootUnsubscribe(subscription, req_id) 371 | await self.send_data(req) 372 | del self.subscriptions[subscription] 373 | 374 | async def vote_subscribe(self) -> None: 375 | """Subscribe to receive notification anytime a new vote is observed in gossip.""" 376 | req_id = self.increment_counter_and_get_id() 377 | req = VoteSubscribe(req_id) 378 | await self.send_data(req) 379 | 380 | async def vote_unsubscribe( 381 | self, 382 | subscription: int, 383 | ) -> None: 384 | """Unsubscribe from vote notifications. 385 | 386 | Args: 387 | subscription: ID of subscription to cancel. 388 | """ 389 | req_id = self.increment_counter_and_get_id() 390 | req = VoteUnsubscribe(subscription, req_id) 391 | await self.send_data(req) 392 | del self.subscriptions[subscription] 393 | 394 | def _process_rpc_response(self, raw: str) -> List[Union[Notification, SubscriptionResult]]: 395 | parsed = parse_websocket_message(raw) 396 | for item in parsed: 397 | if isinstance(item, SoldersSubscriptionError): 398 | subscription = self.sent_subscriptions[item.id] 399 | self.failed_subscriptions[item.id] = subscription 400 | raise SubscriptionError(item, subscription) 401 | if isinstance(item, SubscriptionResult): 402 | self.subscriptions[item.result] = self.sent_subscriptions[item.id] 403 | return cast(List[Union[Notification, SubscriptionResult]], parsed) 404 | 405 | 406 | class connect(ws_connect): # pylint: disable=invalid-name,too-few-public-methods 407 | """Solana RPC websocket connector.""" 408 | 409 | def __init__(self, uri: str = "ws://localhost:8900", **kwargs: Any) -> None: 410 | """Init. Kwargs are passed to `websockets.connect`. 411 | 412 | Args: 413 | uri: The websocket endpoint. 414 | **kwargs: Keyword arguments for ``websockets.legacy.client.connect`` 415 | """ 416 | # Ensure that create_protocol explicitly creates a SolanaWsClientProtocol 417 | kwargs.setdefault("create_protocol", SolanaWsClientProtocol) 418 | super().__init__(uri, **kwargs) 419 | 420 | async def __aenter__(self) -> SolanaWsClientProtocol: 421 | """Overrides to specify the type of protocol explicitly.""" 422 | protocol = await super().__aenter__() 423 | return cast(SolanaWsClientProtocol, protocol) 424 | -------------------------------------------------------------------------------- /src/solana/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Utility functions for solanaweb3.""" 2 | -------------------------------------------------------------------------------- /src/solana/utils/cluster.py: -------------------------------------------------------------------------------- 1 | """Tools for getting RPC cluster information.""" 2 | 3 | from typing import Literal, NamedTuple, Optional 4 | 5 | 6 | class ClusterUrls(NamedTuple): 7 | """A collection of urls for each cluster.""" 8 | 9 | devnet: str 10 | testnet: str 11 | mainnet_beta: str 12 | 13 | 14 | class Endpoint(NamedTuple): 15 | """Container for http and https cluster urls.""" 16 | 17 | http: ClusterUrls 18 | https: ClusterUrls 19 | 20 | 21 | ENDPOINT = Endpoint( 22 | http=ClusterUrls( 23 | devnet="http://api.devnet.solana.com", 24 | testnet="http://api.testnet.solana.com", 25 | mainnet_beta="http://api.mainnet-beta.solana.com/", 26 | ), 27 | https=ClusterUrls( 28 | devnet="https://api.devnet.solana.com", 29 | testnet="https://api.testnet.solana.com", 30 | mainnet_beta="https://api.mainnet-beta.solana.com/", 31 | ), 32 | ) 33 | 34 | 35 | Cluster = Literal["devnet", "testnet", "mainnet-beta"] 36 | 37 | 38 | def cluster_api_url(cluster: Optional[Cluster] = None, tls: bool = True) -> str: 39 | """Retrieve the RPC API URL for the specified cluster. 40 | 41 | :param cluster: The name of the cluster to use. 42 | :param tls: If True, use https. Defaults to True. 43 | """ 44 | urls = ENDPOINT.https if tls else ENDPOINT.http 45 | if cluster is None: 46 | return urls.devnet 47 | return getattr(urls, cluster) 48 | -------------------------------------------------------------------------------- /src/solana/utils/security_txt.py: -------------------------------------------------------------------------------- 1 | """Utils for security.txt.""" 2 | 3 | from dataclasses import dataclass, fields 4 | from typing import Any, List, Optional 5 | 6 | HEADER = "=======BEGIN SECURITY.TXT V1=======\0" 7 | """Header of the security.txt.""" 8 | FOOTER = "=======END SECURITY.TXT V1=======\0" 9 | """Footer of the security.txt.""" 10 | 11 | 12 | @dataclass 13 | class SecurityTxt: 14 | """Security txt data.""" 15 | 16 | # pylint: disable=too-many-instance-attributes 17 | name: str 18 | project_url: str 19 | contacts: str 20 | policy: str 21 | preferred_languages: Optional[str] = None 22 | source_code: Optional[str] = None 23 | encryption: Optional[str] = None 24 | auditors: Optional[str] = None 25 | acknowledgements: Optional[str] = None 26 | expiry: Optional[str] = None 27 | 28 | 29 | class NoSecurityTxtFoundError(Exception): 30 | """Raise when security text is not found.""" 31 | 32 | 33 | def parse_security_txt(data: bytes) -> SecurityTxt: 34 | """Parse and extract security.txt section from the data section of the compiled program. 35 | 36 | Args: 37 | data: Program data in bytes from the ProgramAccount. 38 | 39 | Returns: 40 | The Security Txt. 41 | """ 42 | if not isinstance(data, bytes): 43 | raise TypeError(f"data provided in parse(data) must be bytes, found: {type(data)}") 44 | 45 | s_idx = data.find(bytes(HEADER, "utf-8")) 46 | e_idx = data.find(bytes(FOOTER, "utf-8")) 47 | 48 | if s_idx == -1: 49 | raise NoSecurityTxtFoundError("Program doesn't have security.txt section") 50 | 51 | content_arr = data[s_idx + len(HEADER) : e_idx] 52 | content_da: List[Any] = [[]] 53 | 54 | for char in content_arr: 55 | if char == 0: 56 | content_da.append([]) 57 | else: 58 | content_da[len(content_da) - 1].append(chr(char)) 59 | 60 | content_da.pop() 61 | 62 | content_dict = {} 63 | 64 | for idx, content in enumerate(content_da): 65 | content_da[idx] = "".join(content) 66 | 67 | for iidx, idata in enumerate(content_da): 68 | if any(idata == x.name for x in fields(SecurityTxt)): 69 | next_key = iidx + 1 70 | content_dict.update({str(idata): content_da[next_key]}) 71 | 72 | try: 73 | security_txt = SecurityTxt(**content_dict) 74 | except TypeError as err: 75 | raise err 76 | return security_txt 77 | -------------------------------------------------------------------------------- /src/solana/utils/validate.py: -------------------------------------------------------------------------------- 1 | """Validation utilities.""" 2 | 3 | from __future__ import annotations 4 | 5 | from enum import IntEnum 6 | from typing import Any 7 | 8 | from solders.instruction import Instruction 9 | 10 | 11 | def validate_instruction_keys(instruction: Instruction, expected: int) -> None: 12 | """Verify length of AccountMeta list of a transaction instruction is at least the expected length. 13 | 14 | Args: 15 | instruction: A Instruction object. 16 | expected: The expected length. 17 | """ 18 | if len(instruction.accounts) < expected: 19 | raise ValueError(f"invalid instruction: found {len(instruction.accounts)} keys, expected at least {expected}") 20 | 21 | 22 | def validate_instruction_type(parsed_data: Any, expected_type: IntEnum) -> None: 23 | """Check that the instruction type of the parsed data matches the expected instruction type. 24 | 25 | Args: 26 | parsed_data: Parsed instruction data object with `instruction_type` field. 27 | expected_type: The expected instruction type. 28 | """ 29 | if parsed_data.instruction_type != expected_type: 30 | raise ValueError( 31 | f"invalid instruction; instruction index mismatch {parsed_data.instruction_type} != {expected_type}" 32 | ) 33 | -------------------------------------------------------------------------------- /src/solana/vote_program.py: -------------------------------------------------------------------------------- 1 | """Library to interface with the vote program.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NamedTuple 6 | 7 | from solders.instruction import AccountMeta, Instruction 8 | from solders.pubkey import Pubkey 9 | 10 | from solana.constants import VOTE_PROGRAM_ID 11 | from solana._layouts.vote_instructions import VOTE_INSTRUCTIONS_LAYOUT, InstructionType 12 | 13 | 14 | # Instrection Params 15 | class WithdrawFromVoteAccountParams(NamedTuple): 16 | """Transfer SOL from vote account to identity.""" 17 | 18 | vote_account_from_pubkey: Pubkey 19 | """""" 20 | to_pubkey: Pubkey 21 | """""" 22 | lamports: int 23 | """""" 24 | withdrawer: Pubkey 25 | """""" 26 | 27 | 28 | def withdraw_from_vote_account(params: WithdrawFromVoteAccountParams) -> Instruction: 29 | """Generate an instruction that transfers lamports from a vote account to any other. 30 | 31 | Example: 32 | >>> from solders.pubkey import Pubkey 33 | >>> from solders.keypair import Keypair 34 | >>> vote = Pubkey([0] * 31 + [1]) 35 | >>> withdrawer = Keypair.from_seed(bytes([0]*32)) 36 | >>> instruction = withdraw_from_vote_account( 37 | ... WithdrawFromVoteAccountParams( 38 | ... vote_account_from_pubkey=vote, 39 | ... to_pubkey=withdrawer.pubkey(), 40 | ... withdrawer=withdrawer.pubkey(), 41 | ... lamports=3_000_000_000, 42 | ... ) 43 | ... ) 44 | >>> type(instruction) 45 | 46 | 47 | Returns: 48 | The generated instruction. 49 | """ 50 | data = VOTE_INSTRUCTIONS_LAYOUT.build( 51 | { 52 | "instruction_type": InstructionType.WITHDRAW_FROM_VOTE_ACCOUNT, 53 | "args": {"lamports": params.lamports}, 54 | } 55 | ) 56 | 57 | return Instruction( 58 | accounts=[ 59 | AccountMeta( 60 | pubkey=params.vote_account_from_pubkey, 61 | is_signer=False, 62 | is_writable=True, 63 | ), 64 | AccountMeta(pubkey=params.to_pubkey, is_signer=False, is_writable=True), 65 | AccountMeta(pubkey=params.withdrawer, is_signer=True, is_writable=True), 66 | ], 67 | program_id=VOTE_PROGRAM_ID, 68 | data=data, 69 | ) 70 | -------------------------------------------------------------------------------- /src/spl/__init__.py: -------------------------------------------------------------------------------- 1 | """Solana Program Library packages.""" 2 | -------------------------------------------------------------------------------- /src/spl/memo/__init__.py: -------------------------------------------------------------------------------- 1 | """Client code for interacting with the Memo Program.""" 2 | -------------------------------------------------------------------------------- /src/spl/memo/constants.py: -------------------------------------------------------------------------------- 1 | """Memo program constants.""" 2 | 3 | from solders.pubkey import Pubkey 4 | 5 | MEMO_PROGRAM_ID: Pubkey = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") 6 | """Public key that identifies the Memo program.""" 7 | -------------------------------------------------------------------------------- /src/spl/memo/instructions.py: -------------------------------------------------------------------------------- 1 | """Memo program instructions.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NamedTuple 6 | 7 | from solders.instruction import AccountMeta, Instruction 8 | from solders.pubkey import Pubkey 9 | 10 | 11 | class MemoParams(NamedTuple): 12 | """Create memo transaction params.""" 13 | 14 | program_id: Pubkey 15 | """Memo program account.""" 16 | signer: Pubkey 17 | """Signing account.""" 18 | message: bytes 19 | """Memo message in bytes.""" 20 | 21 | 22 | def decode_create_memo(instruction: Instruction) -> MemoParams: 23 | """Decode a create_memo_instruction and retrieve the instruction params. 24 | 25 | Args: 26 | instruction: The instruction to decode. 27 | 28 | Returns: 29 | The decoded instruction. 30 | """ 31 | return MemoParams( 32 | signer=instruction.accounts[0].pubkey, 33 | message=instruction.data, 34 | program_id=instruction.program_id, 35 | ) 36 | 37 | 38 | def create_memo(params: MemoParams) -> Instruction: 39 | """Creates a transaction instruction that creates a memo. 40 | 41 | Message need to be encoded in bytes. 42 | 43 | Example: 44 | >>> from solders.pubkey import Pubkey 45 | >>> leading_zeros = [0] * 31 46 | >>> signer, memo_program = Pubkey(leading_zeros + [1]), Pubkey(leading_zeros + [2]) 47 | >>> message = bytes("test", encoding="utf8") 48 | >>> params = MemoParams( 49 | ... program_id=memo_program, 50 | ... message=message, 51 | ... signer=signer 52 | ... ) 53 | >>> type(create_memo(params)) 54 | 55 | 56 | Returns: 57 | The instruction to create a memo. 58 | """ 59 | keys = [ 60 | AccountMeta(pubkey=params.signer, is_signer=True, is_writable=True), 61 | ] 62 | return Instruction( 63 | accounts=keys, 64 | program_id=params.program_id, 65 | data=params.message, 66 | ) 67 | -------------------------------------------------------------------------------- /src/spl/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhly/solana-py/4c329fa086d762274ded5c665dd267ded056c744/src/spl/py.typed -------------------------------------------------------------------------------- /src/spl/token/__init__.py: -------------------------------------------------------------------------------- 1 | """Client code for interacting with the SPL Token Program.""" 2 | -------------------------------------------------------------------------------- /src/spl/token/_layouts.py: -------------------------------------------------------------------------------- 1 | """Token instruction layouts.""" 2 | 3 | from enum import IntEnum 4 | 5 | from construct import Bytes, Int8ul, Int32ul, Int64ul, Pass, Switch 6 | from construct import Struct as cStruct 7 | 8 | PUBLIC_KEY_LAYOUT = Bytes(32) 9 | 10 | 11 | class InstructionType(IntEnum): 12 | """Token instruction types.""" 13 | 14 | INITIALIZE_MINT = 0 15 | INITIALIZE_ACCOUNT = 1 16 | INITIALIZE_MULTISIG = 2 17 | TRANSFER = 3 18 | APPROVE = 4 19 | REVOKE = 5 20 | SET_AUTHORITY = 6 21 | MINT_TO = 7 22 | BURN = 8 23 | CLOSE_ACCOUNT = 9 24 | FREEZE_ACCOUNT = 10 25 | THAW_ACCOUNT = 11 26 | TRANSFER2 = 12 27 | APPROVE2 = 13 28 | MINT_TO2 = 14 29 | BURN2 = 15 30 | SYNC_NATIVE = 17 31 | 32 | 33 | _INITIALIZE_MINT_LAYOUT = cStruct( 34 | "decimals" / Int8ul, 35 | "mint_authority" / PUBLIC_KEY_LAYOUT, 36 | "freeze_authority_option" / Int8ul, 37 | "freeze_authority" / PUBLIC_KEY_LAYOUT, 38 | ) 39 | 40 | _INITIALIZE_MULTISIG_LAYOUT = cStruct("m" / Int8ul) 41 | 42 | _AMOUNT_LAYOUT = cStruct("amount" / Int64ul) 43 | 44 | _SET_AUTHORITY_LAYOUT = cStruct( 45 | "authority_type" / Int8ul, 46 | "new_authority_option" / Int8ul, 47 | "new_authority" / PUBLIC_KEY_LAYOUT, 48 | ) 49 | 50 | _AMOUNT2_LAYOUT = cStruct("amount" / Int64ul, "decimals" / Int8ul) 51 | 52 | INSTRUCTIONS_LAYOUT = cStruct( 53 | "instruction_type" / Int8ul, 54 | "args" 55 | / Switch( 56 | lambda this: this.instruction_type, 57 | { 58 | InstructionType.INITIALIZE_MINT: _INITIALIZE_MINT_LAYOUT, 59 | InstructionType.INITIALIZE_ACCOUNT: Pass, 60 | InstructionType.INITIALIZE_MULTISIG: _INITIALIZE_MULTISIG_LAYOUT, 61 | InstructionType.TRANSFER: _AMOUNT_LAYOUT, 62 | InstructionType.APPROVE: _AMOUNT_LAYOUT, 63 | InstructionType.REVOKE: Pass, 64 | InstructionType.SET_AUTHORITY: _SET_AUTHORITY_LAYOUT, 65 | InstructionType.MINT_TO: _AMOUNT_LAYOUT, 66 | InstructionType.BURN: _AMOUNT_LAYOUT, 67 | InstructionType.CLOSE_ACCOUNT: Pass, 68 | InstructionType.FREEZE_ACCOUNT: Pass, 69 | InstructionType.THAW_ACCOUNT: Pass, 70 | InstructionType.TRANSFER2: _AMOUNT2_LAYOUT, 71 | InstructionType.APPROVE2: _AMOUNT2_LAYOUT, 72 | InstructionType.MINT_TO2: _AMOUNT2_LAYOUT, 73 | InstructionType.BURN2: _AMOUNT2_LAYOUT, 74 | }, 75 | ), 76 | ) 77 | 78 | MINT_LAYOUT = cStruct( 79 | "mint_authority_option" / Int32ul, 80 | "mint_authority" / PUBLIC_KEY_LAYOUT, 81 | "supply" / Int64ul, 82 | "decimals" / Int8ul, 83 | "is_initialized" / Int8ul, 84 | "freeze_authority_option" / Int32ul, 85 | "freeze_authority" / PUBLIC_KEY_LAYOUT, 86 | ) 87 | 88 | ACCOUNT_LAYOUT = cStruct( 89 | "mint" / PUBLIC_KEY_LAYOUT, 90 | "owner" / PUBLIC_KEY_LAYOUT, 91 | "amount" / Int64ul, 92 | "delegate_option" / Int32ul, 93 | "delegate" / PUBLIC_KEY_LAYOUT, 94 | "state" / Int8ul, 95 | "is_native_option" / Int32ul, 96 | "is_native" / Int64ul, 97 | "delegated_amount" / Int64ul, 98 | "close_authority_option" / Int32ul, 99 | "close_authority" / PUBLIC_KEY_LAYOUT, 100 | ) 101 | 102 | MULTISIG_LAYOUT = cStruct( 103 | "m" / Int8ul, 104 | "n" / Int8ul, 105 | "is_initialized" / Int8ul, 106 | "signer1" / PUBLIC_KEY_LAYOUT, 107 | "signer2" / PUBLIC_KEY_LAYOUT, 108 | "signer3" / PUBLIC_KEY_LAYOUT, 109 | "signer4" / PUBLIC_KEY_LAYOUT, 110 | "signer5" / PUBLIC_KEY_LAYOUT, 111 | "signer6" / PUBLIC_KEY_LAYOUT, 112 | "signer7" / PUBLIC_KEY_LAYOUT, 113 | "signer8" / PUBLIC_KEY_LAYOUT, 114 | "signer9" / PUBLIC_KEY_LAYOUT, 115 | "signer10" / PUBLIC_KEY_LAYOUT, 116 | "signer11" / PUBLIC_KEY_LAYOUT, 117 | ) 118 | -------------------------------------------------------------------------------- /src/spl/token/constants.py: -------------------------------------------------------------------------------- 1 | """SPL token constants.""" 2 | 3 | from solders.pubkey import Pubkey 4 | 5 | MINT_LEN: int = 82 6 | """Data length of a token mint account.""" 7 | 8 | ACCOUNT_LEN: int = 165 9 | """Data length of a token account.""" 10 | 11 | MULTISIG_LEN: int = 355 12 | """Data length of a multisig token account.""" 13 | 14 | ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") 15 | """Program ID for the associated token account program.""" 16 | 17 | TOKEN_PROGRAM_ID: Pubkey = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") 18 | """Public key that identifies the SPL token program.""" 19 | 20 | TOKEN_2022_PROGRAM_ID: Pubkey = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") 21 | """Public key that identifies the SPL token 2022 program.""" 22 | 23 | WRAPPED_SOL_MINT: Pubkey = Pubkey.from_string("So11111111111111111111111111111111111111112") 24 | """Public key of the "Native Mint" for wrapping SOL to SPL token. 25 | 26 | The Token Program can be used to wrap native SOL. Doing so allows native SOL to be treated like any 27 | other Token program token type and can be useful when being called from other programs that interact 28 | with the Token Program's interface. 29 | """ 30 | 31 | NATIVE_DECIMALS: int = 9 32 | """Number of decimals for SOL and the Wrapped SOL mint.""" 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for Solana.py.""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Fixtures for pytest.""" 2 | 3 | import asyncio 4 | import time 5 | from typing import NamedTuple 6 | 7 | import pytest 8 | from solders.hash import Hash as Blockhash 9 | from solders.keypair import Keypair 10 | from solders.pubkey import Pubkey 11 | 12 | from solana.rpc.api import Client 13 | from solana.rpc.async_api import AsyncClient 14 | from solana.rpc.commitment import Processed 15 | from tests.utils import AIRDROP_AMOUNT, assert_valid_response 16 | 17 | 18 | class Clients(NamedTuple): 19 | """Container for http clients.""" 20 | 21 | sync: Client 22 | async_: AsyncClient 23 | loop: asyncio.AbstractEventLoop 24 | 25 | 26 | @pytest.fixture(scope="module") 27 | def event_loop(): 28 | """Event loop for pytest-asyncio.""" 29 | try: 30 | loop = asyncio.get_running_loop() 31 | except RuntimeError: 32 | loop = asyncio.new_event_loop() 33 | yield loop 34 | loop.close() 35 | 36 | 37 | @pytest.fixture(scope="session") 38 | def stubbed_blockhash() -> Blockhash: 39 | """Arbitrary block hash.""" 40 | return Blockhash.from_string("EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k") 41 | 42 | 43 | @pytest.fixture(scope="session") 44 | def stubbed_receiver() -> Pubkey: 45 | """Arbitrary known public key to be used as receiver.""" 46 | return Pubkey.from_string("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i99") 47 | 48 | 49 | @pytest.fixture(scope="session") 50 | def stubbed_receiver_prefetched_blockhash() -> Pubkey: 51 | """Arbitrary known public key to be used as receiver.""" 52 | return Pubkey.from_string("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i97") 53 | 54 | 55 | @pytest.fixture(scope="session") 56 | def async_stubbed_receiver() -> Pubkey: 57 | """Arbitrary known public key to be used as receiver.""" 58 | return Pubkey.from_string("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i98") 59 | 60 | 61 | @pytest.fixture(scope="session") 62 | def async_stubbed_receiver_prefetched_blockhash() -> Pubkey: 63 | """Arbitrary known public key to be used as receiver.""" 64 | return Pubkey.from_string("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i96") 65 | 66 | 67 | @pytest.fixture(scope="session") 68 | def stubbed_sender() -> Keypair: 69 | """Arbitrary known account to be used as sender.""" 70 | return Keypair.from_seed(bytes([8] * Pubkey.LENGTH)) 71 | 72 | 73 | @pytest.fixture(scope="session") 74 | def stubbed_sender_prefetched_blockhash() -> Keypair: 75 | """Arbitrary known account to be used as sender.""" 76 | return Keypair.from_seed(bytes([9] * Pubkey.LENGTH)) 77 | 78 | 79 | @pytest.fixture(scope="session") 80 | def stubbed_sender_for_token() -> Keypair: 81 | """Arbitrary known account to be used as sender.""" 82 | return Keypair.from_seed(bytes([2] * Pubkey.LENGTH)) 83 | 84 | 85 | @pytest.fixture(scope="session") 86 | def async_stubbed_sender() -> Keypair: 87 | """Another arbitrary known account to be used as sender.""" 88 | return Keypair.from_seed(bytes([7] * Pubkey.LENGTH)) 89 | 90 | 91 | @pytest.fixture(scope="session") 92 | def async_stubbed_sender_prefetched_blockhash() -> Keypair: 93 | """Another arbitrary known account to be used as sender.""" 94 | return Keypair.from_seed(bytes([5] * Pubkey.LENGTH)) 95 | 96 | 97 | @pytest.fixture(scope="session") 98 | def freeze_authority() -> Keypair: 99 | """Arbitrary known account to be used as freeze authority.""" 100 | return Keypair.from_seed(bytes([6] * Pubkey.LENGTH)) 101 | 102 | 103 | @pytest.fixture(scope="session") 104 | def unit_test_http_client() -> Client: 105 | """Client to be used in unit tests.""" 106 | client = Client(commitment=Processed) 107 | return client 108 | 109 | 110 | @pytest.fixture(scope="session") 111 | def unit_test_http_client_async() -> AsyncClient: 112 | """Async client to be used in unit tests.""" 113 | client = AsyncClient(commitment=Processed) 114 | return client 115 | 116 | 117 | @pytest.fixture(scope="module") 118 | def _sleep_for_first_blocks() -> None: 119 | """Blocks 0 and 1 are unavailable so we sleep until they're done.""" 120 | time.sleep(10) 121 | 122 | 123 | @pytest.mark.integration 124 | @pytest.fixture(scope="module") 125 | def test_http_client(docker_ip, docker_services, _sleep_for_first_blocks) -> Client: # pylint: disable=redefined-outer-name 126 | """Test http_client.is_connected.""" 127 | port = docker_services.port_for("localnet", 8899) 128 | http_client = Client(endpoint=f"http://{docker_ip}:{port}", commitment=Processed) 129 | docker_services.wait_until_responsive(timeout=15, pause=1, check=http_client.is_connected) 130 | return http_client 131 | 132 | 133 | @pytest.mark.integration 134 | @pytest.fixture(scope="module") 135 | def test_http_client_async( 136 | docker_ip, 137 | docker_services, 138 | event_loop, 139 | _sleep_for_first_blocks, # pylint: disable=redefined-outer-name 140 | ) -> AsyncClient: 141 | """Test http_client.is_connected.""" 142 | port = docker_services.port_for("localnet", 8899) 143 | http_client = AsyncClient(endpoint=f"http://{docker_ip}:{port}", commitment=Processed) 144 | 145 | def check() -> bool: 146 | return event_loop.run_until_complete(http_client.is_connected()) 147 | 148 | docker_services.wait_until_responsive(timeout=15, pause=1, check=check) 149 | yield http_client 150 | event_loop.run_until_complete(http_client.close()) 151 | 152 | 153 | @pytest.mark.integration 154 | @pytest.fixture(scope="function") 155 | def random_funded_keypair(test_http_client: Client) -> Keypair: 156 | """A new keypair with some lamports.""" 157 | kp = Keypair() 158 | resp = test_http_client.request_airdrop(kp.pubkey(), AIRDROP_AMOUNT) 159 | assert_valid_response(resp) 160 | test_http_client.confirm_transaction(resp.value) 161 | balance = test_http_client.get_balance(kp.pubkey()) 162 | assert balance.value == AIRDROP_AMOUNT 163 | return kp 164 | -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | localnet: 4 | image: "solanalabs/solana:v1.16.27" 5 | ports: 6 | - "8899" 7 | - "8900" 8 | - "9900" 9 | environment: 10 | [ 11 | SOLANA_RUN_SH_VALIDATOR_ARGS=--rpc-pubsub-enable-vote-subscription --rpc-pubsub-enable-block-subscription 12 | ] 13 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """Integration tests.""" 2 | -------------------------------------------------------------------------------- /tests/integration/test_async_token_client.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0401 2 | """Tests for the SPL Token Client.""" 3 | 4 | import pytest 5 | import spl.token._layouts as layouts 6 | from solders.pubkey import Pubkey 7 | from spl.token.async_client import AsyncToken 8 | from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID 9 | 10 | from ..utils import AIRDROP_AMOUNT, OPTS, assert_valid_response 11 | 12 | 13 | @pytest.mark.integration 14 | @pytest.fixture(scope="module") 15 | async def test_token(stubbed_sender, freeze_authority, test_http_client_async) -> AsyncToken: 16 | """Test create mint.""" 17 | resp = await test_http_client_async.request_airdrop(stubbed_sender.pubkey(), AIRDROP_AMOUNT) 18 | await test_http_client_async.confirm_transaction(resp.value) 19 | assert_valid_response(resp) 20 | 21 | expected_decimals = 6 22 | token_client = await AsyncToken.create_mint( 23 | test_http_client_async, 24 | stubbed_sender, 25 | stubbed_sender.pubkey(), 26 | expected_decimals, 27 | TOKEN_PROGRAM_ID, 28 | freeze_authority.pubkey(), 29 | ) 30 | 31 | assert token_client.pubkey 32 | assert token_client.program_id == TOKEN_PROGRAM_ID 33 | assert token_client.payer.pubkey() == stubbed_sender.pubkey() 34 | 35 | resp = await test_http_client_async.get_account_info(token_client.pubkey) 36 | assert_valid_response(resp) 37 | assert resp.value.owner == TOKEN_PROGRAM_ID 38 | 39 | mint_data = layouts.MINT_LAYOUT.parse(resp.value.data) 40 | assert mint_data.is_initialized 41 | assert mint_data.decimals == expected_decimals 42 | assert mint_data.supply == 0 43 | assert Pubkey(mint_data.mint_authority) == stubbed_sender.pubkey() 44 | assert Pubkey(mint_data.freeze_authority) == freeze_authority.pubkey() 45 | return token_client 46 | 47 | 48 | @pytest.mark.integration 49 | @pytest.fixture(scope="module") 50 | async def stubbed_sender_token_account_pk(stubbed_sender, test_token) -> Pubkey: # pylint: disable=redefined-outer-name 51 | """Token account for stubbed sender.""" 52 | return await test_token.create_account(stubbed_sender.pubkey()) 53 | 54 | 55 | @pytest.mark.integration 56 | @pytest.fixture(scope="module") 57 | async def async_stubbed_receiver_token_account_pk( 58 | async_stubbed_receiver, 59 | test_token, # pylint: disable=redefined-outer-name 60 | ) -> Pubkey: 61 | """Token account for stubbed receiver.""" 62 | return await test_token.create_account(async_stubbed_receiver) 63 | 64 | 65 | @pytest.mark.integration 66 | async def test_new_account(stubbed_sender, test_http_client_async, test_token): # pylint: disable=redefined-outer-name 67 | """Test creating a new token account.""" 68 | token_account_pk = await test_token.create_account(stubbed_sender.pubkey()) 69 | resp = await test_http_client_async.get_account_info(token_account_pk) 70 | assert_valid_response(resp) 71 | assert resp.value.owner == TOKEN_PROGRAM_ID 72 | 73 | account_data = layouts.ACCOUNT_LAYOUT.parse(resp.value.data) 74 | assert account_data.state 75 | assert not account_data.amount 76 | assert ( 77 | not account_data.delegate_option 78 | and not account_data.delegated_amount 79 | and Pubkey(account_data.delegate) == Pubkey([0] * 31 + [0]) 80 | ) 81 | assert not account_data.close_authority_option and Pubkey(account_data.close_authority) == Pubkey([0] * 31 + [0]) 82 | assert not account_data.is_native_option and not account_data.is_native 83 | assert Pubkey(account_data.mint) == test_token.pubkey 84 | assert Pubkey(account_data.owner) == stubbed_sender.pubkey() 85 | 86 | 87 | @pytest.mark.integration 88 | async def test_new_associated_account(test_token): # pylint: disable=redefined-outer-name 89 | """Test creating a new associated token account.""" 90 | new_acct = Pubkey([0] * 31 + [0]) 91 | token_account_pubkey = await test_token.create_associated_token_account(new_acct) 92 | expected_token_account_key, _ = new_acct.find_program_address( 93 | seeds=[bytes(new_acct), bytes(TOKEN_PROGRAM_ID), bytes(test_token.pubkey)], 94 | program_id=ASSOCIATED_TOKEN_PROGRAM_ID, 95 | ) 96 | assert token_account_pubkey == expected_token_account_key 97 | 98 | 99 | @pytest.mark.integration 100 | async def test_get_account_info(stubbed_sender, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 101 | """Test get token account info.""" 102 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 103 | assert account_info.is_initialized is True 104 | assert account_info.mint == test_token.pubkey 105 | assert account_info.owner == stubbed_sender.pubkey() 106 | assert account_info.amount == 0 107 | assert account_info.delegate is None 108 | assert account_info.delegated_amount == 0 109 | assert account_info.is_frozen is False 110 | assert account_info.is_native is False 111 | assert account_info.rent_exempt_reserve is None 112 | assert account_info.close_authority is None 113 | 114 | 115 | @pytest.mark.integration 116 | async def test_get_mint_info(stubbed_sender, freeze_authority, test_token): # pylint: disable=redefined-outer-name 117 | """Test get token mint info.""" 118 | mint_info = await test_token.get_mint_info() 119 | assert mint_info.mint_authority == stubbed_sender.pubkey() 120 | assert mint_info.supply == 0 121 | assert mint_info.decimals == 6 122 | assert mint_info.is_initialized is True 123 | assert mint_info.freeze_authority == freeze_authority.pubkey() 124 | 125 | 126 | @pytest.mark.integration 127 | async def test_mint_to(stubbed_sender, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 128 | """Test mint token to account and get balance.""" 129 | expected_amount = 1000 130 | resp = await test_token.mint_to( 131 | dest=stubbed_sender_token_account_pk, 132 | mint_authority=stubbed_sender, 133 | amount=1000, 134 | opts=OPTS, 135 | ) 136 | assert_valid_response(resp) 137 | resp = await test_token.get_balance(stubbed_sender_token_account_pk) 138 | balance_info = resp.value 139 | assert balance_info.amount == str(expected_amount) 140 | assert balance_info.decimals == 6 141 | assert balance_info.ui_amount == 0.001 142 | 143 | 144 | @pytest.mark.integration 145 | async def test_transfer( 146 | stubbed_sender, async_stubbed_receiver_token_account_pk, stubbed_sender_token_account_pk, test_token 147 | ): # pylint: disable=redefined-outer-name 148 | """Test token transfer.""" 149 | expected_amount = 500 150 | resp = await test_token.transfer( 151 | source=stubbed_sender_token_account_pk, 152 | dest=async_stubbed_receiver_token_account_pk, 153 | owner=stubbed_sender, 154 | amount=expected_amount, 155 | opts=OPTS, 156 | ) 157 | assert_valid_response(resp) 158 | resp = await test_token.get_balance(async_stubbed_receiver_token_account_pk) 159 | balance_info = resp.value 160 | assert balance_info.amount == str(expected_amount) 161 | assert balance_info.decimals == 6 162 | assert balance_info.ui_amount == 0.0005 163 | 164 | 165 | @pytest.mark.integration 166 | async def test_burn(stubbed_sender, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 167 | """Test burning tokens.""" 168 | burn_amount = 200 169 | expected_amount = 300 170 | 171 | burn_resp = await test_token.burn( 172 | account=stubbed_sender_token_account_pk, 173 | owner=stubbed_sender, 174 | amount=burn_amount, 175 | multi_signers=None, 176 | opts=OPTS, 177 | ) 178 | assert_valid_response(burn_resp) 179 | 180 | resp = await test_token.get_balance(stubbed_sender_token_account_pk) 181 | balance_info = resp.value 182 | assert balance_info.amount == str(expected_amount) 183 | assert balance_info.decimals == 6 184 | assert balance_info.ui_amount == 0.0003 185 | 186 | 187 | @pytest.mark.integration 188 | async def test_mint_to_checked( 189 | stubbed_sender, 190 | stubbed_sender_token_account_pk, 191 | test_token, 192 | ): # pylint: disable=redefined-outer-name 193 | """Test mint token checked and get balance.""" 194 | expected_amount = 1000 195 | mint_amount = 700 196 | expected_decimals = 6 197 | 198 | mint_resp = await test_token.mint_to_checked( 199 | dest=stubbed_sender_token_account_pk, 200 | mint_authority=stubbed_sender, 201 | amount=mint_amount, 202 | decimals=expected_decimals, 203 | multi_signers=None, 204 | opts=OPTS, 205 | ) 206 | assert_valid_response(mint_resp) 207 | 208 | resp = await test_token.get_balance(stubbed_sender_token_account_pk) 209 | balance_info = resp.value 210 | assert balance_info.amount == str(expected_amount) 211 | assert balance_info.decimals == expected_decimals 212 | assert balance_info.ui_amount == 0.001 213 | 214 | 215 | @pytest.mark.integration 216 | async def test_transfer_checked( 217 | stubbed_sender, async_stubbed_receiver_token_account_pk, stubbed_sender_token_account_pk, test_token 218 | ): # pylint: disable=redefined-outer-name 219 | """Test token transfer.""" 220 | transfer_amount = 500 221 | total_amount = 1000 222 | expected_decimals = 6 223 | 224 | transfer_resp = await test_token.transfer_checked( 225 | source=stubbed_sender_token_account_pk, 226 | dest=async_stubbed_receiver_token_account_pk, 227 | owner=stubbed_sender, 228 | amount=transfer_amount, 229 | decimals=expected_decimals, 230 | multi_signers=None, 231 | opts=OPTS, 232 | ) 233 | assert_valid_response(transfer_resp) 234 | 235 | resp = await test_token.get_balance(async_stubbed_receiver_token_account_pk) 236 | balance_info = resp.value 237 | assert balance_info.amount == str(total_amount) 238 | assert balance_info.decimals == expected_decimals 239 | assert balance_info.ui_amount == 0.001 240 | 241 | 242 | @pytest.mark.integration 243 | async def test_burn_checked(stubbed_sender, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 244 | """Test burning tokens checked.""" 245 | burn_amount = 500 246 | expected_decimals = 6 247 | 248 | burn_resp = await test_token.burn_checked( 249 | account=stubbed_sender_token_account_pk, 250 | owner=stubbed_sender, 251 | amount=burn_amount, 252 | decimals=expected_decimals, 253 | multi_signers=None, 254 | opts=OPTS, 255 | ) 256 | assert_valid_response(burn_resp) 257 | 258 | resp = await test_token.get_balance(stubbed_sender_token_account_pk) 259 | balance_info = resp.value 260 | assert balance_info.amount == str(0) 261 | assert balance_info.decimals == expected_decimals 262 | assert balance_info.ui_amount == 0.0 263 | 264 | 265 | @pytest.mark.integration 266 | async def test_get_accounts(stubbed_sender, test_token): # pylint: disable=redefined-outer-name 267 | """Test get token accounts.""" 268 | resp = await test_token.get_accounts_by_owner_json_parsed(stubbed_sender.pubkey()) 269 | assert_valid_response(resp) 270 | assert len(resp.value) == 2 271 | for resp_data in resp.value: 272 | assert resp_data.pubkey 273 | parsed_data = resp_data.account.data.parsed["info"] 274 | assert parsed_data["owner"] == str(stubbed_sender.pubkey()) 275 | 276 | 277 | @pytest.mark.integration 278 | async def test_approve( 279 | stubbed_sender, 280 | async_stubbed_receiver, 281 | stubbed_sender_token_account_pk, 282 | test_token, 283 | test_http_client_async, 284 | ): # pylint: disable=redefined-outer-name 285 | """Test approval for delgating a token account.""" 286 | expected_amount_delegated = 500 287 | resp = await test_token.approve( 288 | source=stubbed_sender_token_account_pk, 289 | delegate=async_stubbed_receiver, 290 | owner=stubbed_sender.pubkey(), 291 | amount=expected_amount_delegated, 292 | opts=OPTS, 293 | ) 294 | await test_http_client_async.confirm_transaction(resp.value) 295 | assert_valid_response(resp) 296 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 297 | assert account_info.delegate == async_stubbed_receiver 298 | assert account_info.delegated_amount == expected_amount_delegated 299 | 300 | 301 | @pytest.mark.integration 302 | async def test_revoke( 303 | stubbed_sender, 304 | async_stubbed_receiver, 305 | stubbed_sender_token_account_pk, 306 | test_token, 307 | test_http_client_async, 308 | ): # pylint: disable=redefined-outer-name 309 | """Test revoke for undelgating a token account.""" 310 | expected_amount_delegated = 500 311 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 312 | assert account_info.delegate == async_stubbed_receiver 313 | assert account_info.delegated_amount == expected_amount_delegated 314 | 315 | revoke_resp = await test_token.revoke( 316 | account=stubbed_sender_token_account_pk, owner=stubbed_sender.pubkey(), opts=OPTS 317 | ) 318 | await test_http_client_async.confirm_transaction(revoke_resp.value) 319 | assert_valid_response(revoke_resp) 320 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 321 | assert account_info.delegate is None 322 | assert account_info.delegated_amount == 0 323 | 324 | 325 | @pytest.mark.integration 326 | async def test_approve_checked( 327 | stubbed_sender, 328 | async_stubbed_receiver, 329 | stubbed_sender_token_account_pk, 330 | test_token, 331 | test_http_client_async, 332 | ): # pylint: disable=redefined-outer-name 333 | """Test approve_checked for delegating a token account.""" 334 | expected_amount_delegated = 500 335 | resp = await test_token.approve_checked( 336 | source=stubbed_sender_token_account_pk, 337 | delegate=async_stubbed_receiver, 338 | owner=stubbed_sender.pubkey(), 339 | amount=expected_amount_delegated, 340 | decimals=6, 341 | opts=OPTS, 342 | ) 343 | await test_http_client_async.confirm_transaction(resp.value) 344 | assert_valid_response(resp) 345 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 346 | assert account_info.delegate == async_stubbed_receiver 347 | assert account_info.delegated_amount == expected_amount_delegated 348 | 349 | 350 | @pytest.mark.integration 351 | async def test_freeze_account(stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client_async): # pylint: disable=redefined-outer-name 352 | """Test freezing an account.""" 353 | resp = await test_http_client_async.request_airdrop(freeze_authority.pubkey(), AIRDROP_AMOUNT) 354 | await test_http_client_async.confirm_transaction(resp.value) 355 | assert_valid_response(resp) 356 | 357 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 358 | assert account_info.is_frozen is False 359 | 360 | freeze_resp = await test_token.freeze_account(stubbed_sender_token_account_pk, freeze_authority, opts=OPTS) 361 | await test_http_client_async.confirm_transaction(freeze_resp.value) 362 | assert_valid_response(freeze_resp) 363 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 364 | assert account_info.is_frozen is True 365 | 366 | 367 | @pytest.mark.integration 368 | async def test_thaw_account(stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client_async): # pylint: disable=redefined-outer-name 369 | """Test thawing an account.""" 370 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 371 | assert account_info.is_frozen is True 372 | 373 | thaw_resp = await test_token.thaw_account(stubbed_sender_token_account_pk, freeze_authority, opts=OPTS) 374 | await test_http_client_async.confirm_transaction(thaw_resp.value) 375 | assert_valid_response(thaw_resp) 376 | account_info = await test_token.get_account_info(stubbed_sender_token_account_pk) 377 | assert account_info.is_frozen is False 378 | 379 | 380 | @pytest.mark.integration 381 | async def test_close_account( 382 | stubbed_sender, 383 | stubbed_sender_token_account_pk, 384 | async_stubbed_receiver_token_account_pk, 385 | test_token, 386 | test_http_client_async, 387 | ): # pylint: disable=redefined-outer-name 388 | """Test closing a token account.""" 389 | create_resp = await test_http_client_async.get_account_info(stubbed_sender_token_account_pk) 390 | assert_valid_response(create_resp) 391 | assert create_resp.value.data 392 | 393 | close_resp = await test_token.close_account( 394 | account=stubbed_sender_token_account_pk, 395 | dest=async_stubbed_receiver_token_account_pk, 396 | authority=stubbed_sender, 397 | opts=OPTS, 398 | ) 399 | await test_http_client_async.confirm_transaction(close_resp.value) 400 | assert_valid_response(close_resp) 401 | 402 | info_resp = await test_http_client_async.get_account_info(stubbed_sender_token_account_pk) 403 | assert_valid_response(info_resp) 404 | assert info_resp.value is None 405 | 406 | 407 | @pytest.mark.integration 408 | async def test_create_multisig(stubbed_sender, async_stubbed_receiver, test_token, test_http_client): # pylint: disable=redefined-outer-name 409 | """Test creating a multisig account.""" 410 | min_signers = 2 411 | multisig_pubkey = await test_token.create_multisig( 412 | min_signers, [stubbed_sender.pubkey(), async_stubbed_receiver], opts=OPTS 413 | ) 414 | resp = test_http_client.get_account_info(multisig_pubkey) 415 | assert_valid_response(resp) 416 | assert resp.value.owner == TOKEN_PROGRAM_ID 417 | 418 | multisig_data = layouts.MULTISIG_LAYOUT.parse(resp.value.data) 419 | assert multisig_data.is_initialized 420 | assert multisig_data.m == min_signers 421 | assert Pubkey(multisig_data.signer1) == stubbed_sender.pubkey() 422 | assert Pubkey(multisig_data.signer2) == async_stubbed_receiver 423 | -------------------------------------------------------------------------------- /tests/integration/test_memo.py: -------------------------------------------------------------------------------- 1 | """Tests for the Memo program.""" 2 | 3 | import pytest 4 | from solders.keypair import Keypair 5 | from solders.message import Message 6 | from solders.transaction_status import ParsedInstruction 7 | from spl.memo.constants import MEMO_PROGRAM_ID 8 | from spl.memo.instructions import MemoParams, create_memo 9 | 10 | from solana.rpc.api import Client 11 | from solana.rpc.commitment import Finalized 12 | from solders.transaction import Transaction 13 | 14 | from ..utils import AIRDROP_AMOUNT, assert_valid_response 15 | 16 | 17 | @pytest.mark.integration 18 | def test_send_memo_in_transaction(stubbed_sender: Keypair, test_http_client: Client): 19 | """Test sending a memo instruction to localnet.""" 20 | airdrop_resp = test_http_client.request_airdrop(stubbed_sender.pubkey(), AIRDROP_AMOUNT) 21 | assert_valid_response(airdrop_resp) 22 | test_http_client.confirm_transaction(airdrop_resp.value) 23 | raw_message = "test" 24 | message = bytes(raw_message, encoding="utf8") 25 | # Create memo params 26 | memo_params = MemoParams( 27 | program_id=MEMO_PROGRAM_ID, 28 | signer=stubbed_sender.pubkey(), 29 | message=message, 30 | ) 31 | # Create transfer tx to add memo to transaction from stubbed sender 32 | blockhash = test_http_client.get_latest_blockhash().value.blockhash 33 | ixs = [create_memo(memo_params)] 34 | msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) 35 | transfer_tx = Transaction([stubbed_sender], msg, blockhash) 36 | resp = test_http_client.send_transaction(transfer_tx) 37 | assert_valid_response(resp) 38 | txn_id = resp.value 39 | # Txn needs to be finalized in order to parse the logs. 40 | test_http_client.confirm_transaction(txn_id, commitment=Finalized) 41 | resp2_val = test_http_client.get_transaction(txn_id, commitment=Finalized, encoding="jsonParsed").value 42 | assert resp2_val is not None 43 | resp2_transaction = resp2_val.transaction 44 | meta = resp2_transaction.meta 45 | assert meta is not None 46 | messages = meta.log_messages 47 | assert messages is not None 48 | log_message = messages[2].split('"') 49 | assert log_message[1] == raw_message 50 | ixn = resp2_transaction.transaction.message.instructions[0] 51 | assert isinstance(ixn, ParsedInstruction) 52 | assert ixn.parsed == raw_message 53 | assert ixn.program_id == MEMO_PROGRAM_ID 54 | -------------------------------------------------------------------------------- /tests/integration/test_recent_performance_samples.py: -------------------------------------------------------------------------------- 1 | """These tests live in their own file so that their sleeping doesn't slow down other tests.""" 2 | 3 | import time 4 | 5 | from pytest import fixture, mark 6 | 7 | from ..utils import assert_valid_response 8 | 9 | 10 | @fixture(scope="session") 11 | def _wait_until_ready() -> None: 12 | """Sleep for a minute so that performance samples are available.""" 13 | time.sleep(60) 14 | 15 | 16 | @mark.integration 17 | @mark.asyncio 18 | async def test_get_recent_performance_samples_async(test_http_client_async, _wait_until_ready): 19 | """Test get recent performance samples (async).""" 20 | resp = await test_http_client_async.get_recent_performance_samples(4) 21 | assert_valid_response(resp) 22 | 23 | 24 | @mark.integration 25 | def test_get_recent_performance_samples(test_http_client, _wait_until_ready): 26 | """Test get recent performance samples (synchronous).""" 27 | resp = test_http_client.get_recent_performance_samples(4) 28 | assert_valid_response(resp) 29 | -------------------------------------------------------------------------------- /tests/integration/test_token_client.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0401 2 | """Tests for the SPL Token Client.""" 3 | 4 | import pytest 5 | import spl.token._layouts as layouts 6 | from solders.pubkey import Pubkey 7 | from spl.token.client import Token 8 | from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID 9 | 10 | from ..utils import AIRDROP_AMOUNT, OPTS, assert_valid_response 11 | 12 | 13 | @pytest.mark.integration 14 | @pytest.fixture(scope="module") 15 | def test_token(stubbed_sender, freeze_authority, test_http_client) -> Token: 16 | """Test create mint.""" 17 | resp = test_http_client.request_airdrop(stubbed_sender.pubkey(), AIRDROP_AMOUNT) 18 | test_http_client.confirm_transaction(resp.value) 19 | balance = test_http_client.get_balance(stubbed_sender.pubkey()) 20 | assert balance.value == AIRDROP_AMOUNT 21 | expected_decimals = 6 22 | token_client = Token.create_mint( 23 | test_http_client, 24 | stubbed_sender, 25 | stubbed_sender.pubkey(), 26 | expected_decimals, 27 | TOKEN_PROGRAM_ID, 28 | freeze_authority.pubkey(), 29 | ) 30 | 31 | assert token_client.pubkey 32 | assert token_client.program_id == TOKEN_PROGRAM_ID 33 | assert token_client.payer.pubkey() == stubbed_sender.pubkey() 34 | 35 | resp = test_http_client.get_account_info(token_client.pubkey) 36 | assert_valid_response(resp) 37 | assert resp.value.owner == TOKEN_PROGRAM_ID 38 | 39 | mint_data = layouts.MINT_LAYOUT.parse(resp.value.data) 40 | assert mint_data.is_initialized 41 | assert mint_data.decimals == expected_decimals 42 | assert mint_data.supply == 0 43 | assert Pubkey(mint_data.mint_authority) == stubbed_sender.pubkey() 44 | assert Pubkey(mint_data.freeze_authority) == freeze_authority.pubkey() 45 | return token_client 46 | 47 | 48 | @pytest.mark.integration 49 | @pytest.fixture(scope="module") 50 | def stubbed_sender_token_account_pk(stubbed_sender, test_token) -> Pubkey: # pylint: disable=redefined-outer-name 51 | """Token account for stubbed sender.""" 52 | return test_token.create_account(stubbed_sender.pubkey()) 53 | 54 | 55 | @pytest.mark.integration 56 | @pytest.fixture(scope="module") 57 | def stubbed_receiver_token_account_pk(stubbed_receiver, test_token) -> Pubkey: # pylint: disable=redefined-outer-name 58 | """Token account for stubbed receiver.""" 59 | return test_token.create_account(stubbed_receiver) 60 | 61 | 62 | @pytest.mark.integration 63 | def test_new_account(stubbed_sender, test_http_client, test_token): # pylint: disable=redefined-outer-name 64 | """Test creating a new token account.""" 65 | token_account_pk = test_token.create_account(stubbed_sender.pubkey()) 66 | resp = test_http_client.get_account_info(token_account_pk) 67 | assert_valid_response(resp) 68 | assert resp.value.owner == TOKEN_PROGRAM_ID 69 | 70 | account_data = layouts.ACCOUNT_LAYOUT.parse(resp.value.data) 71 | assert account_data.state 72 | assert not account_data.amount 73 | assert ( 74 | not account_data.delegate_option 75 | and not account_data.delegated_amount 76 | and Pubkey(account_data.delegate) == Pubkey([0] * 31 + [0]) 77 | ) 78 | assert not account_data.close_authority_option and Pubkey(account_data.close_authority) == Pubkey([0] * 31 + [0]) 79 | assert not account_data.is_native_option and not account_data.is_native 80 | assert Pubkey(account_data.mint) == test_token.pubkey 81 | assert Pubkey(account_data.owner) == stubbed_sender.pubkey() 82 | 83 | 84 | @pytest.mark.integration 85 | def test_new_associated_account(test_token): # pylint: disable=redefined-outer-name 86 | """Test creating a new associated token account.""" 87 | new_acct = Pubkey([0] * 31 + [0]) 88 | token_account_pubkey = test_token.create_associated_token_account(new_acct) 89 | expected_token_account_key, _ = new_acct.find_program_address( 90 | seeds=[bytes(new_acct), bytes(TOKEN_PROGRAM_ID), bytes(test_token.pubkey)], 91 | program_id=ASSOCIATED_TOKEN_PROGRAM_ID, 92 | ) 93 | assert token_account_pubkey == expected_token_account_key 94 | 95 | 96 | @pytest.mark.integration 97 | def test_get_account_info(stubbed_sender, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 98 | """Test get token account info.""" 99 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 100 | assert account_info.is_initialized is True 101 | assert account_info.mint == test_token.pubkey 102 | assert account_info.owner == stubbed_sender.pubkey() 103 | assert account_info.amount == 0 104 | assert account_info.delegate is None 105 | assert account_info.delegated_amount == 0 106 | assert account_info.is_frozen is False 107 | assert account_info.is_native is False 108 | assert account_info.rent_exempt_reserve is None 109 | assert account_info.close_authority is None 110 | 111 | 112 | @pytest.mark.integration 113 | def test_get_mint_info(stubbed_sender, freeze_authority, test_token): # pylint: disable=redefined-outer-name 114 | """Test get token mint info.""" 115 | mint_info = test_token.get_mint_info() 116 | assert mint_info.mint_authority == stubbed_sender.pubkey() 117 | assert mint_info.supply == 0 118 | assert mint_info.decimals == 6 119 | assert mint_info.is_initialized is True 120 | assert mint_info.freeze_authority == freeze_authority.pubkey() 121 | 122 | 123 | @pytest.mark.integration 124 | def test_mint_to(stubbed_sender, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 125 | """Test mint token to account and get balance.""" 126 | expected_amount = 1000 127 | assert_valid_response( 128 | test_token.mint_to(dest=stubbed_sender_token_account_pk, mint_authority=stubbed_sender, amount=1000, opts=OPTS) 129 | ) 130 | resp = test_token.get_balance(stubbed_sender_token_account_pk) 131 | balance_info = resp.value 132 | assert balance_info.amount == str(expected_amount) 133 | assert balance_info.decimals == 6 134 | assert balance_info.ui_amount == 0.001 135 | 136 | 137 | @pytest.mark.integration 138 | def test_transfer(stubbed_sender, stubbed_receiver_token_account_pk, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 139 | """Test token transfer.""" 140 | expected_amount = 500 141 | assert_valid_response( 142 | test_token.transfer( 143 | source=stubbed_sender_token_account_pk, 144 | dest=stubbed_receiver_token_account_pk, 145 | owner=stubbed_sender, 146 | amount=expected_amount, 147 | opts=OPTS, 148 | ) 149 | ) 150 | resp = test_token.get_balance(stubbed_receiver_token_account_pk) 151 | balance_info = resp.value 152 | assert balance_info.amount == str(expected_amount) 153 | assert balance_info.decimals == 6 154 | assert balance_info.ui_amount == 0.0005 155 | 156 | 157 | @pytest.mark.integration 158 | def test_burn( 159 | stubbed_sender, 160 | stubbed_sender_token_account_pk, 161 | test_token, 162 | ): # pylint: disable=redefined-outer-name 163 | """Test burning tokens.""" 164 | burn_amount = 200 165 | expected_amount = 300 166 | 167 | assert_valid_response( 168 | test_token.burn( 169 | account=stubbed_sender_token_account_pk, 170 | owner=stubbed_sender, 171 | amount=burn_amount, 172 | multi_signers=None, 173 | opts=OPTS, 174 | ) 175 | ) 176 | resp = test_token.get_balance(stubbed_sender_token_account_pk) 177 | balance_info = resp.value 178 | assert balance_info.amount == str(expected_amount) 179 | assert balance_info.decimals == 6 180 | assert balance_info.ui_amount == 0.0003 181 | 182 | 183 | @pytest.mark.integration 184 | def test_mint_to_checked(stubbed_sender, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 185 | """Test mint token checked and get balance.""" 186 | expected_amount = 1000 187 | mint_amount = 700 188 | expected_decimals = 6 189 | 190 | assert_valid_response( 191 | test_token.mint_to_checked( 192 | dest=stubbed_sender_token_account_pk, 193 | mint_authority=stubbed_sender, 194 | amount=mint_amount, 195 | decimals=expected_decimals, 196 | multi_signers=None, 197 | opts=OPTS, 198 | ) 199 | ) 200 | resp = test_token.get_balance(stubbed_sender_token_account_pk) 201 | balance_info = resp.value 202 | assert balance_info.amount == str(expected_amount) 203 | assert balance_info.decimals == expected_decimals 204 | assert balance_info.ui_amount == 0.001 205 | 206 | 207 | @pytest.mark.integration 208 | def test_transfer_checked( 209 | stubbed_sender, stubbed_receiver_token_account_pk, stubbed_sender_token_account_pk, test_token 210 | ): # pylint: disable=redefined-outer-name 211 | """Test token transfer checked.""" 212 | transfer_amount = 500 213 | total_amount = 1000 214 | expected_decimals = 6 215 | 216 | assert_valid_response( 217 | test_token.transfer_checked( 218 | source=stubbed_sender_token_account_pk, 219 | dest=stubbed_receiver_token_account_pk, 220 | owner=stubbed_sender, 221 | amount=transfer_amount, 222 | decimals=expected_decimals, 223 | multi_signers=None, 224 | opts=OPTS, 225 | ) 226 | ) 227 | resp = test_token.get_balance(stubbed_receiver_token_account_pk) 228 | balance_info = resp.value 229 | assert balance_info.amount == str(total_amount) 230 | assert balance_info.decimals == expected_decimals 231 | assert balance_info.ui_amount == 0.001 232 | 233 | 234 | @pytest.mark.integration 235 | def test_burn_checked(stubbed_sender, stubbed_sender_token_account_pk, test_token): # pylint: disable=redefined-outer-name 236 | """Test burning tokens checked.""" 237 | burn_amount = 500 238 | expected_decimals = 6 239 | 240 | assert_valid_response( 241 | test_token.burn_checked( 242 | account=stubbed_sender_token_account_pk, 243 | owner=stubbed_sender, 244 | amount=burn_amount, 245 | decimals=expected_decimals, 246 | multi_signers=None, 247 | opts=OPTS, 248 | ) 249 | ) 250 | resp = test_token.get_balance(stubbed_sender_token_account_pk) 251 | balance_info = resp.value 252 | assert balance_info.amount == str(0) 253 | assert balance_info.decimals == expected_decimals 254 | assert balance_info.ui_amount == 0.0 255 | 256 | 257 | @pytest.mark.integration 258 | def test_get_accounts(stubbed_sender, test_token): # pylint: disable=redefined-outer-name 259 | """Test get token accounts.""" 260 | resp = test_token.get_accounts_by_owner_json_parsed(stubbed_sender.pubkey()) 261 | assert_valid_response(resp) 262 | assert len(resp.value) == 2 263 | for resp_data in resp.value: 264 | assert resp_data.pubkey 265 | parsed_data = resp_data.account.data.parsed["info"] 266 | assert parsed_data["owner"] == str(stubbed_sender.pubkey()) 267 | 268 | 269 | @pytest.mark.integration 270 | def test_approve(stubbed_sender, stubbed_receiver, stubbed_sender_token_account_pk, test_token, test_http_client): # pylint: disable=redefined-outer-name 271 | """Test approval for delegating a token account.""" 272 | expected_amount_delegated = 500 273 | resp = test_token.approve( 274 | source=stubbed_sender_token_account_pk, 275 | delegate=stubbed_receiver, 276 | owner=stubbed_sender.pubkey(), 277 | amount=expected_amount_delegated, 278 | opts=OPTS, 279 | ) 280 | assert_valid_response(resp) 281 | test_http_client.confirm_transaction( 282 | resp.value, 283 | ) 284 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 285 | assert account_info.delegate == stubbed_receiver 286 | assert account_info.delegated_amount == expected_amount_delegated 287 | 288 | 289 | @pytest.mark.integration 290 | def test_revoke(stubbed_sender, stubbed_receiver, stubbed_sender_token_account_pk, test_token, test_http_client): # pylint: disable=redefined-outer-name 291 | """Test revoke for undelegating a token account.""" 292 | expected_amount_delegated = 500 293 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 294 | assert account_info.delegate == stubbed_receiver 295 | assert account_info.delegated_amount == expected_amount_delegated 296 | 297 | revoke_resp = test_token.revoke(account=stubbed_sender_token_account_pk, owner=stubbed_sender.pubkey(), opts=OPTS) 298 | assert_valid_response(revoke_resp) 299 | test_http_client.confirm_transaction(revoke_resp.value) 300 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 301 | assert account_info.delegate is None 302 | assert account_info.delegated_amount == 0 303 | 304 | 305 | @pytest.mark.integration 306 | def test_approve_checked( 307 | stubbed_sender, stubbed_receiver, stubbed_sender_token_account_pk, test_token, test_http_client 308 | ): # pylint: disable=redefined-outer-name 309 | """Test approve_checked for delegating a token account.""" 310 | expected_amount_delegated = 500 311 | resp = test_token.approve_checked( 312 | source=stubbed_sender_token_account_pk, 313 | delegate=stubbed_receiver, 314 | owner=stubbed_sender.pubkey(), 315 | amount=expected_amount_delegated, 316 | decimals=6, 317 | opts=OPTS, 318 | ) 319 | assert_valid_response(resp) 320 | test_http_client.confirm_transaction( 321 | resp.value, 322 | ) 323 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 324 | assert account_info.delegate == stubbed_receiver 325 | assert account_info.delegated_amount == expected_amount_delegated 326 | 327 | 328 | @pytest.mark.integration 329 | def test_freeze_account(stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client): # pylint: disable=redefined-outer-name 330 | """Test freezing an account.""" 331 | resp = test_http_client.request_airdrop(freeze_authority.pubkey(), AIRDROP_AMOUNT) 332 | assert_valid_response(resp) 333 | test_http_client.confirm_transaction( 334 | resp.value, 335 | ) 336 | 337 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 338 | assert account_info.is_frozen is False 339 | 340 | freeze_resp = test_token.freeze_account(stubbed_sender_token_account_pk, freeze_authority, opts=OPTS) 341 | assert_valid_response(freeze_resp) 342 | test_http_client.confirm_transaction(freeze_resp.value) 343 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 344 | assert account_info.is_frozen is True 345 | 346 | 347 | @pytest.mark.integration 348 | def test_thaw_account(stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client): # pylint: disable=redefined-outer-name 349 | """Test thawing an account.""" 350 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 351 | assert account_info.is_frozen is True 352 | 353 | thaw_resp = test_token.thaw_account(stubbed_sender_token_account_pk, freeze_authority, opts=OPTS) 354 | assert_valid_response(thaw_resp) 355 | test_http_client.confirm_transaction(thaw_resp.value) 356 | account_info = test_token.get_account_info(stubbed_sender_token_account_pk) 357 | assert account_info.is_frozen is False 358 | 359 | 360 | @pytest.mark.integration 361 | def test_close_account( 362 | stubbed_sender, 363 | stubbed_sender_token_account_pk, 364 | stubbed_receiver_token_account_pk, 365 | test_token, 366 | test_http_client, 367 | ): # pylint: disable=redefined-outer-name 368 | """Test closing a token account.""" 369 | create_resp = test_http_client.get_account_info(stubbed_sender_token_account_pk) 370 | assert_valid_response(create_resp) 371 | assert create_resp.value.data 372 | 373 | close_resp = test_token.close_account( 374 | account=stubbed_sender_token_account_pk, 375 | dest=stubbed_receiver_token_account_pk, 376 | authority=stubbed_sender, 377 | opts=OPTS, 378 | ) 379 | assert_valid_response(close_resp) 380 | test_http_client.confirm_transaction(close_resp.value) 381 | info_resp = test_http_client.get_account_info(stubbed_sender_token_account_pk) 382 | assert_valid_response(info_resp) 383 | assert info_resp.value is None 384 | 385 | 386 | @pytest.mark.integration 387 | def test_create_multisig(stubbed_sender, stubbed_receiver, test_token, test_http_client): # pylint: disable=redefined-outer-name 388 | """Test creating a multisig account.""" 389 | min_signers = 2 390 | multisig_pubkey = test_token.create_multisig(min_signers, [stubbed_sender.pubkey(), stubbed_receiver], opts=OPTS) 391 | resp = test_http_client.get_account_info(multisig_pubkey) 392 | assert_valid_response(resp) 393 | assert resp.value.owner == TOKEN_PROGRAM_ID 394 | 395 | multisig_data = layouts.MULTISIG_LAYOUT.parse(resp.value.data) 396 | assert multisig_data.is_initialized 397 | assert multisig_data.m == min_signers 398 | assert Pubkey(multisig_data.signer1) == stubbed_sender.pubkey() 399 | assert Pubkey(multisig_data.signer2) == stubbed_receiver 400 | -------------------------------------------------------------------------------- /tests/integration/test_websockets.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-argument,redefined-outer-name 2 | """Tests for the Websocket Client.""" 3 | 4 | from typing import AsyncGenerator, List, Tuple 5 | 6 | import asyncstdlib 7 | import pytest 8 | from solders import system_program as sp 9 | from solders.keypair import Keypair 10 | from solders.message import Message 11 | from solders.pubkey import Pubkey 12 | from solders.rpc.config import RpcTransactionLogsFilter, RpcTransactionLogsFilterMentions 13 | from solders.rpc.requests import AccountSubscribe, AccountUnsubscribe, Body, LogsSubscribe, LogsUnsubscribe 14 | from solders.rpc.responses import ( 15 | AccountNotification, 16 | LogsNotification, 17 | BlockNotification, 18 | ProgramNotification, 19 | RootNotification, 20 | SignatureNotification, 21 | SlotNotification, 22 | SlotUpdateNotification, 23 | SubscriptionResult, 24 | VoteNotification, 25 | ) 26 | from solders.system_program import ID as SYS_PROGRAM_ID 27 | from websockets.legacy.client import WebSocketClientProtocol 28 | 29 | from solana.rpc.async_api import AsyncClient 30 | from solana.rpc.commitment import Finalized 31 | from solana.rpc.websocket_api import SolanaWsClientProtocol, connect 32 | from solders.transaction import Transaction 33 | 34 | from ..utils import AIRDROP_AMOUNT 35 | 36 | 37 | @pytest.fixture 38 | async def websocket( 39 | test_http_client_async: AsyncClient, docker_ip, docker_services 40 | ) -> AsyncGenerator[WebSocketClientProtocol, None]: 41 | """Websocket connection.""" 42 | port = docker_services.port_for("localnet", 8900) 43 | async with connect(uri=f"ws://{docker_ip}:{port}") as client: 44 | yield client 45 | 46 | 47 | @pytest.fixture 48 | async def multiple_subscriptions( 49 | stubbed_sender: Keypair, websocket: SolanaWsClientProtocol 50 | ) -> AsyncGenerator[List[Body], None]: 51 | """Setup multiple subscriptions.""" 52 | reqs: List[Body] = [ 53 | LogsSubscribe(filter_=RpcTransactionLogsFilter.All, id=websocket.increment_counter_and_get_id()), 54 | AccountSubscribe(stubbed_sender.pubkey(), id=websocket.increment_counter_and_get_id()), 55 | ] 56 | await websocket.send_data(reqs) # None 57 | first_resp = await websocket.recv() 58 | msg0 = first_resp[0] 59 | msg1 = first_resp[1] 60 | assert isinstance(msg0, SubscriptionResult) 61 | assert isinstance(msg1, SubscriptionResult) 62 | logs_subscription_id, account_subscription_id = msg0.result, msg1.result 63 | yield reqs 64 | unsubscribe_reqs: List[Body] = [ 65 | LogsUnsubscribe(logs_subscription_id, websocket.increment_counter_and_get_id()), 66 | AccountUnsubscribe(account_subscription_id, websocket.increment_counter_and_get_id()), 67 | ] 68 | await websocket.send_data(unsubscribe_reqs) 69 | 70 | 71 | @pytest.fixture 72 | async def account_subscribed( 73 | stubbed_sender: Keypair, websocket: SolanaWsClientProtocol 74 | ) -> AsyncGenerator[Pubkey, None]: 75 | """Setup account subscription.""" 76 | recipient = Keypair() 77 | await websocket.account_subscribe(recipient.pubkey()) 78 | first_resp = await websocket.recv() 79 | msg = first_resp[0] 80 | assert isinstance(msg, SubscriptionResult) 81 | subscription_id = msg.result 82 | yield recipient.pubkey() 83 | await websocket.account_unsubscribe(subscription_id) 84 | 85 | 86 | @pytest.fixture 87 | async def logs_subscribed(stubbed_sender: Keypair, websocket: SolanaWsClientProtocol) -> AsyncGenerator[None, None]: 88 | """Setup logs subscription.""" 89 | await websocket.logs_subscribe() 90 | first_resp = await websocket.recv() 91 | msg = first_resp[0] 92 | assert isinstance(msg, SubscriptionResult) 93 | subscription_id = msg.result 94 | yield 95 | await websocket.logs_unsubscribe(subscription_id) 96 | 97 | 98 | @pytest.fixture 99 | async def logs_subscribed_mentions_filter( 100 | stubbed_sender: Keypair, websocket: SolanaWsClientProtocol 101 | ) -> AsyncGenerator[None, None]: 102 | """Setup logs subscription with a mentions filter.""" 103 | await websocket.logs_subscribe(RpcTransactionLogsFilterMentions(SYS_PROGRAM_ID)) 104 | first_resp = await websocket.recv() 105 | msg = first_resp[0] 106 | assert isinstance(msg, SubscriptionResult) 107 | subscription_id = msg.result 108 | yield 109 | await websocket.logs_unsubscribe(subscription_id) 110 | 111 | 112 | @pytest.fixture 113 | async def block_subscribed(websocket: SolanaWsClientProtocol) -> AsyncGenerator[None, None]: 114 | """Setup block subscription.""" 115 | await websocket.block_subscribe() 116 | first_resp = await websocket.recv() 117 | msg = first_resp[0] 118 | assert isinstance(msg, SubscriptionResult) 119 | subscription_id = msg.result 120 | yield 121 | await websocket.block_unsubscribe(subscription_id) 122 | 123 | 124 | @pytest.fixture 125 | async def program_subscribed( 126 | websocket: SolanaWsClientProtocol, test_http_client_async: AsyncClient 127 | ) -> AsyncGenerator[Tuple[Keypair, Keypair], None]: 128 | """Setup program subscription.""" 129 | program = Keypair() 130 | owned = Keypair() 131 | airdrop_resp = await test_http_client_async.request_airdrop(owned.pubkey(), AIRDROP_AMOUNT) 132 | await test_http_client_async.confirm_transaction(airdrop_resp.value) 133 | await websocket.program_subscribe(program.pubkey()) 134 | first_resp = await websocket.recv() 135 | msg = first_resp[0] 136 | assert isinstance(msg, SubscriptionResult) 137 | subscription_id = msg.result 138 | yield program, owned 139 | await websocket.program_unsubscribe(subscription_id) 140 | 141 | 142 | @pytest.fixture 143 | async def signature_subscribed( 144 | websocket: SolanaWsClientProtocol, test_http_client_async: AsyncClient 145 | ) -> AsyncGenerator[None, None]: 146 | """Setup signature subscription.""" 147 | recipient = Keypair() 148 | airdrop_resp = await test_http_client_async.request_airdrop(recipient.pubkey(), AIRDROP_AMOUNT) 149 | await websocket.signature_subscribe(airdrop_resp.value) 150 | first_resp = await websocket.recv() 151 | msg = first_resp[0] 152 | assert isinstance(msg, SubscriptionResult) 153 | subscription_id = msg.result 154 | yield 155 | await websocket.signature_unsubscribe(subscription_id) 156 | 157 | 158 | @pytest.fixture 159 | async def slot_subscribed(websocket: SolanaWsClientProtocol) -> AsyncGenerator[None, None]: 160 | """Setup slot subscription.""" 161 | await websocket.slot_subscribe() 162 | first_resp = await websocket.recv() 163 | msg = first_resp[0] 164 | assert isinstance(msg, SubscriptionResult) 165 | subscription_id = msg.result 166 | yield 167 | await websocket.slot_unsubscribe(subscription_id) 168 | 169 | 170 | @pytest.fixture 171 | async def slots_updates_subscribed(websocket: SolanaWsClientProtocol) -> AsyncGenerator[None, None]: 172 | """Setup slots updates subscription.""" 173 | await websocket.slots_updates_subscribe() 174 | first_resp = await websocket.recv() 175 | msg = first_resp[0] 176 | assert isinstance(msg, SubscriptionResult) 177 | subscription_id = msg.result 178 | yield 179 | await websocket.slots_updates_unsubscribe(subscription_id) 180 | 181 | 182 | @pytest.fixture 183 | async def root_subscribed(websocket: SolanaWsClientProtocol) -> AsyncGenerator[None, None]: 184 | """Setup root subscription.""" 185 | await websocket.root_subscribe() 186 | first_resp = await websocket.recv() 187 | msg = first_resp[0] 188 | assert isinstance(msg, SubscriptionResult) 189 | subscription_id = msg.result 190 | yield 191 | await websocket.root_unsubscribe(subscription_id) 192 | 193 | 194 | @pytest.fixture 195 | async def vote_subscribed(websocket: SolanaWsClientProtocol) -> AsyncGenerator[None, None]: 196 | """Setup vote subscription.""" 197 | await websocket.vote_subscribe() 198 | first_resp = await websocket.recv() 199 | msg = first_resp[0] 200 | assert isinstance(msg, SubscriptionResult) 201 | subscription_id = msg.result 202 | yield 203 | await websocket.vote_unsubscribe(subscription_id) 204 | 205 | 206 | @pytest.mark.integration 207 | async def test_multiple_subscriptions( 208 | stubbed_sender: Keypair, 209 | test_http_client_async: AsyncClient, 210 | multiple_subscriptions: List[Body], 211 | websocket: SolanaWsClientProtocol, 212 | ): 213 | """Test subscribing to multiple feeds.""" 214 | await test_http_client_async.request_airdrop(stubbed_sender.pubkey(), AIRDROP_AMOUNT) 215 | async for idx, message in asyncstdlib.enumerate(websocket): 216 | for item in message: 217 | if isinstance(item, (AccountNotification, LogsNotification)): 218 | assert item.result is not None 219 | else: 220 | raise ValueError(f"Unexpected message for this test: {item}") 221 | if idx == len(multiple_subscriptions) - 1: 222 | break 223 | balance = await test_http_client_async.get_balance(stubbed_sender.pubkey(), Finalized) 224 | assert balance.value == AIRDROP_AMOUNT 225 | 226 | 227 | @pytest.mark.integration 228 | async def test_account_subscribe( 229 | test_http_client_async: AsyncClient, websocket: SolanaWsClientProtocol, account_subscribed: Pubkey 230 | ): 231 | """Test account subscription.""" 232 | await test_http_client_async.request_airdrop(account_subscribed, AIRDROP_AMOUNT) 233 | main_resp = await websocket.recv() 234 | msg = main_resp[0] 235 | assert isinstance(msg, AccountNotification) 236 | assert msg.result.value.lamports == AIRDROP_AMOUNT 237 | 238 | 239 | @pytest.mark.integration 240 | async def test_logs_subscribe( 241 | test_http_client_async: AsyncClient, 242 | websocket: SolanaWsClientProtocol, 243 | logs_subscribed: None, 244 | ): 245 | """Test logs subscription.""" 246 | recipient = Keypair().pubkey() 247 | await test_http_client_async.request_airdrop(recipient, AIRDROP_AMOUNT) 248 | main_resp = await websocket.recv() 249 | msg = main_resp[0] 250 | assert isinstance(msg, LogsNotification) 251 | assert msg.result.value.logs[0] == "Program 11111111111111111111111111111111 invoke [1]" 252 | 253 | 254 | @pytest.mark.integration 255 | async def test_logs_subscribe_mentions_filter( 256 | test_http_client_async: AsyncClient, 257 | websocket: SolanaWsClientProtocol, 258 | logs_subscribed_mentions_filter: None, 259 | ): 260 | """Test logs subscription with a mentions filter.""" 261 | recipient = Keypair().pubkey() 262 | await test_http_client_async.request_airdrop(recipient, AIRDROP_AMOUNT) 263 | main_resp = await websocket.recv() 264 | msg = main_resp[0] 265 | assert isinstance(msg, LogsNotification) 266 | assert msg.result.value.logs[0] == "Program 11111111111111111111111111111111 invoke [1]" 267 | 268 | 269 | @pytest.mark.integration 270 | async def test_block_subscribe( 271 | websocket: SolanaWsClientProtocol, 272 | block_subscribed: None, 273 | ): 274 | """Test block subscription.""" 275 | main_resp = await websocket.recv() 276 | msg = main_resp[0] 277 | assert isinstance(msg, BlockNotification) 278 | assert msg.result.value.slot >= 0 279 | 280 | 281 | @pytest.mark.integration 282 | async def test_program_subscribe( 283 | test_http_client_async: AsyncClient, 284 | websocket: SolanaWsClientProtocol, 285 | program_subscribed: Tuple[Keypair, Keypair], 286 | ): 287 | """Test program subscription.""" 288 | program, owned = program_subscribed 289 | ixs = [sp.assign(sp.AssignParams(pubkey=owned.pubkey(), owner=program.pubkey()))] 290 | blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash 291 | msg = Message.new_with_blockhash(ixs, owned.pubkey(), blockhash) 292 | transaction = Transaction([owned], msg, blockhash) 293 | await test_http_client_async.send_transaction(transaction) 294 | main_resp = await websocket.recv() 295 | msg = main_resp[0] 296 | assert isinstance(msg, ProgramNotification) 297 | assert msg.result.value.pubkey == owned.pubkey() 298 | 299 | 300 | @pytest.mark.integration 301 | async def test_signature_subscribe( 302 | websocket: SolanaWsClientProtocol, 303 | signature_subscribed: None, 304 | ): 305 | """Test signature subscription.""" 306 | main_resp = await websocket.recv() 307 | msg = main_resp[0] 308 | assert isinstance(msg, SignatureNotification) 309 | assert msg.result.value.err is None 310 | 311 | 312 | @pytest.mark.integration 313 | async def test_slot_subscribe( 314 | websocket: SolanaWsClientProtocol, 315 | slot_subscribed: None, 316 | ): 317 | """Test slot subscription.""" 318 | main_resp = await websocket.recv() 319 | msg = main_resp[0] 320 | assert isinstance(msg, SlotNotification) 321 | assert msg.result.root >= 0 322 | 323 | 324 | @pytest.mark.integration 325 | async def test_slots_updates_subscribe( 326 | websocket: SolanaWsClientProtocol, 327 | slots_updates_subscribed: None, 328 | ): 329 | """Test slots updates subscription.""" 330 | async for idx, resp in asyncstdlib.enumerate(websocket): 331 | msg = resp[0] 332 | assert isinstance(msg, SlotUpdateNotification) 333 | assert msg.result.slot > 0 334 | if idx == 40: 335 | break 336 | 337 | 338 | @pytest.mark.integration 339 | async def test_root_subscribe( 340 | websocket: SolanaWsClientProtocol, 341 | root_subscribed: None, 342 | ): 343 | """Test root subscription.""" 344 | main_resp = await websocket.recv() 345 | msg = main_resp[0] 346 | assert isinstance(msg, RootNotification) 347 | assert msg.result >= 0 348 | 349 | 350 | @pytest.mark.integration 351 | async def test_vote_subscribe( 352 | websocket: SolanaWsClientProtocol, 353 | vote_subscribed: None, 354 | ): 355 | """Test vote subscription.""" 356 | main_resp = await websocket.recv() 357 | msg = main_resp[0] 358 | assert isinstance(msg, VoteNotification) 359 | assert msg.result.slots 360 | -------------------------------------------------------------------------------- /tests/unit/test_async_client.py: -------------------------------------------------------------------------------- 1 | """Test async client.""" 2 | 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | from httpx import ReadTimeout 7 | from solders.commitment_config import CommitmentLevel 8 | from solders.pubkey import Pubkey 9 | from solders.rpc.config import RpcSignaturesForAddressConfig 10 | from solders.rpc.requests import GetSignaturesForAddress 11 | from solders.signature import Signature 12 | 13 | from solana.constants import SYSTEM_PROGRAM_ID 14 | from solana.exceptions import SolanaRpcException 15 | from solana.rpc.commitment import Finalized 16 | 17 | 18 | async def test_async_client_http_exception(unit_test_http_client_async): 19 | """Test AsyncClient raises native Solana-py exceptions.""" 20 | with patch("httpx.AsyncClient.post") as post_mock: 21 | post_mock.side_effect = ReadTimeout("placeholder") 22 | with pytest.raises(SolanaRpcException) as exc_info: 23 | await unit_test_http_client_async.get_epoch_info() 24 | assert exc_info.type == SolanaRpcException 25 | assert exc_info.value.error_msg == " raised in \"GetEpochInfo\" endpoint request" 26 | 27 | 28 | def test_client_address_sig_args_no_commitment(unit_test_http_client_async): 29 | """Test generating getSignaturesForAddressBody.""" 30 | expected = GetSignaturesForAddress( 31 | SYSTEM_PROGRAM_ID, 32 | RpcSignaturesForAddressConfig( 33 | limit=5, before=Signature.default(), until=Signature.default(), commitment=CommitmentLevel.Processed 34 | ), 35 | ) 36 | actual = unit_test_http_client_async._get_signatures_for_address_body( 37 | Pubkey([0] * 31 + [0]), before=Signature.default(), until=Signature.default(), limit=5, commitment=None 38 | ) 39 | assert expected == actual 40 | 41 | 42 | def test_client_address_sig_args_with_commitment(unit_test_http_client_async): 43 | expected = GetSignaturesForAddress( 44 | SYSTEM_PROGRAM_ID, 45 | RpcSignaturesForAddressConfig(limit=5, commitment=CommitmentLevel.Finalized), 46 | ) 47 | actual = unit_test_http_client_async._get_signatures_for_address_body( 48 | Pubkey([0] * 31 + [0]), None, None, 5, Finalized 49 | ) 50 | assert expected == actual 51 | -------------------------------------------------------------------------------- /tests/unit/test_client.py: -------------------------------------------------------------------------------- 1 | """Test sync client.""" 2 | 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | from httpx import ReadTimeout 7 | from solders.commitment_config import CommitmentLevel 8 | from solders.pubkey import Pubkey 9 | from solders.rpc.config import RpcSignaturesForAddressConfig 10 | from solders.rpc.requests import GetSignaturesForAddress 11 | from solders.signature import Signature 12 | 13 | from solana.constants import SYSTEM_PROGRAM_ID 14 | from solana.exceptions import SolanaRpcException 15 | from solana.rpc.commitment import Finalized 16 | 17 | 18 | def test_client_http_exception(unit_test_http_client): 19 | """Test AsyncClient raises native Solana-py exceptions.""" 20 | with patch("httpx.post") as post_mock: 21 | post_mock.side_effect = ReadTimeout("placeholder") 22 | with pytest.raises(SolanaRpcException) as exc_info: 23 | unit_test_http_client.get_epoch_info() 24 | assert exc_info.type == SolanaRpcException 25 | assert exc_info.value.error_msg == " raised in \"GetEpochInfo\" endpoint request" 26 | 27 | 28 | def test_client_address_sig_args_no_commitment(unit_test_http_client): 29 | """Test generating getSignaturesForAddress body.""" 30 | expected = GetSignaturesForAddress( 31 | SYSTEM_PROGRAM_ID, 32 | RpcSignaturesForAddressConfig( 33 | limit=5, before=Signature.default(), until=Signature.default(), commitment=CommitmentLevel.Processed 34 | ), 35 | ) 36 | actual = unit_test_http_client._get_signatures_for_address_body( 37 | Pubkey([0] * 31 + [0]), before=Signature.default(), until=Signature.default(), limit=5, commitment=None 38 | ) 39 | assert expected == actual 40 | 41 | 42 | def test_client_address_sig_args_with_commitment(unit_test_http_client): 43 | expected = GetSignaturesForAddress( 44 | SYSTEM_PROGRAM_ID, 45 | RpcSignaturesForAddressConfig(limit=5, commitment=CommitmentLevel.Finalized), 46 | ) 47 | actual = unit_test_http_client._get_signatures_for_address_body(Pubkey([0] * 31 + [0]), None, None, 5, Finalized) 48 | assert expected == actual 49 | -------------------------------------------------------------------------------- /tests/unit/test_cluster_api_url.py: -------------------------------------------------------------------------------- 1 | """Test cluster_api_url.""" 2 | 3 | from solana.utils.cluster import cluster_api_url 4 | 5 | 6 | def test_input_output(): 7 | """Test that cluster_api_url generates the expected output.""" 8 | assert cluster_api_url() == "https://api.devnet.solana.com" 9 | assert cluster_api_url("devnet") == "https://api.devnet.solana.com" 10 | assert cluster_api_url("devnet", True) == "https://api.devnet.solana.com" 11 | assert cluster_api_url("devnet", False) == "http://api.devnet.solana.com" 12 | -------------------------------------------------------------------------------- /tests/unit/test_memo_program.py: -------------------------------------------------------------------------------- 1 | from solders.keypair import Keypair 2 | from spl.memo.constants import MEMO_PROGRAM_ID 3 | from spl.memo.instructions import MemoParams, create_memo, decode_create_memo 4 | 5 | 6 | def test_memo(): 7 | """Test creating a memo instruction.""" 8 | params = MemoParams(signer=Keypair().pubkey(), message=b"test", program_id=MEMO_PROGRAM_ID) 9 | assert decode_create_memo(create_memo(params)) == params 10 | -------------------------------------------------------------------------------- /tests/unit/test_security_txt.py: -------------------------------------------------------------------------------- 1 | """Test security txt.""" 2 | 3 | import pytest 4 | 5 | from solana.utils.security_txt import NoSecurityTxtFoundError, parse_security_txt 6 | 7 | 8 | def test_parse_security_text(): 9 | """Test parsing security txt bytes.""" 10 | data = b"=======BEGIN SECURITY.TXT V1=======\x00name\x00Example\x00project_url\x00http://example.com\x00contacts\x00email:example@example.com,link:https://example.com/security,discord:example#1234\x00policy\x00https://github.com/solana-labs/solana/blob/master/SECURITY.md\x00preferred_languages\x00en,de\x00source_code\x00https://github.com/example/example\x00encryption\x00\n-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: Alice's OpenPGP certificate\nComment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n\nmDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U\nb7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE\nExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy\nMVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO\ndypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4\nOARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s\nE9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb\nDAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn\n0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE=\n=iIGO\n-----END PGP PUBLIC KEY BLOCK-----\n\x00auditors\x00Neodyme\x00acknowledgements\x00\nThe following hackers could've stolen all our money but didn't:\n- Neodyme\n\x00=======END SECURITY.TXT V1=======\x00" # noqa: E501 pylint: disable=line-too-long 11 | security_text = parse_security_txt(data) 12 | assert security_text.name 13 | assert security_text.project_url 14 | assert security_text.contacts 15 | assert security_text.policy 16 | 17 | 18 | def test_parse_invalid_security_text(): 19 | """Test parsing security txt with invalid bytes.""" 20 | invalid_data = b"test" 21 | with pytest.raises(NoSecurityTxtFoundError): 22 | parse_security_txt(invalid_data) 23 | 24 | 25 | def test_parse_wrong_data_type(): 26 | """Test parsing security txt string instead of bytes.""" 27 | wrong_type = "test" 28 | with pytest.raises(TypeError): 29 | parse_security_txt(wrong_type) 30 | 31 | 32 | def test_parse_missing_required_fields(): 33 | """Test parsing security txt with missing required data.""" 34 | data = b"=======BEGIN SECURITY.TXT V1=======\x00project_url\x00http://example.com\x00contacts\x00email:example@example.com,link:https://example.com/security,discord:example#1234\x00policy\x00https://github.com/solana-labs/solana/blob/master/SECURITY.md\x00preferred_languages\x00en,de\x00source_code\x00https://github.com/example/example\x00encryption\x00\n-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: Alice's OpenPGP certificate\nComment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n\nmDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U\nb7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE\nExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy\nMVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO\ndypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4\nOARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s\nE9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb\nDAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn\n0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE=\n=iIGO\n-----END PGP PUBLIC KEY BLOCK-----\n\x00auditors\x00Neodyme\x00acknowledgements\x00\nThe following hackers could've stolen all our money but didn't:\n- Neodyme\n\x00=======END SECURITY.TXT V1=======\x00" # noqa: E501 pylint: disable=line-too-long 35 | with pytest.raises(TypeError): 36 | parse_security_txt(data) 37 | -------------------------------------------------------------------------------- /tests/unit/test_spl_token_instructions.py: -------------------------------------------------------------------------------- 1 | """Unit tests for SPL-token instructions.""" 2 | 3 | import spl.token.instructions as spl_token 4 | from solders.pubkey import Pubkey 5 | from solders.system_program import ID as SYSTEM_PROGRAM_ID 6 | from spl.token.constants import TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT, ASSOCIATED_TOKEN_PROGRAM_ID 7 | from spl.token.instructions import get_associated_token_address 8 | 9 | 10 | def test_initialize_mint(stubbed_sender): 11 | """Test initialize mint.""" 12 | mint_authority, freeze_authority = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]) 13 | params_with_freeze = spl_token.InitializeMintParams( 14 | decimals=18, 15 | program_id=TOKEN_PROGRAM_ID, 16 | mint=stubbed_sender.pubkey(), 17 | mint_authority=mint_authority, 18 | freeze_authority=freeze_authority, 19 | ) 20 | instruction = spl_token.initialize_mint(params_with_freeze) 21 | assert spl_token.decode_initialize_mint(instruction) == params_with_freeze 22 | 23 | params_no_freeze = spl_token.InitializeMintParams( 24 | decimals=18, 25 | program_id=TOKEN_PROGRAM_ID, 26 | mint=stubbed_sender.pubkey(), 27 | mint_authority=mint_authority, 28 | ) 29 | instruction = spl_token.initialize_mint(params_no_freeze) 30 | decoded_params = spl_token.decode_initialize_mint(instruction) 31 | assert not decoded_params.freeze_authority 32 | assert decoded_params == params_no_freeze 33 | 34 | 35 | def test_initialize_account(stubbed_sender): 36 | """Test initialize account.""" 37 | new_account, token_mint = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]) 38 | params = spl_token.InitializeAccountParams( 39 | program_id=TOKEN_PROGRAM_ID, 40 | account=new_account, 41 | mint=token_mint, 42 | owner=stubbed_sender.pubkey(), 43 | ) 44 | instruction = spl_token.initialize_account(params) 45 | assert spl_token.decode_initialize_account(instruction) == params 46 | 47 | 48 | def test_initialize_multisig(): 49 | """Test initialize multisig.""" 50 | new_multisig = Pubkey([0] * 31 + [0]) 51 | signers = [Pubkey([0] * 31 + [i + 1]) for i in range(3)] 52 | params = spl_token.InitializeMultisigParams( 53 | program_id=TOKEN_PROGRAM_ID, 54 | multisig=new_multisig, 55 | signers=signers, 56 | m=len(signers), 57 | ) 58 | instruction = spl_token.initialize_multisig(params) 59 | assert spl_token.decode_initialize_multisig(instruction) == params 60 | 61 | 62 | def test_transfer(stubbed_receiver, stubbed_sender): 63 | """Test transfer.""" 64 | params = spl_token.TransferParams( 65 | program_id=TOKEN_PROGRAM_ID, 66 | source=stubbed_sender.pubkey(), 67 | dest=stubbed_receiver, 68 | owner=stubbed_sender.pubkey(), 69 | amount=123, 70 | ) 71 | instruction = spl_token.transfer(params) 72 | assert spl_token.decode_transfer(instruction) == params 73 | 74 | multisig_params = spl_token.TransferParams( 75 | program_id=TOKEN_PROGRAM_ID, 76 | source=stubbed_sender.pubkey(), 77 | dest=stubbed_receiver, 78 | owner=stubbed_sender.pubkey(), 79 | signers=[Pubkey([0] * 31 + [i + 1]) for i in range(3)], 80 | amount=123, 81 | ) 82 | instruction = spl_token.transfer(multisig_params) 83 | assert spl_token.decode_transfer(instruction) == multisig_params 84 | 85 | 86 | def test_approve(stubbed_sender): 87 | """Test approve.""" 88 | delegate_account = Pubkey([0] * 31 + [0]) 89 | params = spl_token.ApproveParams( 90 | program_id=TOKEN_PROGRAM_ID, 91 | source=stubbed_sender.pubkey(), 92 | delegate=delegate_account, 93 | owner=stubbed_sender.pubkey(), 94 | amount=123, 95 | ) 96 | instruction = spl_token.approve(params) 97 | assert spl_token.decode_approve(instruction) == params 98 | 99 | multisig_params = spl_token.ApproveParams( 100 | program_id=TOKEN_PROGRAM_ID, 101 | source=stubbed_sender.pubkey(), 102 | delegate=delegate_account, 103 | owner=stubbed_sender.pubkey(), 104 | signers=[Pubkey([0] * 31 + [i + 1]) for i in range(3)], 105 | amount=123, 106 | ) 107 | instruction = spl_token.approve(multisig_params) 108 | assert spl_token.decode_approve(instruction) == multisig_params 109 | 110 | 111 | def test_revoke(stubbed_sender): 112 | """Test revoke.""" 113 | delegate_account = Pubkey([0] * 31 + [0]) 114 | params = spl_token.RevokeParams( 115 | program_id=TOKEN_PROGRAM_ID, 116 | account=delegate_account, 117 | owner=stubbed_sender.pubkey(), 118 | ) 119 | instruction = spl_token.revoke(params) 120 | assert spl_token.decode_revoke(instruction) == params 121 | 122 | multisig_params = spl_token.RevokeParams( 123 | program_id=TOKEN_PROGRAM_ID, 124 | account=delegate_account, 125 | owner=stubbed_sender.pubkey(), 126 | signers=[Pubkey([0] * 31 + [i + 1]) for i in range(3)], 127 | ) 128 | instruction = spl_token.revoke(multisig_params) 129 | assert spl_token.decode_revoke(instruction) == multisig_params 130 | 131 | 132 | def test_set_authority(): 133 | """Test set authority.""" 134 | account, new_authority, current_authority = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]), Pubkey([0] * 31 + [2]) 135 | params = spl_token.SetAuthorityParams( 136 | program_id=TOKEN_PROGRAM_ID, 137 | account=account, 138 | authority=spl_token.AuthorityType.FREEZE_ACCOUNT, 139 | new_authority=new_authority, 140 | current_authority=current_authority, 141 | ) 142 | instruction = spl_token.set_authority(params) 143 | assert spl_token.decode_set_authority(instruction) == params 144 | 145 | multisig_params = spl_token.SetAuthorityParams( 146 | program_id=TOKEN_PROGRAM_ID, 147 | account=account, 148 | authority=spl_token.AuthorityType.FREEZE_ACCOUNT, 149 | current_authority=current_authority, 150 | signers=[Pubkey([0] * 31 + [i]) for i in range(3, 10)], 151 | ) 152 | instruction = spl_token.set_authority(multisig_params) 153 | decoded_params = spl_token.decode_set_authority(instruction) 154 | assert not decoded_params.new_authority 155 | assert decoded_params == multisig_params 156 | 157 | 158 | def test_mint_to(stubbed_receiver): 159 | """Test mint to.""" 160 | mint, mint_authority = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]) 161 | params = spl_token.MintToParams( 162 | program_id=TOKEN_PROGRAM_ID, 163 | mint=mint, 164 | dest=stubbed_receiver, 165 | mint_authority=mint_authority, 166 | amount=123, 167 | ) 168 | instruction = spl_token.mint_to(params) 169 | assert spl_token.decode_mint_to(instruction) == params 170 | 171 | multisig_params = spl_token.MintToParams( 172 | program_id=TOKEN_PROGRAM_ID, 173 | mint=mint, 174 | dest=stubbed_receiver, 175 | mint_authority=mint_authority, 176 | signers=[Pubkey([0] * 31 + [i]) for i in range(3, 10)], 177 | amount=123, 178 | ) 179 | instruction = spl_token.mint_to(multisig_params) 180 | assert spl_token.decode_mint_to(instruction) == multisig_params 181 | 182 | 183 | def test_burn(stubbed_receiver): 184 | """Test burn.""" 185 | mint, owner = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]) 186 | params = spl_token.BurnParams( 187 | program_id=TOKEN_PROGRAM_ID, 188 | mint=mint, 189 | account=stubbed_receiver, 190 | owner=owner, 191 | amount=123, 192 | ) 193 | instruction = spl_token.burn(params) 194 | assert spl_token.decode_burn(instruction) == params 195 | 196 | multisig_params = spl_token.BurnParams( 197 | program_id=TOKEN_PROGRAM_ID, 198 | mint=mint, 199 | account=stubbed_receiver, 200 | owner=owner, 201 | signers=[Pubkey([0] * 31 + [i]) for i in range(3, 10)], 202 | amount=123, 203 | ) 204 | instruction = spl_token.burn(multisig_params) 205 | assert spl_token.decode_burn(instruction) == multisig_params 206 | 207 | 208 | def test_close_account(stubbed_sender): 209 | """Test close account.""" 210 | token_account = Pubkey([0] * 31 + [0]) 211 | params = spl_token.CloseAccountParams( 212 | program_id=TOKEN_PROGRAM_ID, 213 | account=token_account, 214 | dest=stubbed_sender.pubkey(), 215 | owner=stubbed_sender.pubkey(), 216 | ) 217 | instruction = spl_token.close_account(params) 218 | assert spl_token.decode_close_account(instruction) == params 219 | 220 | multisig_params = spl_token.CloseAccountParams( 221 | program_id=TOKEN_PROGRAM_ID, 222 | account=token_account, 223 | dest=stubbed_sender.pubkey(), 224 | owner=stubbed_sender.pubkey(), 225 | signers=[Pubkey([0] * 31 + [i + 1]) for i in range(3)], 226 | ) 227 | instruction = spl_token.close_account(multisig_params) 228 | assert spl_token.decode_close_account(instruction) == multisig_params 229 | 230 | 231 | def test_freeze_account(stubbed_sender): 232 | """Test freeze account.""" 233 | token_account, mint = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]) 234 | params = spl_token.FreezeAccountParams( 235 | program_id=TOKEN_PROGRAM_ID, 236 | account=token_account, 237 | mint=mint, 238 | authority=stubbed_sender.pubkey(), 239 | ) 240 | instruction = spl_token.freeze_account(params) 241 | assert spl_token.decode_freeze_account(instruction) == params 242 | 243 | multisig_params = spl_token.FreezeAccountParams( 244 | program_id=TOKEN_PROGRAM_ID, 245 | account=token_account, 246 | mint=mint, 247 | authority=stubbed_sender.pubkey(), 248 | multi_signers=[Pubkey([0] * 31 + [i]) for i in range(2, 10)], 249 | ) 250 | instruction = spl_token.freeze_account(multisig_params) 251 | assert spl_token.decode_freeze_account(instruction) == multisig_params 252 | 253 | 254 | def test_thaw_account(stubbed_sender): 255 | """Test thaw account.""" 256 | token_account, mint = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]) 257 | params = spl_token.ThawAccountParams( 258 | program_id=TOKEN_PROGRAM_ID, 259 | account=token_account, 260 | mint=mint, 261 | authority=stubbed_sender.pubkey(), 262 | ) 263 | instruction = spl_token.thaw_account(params) 264 | assert spl_token.decode_thaw_account(instruction) == params 265 | 266 | multisig_params = spl_token.ThawAccountParams( 267 | program_id=TOKEN_PROGRAM_ID, 268 | account=token_account, 269 | mint=mint, 270 | authority=stubbed_sender.pubkey(), 271 | multi_signers=[Pubkey([0] * 31 + [i]) for i in range(2, 10)], 272 | ) 273 | instruction = spl_token.thaw_account(multisig_params) 274 | assert spl_token.decode_thaw_account(instruction) == multisig_params 275 | 276 | 277 | def test_transfer_checked(stubbed_receiver, stubbed_sender): 278 | """Test transfer_checked.""" 279 | mint = Pubkey([0] * 31 + [0]) 280 | params = spl_token.TransferCheckedParams( 281 | program_id=TOKEN_PROGRAM_ID, 282 | source=stubbed_sender.pubkey(), 283 | mint=mint, 284 | dest=stubbed_receiver, 285 | owner=stubbed_sender.pubkey(), 286 | amount=123, 287 | decimals=6, 288 | ) 289 | instruction = spl_token.transfer_checked(params) 290 | assert spl_token.decode_transfer_checked(instruction) == params 291 | 292 | multisig_params = spl_token.TransferCheckedParams( 293 | program_id=TOKEN_PROGRAM_ID, 294 | source=stubbed_sender.pubkey(), 295 | mint=mint, 296 | dest=stubbed_receiver, 297 | owner=stubbed_sender.pubkey(), 298 | signers=[Pubkey([0] * 31 + [i + 1]) for i in range(3)], 299 | amount=123, 300 | decimals=6, 301 | ) 302 | instruction = spl_token.transfer_checked(multisig_params) 303 | assert spl_token.decode_transfer_checked(instruction) == multisig_params 304 | 305 | 306 | def test_approve_checked(stubbed_receiver, stubbed_sender): 307 | """Test approve_checked.""" 308 | mint = Pubkey([0] * 31 + [0]) 309 | params = spl_token.ApproveCheckedParams( 310 | program_id=TOKEN_PROGRAM_ID, 311 | source=stubbed_sender.pubkey(), 312 | mint=mint, 313 | delegate=stubbed_receiver, 314 | owner=stubbed_sender.pubkey(), 315 | amount=123, 316 | decimals=6, 317 | ) 318 | instruction = spl_token.approve_checked(params) 319 | assert spl_token.decode_approve_checked(instruction) == params 320 | 321 | multisig_params = spl_token.ApproveCheckedParams( 322 | program_id=TOKEN_PROGRAM_ID, 323 | source=stubbed_sender.pubkey(), 324 | mint=mint, 325 | delegate=stubbed_receiver, 326 | owner=stubbed_sender.pubkey(), 327 | signers=[Pubkey([0] * 31 + [i + 1]) for i in range(3)], 328 | amount=123, 329 | decimals=6, 330 | ) 331 | instruction = spl_token.approve_checked(multisig_params) 332 | assert spl_token.decode_approve_checked(instruction) == multisig_params 333 | 334 | 335 | def test_mint_to_checked(stubbed_receiver): 336 | """Test mint_to_checked.""" 337 | mint, mint_authority = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]) 338 | params = spl_token.MintToCheckedParams( 339 | program_id=TOKEN_PROGRAM_ID, 340 | mint=mint, 341 | dest=stubbed_receiver, 342 | mint_authority=mint_authority, 343 | amount=123, 344 | decimals=6, 345 | ) 346 | instruction = spl_token.mint_to_checked(params) 347 | assert spl_token.decode_mint_to_checked(instruction) == params 348 | 349 | multisig_params = spl_token.MintToCheckedParams( 350 | program_id=TOKEN_PROGRAM_ID, 351 | mint=mint, 352 | dest=stubbed_receiver, 353 | mint_authority=mint_authority, 354 | signers=[Pubkey([0] * 31 + [i]) for i in range(3, 10)], 355 | amount=123, 356 | decimals=6, 357 | ) 358 | instruction = spl_token.mint_to_checked(multisig_params) 359 | assert spl_token.decode_mint_to_checked(instruction) == multisig_params 360 | 361 | 362 | def test_burn_checked(stubbed_receiver): 363 | """Test burn_checked.""" 364 | mint, owner = Pubkey([0] * 31 + [0]), Pubkey([0] * 31 + [1]) 365 | params = spl_token.BurnCheckedParams( 366 | program_id=TOKEN_PROGRAM_ID, 367 | mint=mint, 368 | account=stubbed_receiver, 369 | owner=owner, 370 | amount=123, 371 | decimals=6, 372 | ) 373 | instruction = spl_token.burn_checked(params) 374 | assert spl_token.decode_burn_checked(instruction) == params 375 | 376 | multisig_params = spl_token.BurnCheckedParams( 377 | program_id=TOKEN_PROGRAM_ID, 378 | mint=mint, 379 | account=stubbed_receiver, 380 | owner=owner, 381 | signers=[Pubkey([0] * 31 + [i]) for i in range(3, 10)], 382 | amount=123, 383 | decimals=6, 384 | ) 385 | instruction = spl_token.burn_checked(multisig_params) 386 | assert spl_token.decode_burn_checked(instruction) == multisig_params 387 | 388 | 389 | def test_sync_native(stubbed_sender): 390 | """Test sync account amount value with lamports.""" 391 | token_account = get_associated_token_address(stubbed_sender.pubkey(), WRAPPED_SOL_MINT) 392 | params = spl_token.SyncNativeParams(program_id=TOKEN_PROGRAM_ID, account=token_account) 393 | 394 | instruction = spl_token.sync_native(params) 395 | decoded_params = spl_token.decode_sync_native(instruction) 396 | assert params == decoded_params 397 | 398 | 399 | def test_create_idempotent_token_account(stubbed_receiver, stubbed_sender): 400 | """Test Create idempotent token account.""" 401 | mint = Pubkey([0] * 31 + [0]) 402 | token_account = get_associated_token_address(stubbed_receiver, mint) 403 | instruction = spl_token.create_idempotent_associated_token_account( 404 | payer=stubbed_sender.pubkey(), 405 | owner=stubbed_receiver, 406 | mint=mint, 407 | ) 408 | 409 | assert instruction.program_id == ASSOCIATED_TOKEN_PROGRAM_ID 410 | assert instruction.data[0] == 1 # CreateIdempotent 411 | assert len(instruction.accounts) == 6 412 | assert instruction.accounts[0].pubkey == stubbed_sender.pubkey() 413 | assert instruction.accounts[0].is_signer 414 | assert instruction.accounts[0].is_writable 415 | assert instruction.accounts[1].pubkey == token_account 416 | assert not instruction.accounts[1].is_signer 417 | assert instruction.accounts[1].is_writable 418 | assert instruction.accounts[2].pubkey == stubbed_receiver 419 | assert not instruction.accounts[2].is_signer 420 | assert not instruction.accounts[2].is_writable 421 | assert instruction.accounts[3].pubkey == mint 422 | assert not instruction.accounts[3].is_signer 423 | assert not instruction.accounts[3].is_writable 424 | assert instruction.accounts[4].pubkey == SYSTEM_PROGRAM_ID 425 | assert not instruction.accounts[4].is_signer 426 | assert not instruction.accounts[4].is_writable 427 | assert instruction.accounts[5].pubkey == TOKEN_PROGRAM_ID 428 | assert not instruction.accounts[5].is_signer 429 | assert not instruction.accounts[5].is_writable 430 | -------------------------------------------------------------------------------- /tests/unit/test_vote_program.py: -------------------------------------------------------------------------------- 1 | """Unit tests for solana.vote_program.""" 2 | 3 | import base64 4 | 5 | from solders.hash import Hash 6 | from solders.keypair import Keypair 7 | from solders.message import Message 8 | from solders.pubkey import Pubkey 9 | import solana.vote_program as vp 10 | 11 | 12 | def test_withdraw_from_vote_account(): 13 | withdrawer_keypair = Keypair.from_bytes( 14 | [ 15 | 134, 16 | 123, 17 | 27, 18 | 208, 19 | 227, 20 | 175, 21 | 253, 22 | 99, 23 | 4, 24 | 81, 25 | 170, 26 | 231, 27 | 186, 28 | 141, 29 | 177, 30 | 142, 31 | 197, 32 | 139, 33 | 94, 34 | 6, 35 | 157, 36 | 2, 37 | 163, 38 | 89, 39 | 150, 40 | 121, 41 | 235, 42 | 86, 43 | 185, 44 | 22, 45 | 1, 46 | 233, 47 | 58, 48 | 133, 49 | 229, 50 | 39, 51 | 212, 52 | 71, 53 | 254, 54 | 72, 55 | 246, 56 | 45, 57 | 160, 58 | 156, 59 | 129, 60 | 199, 61 | 18, 62 | 189, 63 | 53, 64 | 143, 65 | 98, 66 | 72, 67 | 182, 68 | 106, 69 | 69, 70 | 29, 71 | 38, 72 | 145, 73 | 119, 74 | 190, 75 | 13, 76 | 105, 77 | 157, 78 | 112, 79 | ] 80 | ) 81 | vote_account_pubkey = Pubkey.from_string("CWqJy1JpmBcx7awpeANfrPk6AsQKkmego8ujjaYPGFEk") 82 | receiver_account_pubkey = Pubkey.from_string("A1V5gsis39WY42djdTKUFsgE5oamk4nrtg16WnKTuzZK") 83 | recent_blockhash = Hash.from_string("Add1tV7kJgNHhTtx3Dgs6dhC7kyXrGJQZ2tJGW15tLDH") 84 | msg = Message.new_with_blockhash( 85 | [ 86 | vp.withdraw_from_vote_account( 87 | vp.WithdrawFromVoteAccountParams( 88 | vote_account_from_pubkey=vote_account_pubkey, 89 | to_pubkey=receiver_account_pubkey, 90 | withdrawer=withdrawer_keypair.pubkey(), 91 | lamports=2_000_000_000, 92 | ) 93 | ) 94 | ], 95 | withdrawer_keypair.pubkey(), 96 | recent_blockhash, 97 | ) 98 | 99 | # solana withdraw-from-vote-account --dump-transaction-message \ 100 | # CWqJy1JpmBcx7awpeANfrPk6AsQKkmego8ujjaYPGFEk A1V5gsis39WY42djdTKUFsgE5oamk4nrtg16WnKTuzZK \ 101 | # --authorized-withdrawer withdrawer.json \ 102 | # 2 \ 103 | # --blockhash Add1tV7kJgNHhTtx3Dgs6dhC7kyXrGJQZ2tJGW15tLDH \ 104 | # --sign-only -k withdrawer.json 105 | cli_wire_msg = base64.b64decode( # noqa: F841 106 | b"AQABBDqF5SfUR/5I9i2gnIHHEr01j2JItmpFHSaRd74NaZ1wqxUGDtH5ah3TqEKWjcTmfHkpZC1h57NJL8Sx7Q6Olm2F2O70oOvzt1HgIVu+nySaSrWtJiK1eDacPPDWRxCwFgdhSB01dHS7fE12JOvTvbPYNV5z0RBD/A2jU4AAAAAAjxrQaMS7FjmaR++mvFr3XE6XbzMUTMJUIpITrUWBzGwBAwMBAgAMAwAAAACUNXcAAAAA" 107 | ) 108 | js_wire_msg = base64.b64decode( 109 | b"AQABBDqF5SfUR/5I9i2gnIHHEr01j2JItmpFHSaRd74NaZ1whdju9KDr87dR4CFbvp8kmkq1rSYitXg2nDzw1kcQsBarFQYO0flqHdOoQpaNxOZ8eSlkLWHns0kvxLHtDo6WbQdhSB01dHS7fE12JOvTvbPYNV5z0RBD/A2jU4AAAAAAjxrQaMS7FjmaR++mvFr3XE6XbzMUTMJUIpITrUWBzGwBAwMCAQAMAwAAAACUNXcAAAAA" 110 | ) 111 | 112 | serialized_message = bytes(msg) 113 | 114 | assert serialized_message == js_wire_msg 115 | # XXX: Cli message serialization do not sort on account metas producing discrepency 116 | # serialized_message txn == cli_wire_msg 117 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | """Integration test utils.""" 2 | 3 | from typing import get_args 4 | 5 | from solders.rpc.responses import RPCError, RPCResult 6 | 7 | from solana.rpc.commitment import Processed 8 | from solana.rpc.types import TxOpts 9 | 10 | AIRDROP_AMOUNT = 10_000_000_000 11 | 12 | RPC_RESULT_TYPES = get_args(RPCResult) 13 | 14 | 15 | def assert_valid_response(resp: RPCResult): 16 | """Assert valid RPCResult.""" 17 | assert type(resp) in RPC_RESULT_TYPES 18 | assert not isinstance(resp, RPCError.__args__) # type: ignore 19 | 20 | 21 | OPTS = TxOpts(skip_confirmation=False, preflight_commitment=Processed) 22 | --------------------------------------------------------------------------------