├── .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 | [](https://github.com/michaelhly/solanapy/actions?query=workflow%3ACI)
9 | [](https://badge.fury.io/py/solana)
10 | []( https://pypi.python.org/pypi/solana)
11 | [](https://codecov.io/gh/michaelhly/solana-py/branch/master)
12 | [](https://github.com/michaelhly/solana-py/blob/master/LICENSE)
13 | [](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 |
--------------------------------------------------------------------------------